Skip to content
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

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

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

Expand Down
267 changes: 267 additions & 0 deletions packages/tailwindcss/src/compat/config.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { describe, test } from 'vitest'
import { compile } from '..'
import plugin from '../plugin'
import { flattenColorPalette } from './flatten-color-palette'

const css = String.raw

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

describe('theme callbacks', () => {
test('tuple values from the config overwrite `@theme default` tuple-ish values from the CSS theme', async ({
expect,
}) => {
let input = css`
@theme default {
--font-size-base: 0rem;
--font-size-base--line-height: 1rem;
--font-size-md: 0rem;
--font-size-md--line-height: 1rem;
--font-size-xl: 0rem;
--font-size-xl--line-height: 1rem;
}
@theme {
--font-size-base: 100rem;
--font-size-md--line-height: 101rem;
}
@tailwind utilities;
@config "./config.js";
`

let compiler = await compile(input, {
loadConfig: async () => ({
theme: {
extend: {
fontSize: {
base: ['200rem', { lineHeight: '201rem' }],
md: ['200rem', { lineHeight: '201rem' }],
xl: ['200rem', { lineHeight: '201rem' }],
},

// Direct access
lineHeight: ({ theme }) => ({
base: theme('fontSize.base[1].lineHeight'),
md: theme('fontSize.md[1].lineHeight'),
xl: theme('fontSize.xl[1].lineHeight'),
}),

// Tuple access
typography: ({ theme }) => ({
'[class~=lead-base]': {
fontSize: theme('fontSize.base')[0],
...theme('fontSize.base')[1],
},
'[class~=lead-md]': {
fontSize: theme('fontSize.md')[0],
...theme('fontSize.md')[1],
},
'[class~=lead-xl]': {
fontSize: theme('fontSize.xl')[0],
...theme('fontSize.xl')[1],
},
}),
},
},

plugins: [
plugin(function ({ addUtilities, theme }) {
addUtilities({
'.prose': {
...theme('typography'),
},
})
}),
],
}),
})

expect(compiler.build(['leading-base', 'leading-md', 'leading-xl', 'prose']))
.toMatchInlineSnapshot(`
":root {
--font-size-base: 100rem;
--font-size-md--line-height: 101rem;
}
.prose {
[class~=lead-base] {
font-size: 100rem;
line-height: 201rem;
}
[class~=lead-md] {
font-size: 200rem;
line-height: 101rem;
}
[class~=lead-xl] {
font-size: 200rem;
line-height: 201rem;
}
}
.leading-base {
line-height: 201rem;
}
.leading-md {
line-height: 101rem;
}
.leading-xl {
line-height: 201rem;
}
"
`)
})
})

describe('theme overrides order', () => {
test('user theme > js config > default theme', async ({ expect }) => {
let input = css`
@theme default {
--color-red: red;
}
@theme {
--color-blue: blue;
}
@tailwind utilities;
@config "./config.js";
`

let compiler = await compile(input, {
loadConfig: async () => ({
theme: {
extend: {
colors: {
red: 'very-red',
blue: 'very-blue',
},
},
},
}),
})

expect(compiler.build(['bg-red', 'bg-blue'])).toMatchInlineSnapshot(`
":root {
--color-blue: blue;
}
.bg-blue {
background-color: var(--color-blue, blue);
}
.bg-red {
background-color: very-red;
}
"
`)
})

test('user theme > js config > default theme (with nested object)', async ({ expect }) => {
let input = css`
@theme default {
--color-slate-100: #000100;
--color-slate-200: #000200;
--color-slate-300: #000300;
}
@theme {
--color-slate-400: #100400;
--color-slate-500: #100500;
}
@tailwind utilities;
@config "./config.js";
@plugin "./plugin.js";
`

let compiler = await compile(input, {
loadConfig: async () => ({
theme: {
extend: {
colors: {
slate: {
200: '#200200',
400: '#200400',
600: '#200600',
},
},
},
},
}),

loadPlugin: async () => {
return plugin(({ matchUtilities, theme }) => {
matchUtilities(
{
'hover-bg': (value) => {
return {
'&:hover': {
backgroundColor: value,
},
}
},
},
{ values: flattenColorPalette(theme('colors')) },
)
})
},
})

expect(
compiler.build([
'bg-slate-100',
'bg-slate-200',
'bg-slate-300',
'bg-slate-400',
'bg-slate-500',
'bg-slate-600',
'hover-bg-slate-100',
'hover-bg-slate-200',
'hover-bg-slate-300',
'hover-bg-slate-400',
'hover-bg-slate-500',
'hover-bg-slate-600',
]),
).toMatchInlineSnapshot(`
":root {
--color-slate-100: #000100;
--color-slate-300: #000300;
--color-slate-400: #100400;
--color-slate-500: #100500;
}
.bg-slate-100 {
background-color: var(--color-slate-100, #000100);
}
.bg-slate-200 {
background-color: #200200;
}
.bg-slate-300 {
background-color: var(--color-slate-300, #000300);
}
.bg-slate-400 {
background-color: var(--color-slate-400, #100400);
}
.bg-slate-500 {
background-color: var(--color-slate-500, #100500);
}
.bg-slate-600 {
background-color: #200600;
}
.hover-bg-slate-100 {
&:hover {
background-color: #000100;
}
}
.hover-bg-slate-200 {
&:hover {
background-color: #200200;
}
}
.hover-bg-slate-300 {
&:hover {
background-color: #000300;
}
}
.hover-bg-slate-400 {
&:hover {
background-color: #100400;
}
}
.hover-bg-slate-500 {
&:hover {
background-color: #100500;
}
}
.hover-bg-slate-600 {
&:hover {
background-color: #200600;
}
}
"
`)
})
})

describe('default font family compatibility', () => {
test('overriding `fontFamily.sans` sets `--default-font-family`', async ({ expect }) => {
let input = css`
Expand Down
13 changes: 10 additions & 3 deletions packages/tailwindcss/src/compat/config/deep-merge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ export function isPlainObject<T>(value: T): value is T & Record<keyof T, unknown
export function deepMerge<T extends object>(
target: T,
sources: (Partial<T> | null | undefined)[],
customizer: (a: any, b: any) => any,
customizer: (a: any, b: any, keypath: (keyof T)[]) => any,
parentPath: (keyof T)[] = [],
) {
type Key = keyof T
type Value = T[Key]
Expand All @@ -21,14 +22,20 @@ export function deepMerge<T extends object>(
}

for (let k of Reflect.ownKeys(source) as Key[]) {
let merged = customizer(target[k], source[k])
let currentParentPath = [...parentPath, k]
let merged = customizer(target[k], source[k], currentParentPath)

if (merged !== undefined) {
target[k] = merged
} else if (!isPlainObject(target[k]) || !isPlainObject(source[k])) {
target[k] = source[k] as Value
} else {
target[k] = deepMerge({}, [target[k], source[k]], customizer) as Value
target[k] = deepMerge(
{},
[target[k], source[k]],
customizer,
currentParentPath as any,
) as Value
}
}
}
Expand Down
19 changes: 19 additions & 0 deletions packages/tailwindcss/src/compat/flatten-color-palette.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
type Colors = {
[key: string | number]: string | Colors
}

export function flattenColorPalette(colors: Colors) {
Copy link
Member

Choose a reason for hiding this comment

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

TODO: We need to expose this to the end user for backwards compatibility. I believe it's tailwindcss/lib/util/flattenColorPalette

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Really unfortunate it needs to live at tailwindcss/lib/util/flattenColorPalette and not tailwindcss/flattenColorPalette.

Do you think we should do this in this PR or a followup?

Copy link
Member

Choose a reason for hiding this comment

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

Can that be something we configure in our exports config or copy over at build-time or something I wonder?

Copy link
Member

@RobinMalfait RobinMalfait Sep 6, 2024

Choose a reason for hiding this comment

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

I think we would still require a separate file to point to, but the paths should be fixable via exports I believe.

I also think that it should be a default export in the final result so that you can do const flattenColorPalette = require('...') which is what's currently used.

let result: Record<string, string> = {}

for (let [root, children] of Object.entries(colors ?? {})) {
if (typeof children === 'object' && children !== null) {
for (let [parent, value] of Object.entries(flattenColorPalette(children))) {
result[`${root}${parent === 'DEFAULT' ? '' : `-${parent}`}`] = value
}
} else {
result[root] = children
}
}

return result
}
Loading