Skip to content

Commit b1e22e1

Browse files
thecrypticaceadamwathanRobinMalfait
authored
Allow configs to override default CSS theme values in theme() function provided to plugins and configs (#14359)
Previously, given the following CSS and configuration: ```css /* app.css */ @theme default { --font-size-base: 1.25rem; --font-size-base--line-height: 1.5rem; } @tailwind utilities; @config "./config.js"; ``` ```js // config.js export default { theme: { fontSize: { // … base: ['1rem', { lineHeight: '1.75rem' }], }, // … }, }; ``` When a config or a plugin asked for the value of `theme(fontSize.base)` like so: ```js // config.js export default { theme: { // … typography: ({ theme }) => ({ css: { '[class~="lead"]': { fontSize: theme('fontSize.base')[0], ...theme('fontSize.base')[1], }, } }), }, }; ``` We would instead pull the values from the CSS theme even through they're marked with `@theme default`. This would cause the incorrect font size and line height to be used resulting in something like this (in the case of the typography plugin with custom styles): ```css .prose [class~="lead"] { font-size: 1.25rem; line-height: 1.5rem; } ``` After this change we'll now pull the values from the appropriate place (the config in this case) and the correct font size and line height will be used: ```css .prose [class~="lead"] { font-size: 1rem; line-height: 1.75rem; } ``` This will work even when some values are overridden in the CSS theme: ```css /* app.css */ @theme default { --font-size-base: 1.25rem; --font-size-base--line-height: 1.5rem; } @theme { --font-size-base: 2rem; } @tailwind utilities; @config "./config.js"; ``` which would result in the following CSS: ```css .prose [class~="lead"] { font-size: 2rem; line-height: 1.75rem; } ``` --------- Co-authored-by: Adam Wathan <[email protected]> Co-authored-by: Robin Malfait <[email protected]> Co-authored-by: Adam Wathan <[email protected]>
1 parent ee35a1d commit b1e22e1

File tree

7 files changed

+407
-32
lines changed

7 files changed

+407
-32
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1515

1616
- Ensure there is always CLI feedback on save even when no new classes were found ([#14351](https://github.com/tailwindlabs/tailwindcss/pull/14351))
1717
- Properly resolve `theme('someKey.DEFAULT')` when all `--some-key-*` keys have a suffix ([#14354](https://github.com/tailwindlabs/tailwindcss/pull/14354))
18+
- Make sure tuple theme values in JS configs take precedence over `@theme default` values ([#14359](https://github.com/tailwindlabs/tailwindcss/pull/14359))
1819

1920
## [4.0.0-alpha.23] - 2024-09-05
2021

packages/tailwindcss/src/compat/config.test.ts

Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { describe, test } from 'vitest'
22
import { compile } from '..'
33
import plugin from '../plugin'
4+
import { flattenColorPalette } from './flatten-color-palette'
45

56
const css = String.raw
67

@@ -230,6 +231,272 @@ test('Variants in CSS overwrite variants from plugins', async ({ expect }) => {
230231
`)
231232
})
232233

234+
describe('theme callbacks', () => {
235+
test('tuple values from the config overwrite `@theme default` tuple-ish values from the CSS theme', async ({
236+
expect,
237+
}) => {
238+
let input = css`
239+
@theme default {
240+
--font-size-base: 0rem;
241+
--font-size-base--line-height: 1rem;
242+
--font-size-md: 0rem;
243+
--font-size-md--line-height: 1rem;
244+
--font-size-xl: 0rem;
245+
--font-size-xl--line-height: 1rem;
246+
}
247+
@theme {
248+
--font-size-base: 100rem;
249+
--font-size-md--line-height: 101rem;
250+
}
251+
@tailwind utilities;
252+
@config "./config.js";
253+
`
254+
255+
let compiler = await compile(input, {
256+
loadConfig: async () => ({
257+
theme: {
258+
extend: {
259+
fontSize: {
260+
base: ['200rem', { lineHeight: '201rem' }],
261+
md: ['200rem', { lineHeight: '201rem' }],
262+
xl: ['200rem', { lineHeight: '201rem' }],
263+
},
264+
265+
// Direct access
266+
lineHeight: ({ theme }) => ({
267+
base: theme('fontSize.base[1].lineHeight'),
268+
md: theme('fontSize.md[1].lineHeight'),
269+
xl: theme('fontSize.xl[1].lineHeight'),
270+
}),
271+
272+
// Tuple access
273+
typography: ({ theme }) => ({
274+
'[class~=lead-base]': {
275+
fontSize: theme('fontSize.base')[0],
276+
...theme('fontSize.base')[1],
277+
},
278+
'[class~=lead-md]': {
279+
fontSize: theme('fontSize.md')[0],
280+
...theme('fontSize.md')[1],
281+
},
282+
'[class~=lead-xl]': {
283+
fontSize: theme('fontSize.xl')[0],
284+
...theme('fontSize.xl')[1],
285+
},
286+
}),
287+
},
288+
},
289+
290+
plugins: [
291+
plugin(function ({ addUtilities, theme }) {
292+
addUtilities({
293+
'.prose': {
294+
...theme('typography'),
295+
},
296+
})
297+
}),
298+
],
299+
}),
300+
})
301+
302+
expect(compiler.build(['leading-base', 'leading-md', 'leading-xl', 'prose']))
303+
.toMatchInlineSnapshot(`
304+
":root {
305+
--font-size-base: 100rem;
306+
--font-size-md--line-height: 101rem;
307+
}
308+
.prose {
309+
[class~=lead-base] {
310+
font-size: 100rem;
311+
line-height: 201rem;
312+
}
313+
[class~=lead-md] {
314+
font-size: 200rem;
315+
line-height: 101rem;
316+
}
317+
[class~=lead-xl] {
318+
font-size: 200rem;
319+
line-height: 201rem;
320+
}
321+
}
322+
.leading-base {
323+
line-height: 201rem;
324+
}
325+
.leading-md {
326+
line-height: 101rem;
327+
}
328+
.leading-xl {
329+
line-height: 201rem;
330+
}
331+
"
332+
`)
333+
})
334+
})
335+
336+
describe('theme overrides order', () => {
337+
test('user theme > js config > default theme', async ({ expect }) => {
338+
let input = css`
339+
@theme default {
340+
--color-red: red;
341+
}
342+
@theme {
343+
--color-blue: blue;
344+
}
345+
@tailwind utilities;
346+
@config "./config.js";
347+
`
348+
349+
let compiler = await compile(input, {
350+
loadConfig: async () => ({
351+
theme: {
352+
extend: {
353+
colors: {
354+
red: 'very-red',
355+
blue: 'very-blue',
356+
},
357+
},
358+
},
359+
}),
360+
})
361+
362+
expect(compiler.build(['bg-red', 'bg-blue'])).toMatchInlineSnapshot(`
363+
":root {
364+
--color-blue: blue;
365+
}
366+
.bg-blue {
367+
background-color: var(--color-blue, blue);
368+
}
369+
.bg-red {
370+
background-color: very-red;
371+
}
372+
"
373+
`)
374+
})
375+
376+
test('user theme > js config > default theme (with nested object)', async ({ expect }) => {
377+
let input = css`
378+
@theme default {
379+
--color-slate-100: #000100;
380+
--color-slate-200: #000200;
381+
--color-slate-300: #000300;
382+
}
383+
@theme {
384+
--color-slate-400: #100400;
385+
--color-slate-500: #100500;
386+
}
387+
@tailwind utilities;
388+
@config "./config.js";
389+
@plugin "./plugin.js";
390+
`
391+
392+
let compiler = await compile(input, {
393+
loadConfig: async () => ({
394+
theme: {
395+
extend: {
396+
colors: {
397+
slate: {
398+
200: '#200200',
399+
400: '#200400',
400+
600: '#200600',
401+
},
402+
},
403+
},
404+
},
405+
}),
406+
407+
loadPlugin: async () => {
408+
return plugin(({ matchUtilities, theme }) => {
409+
matchUtilities(
410+
{
411+
'hover-bg': (value) => {
412+
return {
413+
'&:hover': {
414+
backgroundColor: value,
415+
},
416+
}
417+
},
418+
},
419+
{ values: flattenColorPalette(theme('colors')) },
420+
)
421+
})
422+
},
423+
})
424+
425+
expect(
426+
compiler.build([
427+
'bg-slate-100',
428+
'bg-slate-200',
429+
'bg-slate-300',
430+
'bg-slate-400',
431+
'bg-slate-500',
432+
'bg-slate-600',
433+
'hover-bg-slate-100',
434+
'hover-bg-slate-200',
435+
'hover-bg-slate-300',
436+
'hover-bg-slate-400',
437+
'hover-bg-slate-500',
438+
'hover-bg-slate-600',
439+
]),
440+
).toMatchInlineSnapshot(`
441+
":root {
442+
--color-slate-100: #000100;
443+
--color-slate-300: #000300;
444+
--color-slate-400: #100400;
445+
--color-slate-500: #100500;
446+
}
447+
.bg-slate-100 {
448+
background-color: var(--color-slate-100, #000100);
449+
}
450+
.bg-slate-200 {
451+
background-color: #200200;
452+
}
453+
.bg-slate-300 {
454+
background-color: var(--color-slate-300, #000300);
455+
}
456+
.bg-slate-400 {
457+
background-color: var(--color-slate-400, #100400);
458+
}
459+
.bg-slate-500 {
460+
background-color: var(--color-slate-500, #100500);
461+
}
462+
.bg-slate-600 {
463+
background-color: #200600;
464+
}
465+
.hover-bg-slate-100 {
466+
&:hover {
467+
background-color: #000100;
468+
}
469+
}
470+
.hover-bg-slate-200 {
471+
&:hover {
472+
background-color: #200200;
473+
}
474+
}
475+
.hover-bg-slate-300 {
476+
&:hover {
477+
background-color: #000300;
478+
}
479+
}
480+
.hover-bg-slate-400 {
481+
&:hover {
482+
background-color: #100400;
483+
}
484+
}
485+
.hover-bg-slate-500 {
486+
&:hover {
487+
background-color: #100500;
488+
}
489+
}
490+
.hover-bg-slate-600 {
491+
&:hover {
492+
background-color: #200600;
493+
}
494+
}
495+
"
496+
`)
497+
})
498+
})
499+
233500
describe('default font family compatibility', () => {
234501
test('overriding `fontFamily.sans` sets `--default-font-family`', async ({ expect }) => {
235502
let input = css`

packages/tailwindcss/src/compat/config/deep-merge.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ export function isPlainObject<T>(value: T): value is T & Record<keyof T, unknown
1010
export function deepMerge<T extends object>(
1111
target: T,
1212
sources: (Partial<T> | null | undefined)[],
13-
customizer: (a: any, b: any) => any,
13+
customizer: (a: any, b: any, keypath: (keyof T)[]) => any,
14+
parentPath: (keyof T)[] = [],
1415
) {
1516
type Key = keyof T
1617
type Value = T[Key]
@@ -21,14 +22,20 @@ export function deepMerge<T extends object>(
2122
}
2223

2324
for (let k of Reflect.ownKeys(source) as Key[]) {
24-
let merged = customizer(target[k], source[k])
25+
let currentParentPath = [...parentPath, k]
26+
let merged = customizer(target[k], source[k], currentParentPath)
2527

2628
if (merged !== undefined) {
2729
target[k] = merged
2830
} else if (!isPlainObject(target[k]) || !isPlainObject(source[k])) {
2931
target[k] = source[k] as Value
3032
} else {
31-
target[k] = deepMerge({}, [target[k], source[k]], customizer) as Value
33+
target[k] = deepMerge(
34+
{},
35+
[target[k], source[k]],
36+
customizer,
37+
currentParentPath as any,
38+
) as Value
3239
}
3340
}
3441
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
type Colors = {
2+
[key: string | number]: string | Colors
3+
}
4+
5+
export function flattenColorPalette(colors: Colors) {
6+
let result: Record<string, string> = {}
7+
8+
for (let [root, children] of Object.entries(colors ?? {})) {
9+
if (typeof children === 'object' && children !== null) {
10+
for (let [parent, value] of Object.entries(flattenColorPalette(children))) {
11+
result[`${root}${parent === 'DEFAULT' ? '' : `-${parent}`}`] = value
12+
}
13+
} else {
14+
result[root] = children
15+
}
16+
}
17+
18+
return result
19+
}

0 commit comments

Comments
 (0)