mirror of
https://github.com/tailwindlabs/tailwindcss.git
synced 2025-12-08 21:36:08 +00:00
Add support for literal values in --value('…') and --modifier('…') (#17304)
This PR adds support for literal values inside the `--value('…')` and
`--modifier('…')` functions. This allows you to safelist some known
values you want to use:
E.g.:
```css
@utility tab-* {
tab-size: --value('revert', 'initial');
}
```
This allows you to use `tab-revert` and `tab-initial` for example.
This commit is contained in:
parent
4c57d9f734
commit
a3316f2ef4
@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- _Experimental_: Add `user-valid` and `user-invalid` variants ([#12370](https://github.com/tailwindlabs/tailwindcss/pull/12370))
|
||||
- _Experimental_: Add `wrap-anywhere`, `wrap-break-word`, and `wrap-normal` utilities ([#12128](https://github.com/tailwindlabs/tailwindcss/pull/12128))
|
||||
- _Experimental_: Add `@source inline(…)` ([#17147](https://github.com/tailwindlabs/tailwindcss/pull/17147))
|
||||
- Add support for literal values in `--value('…')` and `--modifier('…')` ([#17304](https://github.com/tailwindlabs/tailwindcss/pull/17304))
|
||||
|
||||
### [4.0.15] - 2025-03-20
|
||||
|
||||
|
||||
@ -479,13 +479,13 @@ test('Custom functional @utility', async () => {
|
||||
}
|
||||
|
||||
@utility tab-* {
|
||||
tab-size: --value(--tab-size);
|
||||
tab-size: --value(--tab-size, 'revert', 'initial');
|
||||
}
|
||||
|
||||
@utility example-* {
|
||||
font-size: --value(--text);
|
||||
line-height: --value(--text- * --line-height);
|
||||
line-height: --modifier(--leading);
|
||||
line-height: --modifier(--leading, 'normal');
|
||||
}
|
||||
|
||||
@utility -negative-* {
|
||||
@ -507,6 +507,8 @@ test('Custom functional @utility', async () => {
|
||||
expect(classNames).toContain('tab-2')
|
||||
expect(classNames).toContain('tab-4')
|
||||
expect(classNames).toContain('tab-github')
|
||||
expect(classNames).toContain('tab-revert')
|
||||
expect(classNames).toContain('tab-initial')
|
||||
|
||||
expect(classNames).not.toContain('-tab-1')
|
||||
expect(classNames).not.toContain('-tab-2')
|
||||
@ -524,7 +526,7 @@ test('Custom functional @utility', async () => {
|
||||
expect(classNames).not.toContain('--negative-github')
|
||||
|
||||
expect(classNames).toContain('example-xs')
|
||||
expect(classMap.get('example-xs')?.modifiers).toEqual(['foo', 'bar'])
|
||||
expect(classMap.get('example-xs')?.modifiers).toEqual(['normal', 'foo', 'bar'])
|
||||
})
|
||||
|
||||
test('Theme keys with underscores are suggested with underscores', async () => {
|
||||
|
||||
@ -17256,6 +17256,23 @@ describe('custom utilities', () => {
|
||||
expect(await compileCss(input, ['tab-foo'])).toEqual('')
|
||||
})
|
||||
|
||||
test('resolve literal values', async () => {
|
||||
let input = css`
|
||||
@utility tab-* {
|
||||
tab-size: --value('revert');
|
||||
}
|
||||
|
||||
@tailwind utilities;
|
||||
`
|
||||
|
||||
expect(await compileCss(input, ['tab-revert'])).toMatchInlineSnapshot(`
|
||||
".tab-revert {
|
||||
tab-size: revert;
|
||||
}"
|
||||
`)
|
||||
expect(await compileCss(input, ['tab-initial'])).toEqual('')
|
||||
})
|
||||
|
||||
test('resolving bare values with constraints for integer, percentage, and ratio', async () => {
|
||||
let input = css`
|
||||
@utility example-* {
|
||||
@ -17720,6 +17737,7 @@ describe('custom utilities', () => {
|
||||
--value: --value(--value, [length]);
|
||||
--modifier: --modifier(--modifier, [length]);
|
||||
--modifier-with-calc: calc(--modifier(--modifier, [length]) * 2);
|
||||
--modifier-literals: --modifier('literal', 'literal-2');
|
||||
}
|
||||
|
||||
@tailwind utilities;
|
||||
@ -17731,6 +17749,8 @@ describe('custom utilities', () => {
|
||||
'example-sm/7',
|
||||
'example-[12px]',
|
||||
'example-[12px]/[16px]',
|
||||
'example-sm/literal',
|
||||
'example-sm/literal-2',
|
||||
]),
|
||||
).toMatchInlineSnapshot(`
|
||||
".example-\\[12px\\]\\/\\[16px\\] {
|
||||
@ -17745,6 +17765,16 @@ describe('custom utilities', () => {
|
||||
--modifier-with-calc: calc(var(--modifier-7, 28px) * 2);
|
||||
}
|
||||
|
||||
.example-sm\\/literal {
|
||||
--value: var(--value-sm, 14px);
|
||||
--modifier-literals: literal;
|
||||
}
|
||||
|
||||
.example-sm\\/literal-2 {
|
||||
--value: var(--value-sm, 14px);
|
||||
--modifier-literals: literal-2;
|
||||
}
|
||||
|
||||
.example-\\[12px\\] {
|
||||
--value: 12px;
|
||||
}
|
||||
@ -17754,7 +17784,12 @@ describe('custom utilities', () => {
|
||||
}"
|
||||
`)
|
||||
expect(
|
||||
await compileCss(input, ['example-foo', 'example-foo/[12px]', 'example-foo/12']),
|
||||
await compileCss(input, [
|
||||
'example-foo',
|
||||
'example-foo/[12px]',
|
||||
'example-foo/12',
|
||||
'example-sm/unknown-literal',
|
||||
]),
|
||||
).toEqual('')
|
||||
})
|
||||
|
||||
|
||||
@ -4706,6 +4706,7 @@ export function createCssUtility(node: AtRule) {
|
||||
if (IS_VALID_FUNCTIONAL_UTILITY_NAME.test(name)) {
|
||||
// API:
|
||||
//
|
||||
// - `--value('literal')` resolves a literal named value
|
||||
// - `--value(number)` resolves a bare value of type number
|
||||
// - `--value([number])` resolves an arbitrary value of type number
|
||||
// - `--value(--color)` resolves a theme value in the `color` namespace
|
||||
@ -4731,7 +4732,10 @@ export function createCssUtility(node: AtRule) {
|
||||
|
||||
return (designSystem: DesignSystem) => {
|
||||
let valueThemeKeys = new Set<`--${string}`>()
|
||||
let valueLiterals = new Set<string>()
|
||||
|
||||
let modifierThemeKeys = new Set<`--${string}`>()
|
||||
let modifierLiterals = new Set<string>()
|
||||
|
||||
// Pre-process the AST to make it easier to work with.
|
||||
//
|
||||
@ -4747,12 +4751,12 @@ export function createCssUtility(node: AtRule) {
|
||||
|
||||
// Required manipulations:
|
||||
//
|
||||
// - `--value(--spacing)` -> `--value(--spacing-*)`
|
||||
// - `--value(--spacing- *)` -> `--value(--spacing-*)`
|
||||
// - `--value(--text- * --line-height)` -> `--value(--text-*--line-height)`
|
||||
// - `--value(--text --line-height)` -> `--value(--text-*--line-height)`
|
||||
// - `--value(--text-\\* --line-height)` -> `--value(--text-*--line-height)`
|
||||
// - `--value([ *])` -> `--value([*])`
|
||||
// - `--value(--spacing)` -> `--value(--spacing-*)`
|
||||
// - `--value(--spacing- *)` -> `--value(--spacing-*)`
|
||||
// - `--value(--text- * --line-height)` -> `--value(--text-*--line-height)`
|
||||
// - `--value(--text --line-height)` -> `--value(--text-*--line-height)`
|
||||
// - `--value(--text-\\* --line-height)` -> `--value(--text-*--line-height)`
|
||||
// - `--value([ *])` -> `--value([*])`
|
||||
//
|
||||
// Once Prettier / Biome handle these better (e.g.: not crashing without
|
||||
// `\\*` or not inserting whitespace) then most of these can go away.
|
||||
@ -4783,9 +4787,25 @@ export function createCssUtility(node: AtRule) {
|
||||
}
|
||||
fn.nodes = ValueParser.parse(args.join(','))
|
||||
|
||||
// Track the theme keys for suggestions
|
||||
// Track information for suggestions
|
||||
for (let node of fn.nodes) {
|
||||
if (node.kind === 'word' && node.value[0] === '-' && node.value[1] === '-') {
|
||||
// Track literal values
|
||||
if (
|
||||
node.kind === 'word' &&
|
||||
(node.value[0] === '"' || node.value[0] === "'") &&
|
||||
node.value[0] === node.value[node.value.length - 1]
|
||||
) {
|
||||
let value = node.value.slice(1, -1)
|
||||
|
||||
if (fn.value === '--value') {
|
||||
valueLiterals.add(value)
|
||||
} else if (fn.value === '--modifier') {
|
||||
modifierLiterals.add(value)
|
||||
}
|
||||
}
|
||||
|
||||
// Track theme keys
|
||||
else if (node.kind === 'word' && node.value[0] === '-' && node.value[1] === '-') {
|
||||
let value = node.value.replace(/-\*.*$/g, '') as `--${string}`
|
||||
|
||||
if (fn.value === '--value') {
|
||||
@ -4929,16 +4949,23 @@ export function createCssUtility(node: AtRule) {
|
||||
})
|
||||
|
||||
designSystem.utilities.suggest(name.slice(0, -2), () => {
|
||||
return [
|
||||
{
|
||||
values: designSystem.theme
|
||||
.keysInNamespaces(valueThemeKeys)
|
||||
.map((x) => x.replaceAll('_', '.')),
|
||||
modifiers: designSystem.theme
|
||||
.keysInNamespaces(modifierThemeKeys)
|
||||
.map((x) => x.replaceAll('_', '.')),
|
||||
},
|
||||
] satisfies SuggestionGroup[]
|
||||
let values = []
|
||||
for (let value of valueLiterals) {
|
||||
values.push(value)
|
||||
}
|
||||
for (let value of designSystem.theme.keysInNamespaces(valueThemeKeys)) {
|
||||
values.push(value)
|
||||
}
|
||||
|
||||
let modifiers = []
|
||||
for (let modifier of modifierLiterals) {
|
||||
modifiers.push(modifier)
|
||||
}
|
||||
for (let value of designSystem.theme.keysInNamespaces(modifierThemeKeys)) {
|
||||
modifiers.push(value)
|
||||
}
|
||||
|
||||
return [{ values, modifiers }] satisfies SuggestionGroup[]
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -4961,8 +4988,21 @@ function resolveValueFunction(
|
||||
designSystem: DesignSystem,
|
||||
): { nodes: ValueParser.ValueAstNode[]; ratio?: boolean } | undefined {
|
||||
for (let arg of fn.nodes) {
|
||||
// Resolving theme value, e.g.: `--value(--color)`
|
||||
// Resolve literal value, e.g.: `--modifier('closest-side')`
|
||||
if (
|
||||
value.kind === 'named' &&
|
||||
arg.kind === 'word' &&
|
||||
// Should be wreapped in quotes
|
||||
(arg.value[0] === "'" || arg.value[0] === '"') &&
|
||||
arg.value[arg.value.length - 1] === arg.value[0] &&
|
||||
// Values should match
|
||||
arg.value.slice(1, -1) === value.value
|
||||
) {
|
||||
return { nodes: ValueParser.parse(value.value) }
|
||||
}
|
||||
|
||||
// Resolving theme value, e.g.: `--value(--color)`
|
||||
else if (
|
||||
value.kind === 'named' &&
|
||||
arg.kind === 'word' &&
|
||||
arg.value[0] === '-' &&
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user