mirror of
https://github.com/tailwindlabs/tailwindcss.git
synced 2025-12-08 21:36:08 +00:00
Add support for matching multiple utility definitions for one candidate (#14231)
Currently if a plugin adds a utility called `duration` it will take
precedence over the built-in utilities — or any utilities with the same
name in previously included plugins. However, in v3, we emitted matches
from _all_ plugins where possible.
Take this plugin for example which adds utilities for
`animation-duration` via the `duration-*` class:
```ts
import plugin from 'tailwindcss/plugin'
export default plugin(
function ({ matchUtilities, theme }) {
matchUtilities(
{ duration: (value) => ({ animationDuration: value }) },
{ values: theme("animationDuration") },
)
},
{
theme: {
extend: {
animationDuration: ({ theme }) => ({
...theme("transitionDuration"),
}),
}
},
}
)
```
Before this PR this plugin's `duration` utility would override the
built-in `duration` utility so you'd get this for a class like
`duration-3500`:
```css
.duration-3000 {
animation-duration: 3500ms;
}
```
Now, after this PR, we'll emit rules for `transition-duration`
(Tailwind's built-in `duration-*` utility) and `animation-duration`
(from the above plugin) and you'll get this instead:
```css
.duration-3000 {
transition-duration: 3500ms;
}
.duration-3000 {
animation-duration: 3500ms;
}
```
These are output as separate rules to ensure that they can all be sorted
appropriately against other utilities.
---------
Co-authored-by: Philipp Spiess <hello@philippspiess.com>
This commit is contained in:
parent
bc88958855
commit
cc228fbfc3
@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Add support for `tailwindcss/colors` and `tailwindcss/defaultTheme` exports for use with plugins ([#14221](https://github.com/tailwindlabs/tailwindcss/pull/14221))
|
||||
- Add support for the `@tailwindcss/typography` and `@tailwindcss/forms` plugins ([#14221](https://github.com/tailwindlabs/tailwindcss/pull/14221))
|
||||
- Add support for the `theme()` function in CSS and class names ([#14177](https://github.com/tailwindlabs/tailwindcss/pull/14177))
|
||||
- Add support for matching multiple utility definitions for one candidate ([#14231](https://github.com/tailwindlabs/tailwindcss/pull/14231))
|
||||
|
||||
### Fixed
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { candidate, css, html, json, test } from '../utils'
|
||||
|
||||
test(
|
||||
'builds the typography plugin utilities',
|
||||
'builds the `@tailwindcss/typography` plugin utilities',
|
||||
{
|
||||
fs: {
|
||||
'package.json': json`
|
||||
@ -40,7 +40,7 @@ test(
|
||||
)
|
||||
|
||||
test(
|
||||
'builds the forms plugin utilities',
|
||||
'builds the `@tailwindcss/forms` plugin utilities',
|
||||
{
|
||||
fs: {
|
||||
'package.json': json`
|
||||
@ -76,3 +76,39 @@ test(
|
||||
])
|
||||
},
|
||||
)
|
||||
|
||||
test(
|
||||
'builds the `tailwindcss-animate` plugin utilities',
|
||||
{
|
||||
fs: {
|
||||
'package.json': json`
|
||||
{
|
||||
"dependencies": {
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"tailwindcss": "workspace:^",
|
||||
"@tailwindcss/cli": "workspace:^"
|
||||
}
|
||||
}
|
||||
`,
|
||||
'index.html': html`
|
||||
<div class="animate-in fade-in zoom-in duration-350"></div>
|
||||
`,
|
||||
'src/index.css': css`
|
||||
@import 'tailwindcss';
|
||||
@plugin 'tailwindcss-animate';
|
||||
`,
|
||||
},
|
||||
},
|
||||
async ({ fs, exec }) => {
|
||||
await exec('pnpm tailwindcss --input src/index.css --output dist/out.css')
|
||||
|
||||
await fs.expectFileToContain('dist/out.css', [
|
||||
candidate`animate-in`,
|
||||
candidate`fade-in`,
|
||||
candidate`zoom-in`,
|
||||
candidate`duration-350`,
|
||||
'transition-duration: 350ms',
|
||||
'animation-duration: 350ms',
|
||||
])
|
||||
},
|
||||
)
|
||||
|
||||
@ -488,7 +488,6 @@ exports[`getClassList 1`] = `
|
||||
"bg-gradient-to-tl",
|
||||
"bg-gradient-to-tr",
|
||||
"bg-inherit",
|
||||
"bg-inherit",
|
||||
"bg-left",
|
||||
"bg-left-bottom",
|
||||
"bg-left-top",
|
||||
@ -517,7 +516,6 @@ exports[`getClassList 1`] = `
|
||||
"bg-space",
|
||||
"bg-top",
|
||||
"bg-transparent",
|
||||
"bg-transparent",
|
||||
"block",
|
||||
"blur-none",
|
||||
"border",
|
||||
|
||||
@ -15,6 +15,6 @@ const designSystem = buildDesignSystem(new Theme())
|
||||
|
||||
bench('parseCandidate', () => {
|
||||
for (let candidate of candidates) {
|
||||
parseCandidate(candidate, designSystem)
|
||||
Array.from(parseCandidate(candidate, designSystem))
|
||||
}
|
||||
})
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -179,6 +179,7 @@ export type Candidate =
|
||||
modifier: ArbitraryModifier | NamedModifier | null
|
||||
variants: Variant[]
|
||||
important: boolean
|
||||
raw: string
|
||||
}
|
||||
|
||||
/**
|
||||
@ -195,6 +196,7 @@ export type Candidate =
|
||||
variants: Variant[]
|
||||
negative: boolean
|
||||
important: boolean
|
||||
raw: string
|
||||
}
|
||||
|
||||
/**
|
||||
@ -214,9 +216,10 @@ export type Candidate =
|
||||
variants: Variant[]
|
||||
negative: boolean
|
||||
important: boolean
|
||||
raw: string
|
||||
}
|
||||
|
||||
export function parseCandidate(input: string, designSystem: DesignSystem): Candidate | null {
|
||||
export function* parseCandidate(input: string, designSystem: DesignSystem): Iterable<Candidate> {
|
||||
// hover:focus:underline
|
||||
// ^^^^^ ^^^^^^ -> Variants
|
||||
// ^^^^^^^^^ -> Base
|
||||
@ -232,7 +235,7 @@ export function parseCandidate(input: string, designSystem: DesignSystem): Candi
|
||||
|
||||
for (let i = rawVariants.length - 1; i >= 0; --i) {
|
||||
let parsedVariant = designSystem.parseVariant(rawVariants[i])
|
||||
if (parsedVariant === null) return null
|
||||
if (parsedVariant === null) return
|
||||
|
||||
parsedCandidateVariants.push(parsedVariant)
|
||||
}
|
||||
@ -263,12 +266,13 @@ export function parseCandidate(input: string, designSystem: DesignSystem): Candi
|
||||
// Check for an exact match of a static utility first as long as it does not
|
||||
// look like an arbitrary value.
|
||||
if (designSystem.utilities.has(base, 'static') && !base.includes('[')) {
|
||||
return {
|
||||
yield {
|
||||
kind: 'static',
|
||||
root: base,
|
||||
variants: parsedCandidateVariants,
|
||||
negative,
|
||||
important,
|
||||
raw: input,
|
||||
}
|
||||
}
|
||||
|
||||
@ -288,12 +292,12 @@ export function parseCandidate(input: string, designSystem: DesignSystem): Candi
|
||||
// E.g.:
|
||||
//
|
||||
// - `bg-red-500/50/50`
|
||||
if (additionalModifier) return null
|
||||
if (additionalModifier) return
|
||||
|
||||
// Arbitrary properties
|
||||
if (baseWithoutModifier[0] === '[') {
|
||||
// Arbitrary properties should end with a `]`.
|
||||
if (baseWithoutModifier[baseWithoutModifier.length - 1] !== ']') return null
|
||||
if (baseWithoutModifier[baseWithoutModifier.length - 1] !== ']') return
|
||||
|
||||
// The property part of the arbitrary property can only start with a-z
|
||||
// lowercase or a dash `-` in case of vendor prefixes such as `-webkit-`
|
||||
@ -302,7 +306,7 @@ export function parseCandidate(input: string, designSystem: DesignSystem): Candi
|
||||
// Otherwise, it is an invalid candidate, and skip continue parsing.
|
||||
let charCode = baseWithoutModifier.charCodeAt(1)
|
||||
if (charCode !== DASH && !(charCode >= LOWER_A && charCode <= LOWER_Z)) {
|
||||
return null
|
||||
return
|
||||
}
|
||||
|
||||
baseWithoutModifier = baseWithoutModifier.slice(1, -1)
|
||||
@ -315,28 +319,27 @@ export function parseCandidate(input: string, designSystem: DesignSystem): Candi
|
||||
// also verify that the colon is not the first or last character in the
|
||||
// candidate, because that would make it invalid as well.
|
||||
let idx = baseWithoutModifier.indexOf(':')
|
||||
if (idx === -1 || idx === 0 || idx === baseWithoutModifier.length - 1) return null
|
||||
if (idx === -1 || idx === 0 || idx === baseWithoutModifier.length - 1) return
|
||||
|
||||
let property = baseWithoutModifier.slice(0, idx)
|
||||
let value = decodeArbitraryValue(baseWithoutModifier.slice(idx + 1))
|
||||
|
||||
return {
|
||||
yield {
|
||||
kind: 'arbitrary',
|
||||
property,
|
||||
value,
|
||||
modifier: modifierSegment === null ? null : parseModifier(modifierSegment),
|
||||
variants: parsedCandidateVariants,
|
||||
important,
|
||||
raw: input,
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// The root of the utility, e.g.: `bg-red-500`
|
||||
// ^^
|
||||
let root: string | null = null
|
||||
|
||||
// The value of the utility, e.g.: `bg-red-500`
|
||||
// ^^^^^^^
|
||||
let value: string | null = null
|
||||
// The different "versions"" of a candidate that are utilities
|
||||
// e.g. `['bg', 'red-500']` and `['bg-red', '500']`
|
||||
let roots: Iterable<Root>
|
||||
|
||||
// If the base of the utility ends with a `]`, then we know it's an arbitrary
|
||||
// value. This also means that everything before the `[…]` part should be the
|
||||
@ -355,111 +358,111 @@ export function parseCandidate(input: string, designSystem: DesignSystem): Candi
|
||||
// ```
|
||||
if (baseWithoutModifier[baseWithoutModifier.length - 1] === ']') {
|
||||
let idx = baseWithoutModifier.indexOf('-[')
|
||||
if (idx === -1) return null
|
||||
if (idx === -1) return
|
||||
|
||||
root = baseWithoutModifier.slice(0, idx)
|
||||
let root = baseWithoutModifier.slice(0, idx)
|
||||
|
||||
// The root of the utility should exist as-is in the utilities map. If not,
|
||||
// it's an invalid utility and we can skip continue parsing.
|
||||
if (!designSystem.utilities.has(root, 'functional')) return null
|
||||
if (!designSystem.utilities.has(root, 'functional')) return
|
||||
|
||||
value = baseWithoutModifier.slice(idx + 1)
|
||||
let value = baseWithoutModifier.slice(idx + 1)
|
||||
|
||||
roots = [[root, value]]
|
||||
}
|
||||
|
||||
// Not an arbitrary value
|
||||
else {
|
||||
;[root, value] = findRoot(baseWithoutModifier, (root: string) => {
|
||||
roots = findRoots(baseWithoutModifier, (root: string) => {
|
||||
return designSystem.utilities.has(root, 'functional')
|
||||
})
|
||||
}
|
||||
|
||||
// If there's no root, the candidate isn't a valid class and can be discarded.
|
||||
if (root === null) return null
|
||||
for (let [root, value] of roots) {
|
||||
let candidate: Candidate = {
|
||||
kind: 'functional',
|
||||
root,
|
||||
modifier: modifierSegment === null ? null : parseModifier(modifierSegment),
|
||||
value: null,
|
||||
variants: parsedCandidateVariants,
|
||||
negative,
|
||||
important,
|
||||
raw: input,
|
||||
}
|
||||
|
||||
// If the leftover value is an empty string, it means that the value is an
|
||||
// invalid named value, e.g.: `bg-`. This makes the candidate invalid and we
|
||||
// can skip any further parsing.
|
||||
if (value === '') return null
|
||||
if (value === null) {
|
||||
yield candidate
|
||||
continue
|
||||
}
|
||||
|
||||
let candidate: Candidate = {
|
||||
kind: 'functional',
|
||||
root,
|
||||
modifier: modifierSegment === null ? null : parseModifier(modifierSegment),
|
||||
value: null,
|
||||
variants: parsedCandidateVariants,
|
||||
negative,
|
||||
important,
|
||||
}
|
||||
{
|
||||
let startArbitraryIdx = value.indexOf('[')
|
||||
let valueIsArbitrary = startArbitraryIdx !== -1
|
||||
|
||||
if (value === null) return candidate
|
||||
if (valueIsArbitrary) {
|
||||
let arbitraryValue = value.slice(startArbitraryIdx + 1, -1)
|
||||
|
||||
{
|
||||
let startArbitraryIdx = value.indexOf('[')
|
||||
let valueIsArbitrary = startArbitraryIdx !== -1
|
||||
// Extract an explicit typehint if present, e.g. `bg-[color:var(--my-var)])`
|
||||
let typehint = ''
|
||||
for (let i = 0; i < arbitraryValue.length; i++) {
|
||||
let code = arbitraryValue.charCodeAt(i)
|
||||
|
||||
if (valueIsArbitrary) {
|
||||
let arbitraryValue = value.slice(startArbitraryIdx + 1, -1)
|
||||
// If we hit a ":", we're at the end of a typehint.
|
||||
if (code === COLON) {
|
||||
typehint = arbitraryValue.slice(0, i)
|
||||
arbitraryValue = arbitraryValue.slice(i + 1)
|
||||
break
|
||||
}
|
||||
|
||||
// Extract an explicit typehint if present, e.g. `bg-[color:var(--my-var)])`
|
||||
let typehint = ''
|
||||
for (let i = 0; i < arbitraryValue.length; i++) {
|
||||
let code = arbitraryValue.charCodeAt(i)
|
||||
// Keep iterating as long as we've only seen valid typehint characters.
|
||||
if (code === DASH || (code >= LOWER_A && code <= LOWER_Z)) {
|
||||
continue
|
||||
}
|
||||
|
||||
// If we hit a ":", we're at the end of a typehint.
|
||||
if (code === COLON) {
|
||||
typehint = arbitraryValue.slice(0, i)
|
||||
arbitraryValue = arbitraryValue.slice(i + 1)
|
||||
// If we see any other character, there's no typehint so break early.
|
||||
break
|
||||
}
|
||||
|
||||
// Keep iterating as long as we've only seen valid typehint characters.
|
||||
if (code === DASH || (code >= LOWER_A && code <= LOWER_Z)) {
|
||||
continue
|
||||
// If an arbitrary value looks like a CSS variable, we automatically wrap
|
||||
// it with `var(...)`.
|
||||
//
|
||||
// But since some CSS properties accept a `<dashed-ident>` as a value
|
||||
// directly (e.g. `scroll-timeline-name`), we also store the original
|
||||
// value in case the utility matcher is interested in it without
|
||||
// `var(...)`.
|
||||
let dashedIdent: string | null = null
|
||||
if (arbitraryValue[0] === '-' && arbitraryValue[1] === '-') {
|
||||
dashedIdent = arbitraryValue
|
||||
arbitraryValue = `var(${arbitraryValue})`
|
||||
} else {
|
||||
arbitraryValue = decodeArbitraryValue(arbitraryValue)
|
||||
}
|
||||
|
||||
// If we see any other character, there's no typehint so break early.
|
||||
break
|
||||
}
|
||||
|
||||
// If an arbitrary value looks like a CSS variable, we automatically wrap
|
||||
// it with `var(...)`.
|
||||
//
|
||||
// But since some CSS properties accept a `<dashed-ident>` as a value
|
||||
// directly (e.g. `scroll-timeline-name`), we also store the original
|
||||
// value in case the utility matcher is interested in it without
|
||||
// `var(...)`.
|
||||
let dashedIdent: string | null = null
|
||||
if (arbitraryValue[0] === '-' && arbitraryValue[1] === '-') {
|
||||
dashedIdent = arbitraryValue
|
||||
arbitraryValue = `var(${arbitraryValue})`
|
||||
candidate.value = {
|
||||
kind: 'arbitrary',
|
||||
dataType: typehint || null,
|
||||
value: arbitraryValue,
|
||||
dashedIdent,
|
||||
}
|
||||
} else {
|
||||
arbitraryValue = decodeArbitraryValue(arbitraryValue)
|
||||
}
|
||||
// Some utilities support fractions as values, e.g. `w-1/2`. Since it's
|
||||
// ambiguous whether the slash signals a modifier or not, we store the
|
||||
// fraction separately in case the utility matcher is interested in it.
|
||||
let fraction =
|
||||
modifierSegment === null || candidate.modifier?.kind === 'arbitrary'
|
||||
? null
|
||||
: `${value}/${modifierSegment}`
|
||||
|
||||
candidate.value = {
|
||||
kind: 'arbitrary',
|
||||
dataType: typehint || null,
|
||||
value: arbitraryValue,
|
||||
dashedIdent,
|
||||
}
|
||||
} else {
|
||||
// Some utilities support fractions as values, e.g. `w-1/2`. Since it's
|
||||
// ambiguous whether the slash signals a modifier or not, we store the
|
||||
// fraction separately in case the utility matcher is interested in it.
|
||||
let fraction =
|
||||
modifierSegment === null || candidate.modifier?.kind === 'arbitrary'
|
||||
? null
|
||||
: `${value.slice(value.lastIndexOf('-') + 1)}/${modifierSegment}`
|
||||
|
||||
candidate.value = {
|
||||
kind: 'named',
|
||||
value,
|
||||
fraction,
|
||||
candidate.value = {
|
||||
kind: 'named',
|
||||
value,
|
||||
fraction,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return candidate
|
||||
yield candidate
|
||||
}
|
||||
}
|
||||
|
||||
function parseModifier(modifier: string): CandidateModifier {
|
||||
@ -548,67 +551,65 @@ export function parseVariant(variant: string, designSystem: DesignSystem): Varia
|
||||
// - `group-hover/foo/bar`
|
||||
if (additionalModifier) return null
|
||||
|
||||
let [root, value] = findRoot(variantWithoutModifier, (root) => {
|
||||
let roots = findRoots(variantWithoutModifier, (root) => {
|
||||
return designSystem.variants.has(root)
|
||||
})
|
||||
|
||||
// Variant is invalid, therefore the candidate is invalid and we can skip
|
||||
// continue parsing it.
|
||||
if (root === null) return null
|
||||
for (let [root, value] of roots) {
|
||||
switch (designSystem.variants.kind(root)) {
|
||||
case 'static': {
|
||||
// Static variants do not have a value
|
||||
if (value !== null) return null
|
||||
|
||||
switch (designSystem.variants.kind(root)) {
|
||||
case 'static': {
|
||||
// Static variants do not have a value
|
||||
if (value !== null) return null
|
||||
// Static variants do not have a modifier
|
||||
if (modifier !== null) return null
|
||||
|
||||
// Static variants do not have a modifier
|
||||
if (modifier !== null) return null
|
||||
|
||||
return {
|
||||
kind: 'static',
|
||||
root,
|
||||
compounds: designSystem.variants.compounds(root),
|
||||
}
|
||||
}
|
||||
|
||||
case 'functional': {
|
||||
if (value === null) return null
|
||||
|
||||
if (value[0] === '[' && value[value.length - 1] === ']') {
|
||||
return {
|
||||
kind: 'functional',
|
||||
kind: 'static',
|
||||
root,
|
||||
modifier: modifier === null ? null : parseModifier(modifier),
|
||||
value: {
|
||||
kind: 'arbitrary',
|
||||
value: decodeArbitraryValue(value.slice(1, -1)),
|
||||
},
|
||||
compounds: designSystem.variants.compounds(root),
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
kind: 'functional',
|
||||
root,
|
||||
modifier: modifier === null ? null : parseModifier(modifier),
|
||||
value: { kind: 'named', value },
|
||||
compounds: designSystem.variants.compounds(root),
|
||||
case 'functional': {
|
||||
if (value === null) return null
|
||||
|
||||
if (value[0] === '[' && value[value.length - 1] === ']') {
|
||||
return {
|
||||
kind: 'functional',
|
||||
root,
|
||||
modifier: modifier === null ? null : parseModifier(modifier),
|
||||
value: {
|
||||
kind: 'arbitrary',
|
||||
value: decodeArbitraryValue(value.slice(1, -1)),
|
||||
},
|
||||
compounds: designSystem.variants.compounds(root),
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
kind: 'functional',
|
||||
root,
|
||||
modifier: modifier === null ? null : parseModifier(modifier),
|
||||
value: { kind: 'named', value },
|
||||
compounds: designSystem.variants.compounds(root),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case 'compound': {
|
||||
if (value === null) return null
|
||||
case 'compound': {
|
||||
if (value === null) return null
|
||||
|
||||
let subVariant = designSystem.parseVariant(value)
|
||||
if (subVariant === null) return null
|
||||
if (subVariant.compounds === false) return null
|
||||
let subVariant = designSystem.parseVariant(value)
|
||||
if (subVariant === null) return null
|
||||
if (subVariant.compounds === false) return null
|
||||
|
||||
return {
|
||||
kind: 'compound',
|
||||
root,
|
||||
modifier: modifier === null ? null : { kind: 'named', value: modifier },
|
||||
variant: subVariant,
|
||||
compounds: designSystem.variants.compounds(root),
|
||||
return {
|
||||
kind: 'compound',
|
||||
root,
|
||||
modifier: modifier === null ? null : { kind: 'named', value: modifier },
|
||||
variant: subVariant,
|
||||
compounds: designSystem.variants.compounds(root),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -617,12 +618,21 @@ export function parseVariant(variant: string, designSystem: DesignSystem): Varia
|
||||
return null
|
||||
}
|
||||
|
||||
function findRoot(
|
||||
input: string,
|
||||
exists: (input: string) => boolean,
|
||||
): [string | null, string | null] {
|
||||
type Root = [
|
||||
// The root of the utility, e.g.: `bg-red-500`
|
||||
// ^^
|
||||
root: string,
|
||||
|
||||
// The value of the utility, e.g.: `bg-red-500`
|
||||
// ^^^^^^^
|
||||
value: string | null,
|
||||
]
|
||||
|
||||
function* findRoots(input: string, exists: (input: string) => boolean): Iterable<Root> {
|
||||
// If there is an exact match, then that's the root.
|
||||
if (exists(input)) return [input, null]
|
||||
if (exists(input)) {
|
||||
yield [input, null]
|
||||
}
|
||||
|
||||
// Otherwise test every permutation of the input by iteratively removing
|
||||
// everything after the last dash.
|
||||
@ -631,10 +641,9 @@ function findRoot(
|
||||
// Variants starting with `@` are special because they don't need a `-`
|
||||
// after the `@` (E.g.: `@-lg` should be written as `@lg`).
|
||||
if (input[0] === '@' && exists('@')) {
|
||||
return ['@', input.slice(1)]
|
||||
yield ['@', input.slice(1)]
|
||||
}
|
||||
|
||||
return [null, null]
|
||||
return
|
||||
}
|
||||
|
||||
// Determine the root and value by testing permutations of the incoming input.
|
||||
@ -648,11 +657,16 @@ function findRoot(
|
||||
let maybeRoot = input.slice(0, idx)
|
||||
|
||||
if (exists(maybeRoot)) {
|
||||
return [maybeRoot, input.slice(idx + 1)]
|
||||
let root: Root = [maybeRoot, input.slice(idx + 1)]
|
||||
|
||||
// If the leftover value is an empty string, it means that the value is an
|
||||
// invalid named value, e.g.: `bg-`. This makes the candidate invalid and we
|
||||
// can skip any further parsing.
|
||||
if (root[1] === '') break
|
||||
|
||||
yield root
|
||||
}
|
||||
|
||||
idx = input.lastIndexOf('-', idx - 1)
|
||||
} while (idx > 0)
|
||||
|
||||
return [null, null]
|
||||
}
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import { WalkAction, decl, rule, walk, type AstNode, type Rule } from './ast'
|
||||
import { decl, rule, walk, WalkAction, type AstNode, type Rule } from './ast'
|
||||
import { type Candidate, type Variant } from './candidate'
|
||||
import { type DesignSystem } from './design-system'
|
||||
import GLOBAL_PROPERTY_ORDER from './property-order'
|
||||
import { asColor } from './utilities'
|
||||
import { asColor, type Utility } from './utilities'
|
||||
import { compare } from './utils/compare'
|
||||
import { escape } from './utils/escape'
|
||||
import type { Variants } from './variants'
|
||||
@ -17,16 +17,17 @@ export function compileCandidates(
|
||||
{ properties: number[]; variants: bigint; candidate: string }
|
||||
>()
|
||||
let astNodes: AstNode[] = []
|
||||
let candidates = new Map<Candidate, string>()
|
||||
let matches = new Map<string, Candidate[]>()
|
||||
|
||||
// Parse candidates and variants
|
||||
for (let rawCandidate of rawCandidates) {
|
||||
let candidate = designSystem.parseCandidate(rawCandidate)
|
||||
if (candidate === null) {
|
||||
let candidates = designSystem.parseCandidate(rawCandidate)
|
||||
if (candidates.length === 0) {
|
||||
onInvalidCandidate?.(rawCandidate)
|
||||
continue // Bail, invalid candidate
|
||||
}
|
||||
candidates.set(candidate, rawCandidate)
|
||||
|
||||
matches.set(rawCandidate, candidates)
|
||||
}
|
||||
|
||||
// Sort the variants
|
||||
@ -35,29 +36,36 @@ export function compileCandidates(
|
||||
})
|
||||
|
||||
// Create the AST
|
||||
next: for (let [candidate, rawCandidate] of candidates) {
|
||||
let astNode = designSystem.compileAstNodes(rawCandidate)
|
||||
if (astNode === null) {
|
||||
for (let [rawCandidate, candidates] of matches) {
|
||||
let found = false
|
||||
|
||||
for (let candidate of candidates) {
|
||||
let rules = designSystem.compileAstNodes(candidate)
|
||||
if (rules.length === 0) continue
|
||||
|
||||
found = true
|
||||
|
||||
for (let { node, propertySort } of rules) {
|
||||
// Track the variant order which is a number with each bit representing a
|
||||
// variant. This allows us to sort the rules based on the order of
|
||||
// variants used.
|
||||
let variantOrder = 0n
|
||||
for (let variant of candidate.variants) {
|
||||
variantOrder |= 1n << BigInt(variants.indexOf(variant))
|
||||
}
|
||||
|
||||
nodeSorting.set(node, {
|
||||
properties: propertySort,
|
||||
variants: variantOrder,
|
||||
candidate: rawCandidate,
|
||||
})
|
||||
astNodes.push(node)
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
onInvalidCandidate?.(rawCandidate)
|
||||
continue next
|
||||
}
|
||||
|
||||
let { node, propertySort } = astNode
|
||||
|
||||
// Track the variant order which is a number with each bit representing a
|
||||
// variant. This allows us to sort the rules based on the order of
|
||||
// variants used.
|
||||
let variantOrder = 0n
|
||||
for (let variant of candidate.variants) {
|
||||
variantOrder |= 1n << BigInt(variants.indexOf(variant))
|
||||
}
|
||||
|
||||
nodeSorting.set(node, {
|
||||
properties: propertySort,
|
||||
variants: variantOrder,
|
||||
candidate: rawCandidate,
|
||||
})
|
||||
astNodes.push(node)
|
||||
}
|
||||
|
||||
astNodes.sort((a, z) => {
|
||||
@ -99,39 +107,46 @@ export function compileCandidates(
|
||||
}
|
||||
}
|
||||
|
||||
export function compileAstNodes(rawCandidate: string, designSystem: DesignSystem) {
|
||||
let candidate = designSystem.parseCandidate(rawCandidate)
|
||||
if (candidate === null) return null
|
||||
export function compileAstNodes(candidate: Candidate, designSystem: DesignSystem) {
|
||||
let asts = compileBaseUtility(candidate, designSystem)
|
||||
if (asts.length === 0) return []
|
||||
|
||||
let nodes = compileBaseUtility(candidate, designSystem)
|
||||
let rules: {
|
||||
node: AstNode
|
||||
propertySort: number[]
|
||||
}[] = []
|
||||
|
||||
if (!nodes) return null
|
||||
let selector = `.${escape(candidate.raw)}`
|
||||
|
||||
let propertySort = getPropertySort(nodes)
|
||||
for (let nodes of asts) {
|
||||
let propertySort = getPropertySort(nodes)
|
||||
|
||||
if (candidate.important) {
|
||||
applyImportant(nodes)
|
||||
if (candidate.important) {
|
||||
applyImportant(nodes)
|
||||
}
|
||||
|
||||
let node: Rule = {
|
||||
kind: 'rule',
|
||||
selector,
|
||||
nodes,
|
||||
}
|
||||
|
||||
for (let variant of candidate.variants) {
|
||||
let result = applyVariant(node, variant, designSystem.variants)
|
||||
|
||||
// When the variant results in `null`, it means that the variant cannot be
|
||||
// applied to the rule. Discard the candidate and continue to the next
|
||||
// one.
|
||||
if (result === null) return []
|
||||
}
|
||||
|
||||
rules.push({
|
||||
node,
|
||||
propertySort,
|
||||
})
|
||||
}
|
||||
|
||||
let node: Rule = {
|
||||
kind: 'rule',
|
||||
selector: `.${escape(rawCandidate)}`,
|
||||
nodes,
|
||||
}
|
||||
|
||||
for (let variant of candidate.variants) {
|
||||
let result = applyVariant(node, variant, designSystem.variants)
|
||||
|
||||
// When the variant results in `null`, it means that the variant cannot be
|
||||
// applied to the rule. Discard the candidate and continue to the next
|
||||
// one.
|
||||
if (result === null) return null
|
||||
}
|
||||
|
||||
return {
|
||||
node,
|
||||
propertySort,
|
||||
}
|
||||
return rules
|
||||
}
|
||||
|
||||
export function applyVariant(
|
||||
@ -208,6 +223,11 @@ export function applyVariant(
|
||||
if (result === null) return null
|
||||
}
|
||||
|
||||
function isFallbackUtility(utility: Utility) {
|
||||
let types = utility.options?.types ?? []
|
||||
return types.length > 1 && types.includes('any')
|
||||
}
|
||||
|
||||
function compileBaseUtility(candidate: Candidate, designSystem: DesignSystem) {
|
||||
if (candidate.kind === 'arbitrary') {
|
||||
let value: string | null = candidate.value
|
||||
@ -218,24 +238,38 @@ function compileBaseUtility(candidate: Candidate, designSystem: DesignSystem) {
|
||||
value = asColor(value, candidate.modifier, designSystem.theme)
|
||||
}
|
||||
|
||||
if (value === null) return
|
||||
if (value === null) return []
|
||||
|
||||
return [decl(candidate.property, value)]
|
||||
return [[decl(candidate.property, value)]]
|
||||
}
|
||||
|
||||
let utilities = designSystem.utilities.get(candidate.root) ?? []
|
||||
|
||||
for (let i = utilities.length - 1; i >= 0; i--) {
|
||||
let utility = utilities[i]
|
||||
let asts: AstNode[][] = []
|
||||
|
||||
if (candidate.kind !== utility.kind) continue
|
||||
let normalUtilities = utilities.filter((u) => !isFallbackUtility(u))
|
||||
for (let utility of normalUtilities) {
|
||||
if (utility.kind !== candidate.kind) continue
|
||||
|
||||
let compiledNodes = utility.compileFn(candidate)
|
||||
if (compiledNodes === null) return null
|
||||
if (compiledNodes) return compiledNodes
|
||||
if (compiledNodes === undefined) continue
|
||||
if (compiledNodes === null) return asts
|
||||
asts.push(compiledNodes)
|
||||
}
|
||||
|
||||
return null
|
||||
if (asts.length > 0) return asts
|
||||
|
||||
let fallbackUtilities = utilities.filter((u) => isFallbackUtility(u))
|
||||
for (let utility of fallbackUtilities) {
|
||||
if (utility.kind !== candidate.kind) continue
|
||||
|
||||
let compiledNodes = utility.compileFn(candidate)
|
||||
if (compiledNodes === undefined) continue
|
||||
if (compiledNodes === null) return asts
|
||||
asts.push(compiledNodes)
|
||||
}
|
||||
|
||||
return asts
|
||||
}
|
||||
|
||||
function applyImportant(ast: AstNode[]): void {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { toCss } from './ast'
|
||||
import { parseCandidate, parseVariant } from './candidate'
|
||||
import { parseCandidate, parseVariant, type Candidate } from './candidate'
|
||||
import { compileAstNodes, compileCandidates } from './compile'
|
||||
import { getClassList, getVariants, type ClassEntry, type VariantEntry } from './intellisense'
|
||||
import { getClassOrder } from './sort'
|
||||
@ -18,9 +18,9 @@ export type DesignSystem = {
|
||||
getClassList(): ClassEntry[]
|
||||
getVariants(): VariantEntry[]
|
||||
|
||||
parseCandidate(candidate: string): ReturnType<typeof parseCandidate>
|
||||
parseCandidate(candidate: string): Candidate[]
|
||||
parseVariant(variant: string): ReturnType<typeof parseVariant>
|
||||
compileAstNodes(candidate: string): ReturnType<typeof compileAstNodes>
|
||||
compileAstNodes(candidate: Candidate): ReturnType<typeof compileAstNodes>
|
||||
|
||||
getUsedVariants(): ReturnType<typeof parseVariant>[]
|
||||
}
|
||||
@ -30,8 +30,12 @@ export function buildDesignSystem(theme: Theme): DesignSystem {
|
||||
let variants = createVariants(theme)
|
||||
|
||||
let parsedVariants = new DefaultMap((variant) => parseVariant(variant, designSystem))
|
||||
let parsedCandidates = new DefaultMap((candidate) => parseCandidate(candidate, designSystem))
|
||||
let compiledAstNodes = new DefaultMap((candidate) => compileAstNodes(candidate, designSystem))
|
||||
let parsedCandidates = new DefaultMap((candidate) =>
|
||||
Array.from(parseCandidate(candidate, designSystem)),
|
||||
)
|
||||
let compiledAstNodes = new DefaultMap<Candidate>((candidate) =>
|
||||
compileAstNodes(candidate, designSystem),
|
||||
)
|
||||
|
||||
let designSystem: DesignSystem = {
|
||||
theme,
|
||||
@ -69,7 +73,7 @@ export function buildDesignSystem(theme: Theme): DesignSystem {
|
||||
parseVariant(variant: string) {
|
||||
return parsedVariants.get(variant)
|
||||
},
|
||||
compileAstNodes(candidate: string) {
|
||||
compileAstNodes(candidate: Candidate) {
|
||||
return compiledAstNodes.get(candidate)
|
||||
},
|
||||
getUsedVariants() {
|
||||
|
||||
@ -814,6 +814,58 @@ describe('theme', async () => {
|
||||
expect(fn).toHaveBeenCalledWith('magenta') // Not present in CSS or resolved config
|
||||
expect(fn).toHaveBeenCalledWith({}) // Present in the resolved config
|
||||
})
|
||||
|
||||
test('Candidates can match multiple utility definitions', async ({ expect }) => {
|
||||
let input = css`
|
||||
@tailwind utilities;
|
||||
@plugin "my-plugin";
|
||||
`
|
||||
|
||||
let { build } = await compile(input, {
|
||||
loadPlugin: async () => {
|
||||
return plugin(({ addUtilities, matchUtilities }) => {
|
||||
addUtilities({
|
||||
'.foo-bar': {
|
||||
color: 'red',
|
||||
},
|
||||
})
|
||||
|
||||
matchUtilities(
|
||||
{
|
||||
foo: (value) => ({
|
||||
'--my-prop': value,
|
||||
}),
|
||||
},
|
||||
{
|
||||
values: {
|
||||
bar: 'bar-valuer',
|
||||
baz: 'bar-valuer',
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
addUtilities({
|
||||
'.foo-bar': {
|
||||
backgroundColor: 'red',
|
||||
},
|
||||
})
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
expect(build(['foo-bar'])).toMatchInlineSnapshot(`
|
||||
".foo-bar {
|
||||
background-color: red;
|
||||
}
|
||||
.foo-bar {
|
||||
color: red;
|
||||
}
|
||||
.foo-bar {
|
||||
--my-prop: bar-valuer;
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
})
|
||||
|
||||
describe('addUtilities()', () => {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { substituteAtApply } from './apply'
|
||||
import { decl, rule, type AstNode } from './ast'
|
||||
import type { NamedUtilityValue } from './candidate'
|
||||
import type { Candidate, NamedUtilityValue } from './candidate'
|
||||
import { createCompatConfig } from './compat/config/create-compat-config'
|
||||
import { resolveConfig } from './compat/config/resolve-config'
|
||||
import type { UserConfig } from './compat/config/types'
|
||||
@ -157,7 +157,7 @@ function buildPluginApi(
|
||||
)
|
||||
}
|
||||
|
||||
designSystem.utilities.functional(name, (candidate) => {
|
||||
function compileFn(candidate: Extract<Candidate, { kind: 'functional' }>) {
|
||||
// A negative utility was provided but is unsupported
|
||||
if (!options?.supportsNegativeValues && candidate.negative) return
|
||||
|
||||
@ -256,6 +256,10 @@ function buildPluginApi(
|
||||
let ast = objectToAst(fn(value, { modifier }))
|
||||
substituteAtApply(ast, designSystem)
|
||||
return ast
|
||||
}
|
||||
|
||||
designSystem.utilities.functional(name, compileFn, {
|
||||
types,
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
@ -8109,13 +8109,13 @@ test('rounded-s', async () => {
|
||||
}
|
||||
|
||||
.rounded-s-full {
|
||||
border-start-start-radius: 3.40282e38px;
|
||||
border-end-start-radius: 3.40282e38px;
|
||||
border-start-start-radius: var(--radius-full, 9999px);
|
||||
border-end-start-radius: var(--radius-full, 9999px);
|
||||
}
|
||||
|
||||
.rounded-s-none {
|
||||
border-start-start-radius: 0;
|
||||
border-end-start-radius: 0;
|
||||
border-start-start-radius: var(--radius-none, 0px);
|
||||
border-end-start-radius: var(--radius-none, 0px);
|
||||
}
|
||||
|
||||
.rounded-s-sm {
|
||||
@ -8172,13 +8172,13 @@ test('rounded-e', async () => {
|
||||
}
|
||||
|
||||
.rounded-e-full {
|
||||
border-start-end-radius: 3.40282e38px;
|
||||
border-end-end-radius: 3.40282e38px;
|
||||
border-start-end-radius: var(--radius-full, 9999px);
|
||||
border-end-end-radius: var(--radius-full, 9999px);
|
||||
}
|
||||
|
||||
.rounded-e-none {
|
||||
border-start-end-radius: 0;
|
||||
border-end-end-radius: 0;
|
||||
border-start-end-radius: var(--radius-none, 0px);
|
||||
border-end-end-radius: var(--radius-none, 0px);
|
||||
}
|
||||
|
||||
.rounded-e-sm {
|
||||
@ -8237,11 +8237,15 @@ test('rounded-t', async () => {
|
||||
.rounded-t-full {
|
||||
border-top-left-radius: 3.40282e38px;
|
||||
border-top-right-radius: 3.40282e38px;
|
||||
border-top-left-radius: var(--radius-full, 9999px);
|
||||
border-top-right-radius: var(--radius-full, 9999px);
|
||||
}
|
||||
|
||||
.rounded-t-none {
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
border-top-left-radius: var(--radius-none, 0px);
|
||||
border-top-right-radius: var(--radius-none, 0px);
|
||||
}
|
||||
|
||||
.rounded-t-sm {
|
||||
@ -8300,11 +8304,15 @@ test('rounded-r', async () => {
|
||||
.rounded-r-full {
|
||||
border-top-right-radius: 3.40282e38px;
|
||||
border-bottom-right-radius: 3.40282e38px;
|
||||
border-top-right-radius: var(--radius-full, 9999px);
|
||||
border-bottom-right-radius: var(--radius-full, 9999px);
|
||||
}
|
||||
|
||||
.rounded-r-none {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
border-top-right-radius: var(--radius-none, 0px);
|
||||
border-bottom-right-radius: var(--radius-none, 0px);
|
||||
}
|
||||
|
||||
.rounded-r-sm {
|
||||
@ -8363,11 +8371,15 @@ test('rounded-b', async () => {
|
||||
.rounded-b-full {
|
||||
border-bottom-right-radius: 3.40282e38px;
|
||||
border-bottom-left-radius: 3.40282e38px;
|
||||
border-bottom-right-radius: var(--radius-full, 9999px);
|
||||
border-bottom-left-radius: var(--radius-full, 9999px);
|
||||
}
|
||||
|
||||
.rounded-b-none {
|
||||
border-bottom-right-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: var(--radius-none, 0px);
|
||||
border-bottom-left-radius: var(--radius-none, 0px);
|
||||
}
|
||||
|
||||
.rounded-b-sm {
|
||||
@ -8426,11 +8438,15 @@ test('rounded-l', async () => {
|
||||
.rounded-l-full {
|
||||
border-top-left-radius: 3.40282e38px;
|
||||
border-bottom-left-radius: 3.40282e38px;
|
||||
border-top-left-radius: var(--radius-full, 9999px);
|
||||
border-bottom-left-radius: var(--radius-full, 9999px);
|
||||
}
|
||||
|
||||
.rounded-l-none {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
border-top-left-radius: var(--radius-none, 0px);
|
||||
border-bottom-left-radius: var(--radius-none, 0px);
|
||||
}
|
||||
|
||||
.rounded-l-sm {
|
||||
@ -8485,11 +8501,11 @@ test('rounded-ss', async () => {
|
||||
}
|
||||
|
||||
.rounded-ss-full {
|
||||
border-start-start-radius: 3.40282e38px;
|
||||
border-start-start-radius: var(--radius-full, 9999px);
|
||||
}
|
||||
|
||||
.rounded-ss-none {
|
||||
border-start-start-radius: 0;
|
||||
border-start-start-radius: var(--radius-none, 0px);
|
||||
}
|
||||
|
||||
.rounded-ss-sm {
|
||||
@ -8543,11 +8559,11 @@ test('rounded-se', async () => {
|
||||
}
|
||||
|
||||
.rounded-se-full {
|
||||
border-start-end-radius: 3.40282e38px;
|
||||
border-start-end-radius: var(--radius-full, 9999px);
|
||||
}
|
||||
|
||||
.rounded-se-none {
|
||||
border-start-end-radius: 0;
|
||||
border-start-end-radius: var(--radius-none, 0px);
|
||||
}
|
||||
|
||||
.rounded-se-sm {
|
||||
@ -8601,11 +8617,11 @@ test('rounded-ee', async () => {
|
||||
}
|
||||
|
||||
.rounded-ee-full {
|
||||
border-end-end-radius: 3.40282e38px;
|
||||
border-end-end-radius: var(--radius-full, 9999px);
|
||||
}
|
||||
|
||||
.rounded-ee-none {
|
||||
border-end-end-radius: 0;
|
||||
border-end-end-radius: var(--radius-none, 0px);
|
||||
}
|
||||
|
||||
.rounded-ee-sm {
|
||||
@ -8659,11 +8675,11 @@ test('rounded-es', async () => {
|
||||
}
|
||||
|
||||
.rounded-es-full {
|
||||
border-end-start-radius: 3.40282e38px;
|
||||
border-end-start-radius: var(--radius-full, 9999px);
|
||||
}
|
||||
|
||||
.rounded-es-none {
|
||||
border-end-start-radius: 0;
|
||||
border-end-start-radius: var(--radius-none, 0px);
|
||||
}
|
||||
|
||||
.rounded-es-sm {
|
||||
@ -8718,10 +8734,12 @@ test('rounded-tl', async () => {
|
||||
|
||||
.rounded-tl-full {
|
||||
border-top-left-radius: 3.40282e38px;
|
||||
border-top-left-radius: var(--radius-full, 9999px);
|
||||
}
|
||||
|
||||
.rounded-tl-none {
|
||||
border-top-left-radius: 0;
|
||||
border-top-left-radius: var(--radius-none, 0px);
|
||||
}
|
||||
|
||||
.rounded-tl-sm {
|
||||
@ -8776,10 +8794,12 @@ test('rounded-tr', async () => {
|
||||
|
||||
.rounded-tr-full {
|
||||
border-top-right-radius: 3.40282e38px;
|
||||
border-top-right-radius: var(--radius-full, 9999px);
|
||||
}
|
||||
|
||||
.rounded-tr-none {
|
||||
border-top-right-radius: 0;
|
||||
border-top-right-radius: var(--radius-none, 0px);
|
||||
}
|
||||
|
||||
.rounded-tr-sm {
|
||||
@ -8834,10 +8854,12 @@ test('rounded-br', async () => {
|
||||
|
||||
.rounded-br-full {
|
||||
border-bottom-right-radius: 3.40282e38px;
|
||||
border-bottom-right-radius: var(--radius-full, 9999px);
|
||||
}
|
||||
|
||||
.rounded-br-none {
|
||||
border-bottom-right-radius: 0;
|
||||
border-bottom-right-radius: var(--radius-none, 0px);
|
||||
}
|
||||
|
||||
.rounded-br-sm {
|
||||
@ -8892,10 +8914,12 @@ test('rounded-bl', async () => {
|
||||
|
||||
.rounded-bl-full {
|
||||
border-bottom-left-radius: 3.40282e38px;
|
||||
border-bottom-left-radius: var(--radius-full, 9999px);
|
||||
}
|
||||
|
||||
.rounded-bl-none {
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-left-radius: var(--radius-none, 0px);
|
||||
}
|
||||
|
||||
.rounded-bl-sm {
|
||||
@ -12688,6 +12712,9 @@ test('transition', async () => {
|
||||
transition-property: opacity;
|
||||
transition-duration: .1s;
|
||||
transition-timing-function: ease;
|
||||
transition-property: var(--transition-property-opacity, opacity);
|
||||
transition-duration: .1s;
|
||||
transition-timing-function: ease;
|
||||
}
|
||||
|
||||
.transition-shadow {
|
||||
@ -14129,6 +14156,14 @@ test('inset-shadow', async () => {
|
||||
--inset-shadow-sm: inset 0 1px 1px #0000000d;
|
||||
}
|
||||
|
||||
.inset-shadow {
|
||||
inset: var(--inset-shadow, inset 0 2px 4px #0000000d);
|
||||
}
|
||||
|
||||
.inset-shadow-sm {
|
||||
inset: var(--inset-shadow-sm, inset 0 1px 1px #0000000d);
|
||||
}
|
||||
|
||||
.inset-shadow {
|
||||
--tw-inset-shadow: inset 0 2px 4px #0000000d;
|
||||
--tw-inset-shadow-colored: inset 0 2px 4px var(--tw-inset-shadow-color);
|
||||
@ -15096,7 +15131,7 @@ describe('custom utilities', () => {
|
||||
`)
|
||||
})
|
||||
|
||||
test('The later version of a static utility is used', async () => {
|
||||
test('Multiple static utilities are merged', async () => {
|
||||
let { build } = await compile(css`
|
||||
@layer utilities {
|
||||
@tailwind utilities;
|
||||
@ -15116,6 +15151,7 @@ describe('custom utilities', () => {
|
||||
expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(`
|
||||
"@layer utilities {
|
||||
.really-round {
|
||||
--custom-prop: hi;
|
||||
border-radius: 30rem;
|
||||
}
|
||||
}"
|
||||
@ -15159,8 +15195,8 @@ describe('custom utilities', () => {
|
||||
}
|
||||
|
||||
@utility text-sm {
|
||||
font-size: var(--font-size-sm, 0.875rem);
|
||||
line-height: var(--font-size-sm--line-height, 1.25rem);
|
||||
font-size: var(--font-size-sm, 0.8755rem);
|
||||
line-height: var(--font-size-sm--line-height, 1.255rem);
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
`)
|
||||
@ -15171,6 +15207,8 @@ describe('custom utilities', () => {
|
||||
.text-sm {
|
||||
font-size: var(--font-size-sm, .875rem);
|
||||
line-height: var(--font-size-sm--line-height, 1.25rem);
|
||||
font-size: var(--font-size-sm, .8755rem);
|
||||
line-height: var(--font-size-sm--line-height, 1.255rem);
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
}"
|
||||
|
||||
@ -27,14 +27,18 @@ type SuggestionDefinition =
|
||||
hasDefaultValue?: boolean
|
||||
}
|
||||
|
||||
export type UtilityOptions = {
|
||||
types: string[]
|
||||
}
|
||||
|
||||
export type Utility = {
|
||||
kind: 'static' | 'functional'
|
||||
compileFn: CompileFn<any>
|
||||
options?: UtilityOptions
|
||||
}
|
||||
|
||||
export class Utilities {
|
||||
private utilities = new DefaultMap<
|
||||
string,
|
||||
{
|
||||
kind: 'static' | 'functional'
|
||||
compileFn: CompileFn<any>
|
||||
}[]
|
||||
>(() => [])
|
||||
private utilities = new DefaultMap<string, Utility[]>(() => [])
|
||||
|
||||
private completions = new Map<string, () => SuggestionGroup[]>()
|
||||
|
||||
@ -42,8 +46,8 @@ export class Utilities {
|
||||
this.utilities.get(name).push({ kind: 'static', compileFn })
|
||||
}
|
||||
|
||||
functional(name: string, compileFn: CompileFn<'functional'>) {
|
||||
this.utilities.get(name).push({ kind: 'functional', compileFn })
|
||||
functional(name: string, compileFn: CompileFn<'functional'>, options?: UtilityOptions) {
|
||||
this.utilities.get(name).push({ kind: 'functional', compileFn, options })
|
||||
}
|
||||
|
||||
has(name: string, kind: 'static' | 'functional') {
|
||||
@ -2441,9 +2445,6 @@ export function createUtilities(theme: Theme) {
|
||||
}
|
||||
}
|
||||
|
||||
staticUtility('bg-inherit', [['background-color', 'inherit']])
|
||||
staticUtility('bg-transparent', [['background-color', 'transparent']])
|
||||
|
||||
staticUtility('bg-auto', [['background-size', 'auto']])
|
||||
staticUtility('bg-cover', [['background-size', 'cover']])
|
||||
staticUtility('bg-contain', [['background-size', 'contain']])
|
||||
|
||||
1
playgrounds/vite/src/animate.js
Normal file
1
playgrounds/vite/src/animate.js
Normal file
@ -0,0 +1 @@
|
||||
module.exports = require('tailwindcss-animate')
|
||||
6
pnpm-lock.yaml
generated
6
pnpm-lock.yaml
generated
@ -4077,7 +4077,7 @@ snapshots:
|
||||
eslint: 8.57.0
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0)
|
||||
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0)
|
||||
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0)
|
||||
eslint-plugin-jsx-a11y: 6.9.0(eslint@8.57.0)
|
||||
eslint-plugin-react: 7.35.0(eslint@8.57.0)
|
||||
eslint-plugin-react-hooks: 4.6.2(eslint@8.57.0)
|
||||
@ -4101,7 +4101,7 @@ snapshots:
|
||||
enhanced-resolve: 5.17.1
|
||||
eslint: 8.57.0
|
||||
eslint-module-utils: 2.8.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0)
|
||||
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0)
|
||||
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0)
|
||||
fast-glob: 3.3.2
|
||||
get-tsconfig: 4.7.6
|
||||
is-core-module: 2.15.0
|
||||
@ -4123,7 +4123,7 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0):
|
||||
eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0):
|
||||
dependencies:
|
||||
array-includes: 3.1.8
|
||||
array.prototype.findlastindex: 1.2.5
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user