Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
22 changes: 4 additions & 18 deletions packages/tailwindcss/src/functions.ts
Original file line number Diff line number Diff line change
@@ -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'

Expand Down Expand Up @@ -77,25 +76,16 @@ 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)

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
Expand All @@ -107,14 +97,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)
Expand Down
45 changes: 44 additions & 1 deletion packages/tailwindcss/src/plugin-api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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";
Expand Down
40 changes: 30 additions & 10 deletions packages/tailwindcss/src/theme-fn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand All @@ -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
}
}

Expand Down