Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move opacity modifier support into plugin theme() function #14348

Merged
merged 5 commits into from
Sep 5, 2024
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.
philipp-spiess marked this conversation as resolved.
Show resolved Hide resolved
} 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