Resolve values in functional utilities based on @theme options (#15623)

This PR fixes an issue where functional utilities configured via CSS
don't resolve the values correctly.

We always fully resolved the values as-if a `@theme inline` was used.

We used to compile the following code:
```css
@theme {
  --tab-size-github: 8;
}

@utility tab-* {
  tab-size: --value(--tab-size);
}
```

Into:
```css
:root {
  --tab-size-github: 8;
}

.tab-github {
  tab-size: 8;
}
```

But it should be referencing the variable instead:
```css
:root {
  --tab-size-github: 8;
}

.tab-github {
  tab-size: var(--tab-size-github);
}
```

However, if you used `@theme inline reference`, it should inline the
value:
```css
@theme inline reference {
  --tab-size-github: 8;
}

@utility tab-* {
  tab-size: --value(--tab-size);
}
```

This will now correctly compile to:
```css
.tab-github {
  tab-size: 8;
}
```
This commit is contained in:
Robin Malfait 2025-01-14 12:35:04 +01:00 committed by GitHub
parent d2fbdf505f
commit da2da51284
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 227 additions and 55 deletions

View File

@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Add missing `main` and `browser` fields for `@tailwindcss/browser` ([#15594](https://github.com/tailwindlabs/tailwindcss/pull/15594))
- Ensure namespace reset with escaped `*` (e.g.: `--color-\*: initial;`) ([#15603](https://github.com/tailwindlabs/tailwindcss/pull/15603))
- Resolve values in functional utilities based on `@theme` options ([#15623](https://github.com/tailwindlabs/tailwindcss/pull/15623))
- _Upgrade (experimental)_: Pretty print `--spacing(…)` to prevent ambiguity ([#15596](https://github.com/tailwindlabs/tailwindcss/pull/15596))
## [4.0.0-beta.9] - 2025-01-09

View File

@ -17452,22 +17452,22 @@ describe('custom utilities', () => {
expect(await compileCss(input, ['tab-1', 'tab-2', 'tab-4', 'tab-github']))
.toMatchInlineSnapshot(`
".tab-1 {
tab-size: 1;
}
".tab-1 {
tab-size: var(--tab-size-1);
}
.tab-2 {
tab-size: 2;
}
.tab-2 {
tab-size: var(--tab-size-2);
}
.tab-4 {
tab-size: 4;
}
.tab-4 {
tab-size: var(--tab-size-4);
}
.tab-github {
tab-size: 8;
}"
`)
.tab-github {
tab-size: var(--tab-size-github);
}"
`)
expect(await compileCss(input, ['tab-3', 'tab-gitlab'])).toEqual('')
})
@ -17494,19 +17494,19 @@ describe('custom utilities', () => {
expect(await compileCss(input, ['tab-1', 'tab-2', 'tab-4', 'tab-github']))
.toMatchInlineSnapshot(`
".tab-1 {
tab-size: 1;
tab-size: var(--tab-size-1);
}
.tab-2 {
tab-size: 2;
tab-size: var(--tab-size-2);
}
.tab-4 {
tab-size: 4;
tab-size: var(--tab-size-4);
}
.tab-github {
tab-size: 8;
tab-size: var(--tab-size-github);
}"
`)
expect(await compileCss(input, ['tab-3', 'tab-gitlab'])).toEqual('')
@ -17531,19 +17531,19 @@ describe('custom utilities', () => {
expect(await compileCss(input, ['tab-1', 'tab-2', 'tab-4', 'tab-github']))
.toMatchInlineSnapshot(`
".tab-1 {
tab-size: 1;
tab-size: var(--tab-size-1);
}
.tab-2 {
tab-size: 2;
tab-size: var(--tab-size-2);
}
.tab-4 {
tab-size: 4;
tab-size: var(--tab-size-4);
}
.tab-github {
tab-size: 8;
tab-size: var(--tab-size-github);
}"
`)
expect(await compileCss(input, ['tab-3', 'tab-gitlab'])).toEqual('')
@ -17817,7 +17817,7 @@ describe('custom utilities', () => {
}
.tab-github {
tab-size: 8;
tab-size: var(--tab-size-github);
}"
`)
expect(await compileCss(input, ['tab-[#0088cc]', 'tab-[1px]'])).toEqual('')
@ -17849,7 +17849,7 @@ describe('custom utilities', () => {
}
.example-full {
--value: 100%;
--value: var(--example-full);
}"
`)
expect(await compileCss(input, ['example-half', 'example-[#0088cc]'])).toEqual('')
@ -17893,7 +17893,7 @@ describe('custom utilities', () => {
}
.example-full {
--value: 100%;
--value: var(--example-full);
}
.tab-76 {
@ -17905,7 +17905,7 @@ describe('custom utilities', () => {
}
.tab-github {
tab-size: 8;
tab-size: var(--tab-size-github);
}"
`)
expect(
@ -17949,7 +17949,7 @@ describe('custom utilities', () => {
}
.-example-full {
--value: calc(100% * -1);
--value: calc(var(--example-full) * -1);
}
.example-\\[10px\\] {
@ -17961,7 +17961,7 @@ describe('custom utilities', () => {
}
.example-full {
--value: 100%;
--value: var(--example-full);
}"
`)
expect(await compileCss(input, ['example-10'])).toEqual('')
@ -18062,13 +18062,13 @@ describe('custom utilities', () => {
}
.example-sm {
--value: 14px;
--value: var(--value-sm);
}
.example-sm\\/7 {
--value: 14px;
--modifier: 28px;
--modifier-with-calc: calc(28px * 2);
--value: var(--value-sm);
--modifier: var(--modifier-7);
--modifier-with-calc: calc(var(--modifier-7) * 2);
}"
`)
expect(
@ -18091,18 +18091,18 @@ describe('custom utilities', () => {
expect(await compileCss(input, ['example-video', 'example-1/1', 'example-[7/9]']))
.toMatchInlineSnapshot(`
".example-1\\/1 {
--value: 1 / 1;
}
".example-1\\/1 {
--value: 1 / 1;
}
.example-\\[7\\/9\\] {
--value: 7 / 9;
}
.example-\\[7\\/9\\] {
--value: 7 / 9;
}
.example-video {
--value: 16 / 9;
}"
`)
.example-video {
--value: var(--example-video);
}"
`)
expect(await compileCss(input, ['example-foo'])).toEqual('')
})
@ -18124,12 +18124,13 @@ describe('custom utilities', () => {
expect(await compileCss(input, ['example-xs', 'example-xs/6'])).toMatchInlineSnapshot(`
".example-xs {
font-size: .75rem;
line-height: 1.33333;
font-size: var(--text-xs);
line-height: var(--text-xs--line-height);
}
.example-xs\\/6 {
font-size: .75rem;
font-size: var(--text-xs);
line-height: var(--text-xs--line-height);
line-height: 6;
}"
`)
@ -18154,12 +18155,13 @@ describe('custom utilities', () => {
expect(await compileCss(input, ['example-xs', 'example-xs/6'])).toMatchInlineSnapshot(`
".example-xs {
font-size: .75rem;
line-height: 1.33333;
font-size: var(--text-xs);
line-height: var(--text-xs--line-height);
}
.example-xs\\/6 {
font-size: .75rem;
font-size: var(--text-xs);
line-height: var(--text-xs--line-height);
line-height: 6;
}"
`)
@ -18184,12 +18186,13 @@ describe('custom utilities', () => {
expect(await compileCss(input, ['example-xs', 'example-xs/6'])).toMatchInlineSnapshot(`
".example-xs {
font-size: .75rem;
line-height: 1.33333;
font-size: var(--text-xs);
line-height: var(--text-xs--line-height);
}
.example-xs\\/6 {
font-size: .75rem;
font-size: var(--text-xs);
line-height: var(--text-xs--line-height);
line-height: 6;
}"
`)
@ -18214,16 +18217,157 @@ describe('custom utilities', () => {
expect(await compileCss(input, ['example-xs', 'example-xs/6'])).toMatchInlineSnapshot(`
".example-xs {
font-size: .75rem;
line-height: 1.33333;
font-size: var(--text-xs);
line-height: var(--text-xs--line-height);
}
.example-xs\\/6 {
font-size: .75rem;
font-size: var(--text-xs);
line-height: var(--text-xs--line-height);
line-height: 6;
}"
`)
expect(await compileCss(input, ['example-foo', 'example-xs/foo'])).toEqual('')
})
})
test('resolve value based on `@theme`', async () => {
let input = css`
@theme {
--tab-size-github: 8;
}
@utility tab-* {
tab-size: --value(--tab-size);
}
@tailwind utilities;
`
expect(await compileCss(input, ['tab-github'])).toMatchInlineSnapshot(`
":root {
--tab-size-github: 8;
}
.tab-github {
tab-size: var(--tab-size-github);
}"
`)
})
test('resolve value based on `@theme reference`', async () => {
let input = css`
@theme reference {
--tab-size-github: 8;
}
@utility tab-* {
tab-size: --value(--tab-size);
}
@tailwind utilities;
`
expect(await compileCss(input, ['tab-github'])).toMatchInlineSnapshot(`
".tab-github {
tab-size: var(--tab-size-github);
}"
`)
})
test('resolve value based on `@theme inline`', async () => {
let input = css`
@theme inline {
--tab-size-github: 8;
}
@utility tab-* {
tab-size: --value(--tab-size);
}
@tailwind utilities;
`
expect(await compileCss(input, ['tab-github'])).toMatchInlineSnapshot(`
":root {
--tab-size-github: 8;
}
.tab-github {
tab-size: 8;
}"
`)
})
test('resolve value based on `@theme inline reference`', async () => {
let input = css`
@theme inline reference {
--tab-size-github: 8;
}
@utility tab-* {
tab-size: --value(--tab-size);
}
@tailwind utilities;
`
expect(await compileCss(input, ['tab-github'])).toMatchInlineSnapshot(`
".tab-github {
tab-size: 8;
}"
`)
})
test('sub namespaces can live in different @theme blocks (1)', async () => {
let input = `
@theme reference {
--text-xs: 0.75rem;
}
@theme inline reference {
--text-xs--line-height: calc(1 / 0.75);
}
@utility example-* {
font-size: --value(--text);
line-height: --value(--text-*--line-height);
}
@tailwind utilities;
`
expect(await compileCss(input, ['example-xs'])).toMatchInlineSnapshot(`
".example-xs {
font-size: var(--text-xs);
line-height: 1.33333;
}"
`)
})
test('sub namespaces can live in different @theme blocks (2)', async () => {
let input = `
@theme inline reference {
--text-xs: 0.75rem;
}
@theme reference {
--text-xs--line-height: calc(1 / 0.75);
}
@utility example-* {
font-size: --value(--text);
line-height: --value(--text-*--line-height);
}
@tailwind utilities;
`
expect(await compileCss(input, ['example-xs'])).toMatchInlineSnapshot(`
".example-xs {
font-size: .75rem;
line-height: var(--text-xs--line-height);
}"
`)
})
})

View File

@ -4854,8 +4854,35 @@ function resolveValueFunction(
arg.value[0] === '-' &&
arg.value[1] === '-'
) {
let resolved = designSystem.resolveThemeValue(arg.value.replace('*', value.value))
if (resolved) return { nodes: ValueParser.parse(resolved) }
let themeKey = arg.value as `--${string}`
// Resolve the theme value, e.g.: `--value(--color)`
if (themeKey.endsWith('-*')) {
// Without `-*` postfix
themeKey = themeKey.slice(0, -2) as `--${string}`
let resolved = designSystem.theme.resolve(value.value, [themeKey])
if (resolved) return { nodes: ValueParser.parse(resolved) }
}
// Split `--text-*--line-height` into `--text` and `--line-height`
else {
let nestedKeys = themeKey.split('-*') as `--${string}`[]
if (nestedKeys.length <= 1) continue
// Resolve theme values with nested keys, e.g.: `--value(--text-*--line-height)`
let themeKeys = [nestedKeys.shift()!]
let resolved = designSystem.theme.resolveWith(value.value, themeKeys, nestedKeys)
if (resolved) {
let [, options = {}] = resolved
// Resolve the value from the `options`
{
let resolved = options[nestedKeys.pop()!]
if (resolved) return { nodes: ValueParser.parse(resolved) }
}
}
}
}
// Bare value, e.g.: `--value(integer)`