diff --git a/packages/tailwindcss/src/index.ts b/packages/tailwindcss/src/index.ts index f324dfad8..d9726f657 100644 --- a/packages/tailwindcss/src/index.ts +++ b/packages/tailwindcss/src/index.ts @@ -14,6 +14,7 @@ import { import { compileCandidates } from './compile' import * as CSS from './css-parser' import { buildDesignSystem, type DesignSystem } from './design-system' +import { buildPluginApi } from './plugin-api' import { Theme } from './theme' import { segment } from './utils/segment' @@ -21,6 +22,7 @@ const IS_VALID_UTILITY_NAME = /^[a-z][a-zA-Z0-9/%._-]*$/ type PluginAPI = { addVariant(name: string, variant: string | string[] | CssInJs): void + theme(path: string, fallback?: any): any } type Plugin = (api: PluginAPI) => void @@ -280,6 +282,8 @@ export function compile( designSystem.variants.fromAst(name, objectToAst(variant)) } }, + + ...buildPluginApi(designSystem), } for (let plugin of plugins) { diff --git a/packages/tailwindcss/src/plugin-api.ts b/packages/tailwindcss/src/plugin-api.ts new file mode 100644 index 000000000..8260f0795 --- /dev/null +++ b/packages/tailwindcss/src/plugin-api.ts @@ -0,0 +1,53 @@ +import type { DesignSystem } from './design-system' + +export function buildPluginApi(designSystem: DesignSystem) { + return { + theme(path: string, fallback?: any) { + if (path.startsWith('--')) { + if (path.endsWith('-*')) { + return Object.fromEntries( + designSystem.theme.namespace(path.slice(0, -2) as any).entries(), + ) + } + + return designSystem.theme.resolveValue(null, [path] as any) ?? fallback ?? null + } + + let original = path + // Escape dots used inside square brackets + .replace(/\[(.*?)\]/g, (_, value) => `-${value.replace('.', '_')}`) + // Replace dots with dashes + .replace(/\./g, '-') + // Replace camelCase with dashes + .replace(/([a-z])([A-Z])/g, (_, a, b) => `${a}-${b.toLowerCase()}`) + + // Prepend with `--` to match CSS variables + original = `--${original}` + + let map = designSystem.theme.namespace(original as any) + + // Does the requested value exist in the theme + if (map.has(null)) { + // Yes, and there are multiple values in the requested theme namespace + if (map.size > 1) { + return { + DEFAULT: map.get(null), + ...Object.fromEntries(Array.from(map.entries()).filter(([key]) => key !== null)), + } + } + + // Nope, just the one + return map.get(null) + } + + // There is at least one value in the requested theme namespace + // but no default value + if (map.size > 0) { + return Object.fromEntries(map.entries()) + } + + return fallback ?? null + }, + } +} + diff --git a/packages/tailwindcss/src/utilities.test.ts b/packages/tailwindcss/src/utilities.test.ts index 4135b1cdd..af3a2a72b 100644 --- a/packages/tailwindcss/src/utilities.test.ts +++ b/packages/tailwindcss/src/utilities.test.ts @@ -15150,3 +15150,89 @@ describe('custom utilities', () => { ).toThrowError(/should be alphanumeric/) }) }) + +describe('theme function in plugins', () => { + let input = css` + @plugin "my-plugin"; + @theme reference { + --size-2_5: 2.5rem; + + --scrollbar-big: 20px; + --scrollbar-big-properties: auto-hidden; + + --scrollbar-color-light: white; + --scrollbar-color-dark: black; + }, + ` + + test('CSS property notation', () => { + expect.hasAssertions() + + compile(input, { + loadPlugin() { + return ({ theme }) => { + expect(theme('--scrollbar')).toEqual(null) + expect(theme('--scrollbar-*')).toEqual({ + big: '20px', + 'big-properties': 'auto-hidden', + 'color-dark': 'black', + 'color-light': 'white', + }) + + expect(theme('--scrollbar-big')).toEqual('20px') + } + }, + }) + }) + + test('legacy dot notation', () => { + expect.hasAssertions() + + compile(input, { + loadPlugin() { + return ({ theme }) => { + // Accessing via legacy dot notation + expect(theme('size.2_5')).toEqual('2.5rem') + expect(theme('scrollbar')).toEqual({ + big: '20px', + 'big-properties': 'auto-hidden', + 'color-dark': 'black', + 'color-light': 'white', + }) + + expect(theme('scrollbar.big')).toEqual({ + DEFAULT: '20px', + properties: 'auto-hidden', + }) + expect(theme('scrollbar.big.properties')).toEqual('auto-hidden') + + expect(theme('scrollbar.color')).toEqual({ + light: 'white', + dark: 'black', + }) + + expect(theme('scrollbar.foo', 'nope')).toEqual('nope') + expect(theme('somekey', 'nope')).toEqual('nope') + } + }, + }) + }) + + test('legacy square bracket notation', () => { + expect.hasAssertions() + + compile(input, { + loadPlugin() { + return ({ theme }) => { + expect(theme('size[2.5]')).toEqual('2.5rem') + + expect(theme('scrollbar[big]')).toEqual({ + DEFAULT: '20px', + properties: 'auto-hidden', + }) + expect(theme('scrollbar[big][properties]')).toEqual('auto-hidden') + } + }, + }) + }) +})