From bfb94eea7fa4410b3c63ef31731837c73d976edc Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Thu, 5 Sep 2024 10:22:54 +0200 Subject: [PATCH 1/5] Move opacity modifier support into CSS `theme()` function --- packages/tailwindcss/src/functions.test.ts | 2 +- packages/tailwindcss/src/functions.ts | 17 +------- packages/tailwindcss/src/plugin-api.test.ts | 45 ++++++++++++++++++++- packages/tailwindcss/src/theme-fn.ts | 40 +++++++++++++----- 4 files changed, 76 insertions(+), 28 deletions(-) diff --git a/packages/tailwindcss/src/functions.test.ts b/packages/tailwindcss/src/functions.test.ts index f33be7fc0934..b6157bb157aa 100644 --- a/packages/tailwindcss/src/functions.test.ts +++ b/packages/tailwindcss/src/functions.test.ts @@ -436,7 +436,7 @@ describe('theme function', () => { `) }) - test('theme(--color-red-500 / 50%)', async () => { + test.only('theme(--color-red-500 / 50%)', async () => { expect( await compileCss(css` @theme { diff --git a/packages/tailwindcss/src/functions.ts b/packages/tailwindcss/src/functions.ts index 5938b8497307..32e18c4fce37 100644 --- a/packages/tailwindcss/src/functions.ts +++ b/packages/tailwindcss/src/functions.ts @@ -1,6 +1,5 @@ import { walk, type AstNode } from './ast' import type { PluginAPI } from './plugin-api' -import { withAlpha } from './utilities' import * as ValueParser from './value-parser' import { type ValueAstNode } from './value-parser' @@ -77,16 +76,6 @@ function cssThemeFn( path: string, fallbackValues: ValueAstNode[], ): ValueAstNode[] { - let modifier: string | null = null - // Extract an eventual modifier from the path. e.g.: - // - "colors.red.500 / 50%" -> "50%" - // - "foo/bar/baz/50%" -> "50%" - let lastSlash = path.lastIndexOf('/') - if (lastSlash !== -1) { - modifier = path.slice(lastSlash + 1).trim() - path = path.slice(0, lastSlash).trim() - } - let resolvedValue: string | null = null let themeValue = pluginApi.theme(path) @@ -107,14 +96,10 @@ function cssThemeFn( if (!resolvedValue) { throw new Error( - `Could not resolve value for theme function: \`theme(${path}${modifier ? ` / ${modifier}` : ''})\`. Consider checking if the path is correct or provide a fallback value to silence this error.`, + `Could not resolve value for theme function: \`theme(${path})\`. Consider checking if the path is correct or provide a fallback value to silence this error.`, ) } - if (modifier) { - resolvedValue = withAlpha(resolvedValue, modifier) - } - // We need to parse the values recursively since this can resolve with another // `theme()` function definition. return ValueParser.parse(resolvedValue) diff --git a/packages/tailwindcss/src/plugin-api.test.ts b/packages/tailwindcss/src/plugin-api.test.ts index d911b09e5610..087dea0bc433 100644 --- a/packages/tailwindcss/src/plugin-api.test.ts +++ b/packages/tailwindcss/src/plugin-api.test.ts @@ -208,6 +208,49 @@ describe('theme', async () => { `) }) + test('plugin theme can have opacity modifiers', async ({ expect }) => { + let input = css` + @tailwind utilities; + @theme { + --color-red-500: #ef4444; + } + @plugin "my-plugin"; + ` + + let compiler = await compile(input, { + loadPlugin: async () => { + return plugin(function ({ addUtilities, theme }) { + addUtilities({ + '.percentage': { + color: theme('colors.red.500 / 50%'), + }, + '.fraction': { + color: theme('colors.red.500 / 0.5'), + }, + '.variable': { + color: theme('colors.red.500 / var(--opacity)'), + }, + }) + }) + }, + }) + + expect(compiler.build(['percentage', 'fraction', 'variable'])).toMatchInlineSnapshot(` + ".fraction { + color: color-mix(in srgb, #ef4444 50%, transparent); + } + .percentage { + color: color-mix(in srgb, #ef4444 50%, transparent); + } + .variable { + color: color-mix(in srgb, #ef4444 calc(var(--opacity) * 100%), transparent); + } + :root { + --color-red-500: #ef4444; + } + " + `) + }) test('theme value functions are resolved correctly regardless of order', async ({ expect }) => { let input = css` @tailwind utilities; @@ -354,7 +397,7 @@ describe('theme', async () => { `) }) - test('CSS theme values are mreged with JS theme values', async ({ expect }) => { + test('CSS theme values are merged with JS theme values', async ({ expect }) => { let input = css` @tailwind utilities; @plugin "my-plugin"; diff --git a/packages/tailwindcss/src/theme-fn.ts b/packages/tailwindcss/src/theme-fn.ts index 08fd8d6e1eeb..a5e73d30be97 100644 --- a/packages/tailwindcss/src/theme-fn.ts +++ b/packages/tailwindcss/src/theme-fn.ts @@ -2,6 +2,7 @@ import { deepMerge } from './compat/config/deep-merge' import type { UserConfig } from './compat/config/types' import type { DesignSystem } from './design-system' import type { Theme, ThemeKey } from './theme' +import { withAlpha } from './utilities' import { DefaultMap } from './utils/default-map' import { toKeyPath } from './utils/to-key-path' @@ -11,21 +12,40 @@ export function createThemeFn( resolveValue: (value: any) => any, ) { return function theme(path: string, defaultValue?: any) { - let keypath = toKeyPath(path) - let cssValue = readFromCss(designSystem.theme, keypath) - - if (typeof cssValue !== 'object') { - return cssValue + // Extract an eventual modifier from the path. e.g.: + // - "colors.red.500 / 50%" -> "50%" + // - "foo/bar/baz/50%" -> "50%" + let lastSlash = path.lastIndexOf('/') + let modifier: string | null = null + if (lastSlash !== -1) { + modifier = path.slice(lastSlash + 1).trim() + path = path.slice(0, lastSlash).trim() } - let configValue = resolveValue(get(configTheme() ?? {}, keypath) ?? null) + let resolvedValue = (() => { + let keypath = toKeyPath(path) + let cssValue = readFromCss(designSystem.theme, keypath) + + if (typeof cssValue !== 'object') { + return cssValue + } + + let configValue = resolveValue(get(configTheme() ?? {}, keypath) ?? null) + + if (configValue !== null && typeof configValue === 'object' && !Array.isArray(configValue)) { + return deepMerge({}, [configValue, cssValue], (_, b) => b) + } + + // Values from CSS take precedence over values from the config + return cssValue ?? configValue + })() - if (configValue !== null && typeof configValue === 'object' && !Array.isArray(configValue)) { - return deepMerge({}, [configValue, cssValue], (_, b) => b) + // Apply the opacity modifier if present + if (modifier && typeof resolvedValue === 'string') { + resolvedValue = withAlpha(resolvedValue, modifier) } - // Values from CSS take precedence over values from the config - return cssValue ?? configValue ?? defaultValue + return resolvedValue ?? defaultValue } } From 73368bdf81b08b8fe2c5d91efc25dad44960cd59 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Thu, 5 Sep 2024 10:26:44 +0200 Subject: [PATCH 2/5] Add changelog entry --- CHANGELOG.md | 4 +++- packages/tailwindcss/src/functions.test.ts | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dc804208f3aa..9da1173487c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -- Nothing yet! +### Added + +- Add opacity modifier support to the `theme()` function in plugins ([#14348](https://github.com/tailwindlabs/tailwindcss/pull/14348)) ## [4.0.0-alpha.22] - 2024-09-04 diff --git a/packages/tailwindcss/src/functions.test.ts b/packages/tailwindcss/src/functions.test.ts index b6157bb157aa..f33be7fc0934 100644 --- a/packages/tailwindcss/src/functions.test.ts +++ b/packages/tailwindcss/src/functions.test.ts @@ -436,7 +436,7 @@ describe('theme function', () => { `) }) - test.only('theme(--color-red-500 / 50%)', async () => { + test('theme(--color-red-500 / 50%)', async () => { expect( await compileCss(css` @theme { From 00e233f291fd9c20ed642076e26f27b9a430512b Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Thu, 5 Sep 2024 10:28:28 +0200 Subject: [PATCH 3/5] Avoid second `Array.isArray` call --- packages/tailwindcss/src/functions.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/tailwindcss/src/functions.ts b/packages/tailwindcss/src/functions.ts index 32e18c4fce37..30005e3c1b5e 100644 --- a/packages/tailwindcss/src/functions.ts +++ b/packages/tailwindcss/src/functions.ts @@ -79,12 +79,13 @@ function cssThemeFn( let resolvedValue: string | null = null let themeValue = pluginApi.theme(path) - if (Array.isArray(themeValue) && themeValue.length === 2) { + let isArray = Array.isArray(themeValue) + if (isArray && themeValue.length === 2) { // When a tuple is returned, return the first element resolvedValue = themeValue[0] // We otherwise only ignore string values here, objects (and namespace maps) // are treated as non-resolved values for the CSS `theme()` function. - } else if (Array.isArray(themeValue)) { + } else if (isArray) { resolvedValue = themeValue.join(', ') } else if (typeof themeValue === 'string') { resolvedValue = themeValue From 8e83b35012470e7ccf1ea712006b848602fa897e Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Thu, 5 Sep 2024 11:23:32 +0200 Subject: [PATCH 4/5] Clarify comment --- packages/tailwindcss/src/functions.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/tailwindcss/src/functions.ts b/packages/tailwindcss/src/functions.ts index 30005e3c1b5e..6dd68bab2d8c 100644 --- a/packages/tailwindcss/src/functions.ts +++ b/packages/tailwindcss/src/functions.ts @@ -83,11 +83,12 @@ function cssThemeFn( if (isArray && themeValue.length === 2) { // When a tuple is returned, return the first element resolvedValue = themeValue[0] - // We otherwise only ignore string values here, objects (and namespace maps) - // are treated as non-resolved values for the CSS `theme()` function. } else if (isArray) { + // Arrays get serialized into a comma-separated lists resolvedValue = themeValue.join(', ') } else if (typeof themeValue === 'string') { + // Otherwise only allow string values here, objects (and namespace maps) + // are treated as non-resolved values for the CSS `theme()` function. resolvedValue = themeValue } From b1adb0347061dd22ca21d78986222b00b792e186 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Thu, 5 Sep 2024 11:26:01 +0200 Subject: [PATCH 5/5] Clarify file names between CSS and theme function files --- .../src/compat/config/resolve-config.ts | 2 +- .../{theme-fn.ts => compat/plugin-functions.ts} | 14 +++++++------- .../{functions.test.ts => css-functions.test.ts} | 0 .../src/{functions.ts => css-functions.ts} | 0 packages/tailwindcss/src/index.ts | 2 +- packages/tailwindcss/src/plugin-api.ts | 2 +- 6 files changed, 10 insertions(+), 10 deletions(-) rename packages/tailwindcss/src/{theme-fn.ts => compat/plugin-functions.ts} (93%) rename packages/tailwindcss/src/{functions.test.ts => css-functions.test.ts} (100%) rename packages/tailwindcss/src/{functions.ts => css-functions.ts} (100%) diff --git a/packages/tailwindcss/src/compat/config/resolve-config.ts b/packages/tailwindcss/src/compat/config/resolve-config.ts index 85dcb03762b7..0add9ccd7ecb 100644 --- a/packages/tailwindcss/src/compat/config/resolve-config.ts +++ b/packages/tailwindcss/src/compat/config/resolve-config.ts @@ -1,6 +1,6 @@ import type { DesignSystem } from '../../design-system' import type { PluginWithConfig } from '../../plugin-api' -import { createThemeFn } from '../../theme-fn' +import { createThemeFn } from '../plugin-functions' import { deepMerge, isPlainObject } from './deep-merge' import { type ResolvedConfig, diff --git a/packages/tailwindcss/src/theme-fn.ts b/packages/tailwindcss/src/compat/plugin-functions.ts similarity index 93% rename from packages/tailwindcss/src/theme-fn.ts rename to packages/tailwindcss/src/compat/plugin-functions.ts index a5e73d30be97..ef0a6d0c8b59 100644 --- a/packages/tailwindcss/src/theme-fn.ts +++ b/packages/tailwindcss/src/compat/plugin-functions.ts @@ -1,10 +1,10 @@ -import { deepMerge } from './compat/config/deep-merge' -import type { UserConfig } from './compat/config/types' -import type { DesignSystem } from './design-system' -import type { Theme, ThemeKey } from './theme' -import { withAlpha } from './utilities' -import { DefaultMap } from './utils/default-map' -import { toKeyPath } from './utils/to-key-path' +import type { DesignSystem } from '../design-system' +import type { Theme, ThemeKey } from '../theme' +import { withAlpha } from '../utilities' +import { DefaultMap } from '../utils/default-map' +import { toKeyPath } from '../utils/to-key-path' +import { deepMerge } from './config/deep-merge' +import type { UserConfig } from './config/types' export function createThemeFn( designSystem: DesignSystem, diff --git a/packages/tailwindcss/src/functions.test.ts b/packages/tailwindcss/src/css-functions.test.ts similarity index 100% rename from packages/tailwindcss/src/functions.test.ts rename to packages/tailwindcss/src/css-functions.test.ts diff --git a/packages/tailwindcss/src/functions.ts b/packages/tailwindcss/src/css-functions.ts similarity index 100% rename from packages/tailwindcss/src/functions.ts rename to packages/tailwindcss/src/css-functions.ts diff --git a/packages/tailwindcss/src/index.ts b/packages/tailwindcss/src/index.ts index e31a82f2227c..985d6899c1e0 100644 --- a/packages/tailwindcss/src/index.ts +++ b/packages/tailwindcss/src/index.ts @@ -3,9 +3,9 @@ import { substituteAtApply } from './apply' import { comment, decl, rule, toCss, walk, WalkAction, type Rule } from './ast' import type { UserConfig } from './compat/config/types' import { compileCandidates } from './compile' +import { substituteFunctions, THEME_FUNCTION_INVOCATION } from './css-functions' import * as CSS from './css-parser' import { buildDesignSystem, type DesignSystem } from './design-system' -import { substituteFunctions, THEME_FUNCTION_INVOCATION } from './functions' import { registerPlugins, type CssPluginOptions, type Plugin } from './plugin-api' import { Theme } from './theme' import { segment } from './utils/segment' diff --git a/packages/tailwindcss/src/plugin-api.ts b/packages/tailwindcss/src/plugin-api.ts index 53732e37b740..38b2081422d2 100644 --- a/packages/tailwindcss/src/plugin-api.ts +++ b/packages/tailwindcss/src/plugin-api.ts @@ -6,8 +6,8 @@ import { createCompatConfig } from './compat/config/create-compat-config' import { resolveConfig, type ConfigFile } from './compat/config/resolve-config' import type { ResolvedConfig, UserConfig } from './compat/config/types' import { darkModePlugin } from './compat/dark-mode' +import { createThemeFn } from './compat/plugin-functions' import type { DesignSystem } from './design-system' -import { createThemeFn } from './theme-fn' import { withAlpha, withNegative } from './utilities' import { inferDataType } from './utils/infer-data-type' import { segment } from './utils/segment'