diff --git a/CHANGELOG.md b/CHANGELOG.md index 98231ff237fb..a84ace364968 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/packages/@tailwindcss-upgrade/src/codemods/css/migrate-media-screen.ts b/packages/@tailwindcss-upgrade/src/codemods/css/migrate-media-screen.ts index f4526209d89a..c10e9ee99094 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/css/migrate-media-screen.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/css/migrate-media-screen.ts @@ -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 || {} diff --git a/packages/tailwindcss/src/compat/apply-compat-hooks.ts b/packages/tailwindcss/src/compat/apply-compat-hooks.ts index 78c7d707877e..2f88ed800307 100644 --- a/packages/tailwindcss/src/compat/apply-compat-hooks.ts +++ b/packages/tailwindcss/src/compat/apply-compat-hooks.ts @@ -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 } } diff --git a/packages/tailwindcss/src/compat/config.test.ts b/packages/tailwindcss/src/compat/config.test.ts index 196115d6357e..8256f663ca17 100644 --- a/packages/tailwindcss/src/compat/config.test.ts +++ b/packages/tailwindcss/src/compat/config.test.ts @@ -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: '', } }, },