Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for inline option when defining @theme values #14095

Merged
merged 5 commits into from
Aug 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion packages/@tailwindcss-postcss/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
140 changes: 136 additions & 4 deletions packages/tailwindcss/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1075,7 +1075,7 @@ describe('Parsing themes values from CSS', () => {
@theme {
--color-tomato: #e10c04;
}
@media reference {
@media theme(reference) {
@theme {
--color-potato: #ac855b;
}
Expand Down Expand Up @@ -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;
}
Expand All @@ -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', () => {
Expand Down
43 changes: 31 additions & 12 deletions packages/tailwindcss/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
}

Comment on lines +36 to +50
Copy link
Member Author

@adamwathan adamwathan Aug 1, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@RobinMalfait Decided to extract this function because the work was being done twice, and themeOptions didn't feel like a good name for an array that contained:

['@theme', 'reference', 'inline']

Refactored to a for loop to avoid the extra loop that calling includes twice would incur, but admittedly sad it's more code.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, makes sense.

One potential thing we can do is if both options are true early return because there is no need to keep looping if everything is found already. But since @theme is so specific to Tailwind, there is no need for that optimization (yet).

export function compile(
css: string,
{ loadPlugin = throwOnPlugin }: CompileOptions = {},
Expand Down Expand Up @@ -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 }) => {
Expand All @@ -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
}

Expand Down Expand Up @@ -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 })
})
})

Expand Down
14 changes: 11 additions & 3 deletions packages/tailwindcss/src/theme.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { escape } from './utils/escape'

export class Theme {
constructor(private values = new Map<string, { value: string; isReference: boolean }>()) {}
constructor(
private values = new Map<string, { value: string; isReference: boolean; isInline: boolean }>(),
) {}

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}\``)
Expand All @@ -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 })
}
}

Expand Down Expand Up @@ -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)
}

Expand Down
Loading