diff --git a/CHANGELOG.md b/CHANGELOG.md index 68891f774323..3154c5fe3860 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Expose timing information in debug mode ([#14553](https://github.com/tailwindlabs/tailwindcss/pull/14553)) - Add support for `blocklist` in config files ([#14556](https://github.com/tailwindlabs/tailwindcss/pull/14556)) - Add `color-scheme` utilities ([#14567](https://github.com/tailwindlabs/tailwindcss/pull/14567)) +- Add support wrapping utilities in a selector ([#14448](https://github.com/tailwindlabs/tailwindcss/pull/14448)) +- Add support marking all utilities as `!important` ([#14448](https://github.com/tailwindlabs/tailwindcss/pull/14448)) - _Experimental_: Migrate `@import "tailwindcss/tailwind.css"` to `@import "tailwindcss"` ([#14514](https://github.com/tailwindlabs/tailwindcss/pull/14514)) - _Experimental_: Migrate `@apply` utilities with the template codemods ([#14574](https://github.com/tailwindlabs/tailwindcss/pull/14574)) - _Experimental_: Add template codemods for migrating variant order ([#14524](https://github.com/tailwindlabs/tailwindcss/pull/14524])) diff --git a/packages/tailwindcss/src/compat/apply-compat-hooks.ts b/packages/tailwindcss/src/compat/apply-compat-hooks.ts index 915cfea427db..f702e5202292 100644 --- a/packages/tailwindcss/src/compat/apply-compat-hooks.ts +++ b/packages/tailwindcss/src/compat/apply-compat-hooks.ts @@ -1,4 +1,4 @@ -import { toCss, walk, type AstNode } from '../ast' +import { rule, toCss, walk, WalkAction, type AstNode } from '../ast' import type { DesignSystem } from '../design-system' import type { Theme, ThemeKey } from '../theme' import { withAlpha } from '../utilities' @@ -229,6 +229,29 @@ export async function applyCompatibilityHooks({ designSystem.theme.prefix = resolvedConfig.prefix } + // If an important strategy has already been set in CSS don't override it + if (!designSystem.important && resolvedConfig.important === true) { + designSystem.important = true + } + + if (typeof resolvedConfig.important === 'string') { + let wrappingSelector = resolvedConfig.important + + walk(ast, (node, { replaceWith, parent }) => { + if (node.kind !== 'rule') return + if (node.selector !== '@tailwind utilities') return + + // The AST node was already manually wrapped so there's nothing to do + if (parent?.kind === 'rule' && parent.selector === wrappingSelector) { + return WalkAction.Stop + } + + replaceWith(rule(wrappingSelector, [node])) + + return WalkAction.Stop + }) + } + for (let candidate of resolvedConfig.blocklist) { designSystem.invalidCandidates.add(candidate) } diff --git a/packages/tailwindcss/src/compat/config.test.ts b/packages/tailwindcss/src/compat/config.test.ts index bb25a02317d5..8660808c12c6 100644 --- a/packages/tailwindcss/src/compat/config.test.ts +++ b/packages/tailwindcss/src/compat/config.test.ts @@ -1372,6 +1372,78 @@ test('a prefix must be letters only', async () => { ) }) +test('important: `#app`', async () => { + let input = css` + @tailwind utilities; + @config "./config.js"; + + @utility custom { + color: red; + } + ` + + let compiler = await compile(input, { + loadModule: async (_, base) => ({ + base, + module: { important: '#app' }, + }), + }) + + expect(compiler.build(['underline', 'hover:line-through', 'custom'])).toMatchInlineSnapshot(` + "#app { + .custom { + color: red; + } + .underline { + text-decoration-line: underline; + } + .hover\\:line-through { + &:hover { + @media (hover: hover) { + text-decoration-line: line-through; + } + } + } + } + " + `) +}) + +test('important: true', async () => { + let input = css` + @tailwind utilities; + @config "./config.js"; + + @utility custom { + color: red; + } + ` + + let compiler = await compile(input, { + loadModule: async (_, base) => ({ + base, + module: { important: true }, + }), + }) + + expect(compiler.build(['underline', 'hover:line-through', 'custom'])).toMatchInlineSnapshot(` + ".custom { + color: red!important; + } + .underline { + text-decoration-line: underline!important; + } + .hover\\:line-through { + &:hover { + @media (hover: hover) { + text-decoration-line: line-through!important; + } + } + } + " + `) +}) + test('blocklisted canddiates are not generated', async () => { let compiler = await compile( css` diff --git a/packages/tailwindcss/src/compat/config/resolve-config.ts b/packages/tailwindcss/src/compat/config/resolve-config.ts index acf89c3cf024..81aa4dacada8 100644 --- a/packages/tailwindcss/src/compat/config/resolve-config.ts +++ b/packages/tailwindcss/src/compat/config/resolve-config.ts @@ -29,6 +29,7 @@ interface ResolutionContext { let minimal: ResolvedConfig = { blocklist: [], prefix: '', + important: false, darkMode: null, theme: {}, plugins: [], @@ -69,6 +70,10 @@ export function resolveConfig(design: DesignSystem, files: ConfigFile[]): Resolv if ('blocklist' in config && config.blocklist !== undefined) { ctx.result.blocklist = config.blocklist ?? [] } + + if ('important' in config && config.important !== undefined) { + ctx.result.important = config.important ?? false + } } // Merge themes diff --git a/packages/tailwindcss/src/compat/config/types.ts b/packages/tailwindcss/src/compat/config/types.ts index 289e1bae0cfd..313a7dba8b9b 100644 --- a/packages/tailwindcss/src/compat/config/types.ts +++ b/packages/tailwindcss/src/compat/config/types.ts @@ -87,3 +87,12 @@ export interface UserConfig { export interface ResolvedConfig { blocklist: string[] } + +// `important` support +export interface UserConfig { + important?: boolean | string +} + +export interface ResolvedConfig { + important: boolean | string +} diff --git a/packages/tailwindcss/src/compile.ts b/packages/tailwindcss/src/compile.ts index c5de56c4c64a..f7d9c5125fa8 100644 --- a/packages/tailwindcss/src/compile.ts +++ b/packages/tailwindcss/src/compile.ts @@ -140,7 +140,7 @@ export function compileAstNodes(candidate: Candidate, designSystem: DesignSystem for (let nodes of asts) { let propertySort = getPropertySort(nodes) - if (candidate.important) { + if (candidate.important || designSystem.important) { applyImportant(nodes) } diff --git a/packages/tailwindcss/src/design-system.ts b/packages/tailwindcss/src/design-system.ts index e2b07a24b117..90dbbdba7108 100644 --- a/packages/tailwindcss/src/design-system.ts +++ b/packages/tailwindcss/src/design-system.ts @@ -15,6 +15,9 @@ export type DesignSystem = { invalidCandidates: Set + // Whether to mark utility declarations as !important + important: boolean + getClassOrder(classes: string[]): [string, bigint | null][] getClassList(): ClassEntry[] getVariants(): VariantEntry[] @@ -48,6 +51,7 @@ export function buildDesignSystem(theme: Theme): DesignSystem { variants, invalidCandidates: new Set(), + important: false, candidatesToCss(classes: string[]) { let result: (string | null)[] = [] diff --git a/packages/tailwindcss/src/important.test.ts b/packages/tailwindcss/src/important.test.ts new file mode 100644 index 000000000000..fe7debf20f8e --- /dev/null +++ b/packages/tailwindcss/src/important.test.ts @@ -0,0 +1,89 @@ +import { expect, test } from 'vitest' +import { compile } from '.' + +const css = String.raw + +test('Utilities can be wrapped in a selector', async () => { + // This is the v4 equivalent of `important: "#app"` from v3 + let input = css` + #app { + @tailwind utilities; + } + ` + + let compiler = await compile(input) + + expect(compiler.build(['underline', 'hover:line-through'])).toMatchInlineSnapshot(` + "#app { + .underline { + text-decoration-line: underline; + } + .hover\\:line-through { + &:hover { + @media (hover: hover) { + text-decoration-line: line-through; + } + } + } + } + " + `) +}) + +test('Utilities can be marked with important', async () => { + // This is the v4 equivalent of `important: true` from v3 + let input = css` + @import 'tailwindcss/utilities' important; + ` + + let compiler = await compile(input, { + loadStylesheet: async (id: string, base: string) => ({ + base, + content: '@tailwind utilities;', + }), + }) + + expect(compiler.build(['underline', 'hover:line-through'])).toMatchInlineSnapshot(` + ".underline { + text-decoration-line: underline!important; + } + .hover\\:line-through { + &:hover { + @media (hover: hover) { + text-decoration-line: line-through!important; + } + } + } + " + `) +}) + +test('Utilities can be wrapped with a selector and marked as important', async () => { + // This does not have a direct equivalent in v3 but works as a consequence of + // the new APIs + let input = css` + @media important { + #app { + @tailwind utilities; + } + } + ` + + let compiler = await compile(input) + + expect(compiler.build(['underline', 'hover:line-through'])).toMatchInlineSnapshot(` + "#app { + .underline { + text-decoration-line: underline!important; + } + .hover\\:line-through { + &:hover { + @media (hover: hover) { + text-decoration-line: line-through!important; + } + } + } + } + " + `) +}) diff --git a/packages/tailwindcss/src/index.ts b/packages/tailwindcss/src/index.ts index 9f8968f269e2..2d247b3aa5b8 100644 --- a/packages/tailwindcss/src/index.ts +++ b/packages/tailwindcss/src/index.ts @@ -76,6 +76,7 @@ async function parseCss( await substituteAtImports(ast, base, loadStylesheet) // Find all `@theme` declarations + let important: boolean | null = null let theme = new Theme() let customVariants: ((designSystem: DesignSystem) => void)[] = [] let customUtilities: ((designSystem: DesignSystem) => void)[] = [] @@ -236,6 +237,35 @@ async function parseCss( return WalkAction.Skip } + if (node.selector.startsWith('@media')) { + let features = segment(node.selector.slice(6), ' ') + let shouldReplace = true + + for (let i = 0; i < features.length; i++) { + let part = features[i] + + // Drop instances of `@media important` + // + // We support `@import "tailwindcss" important` to mark all declarations + // in generated utilities as `!important`. + if (part === 'important') { + important = true + shouldReplace = true + features[i] = '' + } + } + + let remaining = features.filter(Boolean).join(' ') + + node.selector = `@media ${remaining}` + + if (remaining.trim() === '' && shouldReplace) { + replaceWith(node.nodes) + } + + return WalkAction.Skip + } + if (node.selector !== '@theme' && !node.selector.startsWith('@theme ')) return let [themeOptions, themePrefix] = parseThemeOptions(node.selector) @@ -288,6 +318,10 @@ async function parseCss( let designSystem = buildDesignSystem(theme) + if (important) { + designSystem.important = important + } + // Apply hooks from backwards compatibility layer. This function takes a lot // of random arguments because it really just needs access to "the world" to // do whatever ungodly things it needs to do to make things backwards diff --git a/packages/tailwindcss/src/intellisense.test.ts b/packages/tailwindcss/src/intellisense.test.ts index f6a906c28e96..c61d2295119a 100644 --- a/packages/tailwindcss/src/intellisense.test.ts +++ b/packages/tailwindcss/src/intellisense.test.ts @@ -1,7 +1,10 @@ import { expect, test } from 'vitest' +import { __unstable__loadDesignSystem } from '.' import { buildDesignSystem } from './design-system' import { Theme } from './theme' +const css = String.raw + function loadDesignSystem() { let theme = new Theme() theme.add('--spacing-0_5', '0.125rem') @@ -104,3 +107,70 @@ test('Can produce CSS per candidate using `candidatesToCss`', () => { ] `) }) + +test('Utilities do not show wrapping selector in intellisense', async () => { + let input = css` + @import 'tailwindcss/utilities'; + @config './config.js'; + ` + + let design = await __unstable__loadDesignSystem(input, { + loadStylesheet: async (_, base) => ({ + base, + content: '@tailwind utilities;', + }), + loadModule: async () => ({ + base: '', + module: { + important: '#app', + }, + }), + }) + + expect(design.candidatesToCss(['underline', 'hover:line-through'])).toMatchInlineSnapshot(` + [ + ".underline { + text-decoration-line: underline; + } + ", + ".hover\\:line-through { + &:hover { + @media (hover: hover) { + text-decoration-line: line-through; + } + } + } + ", + ] + `) +}) + +test('Utilities, when marked as important, show as important in intellisense', async () => { + let input = css` + @import 'tailwindcss/utilities' important; + ` + + let design = await __unstable__loadDesignSystem(input, { + loadStylesheet: async (_, base) => ({ + base, + content: '@tailwind utilities;', + }), + }) + + expect(design.candidatesToCss(['underline', 'hover:line-through'])).toMatchInlineSnapshot(` + [ + ".underline { + text-decoration-line: underline!important; + } + ", + ".hover\\:line-through { + &:hover { + @media (hover: hover) { + text-decoration-line: line-through!important; + } + } + } + ", + ] + `) +})