Revert "Temporarily revert changes to `@utility"

This reverts commit 1aab04cebff39a5a1b89ff22c6655cf1f549b7a1.
This commit is contained in:
Jordan Pittman 2025-03-20 17:00:23 -04:00
parent 498b06f2d9
commit 91c0d56d0f
4 changed files with 228 additions and 69 deletions

View File

@ -19,6 +19,8 @@ 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))
- Add suggestions when `--spacing(--value(integer, number))` is used ([#17308](https://github.com/tailwindlabs/tailwindcss/pull/17308))
### [4.0.15] - 2025-03-20

View File

@ -476,16 +476,31 @@ test('Custom functional @utility', async () => {
--leading-foo: 1.5;
--leading-bar: 2;
--spacing: 0.25rem;
--spacing-custom: 123px;
}
@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 with-custom-spacing-* {
size: --value(--spacing);
}
@utility with-integer-spacing-* {
size: --spacing(--value(integer));
}
@utility with-number-spacing-* {
size: --spacing(--value(number));
}
@utility -negative-* {
@ -507,12 +522,32 @@ 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')
expect(classNames).not.toContain('-tab-4')
expect(classNames).not.toContain('-tab-github')
expect(classNames).toContain('with-custom-spacing-custom')
expect(classNames).not.toContain('with-custom-spacing-0')
expect(classNames).not.toContain('with-custom-spacing-0.5')
expect(classNames).not.toContain('with-custom-spacing-1')
expect(classNames).not.toContain('with-custom-spacing-1.5')
expect(classNames).not.toContain('with-integer-spacing-custom')
expect(classNames).toContain('with-integer-spacing-0')
expect(classNames).not.toContain('with-integer-spacing-0.5')
expect(classNames).toContain('with-integer-spacing-1')
expect(classNames).not.toContain('with-integer-spacing-1.5')
expect(classNames).not.toContain('with-number-spacing-custom')
expect(classNames).toContain('with-number-spacing-0')
expect(classNames).toContain('with-number-spacing-0.5')
expect(classNames).toContain('with-number-spacing-1')
expect(classNames).toContain('with-number-spacing-1.5')
expect(classNames).toContain('-negative-1')
expect(classNames).toContain('-negative-2')
expect(classNames).toContain('-negative-4')
@ -524,7 +559,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 () => {

View File

@ -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('')
})

View File

@ -29,6 +29,43 @@ import * as ValueParser from './value-parser'
const IS_VALID_STATIC_UTILITY_NAME = /^-?[a-z][a-zA-Z0-9/%._-]*$/
const IS_VALID_FUNCTIONAL_UTILITY_NAME = /^-?[a-z][a-zA-Z0-9/%._-]*-\*$/
const DEFAULT_SPACING_SUGGESTIONS = [
'0',
'0.5',
'1',
'1.5',
'2',
'2.5',
'3',
'3.5',
'4',
'5',
'6',
'7',
'8',
'9',
'10',
'11',
'12',
'14',
'16',
'20',
'24',
'28',
'32',
'36',
'40',
'44',
'48',
'52',
'56',
'60',
'64',
'72',
'80',
'96',
]
type CompileFn<T extends Candidate['kind']> = (
value: Extract<Candidate, { kind: T }>,
) => AstNode[] | undefined | null
@ -476,44 +513,7 @@ export function createUtilities(theme: Theme) {
suggest(name, () => [
{
values: theme.get(['--spacing'])
? [
'0',
'0.5',
'1',
'1.5',
'2',
'2.5',
'3',
'3.5',
'4',
'5',
'6',
'7',
'8',
'9',
'10',
'11',
'12',
'14',
'16',
'20',
'24',
'28',
'32',
'36',
'40',
'44',
'48',
'52',
'56',
'60',
'64',
'72',
'80',
'96',
]
: [],
values: theme.get(['--spacing']) ? DEFAULT_SPACING_SUGGESTIONS : [],
supportsNegative,
supportsFractions,
valueThemeKeys: themeKeys,
@ -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
@ -4730,8 +4731,20 @@ export function createCssUtility(node: AtRule) {
// If you then use `foo-1/2`, this is invalid, because the modifier is not used.
return (designSystem: DesignSystem) => {
let valueThemeKeys = new Set<`--${string}`>()
let modifierThemeKeys = new Set<`--${string}`>()
let storage = {
'--value': {
usedSpacingInteger: false,
usedSpacingNumber: false,
themeKeys: new Set<`--${string}`>(),
literals: new Set<string>(),
},
'--modifier': {
usedSpacingInteger: false,
usedSpacingNumber: false,
themeKeys: new Set<`--${string}`>(),
literals: new Set<string>(),
},
}
// Pre-process the AST to make it easier to work with.
//
@ -4747,17 +4760,52 @@ 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.
ValueParser.walk(declarationValueAst, (fn) => {
if (fn.kind !== 'function') return
// Track usage of `--spacing(…)`
if (
fn.value === '--spacing' &&
// Quick bail check if we already know that `--value` and `--modifier` are
// using the full `--spacing` theme scale.
!(storage['--modifier'].usedSpacingNumber && storage['--value'].usedSpacingNumber)
) {
ValueParser.walk(fn.nodes, (node) => {
if (node.kind !== 'function') return
if (node.value !== '--value' && node.value !== '--modifier') return
const key = node.value
for (let arg of node.nodes) {
if (arg.kind !== 'word') continue
if (arg.value === 'integer') {
storage[key].usedSpacingInteger ||= true
} else if (arg.value === 'number') {
storage[key].usedSpacingNumber ||= true
// Once both `--value` and `--modifier` are using the full
// `number` spacing scale, then there's no need to continue
if (
storage['--modifier'].usedSpacingNumber &&
storage['--value'].usedSpacingNumber
) {
return ValueParser.ValueWalkAction.Stop
}
}
}
})
return ValueParser.ValueWalkAction.Continue
}
if (fn.value !== '--value' && fn.value !== '--modifier') return
let args = segment(ValueParser.toCss(fn.nodes), ',')
@ -4783,16 +4831,22 @@ 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] === '-') {
let value = node.value.replace(/-\*.*$/g, '') as `--${string}`
// 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)
storage[fn.value].literals.add(value)
}
if (fn.value === '--value') {
valueThemeKeys.add(value)
} else if (fn.value === '--modifier') {
modifierThemeKeys.add(value)
}
// Track theme keys
else if (node.kind === 'word' && node.value[0] === '-' && node.value[1] === '-') {
let value = node.value.replace(/-\*.*$/g, '') as `--${string}`
storage[fn.value].themeKeys.add(value)
}
}
})
@ -4929,16 +4983,36 @@ 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: string[] = []
let modifiers: string[] = []
for (let [target, { literals, usedSpacingNumber, usedSpacingInteger, themeKeys }] of [
[values, storage['--value']],
[modifiers, storage['--modifier']],
] as const) {
// Suggest literal values. E.g.: `--value('literal')`
for (let value of literals) {
target.push(value)
}
// Suggest `--spacing(…)` values. E.g.: `--spacing(--value(integer))`
if (usedSpacingNumber) {
target.push(...DEFAULT_SPACING_SUGGESTIONS)
} else if (usedSpacingInteger) {
for (let value of DEFAULT_SPACING_SUGGESTIONS) {
if (isPositiveInteger(value)) {
target.push(value)
}
}
}
// Suggest theme values. E.g.: `--value(--color-*)`
for (let value of designSystem.theme.keysInNamespaces(themeKeys)) {
target.push(value)
}
}
return [{ values, modifiers }] satisfies SuggestionGroup[]
})
}
}
@ -4961,8 +5035,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] === '-' &&