diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f13d7210..0991de2e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -- Nothing yet! +### Added + +- Add support for `inline` option when defining `@theme` values ([#14095](https://github.com/tailwindlabs/tailwindcss/pull/14095)) ## [4.0.0-alpha.18] - 2024-07-25 diff --git a/packages/@tailwindcss-postcss/src/index.test.ts b/packages/@tailwindcss-postcss/src/index.test.ts index 4d376db9a..7aeae5c6c 100644 --- a/packages/@tailwindcss-postcss/src/index.test.ts +++ b/packages/@tailwindcss-postcss/src/index.test.ts @@ -94,7 +94,7 @@ test('@apply can be used without emitting the theme in the CSS file', async () = // `@apply` is used. let result = await processor.process( css` - @import 'tailwindcss/theme.css' reference; + @import 'tailwindcss/theme.css' theme(reference); .foo { @apply text-red-500; } diff --git a/packages/tailwindcss/src/index.test.ts b/packages/tailwindcss/src/index.test.ts index bb0f355a6..9e1a5a8dd 100644 --- a/packages/tailwindcss/src/index.test.ts +++ b/packages/tailwindcss/src/index.test.ts @@ -1075,7 +1075,7 @@ describe('Parsing themes values from CSS', () => { @theme { --color-tomato: #e10c04; } - @media reference { + @media theme(reference) { @theme { --color-potato: #ac855b; } @@ -1106,11 +1106,11 @@ describe('Parsing themes values from CSS', () => { `) }) - test('`@media reference` can only contain `@theme` rules', () => { + test('`@media theme(…)` can only contain `@theme` rules', () => { expect(() => compileCss( css` - @media reference { + @media theme(reference) { .not-a-theme-rule { color: cursed; } @@ -1120,9 +1120,141 @@ describe('Parsing themes values from CSS', () => { ['bg-tomato', 'bg-potato', 'bg-avocado'], ), ).toThrowErrorMatchingInlineSnapshot( - `[Error: Files imported with \`@import "…" reference\` must only contain \`@theme\` blocks.]`, + `[Error: Files imported with \`@import "…" theme(…)\` must only contain \`@theme\` blocks.]`, ) }) + + test('theme values added as `inline` are not wrapped in `var(…)` when used as utility values', () => { + expect( + compileCss( + css` + @theme inline { + --color-tomato: #e10c04; + --color-potato: #ac855b; + --color-primary: var(--primary); + } + + @tailwind utilities; + `, + ['bg-tomato', 'bg-potato', 'bg-primary'], + ), + ).toMatchInlineSnapshot(` + ":root { + --color-tomato: #e10c04; + --color-potato: #ac855b; + --color-primary: var(--primary); + } + + .bg-potato { + background-color: #ac855b; + } + + .bg-primary { + background-color: var(--primary); + } + + .bg-tomato { + background-color: #e10c04; + }" + `) + }) + + test('wrapping `@theme` with `@media theme(inline)` behaves like `@theme inline` to support `@import` statements', () => { + expect( + compileCss( + css` + @media theme(inline) { + @theme { + --color-tomato: #e10c04; + --color-potato: #ac855b; + --color-primary: var(--primary); + } + } + + @tailwind utilities; + `, + ['bg-tomato', 'bg-potato', 'bg-primary'], + ), + ).toMatchInlineSnapshot(` + ":root { + --color-tomato: #e10c04; + --color-potato: #ac855b; + --color-primary: var(--primary); + } + + .bg-potato { + background-color: #ac855b; + } + + .bg-primary { + background-color: var(--primary); + } + + .bg-tomato { + background-color: #e10c04; + }" + `) + }) + + test('`inline` and `reference` can be used together', () => { + expect( + compileCss( + css` + @theme reference inline { + --color-tomato: #e10c04; + --color-potato: #ac855b; + --color-primary: var(--primary); + } + + @tailwind utilities; + `, + ['bg-tomato', 'bg-potato', 'bg-primary'], + ), + ).toMatchInlineSnapshot(` + ".bg-potato { + background-color: #ac855b; + } + + .bg-primary { + background-color: var(--primary); + } + + .bg-tomato { + background-color: #e10c04; + }" + `) + }) + + test('`inline` and `reference` can be used together in `media(…)`', () => { + expect( + compileCss( + css` + @media theme(reference inline) { + @theme { + --color-tomato: #e10c04; + --color-potato: #ac855b; + --color-primary: var(--primary); + } + } + + @tailwind utilities; + `, + ['bg-tomato', 'bg-potato', 'bg-primary'], + ), + ).toMatchInlineSnapshot(` + ".bg-potato { + background-color: #ac855b; + } + + .bg-primary { + background-color: var(--primary); + } + + .bg-tomato { + background-color: #e10c04; + }" + `) + }) }) describe('plugins', () => { diff --git a/packages/tailwindcss/src/index.ts b/packages/tailwindcss/src/index.ts index f324dfad8..73b040d4e 100644 --- a/packages/tailwindcss/src/index.ts +++ b/packages/tailwindcss/src/index.ts @@ -33,6 +33,21 @@ function throwOnPlugin(): never { throw new Error('No `loadPlugin` function provided to `compile`') } +function parseThemeOptions(selector: string) { + let isReference = false + let isInline = false + + for (let option of segment(selector.slice(6) /* '@theme'.length */, ' ')) { + if (option === 'reference') { + isReference = true + } else if (option === 'inline') { + isInline = true + } + } + + return { isReference, isInline } +} + export function compile( css: string, { loadPlugin = throwOnPlugin }: CompileOptions = {}, @@ -152,28 +167,32 @@ export function compile( } } - // Drop instances of `@media reference` + // Drop instances of `@media theme(…)` // - // We support `@import "tailwindcss/theme" reference` as a way to import an external theme file - // as a reference, which becomes `@media reference { … }` when the `@import` is processed. - if (node.selector === '@media reference') { + // We support `@import "tailwindcss/theme" theme(reference)` as a way to + // import an external theme file as a reference, which becomes `@media + // theme(reference) { … }` when the `@import` is processed. + if (node.selector.startsWith('@media theme(')) { + let themeParams = node.selector.slice(13, -1) + walk(node.nodes, (child) => { if (child.kind !== 'rule') { throw new Error( - 'Files imported with `@import "…" reference` must only contain `@theme` blocks.', + 'Files imported with `@import "…" theme(…)` must only contain `@theme` blocks.', ) } if (child.selector === '@theme') { - child.selector = '@theme reference' + child.selector = '@theme ' + themeParams return WalkAction.Skip } }) replaceWith(node.nodes) + return WalkAction.Skip } - if (node.selector !== '@theme' && node.selector !== '@theme reference') return + if (node.selector !== '@theme' && !node.selector.startsWith('@theme ')) return - let isReference = node.selector === '@theme reference' + let { isReference, isInline } = parseThemeOptions(node.selector) // Record all custom properties in the `@theme` declaration walk(node.nodes, (child, { replaceWith }) => { @@ -187,7 +206,7 @@ export function compile( if (child.kind === 'comment') return if (child.kind === 'declaration' && child.property.startsWith('--')) { - theme.add(child.property, child.value, isReference) + theme.add(child.property, child.value, { isReference, isInline }) return } @@ -395,15 +414,15 @@ export function __unstable__loadDesignSystem(css: string) { walk(ast, (node) => { if (node.kind !== 'rule') return - if (node.selector !== '@theme' && node.selector !== '@theme reference') return - let isReference = node.selector === '@theme reference' + if (node.selector !== '@theme' && !node.selector.startsWith('@theme ')) return + let { isReference, isInline } = parseThemeOptions(node.selector) // Record all custom properties in the `@theme` declaration walk([node], (node) => { if (node.kind !== 'declaration') return if (!node.property.startsWith('--')) return - theme.add(node.property, node.value, isReference) + theme.add(node.property, node.value, { isReference, isInline }) }) }) diff --git a/packages/tailwindcss/src/theme.ts b/packages/tailwindcss/src/theme.ts index 8ac6a90cc..7d150c350 100644 --- a/packages/tailwindcss/src/theme.ts +++ b/packages/tailwindcss/src/theme.ts @@ -1,9 +1,11 @@ import { escape } from './utils/escape' export class Theme { - constructor(private values = new Map()) {} + constructor( + private values = new Map(), + ) {} - add(key: string, value: string, isReference = false): void { + add(key: string, value: string, { isReference = false, isInline = false } = {}): void { if (key.endsWith('-*')) { if (value !== 'initial') { throw new Error(`Invalid theme value \`${value}\` for namespace \`${key}\``) @@ -18,7 +20,7 @@ export class Theme { if (value === 'initial') { this.values.delete(key) } else { - this.values.set(key, { value, isReference }) + this.values.set(key, { value, isReference, isInline }) } } @@ -91,6 +93,12 @@ export class Theme { if (!themeKey) return null + let value = this.values.get(themeKey)! + + if (value.isInline) { + return value.value + } + return this.#var(themeKey) }