Move opacity modifier support into plugin theme() function (#14348)
This PR moves support for opacity modifies from the CSS `theme()`
function into the plugin `theme()` implementation, this will allow
plugins to use this, too:
```ts
let plugin = 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)'),
},
})
})
}
```
There's a small behavioral change for the CSS `theme()` function. Since
tuples are resolved by default for the CSS `theme()` function only,
these will no longer have opacity applied to their first values. This is
probably fine given the reduced complexity as I don't expect the first
values of tuples to be colors and the fix would mean we would have to
parse the modifier in different places.
This commit is contained in:
parent
d9558bbc4c
commit
8f8803d855
8 changed files with 93 additions and 41 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -1,9 +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 { 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,
|
||||
|
|
@ -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 (configValue !== null && typeof configValue === 'object' && !Array.isArray(configValue)) {
|
||||
return deepMerge({}, [configValue, cssValue], (_, b) => b)
|
||||
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
|
||||
})()
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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,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
|
||||
}
|
||||
|
||||
|
|
@ -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)
|
||||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue