diff --git a/packages/tailwindcss/src/plugin-api.test.ts b/packages/tailwindcss/src/plugin-api.test.ts index a9159c86b..3c8b68aa6 100644 --- a/packages/tailwindcss/src/plugin-api.test.ts +++ b/packages/tailwindcss/src/plugin-api.test.ts @@ -101,10 +101,55 @@ describe('theme', async () => { }) expect(compiler.build(['scrollbar-red-500', 'scrollbar-russet-700'])).toMatchInlineSnapshot(` - ".scrollbar-russet-700 { + ".scrollbar-red-500 { + scrollbar-color: var(--color-red-500, #ef4444); + } + .scrollbar-russet-700 { scrollbar-color: #7a4724; } " `) }) + + test('plugin theme values can reference legacy theme keys that have been replaced with bare value support', async ({ + expect, + }) => { + let input = css` + @tailwind utilities; + @plugin "my-plugin"; + ` + + let compiler = await compile(input, { + loadPlugin: async () => { + return plugin( + function ({ matchUtilities, theme }) { + matchUtilities( + { + 'animate-duration': (value) => ({ 'animation-duration': value }), + }, + { + values: theme('animationDuration'), + }, + ) + }, + { + theme: { + extend: { + animationDuration: ({ theme }: { theme: (path: string) => any }) => ({ + ...theme('transitionDuration'), + }), + }, + }, + }, + ) + }, + }) + + expect(compiler.build(['animate-duration-316'])).toMatchInlineSnapshot(` + ".animate-duration-316 { + animation-duration: 316ms; + } + " + `) + }) }) diff --git a/packages/tailwindcss/src/plugin-api.ts b/packages/tailwindcss/src/plugin-api.ts index 6dd3a6026..2890329b0 100644 --- a/packages/tailwindcss/src/plugin-api.ts +++ b/packages/tailwindcss/src/plugin-api.ts @@ -1,7 +1,6 @@ import { substituteAtApply } from './apply' import { objectToAst, rule, type AstNode, type CssInJs } from './ast' import type { DesignSystem } from './design-system' -import type { ThemeKey } from './theme' import { withAlpha, withNegative } from './utilities' import { inferDataType } from './utils/infer-data-type' import { segment } from './utils/segment' @@ -26,7 +25,9 @@ export type PluginAPI = { options?: Partial<{ type: string | string[] supportsNegativeValues: boolean - values: Record + values: Record & { + [BARE_VALUE]?: (value: string) => string | null | undefined + } modifiers: 'any' | Record }>, ): void @@ -132,7 +133,7 @@ export function buildPluginApi( let isColor = types.includes('color') // Resolve the candidate value - let value: string | null + let value: string | null = null { let values = options?.values ?? {} @@ -152,8 +153,10 @@ export function buildPluginApi( value = values.DEFAULT ?? null } else if (candidate.value.kind === 'arbitrary') { value = candidate.value.value - } else { - value = values[candidate.value.value] ?? null + } else if (values[candidate.value.value]) { + value = values[candidate.value.value] + } else if (values[BARE_VALUE]) { + value = values[BARE_VALUE](candidate.value.value) ?? null } } @@ -199,51 +202,30 @@ export function buildPluginApi( } }, theme(path) { - let parts = segment(path, '.') + if (path === 'colors') { + let result: Record = resolvedConfig.theme?.colors ?? {} + for (let [key] of designSystem.theme.namespace('--color')) { + result[key ?? 'DEFAULT'] = designSystem.theme.resolve(key, ['--color'])! + } + + return result + } + + let parts = segment(path, '.') let result = resolvedConfig.theme ?? {} for (let part of parts) { result = result?.[part] } - if ( - [ - 'accentColor', - 'backgroundColor', - 'borderColor', - 'boxShadowColor', - 'caretColor', - 'divideColor', - 'fill', - 'gradientColorStops', - 'outlineColor', - 'placeholderColor', - 'ringColor', - 'ringOffsetColor', - 'stroke', - 'textColor', - 'textDecorationColor', - ].includes(path) - ) { - // Convert camelCase to kebab-case: - // https://github.com/postcss/postcss-js/blob/b3db658b932b42f6ac14ca0b1d50f50c4569805b/parser.js#L30-L35 - let newPath = `--${path.replace(/([A-Z])/g, '-$1').toLowerCase()}` as ThemeKey - - for (let [key] of designSystem.theme.namespace('--color')) { - result[key ?? 'DEFAULT'] = designSystem.theme.resolve(key, ['--color']) - } - - for (let [key] of designSystem.theme.namespace(newPath)) { - result[key ?? 'DEFAULT'] = designSystem.theme.resolve(key, [newPath]) - } - } - return result }, } } +const BARE_VALUE = Symbol('BARE_VALUE') + export function registerPlugins(plugins: Plugin[], designSystem: DesignSystem, ast: AstNode[]) { let pluginObjects = [] @@ -271,7 +253,23 @@ export function registerPlugins(plugins: Plugin[], designSystem: DesignSystem, a for (let { config } of pluginObjects) { for (let key in config?.theme?.extend) { resolvedConfig.theme[key] ??= {} - Object.assign(resolvedConfig.theme[key], config?.theme.extend[key]) + let value = config.theme.extend[key] + if (value instanceof Function) { + value = value({ + theme(path: string) { + if (path === 'transitionDuration') { + return { + [BARE_VALUE]: (value: string) => { + if (!Number.isNaN(Number(value))) { + return `${value}ms` + } + }, + } + } + }, + }) + } + Object.assign(resolvedConfig.theme[key], value) } }