Make --theme(…) return CSS variables (#17036)

Closes #16945

This PR changes the `--theme(…)` function now return CSS `var(…)`
definitions unless used in places where `var(…)` is not valid CSS (e.g.
in `@media (width >= theme(--breakpoint-md))`):

```css
/* input */
@theme {
  --color-red: red;
}
.red {
  color: --theme(--color-red);
}

/* output */
:root, :host {
  --color-red: red;
}
.red {
  color: var(--color-red);
}
```

Furthermore, this adds an `--theme(… inline)` option to the `--theme(…)`
function to force the resolution to be inline, e.g.:

```css
/* input */
@theme {
  --color-red: red;
}
.red {
  color: --theme(--color-red inline);
}

/* output */
.red {
  color: red;
}
```

This PR also changes preflight and the default theme to use this new
`--theme(…)` function to ensure variables are prefixed correctly.

## Test plan

- Added unit tests and a test that pulls in the whole preflight under a
prefix theme.
This commit is contained in:
Philipp Spiess 2025-03-20 12:51:22 +01:00 • committed by GitHub
parent a1acaeeee0
commit ca7b10e7d3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 635 additions and 44 deletions

View file

@ -26,6 +26,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Fix `border-[12px_4px]` being interpreted as a `border-color` instead of a `border-width` ([#17248](https://github.com/tailwindlabs/tailwindcss/pull/17248))
- Use the `oklab(…)` function when applying opacity to `currentColor` to work around a crash in Safari 16.4 and 16.5 ([#17247](https://github.com/tailwindlabs/tailwindcss/pull/17247))
- Pre-process `<template lang="…">` in Vue files ([#17252](https://github.com/tailwindlabs/tailwindcss/pull/17252))
- Ensure that all CSS variables used by preflight are prefixed ([#17036](https://github.com/tailwindlabs/tailwindcss/pull/17036))
### Changed
- The `--theme(…)` function now returns CSS variables from your theme variables unless used inside positions where CSS variables are invalid (e.g. inside `@media` queries) ([#17036](https://github.com/tailwindlabs/tailwindcss/pull/17036))
- Remove redundant `line-height: initial` from Preflight ([#15212](https://github.com/tailwindlabs/tailwindcss/pull/15212))
- Prevent segfault when loaded in a worker thread on Linux ([#17276](https://github.com/tailwindlabs/tailwindcss/pull/17276))
- Ensure multiple `--value(…)` or `--modifier(…)` calls don't delete subsequent declarations ([#17273](https://github.com/tailwindlabs/tailwindcss/pull/17273))

View file

@ -10,11 +10,7 @@ exports[`\`@import 'tailwindcss'\` is replaced with the generated CSS 1`] = `
--text-2xl--line-height: calc(2 / 1.5);
--font-weight-bold: 700;
--default-font-family: var(--font-sans);
--default-font-feature-settings: var(--font-sans--font-feature-settings);
--default-font-variation-settings: var(--font-sans--font-variation-settings);
--default-mono-font-family: var(--font-mono);
--default-mono-font-feature-settings: var(--font-mono--font-feature-settings);
--default-mono-font-variation-settings: var(--font-mono--font-variation-settings);
}
}

View file

@ -19,7 +19,7 @@ export function migrateMediaScreen({
let screens = resolvedConfig?.theme?.screens || {}
let mediaQueries = new DefaultMap<string, string | null>((name) => {
let value = designSystem?.resolveThemeValue(`--breakpoint-${name}`) ?? screens?.[name]
let value = designSystem?.resolveThemeValue(`--breakpoint-${name}`, true) ?? screens?.[name]
if (typeof value === 'string') return `(width >= theme(--breakpoint-${name}))`
return value ? buildMediaQuery(value) : null
})

View file

@ -139,9 +139,9 @@ export async function legacyClasses(
if (fromThemeKey && toThemeKey) {
// Migrating something that resolves to a value in the theme.
let customFrom = designSystem.resolveThemeValue(fromThemeKey)
let defaultFrom = defaultDesignSystem.resolveThemeValue(fromThemeKey)
let customTo = designSystem.resolveThemeValue(toThemeKey)
let customFrom = designSystem.resolveThemeValue(fromThemeKey, true)
let defaultFrom = defaultDesignSystem.resolveThemeValue(fromThemeKey, true)
let customTo = designSystem.resolveThemeValue(toThemeKey, true)
let defaultTo = defaultDesignSystem.resolveThemeValue(toThemeKey)
// The new theme value is not defined, which means we can't safely

View file

@ -30,7 +30,7 @@ html,
line-height: 1.5; /* 1 */
-webkit-text-size-adjust: 100%; /* 2 */
tab-size: 4; /* 3 */
font-family: var(
font-family: --theme(
--default-font-family,
ui-sans-serif,
system-ui,
@ -40,8 +40,8 @@ html,
'Segoe UI Symbol',
'Noto Color Emoji'
); /* 4 */
font-feature-settings: var(--default-font-feature-settings, normal); /* 5 */
font-variation-settings: var(--default-font-variation-settings, normal); /* 6 */
font-feature-settings: --theme(--default-font-feature-settings, normal); /* 5 */
font-variation-settings: --theme(--default-font-variation-settings, normal); /* 6 */
-webkit-tap-highlight-color: transparent; /* 7 */
}
@ -110,7 +110,7 @@ code,
kbd,
samp,
pre {
font-family: var(
font-family: --theme(
--default-mono-font-family,
ui-monospace,
SFMono-Regular,
@ -121,8 +121,8 @@ pre {
'Courier New',
monospace
); /* 1 */
font-feature-settings: var(--default-mono-font-feature-settings, normal); /* 2 */
font-variation-settings: var(--default-mono-font-variation-settings, normal); /* 3 */
font-feature-settings: --theme(--default-mono-font-feature-settings, normal); /* 2 */
font-variation-settings: --theme(--default-mono-font-variation-settings, normal); /* 3 */
font-size: 1em; /* 4 */
}

View file

@ -92,3 +92,248 @@ exports[`compiling CSS > \`@tailwind utilities\` is replaced by utilities using
initial-value: 0 0 #0000;
}"
`;
exports[`compiling CSS > prefix all CSS variables inside preflight 1`] = `
"@layer theme {
:root, :host {
--tw-font-sans: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--tw-font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
--tw-default-font-family: var(--tw-font-sans);
--tw-default-mono-font-family: var(--tw-font-mono);
}
}
@layer base {
*, :after, :before, ::backdrop {
box-sizing: border-box;
border: 0 solid;
margin: 0;
padding: 0;
}
::file-selector-button {
box-sizing: border-box;
border: 0 solid;
margin: 0;
padding: 0;
}
html, :host {
-webkit-text-size-adjust: 100%;
tab-size: 4;
line-height: 1.5;
font-family: var(--tw-default-font-family, ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");
font-feature-settings: var(--tw-default-font-feature-settings, normal);
font-variation-settings: var(--tw-default-font-variation-settings, normal);
-webkit-tap-highlight-color: transparent;
}
hr {
height: 0;
color: inherit;
border-top-width: 1px;
}
abbr:where([title]) {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
}
h1, h2, h3, h4, h5, h6 {
font-size: inherit;
font-weight: inherit;
}
a {
color: inherit;
-webkit-text-decoration: inherit;
-webkit-text-decoration: inherit;
-webkit-text-decoration: inherit;
text-decoration: inherit;
}
b, strong {
font-weight: bolder;
}
code, kbd, samp, pre {
font-family: var(--tw-default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);
font-feature-settings: var(--tw-default-mono-font-feature-settings, normal);
font-variation-settings: var(--tw-default-mono-font-variation-settings, normal);
font-size: 1em;
}
small {
font-size: 80%;
}
sub, sup {
vertical-align: baseline;
font-size: 75%;
line-height: 0;
position: relative;
}
sub {
bottom: -.25em;
}
sup {
top: -.5em;
}
table {
text-indent: 0;
border-color: inherit;
border-collapse: collapse;
}
:-moz-focusring {
outline: auto;
}
progress {
vertical-align: baseline;
}
summary {
display: list-item;
}
ol, ul, menu {
list-style: none;
}
img, svg, video, canvas, audio, iframe, embed, object {
vertical-align: middle;
display: block;
}
img, video {
max-width: 100%;
height: auto;
}
button, input, select, optgroup, textarea {
font: inherit;
font-feature-settings: inherit;
font-variation-settings: inherit;
letter-spacing: inherit;
color: inherit;
opacity: 1;
background-color: #0000;
border-radius: 0;
}
::file-selector-button {
font: inherit;
font-feature-settings: inherit;
font-variation-settings: inherit;
letter-spacing: inherit;
color: inherit;
opacity: 1;
background-color: #0000;
border-radius: 0;
}
:where(select:is([multiple], [size])) optgroup {
font-weight: bolder;
}
:where(select:is([multiple], [size])) optgroup option {
padding-inline-start: 20px;
}
::file-selector-button {
margin-inline-end: 4px;
}
::placeholder {
opacity: 1;
color: oklab(from currentColor l a b / 50%);
}
textarea {
resize: vertical;
}
::-webkit-search-decoration {
-webkit-appearance: none;
}
::-webkit-date-and-time-value {
min-height: 1lh;
text-align: inherit;
}
::-webkit-datetime-edit {
display: inline-flex;
}
::-webkit-datetime-edit-fields-wrapper {
padding: 0;
}
::-webkit-datetime-edit {
padding-block: 0;
}
::-webkit-datetime-edit-year-field {
padding-block: 0;
}
::-webkit-datetime-edit-month-field {
padding-block: 0;
}
::-webkit-datetime-edit-day-field {
padding-block: 0;
}
::-webkit-datetime-edit-hour-field {
padding-block: 0;
}
::-webkit-datetime-edit-minute-field {
padding-block: 0;
}
::-webkit-datetime-edit-second-field {
padding-block: 0;
}
::-webkit-datetime-edit-millisecond-field {
padding-block: 0;
}
::-webkit-datetime-edit-meridiem-field {
padding-block: 0;
}
:-moz-ui-invalid {
box-shadow: none;
}
button, input:where([type="button"], [type="reset"], [type="submit"]) {
appearance: button;
}
::file-selector-button {
appearance: button;
}
::-webkit-inner-spin-button {
height: auto;
}
::-webkit-outer-spin-button {
height: auto;
}
[hidden]:where(:not([hidden="until-found"])) {
display: none !important;
}
}
@layer components, utilities;"
`;

View file

@ -283,6 +283,13 @@ export function optimizeAst(ast: AstNode[], designSystem: DesignSystem) {
// Track variables defined in `@theme`
if (context.theme && node.property[0] === '-' && node.property[1] === '-') {
// Variables that resolve to `initial` should never be emitted. This can happen because of
// the `--theme(…)` being used and evaluated lazily
if (node.value === 'initial') {
node.value = undefined
return
}
if (!context.keyframes) {
cssThemeVariables.get(parent).add(node)
}

View file

@ -134,9 +134,9 @@ export async function applyCompatibilityHooks({
// compatibility concerns localized to our compatibility layer.
let resolveThemeVariableValue = designSystem.resolveThemeValue
designSystem.resolveThemeValue = function resolveThemeValue(path: string) {
designSystem.resolveThemeValue = function resolveThemeValue(path: string, forceInline?: boolean) {
if (path.startsWith('--')) {
return resolveThemeVariableValue(path)
return resolveThemeVariableValue(path, forceInline)
}
// If the theme value is not found in the simple resolver, we upgrade to the full backward
@ -149,7 +149,7 @@ export async function applyCompatibilityHooks({
configs: [],
pluginDetails: [],
})
return designSystem.resolveThemeValue(path)
return designSystem.resolveThemeValue(path, forceInline)
}
// If there are no plugins or configs registered, we don't need to register
@ -260,8 +260,8 @@ function upgradeToFullPluginSupport({
// config files are actually being used. In the future we may want to optimize
// this further by only doing this if plugins or config files _actually_
// registered JS config objects.
designSystem.resolveThemeValue = function resolveThemeValue(path: string, defaultValue?: string) {
let resolvedValue = pluginApi.theme(path, defaultValue)
designSystem.resolveThemeValue = function resolveThemeValue(path: string, forceInline?: boolean) {
let resolvedValue = pluginApi.theme(path, forceInline)
if (Array.isArray(resolvedValue) && resolvedValue.length === 2) {
// When a tuple is returned, return the first element

View file

@ -152,6 +152,27 @@ describe('--theme(…)', () => {
color: --theme(--color-red-500);
}
`),
).toMatchInlineSnapshot(`
":root, :host {
--color-red-500: red;
}
.red {
color: var(--color-red-500);
}"
`)
})
test('--theme(--color-red-500 inline)', async () => {
expect(
await compileCss(css`
@theme {
--color-red-500: #f00;
}
.red {
color: --theme(--color-red-500 inline);
}
`),
).toMatchInlineSnapshot(`
".red {
color: red;
@ -159,7 +180,184 @@ describe('--theme(…)', () => {
`)
})
test('--theme(…) can only be used with CSS variables from your theme', async () => {
test('--theme(--color-red-500/50)', async () => {
expect(
await compileCss(css`
@theme {
--color-red-500: #f00;
}
.red {
color: --theme(--color-red-500/0.5);
}
`),
).toMatchInlineSnapshot(`
":root, :host {
--color-red-500: red;
}
.red {
color: color-mix(in oklab, var(--color-red-500) 50%, transparent);
}"
`)
})
test('--theme(--color-red-500/50 inline)', async () => {
expect(
await compileCss(css`
@theme {
--color-red-500: #f00;
}
.red {
color: --theme(--color-red-500/50 inline);
}
`),
).toMatchInlineSnapshot(`
".red {
color: oklab(62.7955% .224863 .125846);
}"
`)
})
test('--theme(…) injects the fallback when the value it refers is set to a `--theme(…)` function with the fallback `initial`', async () => {
expect(
await compileCss(css`
@theme prefix(tw) {
--default-font-family: --theme(--font-family, initial);
}
.red {
font-family: --theme(--default-font-family, Potato Sans, sans-serif);
}
`),
).toMatchInlineSnapshot(`
".red {
font-family: var(--tw-default-font-family, Potato Sans, sans-serif);
}"
`)
})
test('--theme(…) injects the fallback when the value it refers is set to a `--theme(…)` function with the fallback `initial` in @reference mode', async () => {
expect(
await compileCss(css`
@theme reference prefix(tw) {
--default-font-family: --theme(--font-family, initial);
}
.red {
font-family: --theme(--default-font-family, Potato Sans, sans-serif);
}
`),
).toMatchInlineSnapshot(`
".red {
font-family: var(--tw-default-font-family, Potato Sans, sans-serif);
}"
`)
})
test('--theme(…) resolves with the fallback when the value it refers is set to a `--theme(… inline)` function with the fallback `initial`', async () => {
expect(
await compileCss(css`
@theme prefix(tw) {
--default-font-family: --theme(--font-family inline, initial);
}
.red {
font-family: --theme(--default-font-family inline, Potato Sans, sans-serif);
}
`),
).toMatchInlineSnapshot(`
".red {
font-family: Potato Sans, sans-serif;
}"
`)
})
test('--theme(…) resolves with the fallback when the value it refers is set to a `--theme(… inline)` function with the fallback `initial` in @reference mode', async () => {
expect(
await compileCss(css`
@theme reference prefix(tw) {
--default-font-family: --theme(--font-family inline, initial);
}
.red {
font-family: --theme(--default-font-family inline, Potato Sans, sans-serif);
}
`),
).toMatchInlineSnapshot(`
".red {
font-family: Potato Sans, sans-serif;
}"
`)
})
test('--theme(…) does not inject the fallback if the fallback is `initial`', async () => {
expect(
await compileCss(
css`
@theme prefix(tw) {
--font-sans:
ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
'Segoe UI Symbol', 'Noto Color Emoji';
--default-font-family: --theme(--font-sans, initial);
}
@layer base {
html {
font-family: --theme(--default-font-family, sans-serif);
}
}
@tailwind utilities;
`,
['tw:font-sans'],
),
).toMatchInlineSnapshot(`
":root, :host {
--tw-font-sans: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--tw-default-font-family: var(--tw-font-sans);
}
@layer base {
html {
font-family: var(--tw-default-font-family, sans-serif);
}
}
.tw\\:font-sans {
font-family: var(--tw-font-sans);
}"
`)
})
test('--theme(…) forces the value to be retrieved as inline when used inside an at-rule', async () => {
expect(
await compileCss(css`
@theme {
--breakpoint-md: 48rem;
--breakpoint-lg: 64rem;
}
@custom-media --md (width >= --theme(--breakpoint-md));
@media (--md) {
.blue {
color: blue;
}
}
@media (width >= --theme(--breakpoint-lg)) {
.red {
color: red;
}
}
`),
).toMatchInlineSnapshot(`
"@media (width >= 48rem) {
.blue {
color: #00f;
}
}
@media (width >= 64rem) {
.red {
color: red;
}
}"
`)
})
test('--theme(…) can only be used with CSS variables from your @theme', async () => {
expect(() =>
compileCss(css`
@theme {
@ -172,6 +370,19 @@ describe('--theme(…)', () => {
).rejects.toThrowErrorMatchingInlineSnapshot(
`[Error: The --theme(…) function can only be used with CSS variables from your theme.]`,
)
expect(() =>
compileCss(css`
@theme {
--color-red-500: #f00;
}
.red {
color: --theme(--color-green-500);
}
`),
).rejects.toThrowErrorMatchingInlineSnapshot(
`[Error: Could not resolve value for theme function: \`theme(--color-green-500)\`. Consider checking if the variable name is correct or provide a fallback value to silence this error.]`,
)
})
})

View file

@ -5,14 +5,22 @@ import { withAlpha } from './utilities'
import { segment } from './utils/segment'
import * as ValueParser from './value-parser'
const functions: Record<string, (designSystem: DesignSystem, ...args: string[]) => string> = {
const CSS_FUNCTIONS: Record<
string,
(designSystem: DesignSystem, source: AstNode, ...args: string[]) => string
> = {
'--alpha': alpha,
'--spacing': spacing,
'--theme': theme,
theme: legacyTheme,
}
function alpha(_designSystem: DesignSystem, value: string, ...rest: string[]) {
function alpha(
_designSystem: DesignSystem,
_source: AstNode,
value: string,
...rest: string[]
): string {
let [color, alpha] = segment(value, '/').map((v) => v.trim())
if (!color || !alpha) {
@ -30,7 +38,12 @@ function alpha(_designSystem: DesignSystem, value: string, ...rest: string[]) {
return withAlpha(color, alpha)
}
function spacing(designSystem: DesignSystem, value: string, ...rest: string[]) {
function spacing(
designSystem: DesignSystem,
_source: AstNode,
value: string,
...rest: string[]
): string {
if (!value) {
throw new Error(`The --spacing(…) function requires an argument, but received none.`)
}
@ -51,15 +64,73 @@ function spacing(designSystem: DesignSystem, value: string, ...rest: string[]) {
return `calc(${multiplier} * ${value})`
}
function theme(designSystem: DesignSystem, path: string, ...fallback: string[]) {
function theme(
designSystem: DesignSystem,
source: AstNode,
path: string,
...fallback: string[]
): string {
if (!path.startsWith('--')) {
throw new Error(`The --theme(…) function can only be used with CSS variables from your theme.`)
}
return legacyTheme(designSystem, path, ...fallback)
let inline = false
// Handle `--theme(… inline)` to force inline resolution
if (path.endsWith(' inline')) {
inline = true
path = path.slice(0, -7)
}
// If the `--theme(…)` function is used within an at-rule (e.g. `@media (width >= --theme(…)))`,
// we have to always inline the result since CSS does not support CSS variables in these positions
if (source.kind === 'at-rule') {
inline = true
}
let resolvedValue = designSystem.resolveThemeValue(path, inline)
if (!resolvedValue) {
if (fallback.length > 0) return fallback.join(', ')
throw new Error(
`Could not resolve value for theme function: \`theme(${path})\`. Consider checking if the variable name is correct or provide a fallback value to silence this error.`,
)
}
if (fallback.length === 0) {
return resolvedValue
}
let joinedFallback = fallback.join(', ')
if (joinedFallback === 'initial') return resolvedValue
// When the resolved value returns `initial`, resolve with the the fallback value
if (resolvedValue === 'initial') return joinedFallback
// Inject the fallback of a `--theme(…)` function into the fallback of a referenced `--theme(…)`
// function or `var(…)` declaration. If the referenced function already defines a fallback, we use
// a potential fallback value of `initial` in the referenced function to determine if we should
// inject the fallback value of the caller. If that's not the case, we keep the fallback as-is
// (this is needed for theme variables in reference-mode).
if (
resolvedValue.startsWith('var(') ||
resolvedValue.startsWith('theme(') ||
resolvedValue.startsWith('--theme(')
) {
let valueAst = ValueParser.parse(resolvedValue)
injectFallbackForInitialFallback(valueAst, joinedFallback)
return ValueParser.toCss(valueAst)
}
return resolvedValue
}
function legacyTheme(designSystem: DesignSystem, path: string, ...fallback: string[]) {
function legacyTheme(
designSystem: DesignSystem,
_source: AstNode,
path: string,
...fallback: string[]
): string {
path = eventuallyUnquote(path)
let resolvedValue = designSystem.resolveThemeValue(path)
@ -78,7 +149,7 @@ function legacyTheme(designSystem: DesignSystem, path: string, ...fallback: stri
}
export const THEME_FUNCTION_INVOCATION = new RegExp(
Object.keys(functions)
Object.keys(CSS_FUNCTIONS)
.map((x) => `${x}\\(`)
.join('|'),
)
@ -89,7 +160,7 @@ export function substituteFunctions(ast: AstNode[], designSystem: DesignSystem)
// Find all declaration values
if (node.kind === 'declaration' && node.value && THEME_FUNCTION_INVOCATION.test(node.value)) {
features |= Features.ThemeFunction
node.value = substituteFunctionsInValue(node.value, designSystem)
node.value = substituteFunctionsInValue(node.value, node, designSystem)
return
}
@ -103,19 +174,27 @@ export function substituteFunctions(ast: AstNode[], designSystem: DesignSystem)
THEME_FUNCTION_INVOCATION.test(node.params)
) {
features |= Features.ThemeFunction
node.params = substituteFunctionsInValue(node.params, designSystem)
node.params = substituteFunctionsInValue(node.params, node, designSystem)
}
}
})
return features
}
export function substituteFunctionsInValue(value: string, designSystem: DesignSystem): string {
export function substituteFunctionsInValue(
value: string,
source: AstNode,
designSystem: DesignSystem,
): string {
let ast = ValueParser.parse(value)
ValueParser.walk(ast, (node, { replaceWith }) => {
if (node.kind === 'function' && node.value in functions) {
if (node.kind === 'function' && node.value in CSS_FUNCTIONS) {
let args = segment(ValueParser.toCss(node.nodes).trim(), ',').map((x) => x.trim())
let result = functions[node.value as keyof typeof functions](designSystem, ...args)
let result = CSS_FUNCTIONS[node.value as keyof typeof CSS_FUNCTIONS](
designSystem,
source,
...args,
)
return replaceWith(ValueParser.parse(result))
}
})
@ -142,3 +221,22 @@ function eventuallyUnquote(value: string) {
return unquoted
}
function injectFallbackForInitialFallback(ast: ValueParser.ValueAstNode[], fallback: string): void {
ValueParser.walk(ast, (node) => {
if (node.kind !== 'function') return
if (node.value !== 'var' && node.value !== 'theme' && node.value !== '--theme') return
if (node.nodes.length === 1) {
node.nodes.push({
kind: 'word',
value: `, ${fallback}`,
})
} else {
let lastNode = node.nodes[node.nodes.length - 1]
if (lastNode.kind === 'word' && lastNode.value === 'initial') {
lastNode.value = fallback
}
}
})
}

View file

@ -4,7 +4,7 @@ import { compileAstNodes, compileCandidates } from './compile'
import { substituteFunctions } from './css-functions'
import { getClassList, getVariants, type ClassEntry, type VariantEntry } from './intellisense'
import { getClassOrder } from './sort'
import type { Theme, ThemeKey } from './theme'
import { Theme, ThemeOptions, type ThemeKey } from './theme'
import { Utilities, createUtilities, withAlpha } from './utilities'
import { DefaultMap } from './utils/default-map'
import { extractUsedVariables } from './utils/variables'
@ -29,7 +29,7 @@ export type DesignSystem = {
compileAstNodes(candidate: Candidate): ReturnType<typeof compileAstNodes>
getVariantOrder(): Map<Variant, number>
resolveThemeValue(path: string): string | undefined
resolveThemeValue(path: string, forceInline?: boolean): string | undefined
trackUsedVariables(raw: string): void
@ -150,7 +150,7 @@ export function buildDesignSystem(theme: Theme): DesignSystem {
return order
},
resolveThemeValue(path: `${ThemeKey}` | `${ThemeKey}${string}`) {
resolveThemeValue(path: `${ThemeKey}` | `${ThemeKey}${string}`, forceInline: boolean = true) {
// Extract an eventual modifier from the path. e.g.:
// - "--color-red-500 / 50%" -> "50%"
let lastSlash = path.lastIndexOf('/')
@ -160,7 +160,9 @@ export function buildDesignSystem(theme: Theme): DesignSystem {
path = path.slice(0, lastSlash).trim() as ThemeKey
}
let themeValue = theme.get([path]) ?? undefined
let themeValue =
theme.resolve(null, [path], forceInline ? ThemeOptions.INLINE : ThemeOptions.NONE) ??
undefined
// Apply the opacity modifier if present
if (modifier && themeValue) {

View file

@ -115,6 +115,29 @@ describe('compiling CSS', () => {
).toMatchSnapshot()
})
test('prefix all CSS variables inside preflight', async () => {
expect(
await compileCss(
css`
@import 'tailwindcss' prefix(tw);
@tailwind utilities;
`,
['font-mono'],
{
async loadStylesheet(id) {
return {
content: fs.readFileSync(
path.resolve(__dirname, '..', id === 'tailwindcss' ? 'index.css' : id),
'utf-8',
),
base: '',
}
},
},
),
).toMatchSnapshot()
})
test('unescapes underscores to spaces inside arbitrary values except for `url()` and first argument of `var()` and `theme()`', async () => {
expect(
await compileCss(

View file

@ -200,14 +200,18 @@ export class Theme {
value.options |= ThemeOptions.USED
}
resolve(candidateValue: string | null, themeKeys: ThemeKey[]): string | null {
resolve(
candidateValue: string | null,
themeKeys: ThemeKey[],
options: ThemeOptions = ThemeOptions.NONE,
): string | null {
let themeKey = this.#resolveKey(candidateValue, themeKeys)
if (!themeKey) return null
let value = this.values.get(themeKey)!
if (value.options & ThemeOptions.INLINE) {
if ((options | value.options) & ThemeOptions.INLINE) {
return value.value
}

View file

@ -434,12 +434,12 @@
--default-transition-duration: 150ms;
--default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
--default-font-family: var(--font-sans);
--default-font-feature-settings: var(--font-sans--font-feature-settings);
--default-font-variation-settings: var(--font-sans--font-variation-settings);
--default-mono-font-family: var(--font-mono);
--default-mono-font-feature-settings: var(--font-mono--font-feature-settings);
--default-mono-font-variation-settings: var(--font-mono--font-variation-settings);
--default-font-family: --theme(--font-sans, initial);
--default-font-feature-settings: --theme(--font-sans--font-feature-settings, initial);
--default-font-variation-settings: --theme(--font-sans--font-variation-settings, initial);
--default-mono-font-family: --theme(--font-mono, initial);
--default-mono-font-feature-settings: --theme(--font-mono--font-feature-settings, initial);
--default-mono-font-variation-settings: --theme(--font-mono--font-variation-settings, initial);
}
/* Deprecated */