Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Next Next commit
Move opacity modifier support into CSS theme() function
  • Loading branch information
philipp-spiess committed Sep 5, 2024
commit bfb94eea7fa4410b3c63ef31731837c73d976edc
2 changes: 1 addition & 1 deletion packages/tailwindcss/src/functions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
17 changes: 1 addition & 16 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,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)

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