mirror of
https://github.com/tailwindlabs/tailwindcss.git
synced 2025-12-08 21:36:08 +00:00
* ensure we don't crash on deleted files * change return type of `compile` to include a `rebuild()` function This will allow us in the future to perform incremental rebuilds after the initial rebuild. This is purely the API change so that we can prepare all the call sites to use this new API. * set `@tailwind utilities` nodes Instead of replacing the node that represents the `@tailwind utilities` with the generated AST nodes from the rawCandidates, we will set the nodes of the `@tailwind utilities` rule to the AST nodes instead. This way we dont' have to remove and replace the `@tailwind utilities` rule with `n` new nodes. This will later allow us to track the `@tailwindcss utilities` rule itself and update its `nodes` for incremental rebuilds. This also requires a small change to the printer where we now need to print the children of the `@tailwind utilities` rule. Note: we keep the same `depth` as-if the `@tailwindcss utilities` rule was not there. Otherwise additional indentation would be present. * move sorting to the `ast.sort()` call This will allow us to keep sorting AST nodes in a single spot. * move parser functions to the `DesignSystem` This allows us to put all the parsers in the `DesignSystem`, this allows us to scope the parsers to the current design system (the current theme, current utility values and variants). The implementation of these parsers are also using a `DefaultMap` implementation. This allows us to make use of caching and only parse a candidate, parse a variant or compile AST nodes for a given raw candidate once if we've already done this work in the past. Again, this is scoped to the `DesignSystem` itself. This means that if the corresponding theme changes, then we will create a new `DesignSystem` entirely and the caches will be garbage collected. This is important because a candidate like `bg-primary` can be invalid in `DesignSystem` A, but can be valid in `DesignSystem` B and vice versa. * ensure we sort variants alphabetically by root For incremental rebuilds we don't know all the used variants upfront, which means that we can't sort them upfront either (what we used to do). This code now allows us to sort the variants deterministically when sorting the variants themselves instead of relying on the fact that they used to be sorted before. The sort itself could change slightly compared to the previous implementation (especially when you used stacked variants in your candidates), but it will still be deterministic. * replace `localeCompare` comparisons Use cheaper comparisons than `localeCompare` when comparing 2 strings. We currently don't care if it is 100% correctly sorted, but we just want consistent sorting. This is currently faster compared to `localeCompare`. Another benefit is that `localeCompare` could result in non-deterministic results if the CSS was generated on 2 separate computers where the `locale` is different. We could solve that by adding a dedicated locale, but it would still be slower compared to this. * track invalid candidates When an incoming raw candidates doesn't produce any output, then we can mark it as an invalid candidate. This will allow us to reduce the amount of candidates to handle in incremental rebuilds. * add initial incremental rebuild implementation This includes a number of steps: 1. Track the `@tailwind utilities` rule, so that we can adjust its nodes later without re-parsing the full incoming CSS. 2. Add the new incoming raw candidates to the existing set of candidates. 3. Parse the merged set to `compileCandidates` (this can accept any `Iterable<string>`, which means `string[]`, `Set<string>`, ...) 4. Get the new AST nodes, update the `@tailwind utilities` rule's nodes and re-print the AST to CSS. * improvement 1: ignore known invalid candidates This will reduce the amount of candidates to handle. They would eventually be skipped anyway, but now we don't even have to re-parse (and hit a cache) at all. * improvement 2: skip work, when generated AST is the same Currently incremental rebuilds are additive, which means that we are not keeping track if we should remove CSS again in development. We can exploit this information, because now we can quickly check the amoutn of generated AST nodes. - If they are the same then nothing new is generated — this means that we can re-use the previous compiled CSS. We don't even have to re-print the AST because we already did do that work in the past. - If there are more AST nodes, something new is generated — this means that we should update the `@tailwind utilities` rule and re-print the CSS. We can store the result for future incremental rebuilds. * improvement 3: skip work if no new candidates are detected - We already know a set of candidates from previous runs. - We also already know a set of candidates that are invalid and don't produce anything. This means that an incremental rebuild could give us a new set of candidates that either already exist or are invalid. If nothing changes, then we can re-use the compiled CSS. This actually happens more often than you think, and the bigger your project is the better this optimization will be. For example: ``` // Imagine file A exists: <div class="flex items-center justify-center"></div> <button class="text-red-500">Delete</button> ``` ``` // Now you add a second file B: <div class="text-red-500 flex"></div> ``` You just created a brand new file with a bunch of HTML elements and classes, yet all of the candidates in file B already exist in file A, so nothing changes to the actual generated CSS. Now imagine the other hundreds of files that already contain hundreds of classes. The beauty of this optimization is two-fold: - On small projects, compiling is very fast even without this check. This means it is performant. - On bigger projects, we will be able to re-use existing candidates. This means it stays performant. * remove `getAstNodeSize` We can move this up the tree and move it to the `rebuild` function instead. * remove invalid candidate tracking from `DesignSystem` This isn't used anywhere but only in the `rebuild` of the compile step. This allows us to remove it entirely from core logic, and move it up the chain where it is needed. * replace `throwOnInvalidCandidate` with `onInvalidCanidate` This was only needed for working with `@apply`, now this logic _only_ exists in the code path where we are handling `@apply`. * update `compile` API signature * update callsite of `compile()` function * fix typo
704 lines
21 KiB
TypeScript
704 lines
21 KiB
TypeScript
import { decl, rule, type Rule } from './ast'
|
|
import { type Variant } from './candidate'
|
|
import type { Theme } from './theme'
|
|
import { DefaultMap } from './utils/default-map'
|
|
|
|
type VariantFn<T extends Variant['kind']> = (
|
|
rule: Rule,
|
|
variant: Extract<Variant, { kind: T }>,
|
|
) => null | void
|
|
|
|
type CompareFn = (a: Variant, z: Variant) => number
|
|
|
|
export class Variants {
|
|
private compareFns = new Map<number, CompareFn>()
|
|
private variants = new Map<
|
|
string,
|
|
{
|
|
kind: Variant['kind']
|
|
order: number
|
|
applyFn: VariantFn<any>
|
|
compounds: boolean
|
|
}
|
|
>()
|
|
|
|
private completions = new Map<string, () => string[]>()
|
|
|
|
/**
|
|
* Registering a group of variants should result in the same sort number for
|
|
* all the variants. This is to ensure that the variants are applied in the
|
|
* correct order.
|
|
*/
|
|
private groupOrder: null | number = null
|
|
|
|
/**
|
|
* Keep track of the last sort order instead of using the size of the map to
|
|
* avoid unnecessarily skipping order numbers.
|
|
*/
|
|
private lastOrder = 0
|
|
|
|
static(name: string, applyFn: VariantFn<'static'>, { compounds }: { compounds?: boolean } = {}) {
|
|
this.set(name, { kind: 'static', applyFn, compounds: compounds ?? true })
|
|
}
|
|
|
|
functional(
|
|
name: string,
|
|
applyFn: VariantFn<'functional'>,
|
|
{ compounds }: { compounds?: boolean } = {},
|
|
) {
|
|
this.set(name, { kind: 'functional', applyFn, compounds: compounds ?? true })
|
|
}
|
|
|
|
compound(
|
|
name: string,
|
|
applyFn: VariantFn<'compound'>,
|
|
{ compounds }: { compounds?: boolean } = {},
|
|
) {
|
|
this.set(name, { kind: 'compound', applyFn, compounds: compounds ?? true })
|
|
}
|
|
|
|
group(fn: () => void, compareFn?: CompareFn) {
|
|
this.groupOrder = this.nextOrder()
|
|
if (compareFn) this.compareFns.set(this.groupOrder, compareFn)
|
|
fn()
|
|
this.groupOrder = null
|
|
}
|
|
|
|
has(name: string) {
|
|
return this.variants.has(name)
|
|
}
|
|
|
|
get(name: string) {
|
|
return this.variants.get(name)
|
|
}
|
|
|
|
kind(name: string) {
|
|
return this.variants.get(name)?.kind!
|
|
}
|
|
|
|
compounds(name: string) {
|
|
return this.variants.get(name)?.compounds!
|
|
}
|
|
|
|
suggest(name: string, suggestions: () => string[]) {
|
|
this.completions.set(name, suggestions)
|
|
}
|
|
|
|
getCompletions(name: string) {
|
|
return this.completions.get(name)?.() ?? []
|
|
}
|
|
|
|
compare(a: Variant | null, z: Variant | null): number {
|
|
if (a === z) return 0
|
|
if (a === null) return -1
|
|
if (z === null) return 1
|
|
|
|
if (a.kind === 'arbitrary' && z.kind === 'arbitrary') {
|
|
return a.selector < z.selector ? -1 : 1
|
|
} else if (a.kind === 'arbitrary') {
|
|
return 1
|
|
} else if (z.kind === 'arbitrary') {
|
|
return -1
|
|
}
|
|
|
|
let aOrder = this.variants.get(a.root)!.order
|
|
let zOrder = this.variants.get(z.root)!.order
|
|
|
|
let orderedByVariant = aOrder - zOrder
|
|
if (orderedByVariant !== 0) return orderedByVariant
|
|
|
|
if (a.kind === 'compound' && z.kind === 'compound') {
|
|
return this.compare(a.variant, z.variant)
|
|
}
|
|
|
|
let compareFn = this.compareFns.get(aOrder)
|
|
if (compareFn === undefined) return 0
|
|
|
|
return compareFn(a, z) || (a.root < z.root ? -1 : 1)
|
|
}
|
|
|
|
keys() {
|
|
return this.variants.keys()
|
|
}
|
|
|
|
entries() {
|
|
return this.variants.entries()
|
|
}
|
|
|
|
private set<T extends Variant['kind']>(
|
|
name: string,
|
|
{ kind, applyFn, compounds }: { kind: T; applyFn: VariantFn<T>; compounds: boolean },
|
|
) {
|
|
// In test mode, throw an error if we accidentally override another variant
|
|
// by mistake when implementing a new variant that shares the same root
|
|
// without realizing the definitions need to be merged.
|
|
if (process.env.NODE_ENV === 'test') {
|
|
if (this.variants.has(name)) {
|
|
throw new Error(`Duplicate variant prefix [${name}]`)
|
|
}
|
|
}
|
|
|
|
this.lastOrder = this.nextOrder()
|
|
this.variants.set(name, {
|
|
kind,
|
|
applyFn,
|
|
order: this.lastOrder,
|
|
compounds,
|
|
})
|
|
}
|
|
|
|
private nextOrder() {
|
|
return this.groupOrder ?? this.lastOrder + 1
|
|
}
|
|
}
|
|
|
|
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
|
|
// most performant.
|
|
let variants = new Variants()
|
|
|
|
/**
|
|
* Register a static variant like `hover`.
|
|
*/
|
|
function staticVariant(
|
|
name: string,
|
|
selectors: string[],
|
|
{ compounds }: { compounds?: boolean } = {},
|
|
) {
|
|
variants.static(
|
|
name,
|
|
(r) => {
|
|
r.nodes = selectors.map((selector) => rule(selector, r.nodes))
|
|
},
|
|
{ compounds },
|
|
)
|
|
}
|
|
|
|
variants.static('force', () => {}, { compounds: false })
|
|
staticVariant('*', ['& > *'], { compounds: false })
|
|
|
|
variants.compound('not', (ruleNode) => {
|
|
ruleNode.selector = `&:not(${ruleNode.selector.replace('&', '*')})`
|
|
})
|
|
|
|
variants.compound('group', (ruleNode, variant) => {
|
|
// Name the group by appending the modifier to `group` class itself if
|
|
// present.
|
|
let groupSelector = variant.modifier
|
|
? `:where(.group\\/${variant.modifier.value})`
|
|
: ':where(.group)'
|
|
|
|
// 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.
|
|
ruleNode.selector = ruleNode.selector.replace('&', groupSelector)
|
|
|
|
// Use `:where` to make sure the specificity of group variants isn't higher
|
|
// than the specificity of other variants.
|
|
ruleNode.selector = `&:is(${ruleNode.selector} *)`
|
|
})
|
|
|
|
variants.suggest('group', () => {
|
|
return Array.from(variants.keys()).filter((name) => {
|
|
return variants.get(name)?.compounds ?? false
|
|
})
|
|
})
|
|
|
|
variants.compound('peer', (ruleNode, variant) => {
|
|
// Name the peer by appending the modifier to `peer` class itself if
|
|
// present.
|
|
let peerSelector = variant.modifier
|
|
? `:where(.peer\\/${variant.modifier.value})`
|
|
: ':where(.peer)'
|
|
|
|
// 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 `.peer` class the way we'd need to for these variants, so we
|
|
// need to replace it in the selector ourselves.
|
|
ruleNode.selector = ruleNode.selector.replace('&', peerSelector)
|
|
|
|
// Use `:where` to make sure the specificity of peer variants isn't higher
|
|
// than the specificity of other variants.
|
|
ruleNode.selector = `&:is(${ruleNode.selector} ~ *)`
|
|
})
|
|
|
|
variants.suggest('peer', () => {
|
|
return Array.from(variants.keys()).filter((name) => {
|
|
return variants.get(name)?.compounds ?? false
|
|
})
|
|
})
|
|
|
|
staticVariant('first-letter', ['&::first-letter'], { compounds: false })
|
|
staticVariant('first-line', ['&::first-line'], { compounds: false })
|
|
|
|
// TODO: Remove alpha vars or no?
|
|
staticVariant('marker', ['& *::marker', '&::marker'], { compounds: false })
|
|
|
|
staticVariant('selection', ['& *::selection', '&::selection'], { compounds: false })
|
|
staticVariant('file', ['&::file-selector-button'], { compounds: false })
|
|
staticVariant('placeholder', ['&::placeholder'], { compounds: false })
|
|
staticVariant('backdrop', ['&::backdrop'], { compounds: false })
|
|
|
|
{
|
|
function contentProperties() {
|
|
return rule('@at-root', [
|
|
rule('@property --tw-content', [
|
|
decl('syntax', '"*"'),
|
|
decl('initial-value', '""'),
|
|
decl('inherits', 'false'),
|
|
]),
|
|
])
|
|
}
|
|
variants.static(
|
|
'before',
|
|
(v) => {
|
|
v.nodes = [
|
|
rule('&::before', [
|
|
contentProperties(),
|
|
decl('content', 'var(--tw-content)'),
|
|
...v.nodes,
|
|
]),
|
|
]
|
|
},
|
|
{ compounds: false },
|
|
)
|
|
|
|
variants.static(
|
|
'after',
|
|
(v) => {
|
|
v.nodes = [
|
|
rule('&::after', [contentProperties(), decl('content', 'var(--tw-content)'), ...v.nodes]),
|
|
]
|
|
},
|
|
{ compounds: false },
|
|
)
|
|
}
|
|
|
|
let pseudos: [name: string, selector: string][] = [
|
|
// Positional
|
|
['first', '&:first-child'],
|
|
['last', '&:last-child'],
|
|
['only', '&:only-child'],
|
|
['odd', '&:nth-child(odd)'],
|
|
['even', '&:nth-child(even)'],
|
|
['first-of-type', '&:first-of-type'],
|
|
['last-of-type', '&:last-of-type'],
|
|
['only-of-type', '&:only-of-type'],
|
|
|
|
// State
|
|
// TODO: Remove alpha vars or no?
|
|
['visited', '&:visited'],
|
|
|
|
['target', '&:target'],
|
|
['open', '&[open]'],
|
|
|
|
// Forms
|
|
['default', '&:default'],
|
|
['checked', '&:checked'],
|
|
['indeterminate', '&:indeterminate'],
|
|
['placeholder-shown', '&:placeholder-shown'],
|
|
['autofill', '&:autofill'],
|
|
['optional', '&:optional'],
|
|
['required', '&:required'],
|
|
['valid', '&:valid'],
|
|
['invalid', '&:invalid'],
|
|
['in-range', '&:in-range'],
|
|
['out-of-range', '&:out-of-range'],
|
|
['read-only', '&:read-only'],
|
|
|
|
// Content
|
|
['empty', '&:empty'],
|
|
|
|
// Interactive
|
|
['focus-within', '&:focus-within'],
|
|
[
|
|
'hover',
|
|
'&:hover',
|
|
// TODO: Update tests for this:
|
|
// v => {
|
|
// v.nodes = [
|
|
// rule('@media (hover: hover) and (pointer: fine)', [
|
|
// rule('&:hover', v.nodes),
|
|
// ]),
|
|
// ]
|
|
// }
|
|
],
|
|
['focus', '&:focus'],
|
|
['focus-visible', '&:focus-visible'],
|
|
['active', '&:active'],
|
|
['enabled', '&:enabled'],
|
|
['disabled', '&:disabled'],
|
|
]
|
|
|
|
for (let [key, value] of pseudos) {
|
|
staticVariant(key, [value])
|
|
}
|
|
|
|
variants.compound('has', (ruleNode) => {
|
|
ruleNode.selector = `&:has(${ruleNode.selector.replace('&', '*')})`
|
|
})
|
|
|
|
variants.suggest('has', () => {
|
|
return Array.from(variants.keys()).filter((name) => {
|
|
return variants.get(name)?.compounds ?? false
|
|
})
|
|
})
|
|
|
|
variants.functional('aria', (ruleNode, variant) => {
|
|
if (variant.value === null) return null
|
|
if (variant.value.kind === 'arbitrary') {
|
|
ruleNode.nodes = [rule(`&[aria-${variant.value.value}]`, ruleNode.nodes)]
|
|
} else {
|
|
ruleNode.nodes = [rule(`&[aria-${variant.value.value}="true"]`, ruleNode.nodes)]
|
|
}
|
|
})
|
|
|
|
variants.suggest('aria', () => [
|
|
'busy',
|
|
'checked',
|
|
'disabled',
|
|
'expanded',
|
|
'hidden',
|
|
'pressed',
|
|
'readonly',
|
|
'required',
|
|
'selected',
|
|
])
|
|
|
|
variants.functional('data', (ruleNode, variant) => {
|
|
if (variant.value === null) return null
|
|
|
|
ruleNode.nodes = [rule(`&[data-${variant.value.value}]`, ruleNode.nodes)]
|
|
})
|
|
|
|
variants.functional(
|
|
'supports',
|
|
(ruleNode, variant) => {
|
|
if (variant.value === null) return null
|
|
|
|
let value = variant.value.value
|
|
if (value === null) return null
|
|
|
|
// When `supports-[...]:flex` is used, with `not()`, `and()` or
|
|
// `selector()`, then we know that want to use this directly as the
|
|
// supports condition as-is.
|
|
if (/^\w*\s*\(/.test(value)) {
|
|
// Chrome has a bug where `(condition1)or(condition2)` is not valid, but
|
|
// `(condition1) or (condition2)` is supported.
|
|
let query = value.replace(/\b(and|or|not)\b/g, ' $1 ')
|
|
|
|
ruleNode.nodes = [rule(`@supports ${query}`, ruleNode.nodes)]
|
|
return
|
|
}
|
|
|
|
// When `supports-[display]` is used as a shorthand, we need to make sure
|
|
// that this becomes a valid CSS supports condition.
|
|
//
|
|
// E.g.: `supports-[display]` -> `@supports (display: var(--tw))`
|
|
if (!value.includes(':')) {
|
|
value = `${value}: var(--tw)`
|
|
}
|
|
|
|
// When `supports-[display:flex]` is used, we need to make sure that this
|
|
// becomes a valid CSS supports condition by wrapping it in parens.
|
|
//
|
|
// E.g.: `supports-[display:flex]` -> `@supports (display: flex)`
|
|
//
|
|
// We also have to make sure that we wrap the value in parens if the last
|
|
// character is a paren already for situations where we are testing
|
|
// against a CSS custom property.
|
|
//
|
|
// E.g.: `supports-[display]:flex` -> `@supports (display: var(--tw))`
|
|
if (value[0] !== '(' || value[value.length - 1] !== ')') {
|
|
value = `(${value})`
|
|
}
|
|
|
|
ruleNode.nodes = [rule(`@supports ${value}`, ruleNode.nodes)]
|
|
},
|
|
{ compounds: false },
|
|
)
|
|
|
|
staticVariant('motion-safe', ['@media (prefers-reduced-motion: no-preference)'], {
|
|
compounds: false,
|
|
})
|
|
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 })
|
|
|
|
{
|
|
// Helper to compare variants by their resolved values, this is used by the
|
|
// responsive variants (`sm`, `md`, ...), `min-*`, `max-*` and container
|
|
// queries (`@`).
|
|
function compareBreakpoints(
|
|
a: Variant,
|
|
z: Variant,
|
|
direction: 'asc' | 'desc',
|
|
lookup: { get(v: Variant): string | null },
|
|
) {
|
|
if (a === z) return 0
|
|
let aValue = lookup.get(a)
|
|
if (aValue === null) return direction === 'asc' ? -1 : 1
|
|
|
|
let zValue = lookup.get(z)
|
|
if (zValue === null) return direction === 'asc' ? 1 : -1
|
|
|
|
if (aValue === zValue) return 0
|
|
|
|
// Assumption: when a `(` exists, we are dealing with a CSS function.
|
|
//
|
|
// E.g.: `calc(100% - 1rem)`
|
|
let aIsCssFunction = aValue.indexOf('(')
|
|
let zIsCssFunction = zValue.indexOf('(')
|
|
|
|
let aBucket =
|
|
aIsCssFunction === -1
|
|
? // No CSS function found, bucket by unit instead
|
|
aValue.replace(/[\d.]+/g, '')
|
|
: // CSS function found, bucket by function name
|
|
aValue.slice(0, aIsCssFunction)
|
|
|
|
let zBucket =
|
|
zIsCssFunction === -1
|
|
? // No CSS function found, bucket by unit
|
|
zValue.replace(/[\d.]+/g, '')
|
|
: // CSS function found, bucket by function name
|
|
zValue.slice(0, zIsCssFunction)
|
|
|
|
let order =
|
|
// Compare by bucket name
|
|
(aBucket === zBucket ? 0 : aBucket < zBucket ? -1 : 1) ||
|
|
// If bucket names are the same, compare by value
|
|
(direction === 'asc'
|
|
? parseInt(aValue) - parseInt(zValue)
|
|
: parseInt(zValue) - parseInt(aValue))
|
|
|
|
// If the groups are the same, and the contents are not numbers, the
|
|
// `order` will result in `NaN`. In this case, we want to make sorting
|
|
// stable by falling back to a string comparison.
|
|
//
|
|
// This can happen when using CSS functions such as `calc`.
|
|
//
|
|
// E.g.:
|
|
//
|
|
// - `min-[calc(100%-1rem)]` and `min-[calc(100%-2rem)]`
|
|
// - `@[calc(100%-1rem)]` and `@[calc(100%-2rem)]`
|
|
//
|
|
// In this scenario, we want to alphabetically sort `calc(100%-1rem)` and
|
|
// `calc(100%-2rem)` to make it deterministic.
|
|
if (Number.isNaN(order)) {
|
|
return aValue < zValue ? -1 : 1
|
|
}
|
|
|
|
return order
|
|
}
|
|
|
|
// Breakpoints
|
|
{
|
|
let breakpoints = theme.namespace('--breakpoint')
|
|
let resolvedBreakpoints = new DefaultMap((variant: Variant) => {
|
|
switch (variant.kind) {
|
|
case 'static': {
|
|
return breakpoints.get(variant.root) ?? null
|
|
}
|
|
|
|
case 'functional': {
|
|
if (variant.value === null) return null
|
|
|
|
let value: string | null = null
|
|
|
|
if (variant.value.kind === 'arbitrary') {
|
|
value = variant.value.value
|
|
} else if (variant.value.kind === 'named') {
|
|
value = theme.resolve(variant.value.value, ['--breakpoint'])
|
|
}
|
|
|
|
if (!value) return null
|
|
if (value.includes('var(')) return null
|
|
|
|
return value
|
|
}
|
|
case 'arbitrary':
|
|
case 'compound':
|
|
return null
|
|
}
|
|
})
|
|
|
|
// Max
|
|
variants.group(
|
|
() => {
|
|
variants.functional(
|
|
'max',
|
|
(ruleNode, variant) => {
|
|
let value = resolvedBreakpoints.get(variant)
|
|
if (value === null) return null
|
|
|
|
ruleNode.nodes = [rule(`@media (width < ${value})`, ruleNode.nodes)]
|
|
},
|
|
{ compounds: false },
|
|
)
|
|
},
|
|
(a, z) => compareBreakpoints(a, z, 'desc', resolvedBreakpoints),
|
|
)
|
|
|
|
variants.suggest(
|
|
'max',
|
|
() => Array.from(breakpoints.keys()).filter((key) => key !== null) as string[],
|
|
)
|
|
|
|
// Min
|
|
variants.group(
|
|
() => {
|
|
// Registers breakpoint variants like `sm`, `md`, `lg`, etc.
|
|
for (let [key, value] of theme.namespace('--breakpoint')) {
|
|
if (key === null) continue
|
|
variants.static(
|
|
key,
|
|
(ruleNode) => {
|
|
ruleNode.nodes = [rule(`@media (width >= ${value})`, ruleNode.nodes)]
|
|
},
|
|
{ compounds: false },
|
|
)
|
|
}
|
|
|
|
variants.functional(
|
|
'min',
|
|
(ruleNode, variant) => {
|
|
let value = resolvedBreakpoints.get(variant)
|
|
if (value === null) return null
|
|
|
|
ruleNode.nodes = [rule(`@media (width >= ${value})`, ruleNode.nodes)]
|
|
},
|
|
{ compounds: false },
|
|
)
|
|
},
|
|
(a, z) => compareBreakpoints(a, z, 'asc', resolvedBreakpoints),
|
|
)
|
|
|
|
variants.suggest(
|
|
'min',
|
|
() => Array.from(breakpoints.keys()).filter((key) => key !== null) as string[],
|
|
)
|
|
}
|
|
|
|
{
|
|
let widths = theme.namespace('--width')
|
|
|
|
// Container queries
|
|
let resolvedWidths = new DefaultMap((variant: Variant) => {
|
|
switch (variant.kind) {
|
|
case 'functional': {
|
|
if (variant.value === null) return null
|
|
|
|
let value: string | null = null
|
|
|
|
if (variant.value.kind === 'arbitrary') {
|
|
value = variant.value.value
|
|
} else if (variant.value.kind === 'named') {
|
|
value = theme.resolve(variant.value.value, ['--width'])
|
|
}
|
|
|
|
if (!value) return null
|
|
if (value.includes('var(')) return null
|
|
|
|
return value
|
|
}
|
|
case 'static':
|
|
case 'arbitrary':
|
|
case 'compound':
|
|
return null
|
|
}
|
|
})
|
|
|
|
variants.group(
|
|
() => {
|
|
variants.functional(
|
|
'@max',
|
|
(ruleNode, variant) => {
|
|
let value = resolvedWidths.get(variant)
|
|
if (value === null) return null
|
|
|
|
ruleNode.nodes = [
|
|
rule(
|
|
variant.modifier
|
|
? `@container ${variant.modifier.value} (width < ${value})`
|
|
: `@container (width < ${value})`,
|
|
ruleNode.nodes,
|
|
),
|
|
]
|
|
},
|
|
{ compounds: false },
|
|
)
|
|
},
|
|
(a, z) => compareBreakpoints(a, z, 'desc', resolvedWidths),
|
|
)
|
|
|
|
variants.suggest(
|
|
'@max',
|
|
() => Array.from(widths.keys()).filter((key) => key !== null) as string[],
|
|
)
|
|
|
|
variants.group(
|
|
() => {
|
|
variants.functional(
|
|
'@',
|
|
(ruleNode, variant) => {
|
|
let value = resolvedWidths.get(variant)
|
|
if (value === null) return null
|
|
|
|
ruleNode.nodes = [
|
|
rule(
|
|
variant.modifier
|
|
? `@container ${variant.modifier.value} (width >= ${value})`
|
|
: `@container (width >= ${value})`,
|
|
ruleNode.nodes,
|
|
),
|
|
]
|
|
},
|
|
{ compounds: false },
|
|
)
|
|
variants.functional(
|
|
'@min',
|
|
(ruleNode, variant) => {
|
|
let value = resolvedWidths.get(variant)
|
|
if (value === null) return null
|
|
|
|
ruleNode.nodes = [
|
|
rule(
|
|
variant.modifier
|
|
? `@container ${variant.modifier.value} (width >= ${value})`
|
|
: `@container (width >= ${value})`,
|
|
ruleNode.nodes,
|
|
),
|
|
]
|
|
},
|
|
{ compounds: false },
|
|
)
|
|
},
|
|
(a, z) => compareBreakpoints(a, z, 'asc', resolvedWidths),
|
|
)
|
|
|
|
variants.suggest(
|
|
'@min',
|
|
() => Array.from(widths.keys()).filter((key) => key !== null) as string[],
|
|
)
|
|
}
|
|
}
|
|
|
|
staticVariant('portrait', ['@media (orientation: portrait)'], { compounds: false })
|
|
staticVariant('landscape', ['@media (orientation: landscape)'], { compounds: false })
|
|
|
|
staticVariant('ltr', ['&:where([dir="ltr"], [dir="ltr"] *)'])
|
|
staticVariant('rtl', ['&:where([dir="rtl"], [dir="rtl"] *)'])
|
|
|
|
staticVariant('dark', ['@media (prefers-color-scheme: dark)'], { compounds: false })
|
|
|
|
staticVariant('print', ['@media print'], { compounds: false })
|
|
|
|
staticVariant('forced-colors', ['@media (forced-colors: active)'], { compounds: false })
|
|
|
|
return variants
|
|
}
|