diff --git a/CHANGELOG.md b/CHANGELOG.md index bc308f6c655c..e1fbf317762e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - _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 diff --git a/packages/tailwindcss/src/intellisense.test.ts b/packages/tailwindcss/src/intellisense.test.ts index 1166b1a73621..24e418be3fa9 100644 --- a/packages/tailwindcss/src/intellisense.test.ts +++ b/packages/tailwindcss/src/intellisense.test.ts @@ -476,6 +476,9 @@ test('Custom functional @utility', async () => { --leading-foo: 1.5; --leading-bar: 2; + + --spacing: 0.25rem; + --spacing-custom: 123px; } @utility tab-* { @@ -488,6 +491,18 @@ test('Custom functional @utility', async () => { 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-* { margin: --value(--tab-size- *); } @@ -515,6 +530,24 @@ test('Custom functional @utility', async () => { 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') diff --git a/packages/tailwindcss/src/utilities.ts b/packages/tailwindcss/src/utilities.ts index 71c53de85912..84b5d61c6c98 100644 --- a/packages/tailwindcss/src/utilities.ts +++ b/packages/tailwindcss/src/utilities.ts @@ -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 = ( value: Extract, ) => 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, @@ -4731,11 +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 valueLiterals = new Set() - - let modifierThemeKeys = new Set<`--${string}`>() - let modifierLiterals = new Set() + let storage = { + '--value': { + usedSpacingInteger: false, + usedSpacingNumber: false, + themeKeys: new Set<`--${string}`>(), + literals: new Set(), + }, + '--modifier': { + usedSpacingInteger: false, + usedSpacingNumber: false, + themeKeys: new Set<`--${string}`>(), + literals: new Set(), + }, + } // Pre-process the AST to make it easier to work with. // @@ -4762,6 +4771,41 @@ export function createCssUtility(node: AtRule) { // `\\*` 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), ',') @@ -4796,23 +4840,13 @@ export function createCssUtility(node: AtRule) { 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) - } + storage[fn.value].literals.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') { - valueThemeKeys.add(value) - } else if (fn.value === '--modifier') { - modifierThemeKeys.add(value) - } + storage[fn.value].themeKeys.add(value) } } }) @@ -4949,20 +4983,33 @@ export function createCssUtility(node: AtRule) { }) designSystem.utilities.suggest(name.slice(0, -2), () => { - let values = [] - for (let value of valueLiterals) { - values.push(value) - } - for (let value of designSystem.theme.keysInNamespaces(valueThemeKeys)) { - values.push(value) - } + 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) + } - let modifiers = [] - for (let modifier of modifierLiterals) { - modifiers.push(modifier) - } - for (let value of designSystem.theme.keysInNamespaces(modifierThemeKeys)) { - modifiers.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[]