Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion packages/tailwindcss/src/compat/config/resolve-config.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,31 +1,51 @@
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 { 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,
configTheme: () => UserConfig['theme'],
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
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,27 +76,19 @@ 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) {
// 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
}

Expand All @@ -107,14 +98,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
2 changes: 1 addition & 1 deletion packages/tailwindcss/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
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
2 changes: 1 addition & 1 deletion packages/tailwindcss/src/plugin-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down