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:
parent
a1acaeeee0
commit
ca7b10e7d3
14 changed files with 635 additions and 44 deletions
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;"
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.]`,
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue