mirror of
https://github.com/tailwindlabs/tailwindcss.git
synced 2025-12-08 21:36:08 +00:00
Revert "Temporarily revert changes to `@utility"
This reverts commit 1aab04cebff39a5a1b89ff22c6655cf1f549b7a1.
This commit is contained in:
parent
498b06f2d9
commit
91c0d56d0f
@ -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
|
||||
|
||||
|
||||
@ -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 () => {
|
||||
|
||||
@ -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('')
|
||||
})
|
||||
|
||||
|
||||
@ -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] === '-' &&
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user