Fix resolving colors via theme(…) in compat mode with nested objects (#19097)
This PR fixes an issue when loading (nested) colors from a config file
and later referencing it via the `theme(…)` function in CSS.
Given a config like this:
```js
module.exports = {
theme: {
colors: {
foo: 'var(--foo-foo)',
'foo-bar': 'var(--foo-foo-bar)',
},
},
}
```
We internally map this into the design system. The issue here is that
the `foo` and `foo-bar` are overlapping and it behaves more like this:
```js
{
foo: {
DEFAULT: 'var(--foo-foo)',
bar: 'var(--foo-foo-bar)'
},
}
```
So while we can easily resolve `colors.foo-bar`, the `colors.foo` would
result in the object with a `DEFAULT` key. This PR solves that by using
the `DEFAULT` key if we end up with an object that has it.
If you end up resolving an object (`theme(colors)`) then the behavior is
unchanged.
## Test plan
1. Added a test based on the config in the issue (which failed before
this fix).
2. Also simplified the test case after identifying the problem (with the
`DEFAULT` key).
Fixes: #19091
This commit is contained in:
parent
561983d7e5
commit
0c14df1a15
4 changed files with 93 additions and 10 deletions
|
|
@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
- Fix Safari devtools rendering issue due to `color-mix` fallback ([#19069](https://github.com/tailwindlabs/tailwindcss/pull/19069))
|
||||
- Suppress Lightning CSS warnings about `:deep`, `:slotted` and `:global` ([#19094](https://github.com/tailwindlabs/tailwindcss/pull/19094))
|
||||
- Fix resolving theme keys when starting with the name of another theme key in JS configs and plugins ([#19097](https://github.com/tailwindlabs/tailwindcss/pull/19097))
|
||||
|
||||
## [4.1.14] - 2025-10-01
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ export function migrateMediaScreen({
|
|||
if (!designSystem || !userConfig) return
|
||||
|
||||
let { resolvedConfig } = resolveConfig(designSystem, [
|
||||
{ base: '', config: userConfig, reference: false },
|
||||
{ base: '', config: userConfig, reference: false, src: undefined },
|
||||
])
|
||||
let screens = resolvedConfig?.theme?.screens || {}
|
||||
|
||||
|
|
|
|||
|
|
@ -309,15 +309,29 @@ function upgradeToFullPluginSupport({
|
|||
|
||||
let resolvedValue = sharedPluginApi.theme(path, undefined)
|
||||
|
||||
// When a tuple is returned, return the first element
|
||||
if (Array.isArray(resolvedValue) && resolvedValue.length === 2) {
|
||||
// When a tuple is returned, return the first element
|
||||
return resolvedValue[0]
|
||||
} else if (Array.isArray(resolvedValue)) {
|
||||
// Arrays get serialized into a comma-separated lists
|
||||
}
|
||||
|
||||
// Arrays get serialized into a comma-separated lists
|
||||
else if (Array.isArray(resolvedValue)) {
|
||||
return resolvedValue.join(', ')
|
||||
} else if (typeof resolvedValue === 'string') {
|
||||
// Otherwise only allow string values here, objects (and namespace maps)
|
||||
// are treated as non-resolved values for the CSS `theme()` function.
|
||||
}
|
||||
|
||||
// If we're dealing with an object that has the `DEFAULT` key, return the
|
||||
// default value
|
||||
else if (
|
||||
typeof resolvedValue === 'object' &&
|
||||
resolvedValue !== null &&
|
||||
'DEFAULT' in resolvedValue
|
||||
) {
|
||||
return resolvedValue.DEFAULT
|
||||
}
|
||||
|
||||
// Otherwise only allow string values here, objects (and namespace maps)
|
||||
// are treated as non-resolved values for the CSS `theme()` function.
|
||||
else if (typeof resolvedValue === 'string') {
|
||||
return resolvedValue
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ test('Config files can add content', async () => {
|
|||
`
|
||||
|
||||
let compiler = await compile(input, {
|
||||
loadModule: async () => ({ module: { content: ['./file.txt'] }, base: '/root' }),
|
||||
loadModule: async () => ({ module: { content: ['./file.txt'] }, base: '/root', path: '' }),
|
||||
})
|
||||
|
||||
expect(compiler.sources).toEqual([{ base: '/root', pattern: './file.txt', negated: false }])
|
||||
|
|
@ -25,7 +25,7 @@ test('Config files can change dark mode (media)', async () => {
|
|||
`
|
||||
|
||||
let compiler = await compile(input, {
|
||||
loadModule: async () => ({ module: { darkMode: 'media' }, base: '/root' }),
|
||||
loadModule: async () => ({ module: { darkMode: 'media' }, base: '/root', path: '' }),
|
||||
})
|
||||
|
||||
expect(compiler.build(['dark:underline'])).toMatchInlineSnapshot(`
|
||||
|
|
@ -45,7 +45,7 @@ test('Config files can change dark mode (selector)', async () => {
|
|||
`
|
||||
|
||||
let compiler = await compile(input, {
|
||||
loadModule: async () => ({ module: { darkMode: 'selector' }, base: '/root' }),
|
||||
loadModule: async () => ({ module: { darkMode: 'selector' }, base: '/root', path: '' }),
|
||||
})
|
||||
|
||||
expect(compiler.build(['dark:underline'])).toMatchInlineSnapshot(`
|
||||
|
|
@ -68,6 +68,7 @@ test('Config files can change dark mode (variant)', async () => {
|
|||
loadModule: async () => ({
|
||||
module: { darkMode: ['variant', '&:where(:not(.light))'] },
|
||||
base: '/root',
|
||||
path: '',
|
||||
}),
|
||||
})
|
||||
|
||||
|
|
@ -101,6 +102,7 @@ test('Config files can add plugins', async () => {
|
|||
],
|
||||
},
|
||||
base: '/root',
|
||||
path: '',
|
||||
}),
|
||||
})
|
||||
|
||||
|
|
@ -128,6 +130,7 @@ test('Plugins loaded from config files can contribute to the config', async () =
|
|||
],
|
||||
},
|
||||
base: '/root',
|
||||
path: '',
|
||||
}),
|
||||
})
|
||||
|
||||
|
|
@ -157,6 +160,7 @@ test('Config file presets can contribute to the config', async () => {
|
|||
],
|
||||
},
|
||||
base: '/root',
|
||||
path: '',
|
||||
}),
|
||||
})
|
||||
|
||||
|
|
@ -198,6 +202,7 @@ test('Config files can affect the theme', async () => {
|
|||
],
|
||||
},
|
||||
base: '/root',
|
||||
path: '',
|
||||
}),
|
||||
})
|
||||
|
||||
|
|
@ -212,6 +217,48 @@ test('Config files can affect the theme', async () => {
|
|||
`)
|
||||
})
|
||||
|
||||
// https://github.com/tailwindlabs/tailwindcss/issues/19091
|
||||
test('Accessing a default color if a sub-color exists via CSS should work as expected', async () => {
|
||||
let input = css`
|
||||
@tailwind utilities;
|
||||
@config "./config.js";
|
||||
|
||||
.example {
|
||||
color: theme('colors.foo-bar');
|
||||
border-color: theme('colors.foo');
|
||||
}
|
||||
`
|
||||
|
||||
let compiler = await compile(input, {
|
||||
loadModule: async () => ({
|
||||
module: {
|
||||
theme: {
|
||||
// Internally this object gets converted to something like:
|
||||
// ```
|
||||
// {
|
||||
// foo: { DEFAULT: 'var(--foo-foo)', bar: 'var(--foo-foo-bar)' },
|
||||
// }
|
||||
// ```
|
||||
colors: {
|
||||
foo: 'var(--foo-foo)',
|
||||
'foo-bar': 'var(--foo-foo-bar)',
|
||||
},
|
||||
},
|
||||
},
|
||||
base: '/root',
|
||||
path: '',
|
||||
}),
|
||||
})
|
||||
|
||||
expect(compiler.build([])).toMatchInlineSnapshot(`
|
||||
".example {
|
||||
color: var(--foo-foo-bar);
|
||||
border-color: var(--foo-foo);
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
test('Variants in CSS overwrite variants from plugins', async () => {
|
||||
let input = css`
|
||||
@tailwind utilities;
|
||||
|
|
@ -231,6 +278,7 @@ test('Variants in CSS overwrite variants from plugins', async () => {
|
|||
],
|
||||
},
|
||||
base: '/root',
|
||||
path: '',
|
||||
}),
|
||||
})
|
||||
|
||||
|
|
@ -317,6 +365,7 @@ describe('theme callbacks', () => {
|
|||
],
|
||||
} satisfies Config,
|
||||
base: '/root',
|
||||
path: '',
|
||||
}),
|
||||
})
|
||||
|
||||
|
|
@ -391,6 +440,7 @@ describe('theme overrides order', () => {
|
|||
},
|
||||
},
|
||||
base: '/root',
|
||||
path: '',
|
||||
}),
|
||||
})
|
||||
|
||||
|
|
@ -442,6 +492,7 @@ describe('theme overrides order', () => {
|
|||
},
|
||||
} satisfies Config,
|
||||
base: '/root',
|
||||
path: '',
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
|
|
@ -460,6 +511,7 @@ describe('theme overrides order', () => {
|
|||
)
|
||||
}),
|
||||
base: '/root',
|
||||
path: '',
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -562,6 +614,7 @@ describe('default font family compatibility', () => {
|
|||
},
|
||||
},
|
||||
base: '/root',
|
||||
path: '',
|
||||
}),
|
||||
})
|
||||
|
||||
|
|
@ -596,6 +649,7 @@ describe('default font family compatibility', () => {
|
|||
},
|
||||
},
|
||||
base: '/root',
|
||||
path: '',
|
||||
}),
|
||||
})
|
||||
|
||||
|
|
@ -631,6 +685,7 @@ describe('default font family compatibility', () => {
|
|||
},
|
||||
},
|
||||
base: '/root',
|
||||
path: '',
|
||||
}),
|
||||
})
|
||||
|
||||
|
|
@ -669,6 +724,7 @@ describe('default font family compatibility', () => {
|
|||
},
|
||||
},
|
||||
base: '/root',
|
||||
path: '',
|
||||
}),
|
||||
})
|
||||
|
||||
|
|
@ -708,6 +764,7 @@ describe('default font family compatibility', () => {
|
|||
},
|
||||
},
|
||||
base: '/root',
|
||||
path: '',
|
||||
}),
|
||||
})
|
||||
|
||||
|
|
@ -745,6 +802,7 @@ describe('default font family compatibility', () => {
|
|||
},
|
||||
},
|
||||
base: '/root',
|
||||
path: '',
|
||||
}),
|
||||
})
|
||||
|
||||
|
|
@ -779,6 +837,7 @@ describe('default font family compatibility', () => {
|
|||
},
|
||||
},
|
||||
base: '/root',
|
||||
path: '',
|
||||
}),
|
||||
})
|
||||
|
||||
|
|
@ -806,6 +865,7 @@ describe('default font family compatibility', () => {
|
|||
},
|
||||
},
|
||||
base: '/root',
|
||||
path: '',
|
||||
}),
|
||||
})
|
||||
|
||||
|
|
@ -840,6 +900,7 @@ describe('default font family compatibility', () => {
|
|||
},
|
||||
},
|
||||
base: '/root',
|
||||
path: '',
|
||||
}),
|
||||
})
|
||||
|
||||
|
|
@ -875,6 +936,7 @@ describe('default font family compatibility', () => {
|
|||
},
|
||||
},
|
||||
base: '/root',
|
||||
path: '',
|
||||
}),
|
||||
})
|
||||
|
||||
|
|
@ -913,6 +975,7 @@ describe('default font family compatibility', () => {
|
|||
},
|
||||
},
|
||||
base: '/root',
|
||||
path: '',
|
||||
}),
|
||||
})
|
||||
|
||||
|
|
@ -952,6 +1015,7 @@ describe('default font family compatibility', () => {
|
|||
},
|
||||
},
|
||||
base: '/root',
|
||||
path: '',
|
||||
}),
|
||||
})
|
||||
|
||||
|
|
@ -989,6 +1053,7 @@ describe('default font family compatibility', () => {
|
|||
},
|
||||
},
|
||||
base: '/root',
|
||||
path: '',
|
||||
}),
|
||||
})
|
||||
|
||||
|
|
@ -1021,6 +1086,7 @@ test('creates variants for `data`, `supports`, and `aria` theme options at the s
|
|||
},
|
||||
},
|
||||
base: '/root',
|
||||
path: '',
|
||||
}),
|
||||
})
|
||||
|
||||
|
|
@ -1113,6 +1179,7 @@ test('merges css breakpoints with js config screens', async () => {
|
|||
},
|
||||
},
|
||||
base: '/root',
|
||||
path: '',
|
||||
}),
|
||||
})
|
||||
|
||||
|
|
@ -1596,6 +1663,7 @@ test('handles setting theme keys to null', async () => {
|
|||
},
|
||||
},
|
||||
base: '/root',
|
||||
path: '',
|
||||
}
|
||||
},
|
||||
},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue