Work on theme value merging
This commit is contained in:
parent
f2b7fbae6b
commit
bb89c7c6df
4 changed files with 214 additions and 2 deletions
|
|
@ -195,3 +195,37 @@ test('Config files can affect the theme', async ({ expect }) => {
|
|||
"
|
||||
`)
|
||||
})
|
||||
|
||||
test('Variants in CSS overwrite variants from plugins', async ({ expect }) => {
|
||||
let input = css`
|
||||
@tailwind utilities;
|
||||
@config "./config.js";
|
||||
@variant dark (&:is(.my-dark));
|
||||
@variant light (&:is(.my-light));
|
||||
`
|
||||
|
||||
let compiler = await compile(input, {
|
||||
loadConfig: async () => ({
|
||||
darkMode: ['variant', '&:is(.dark)'],
|
||||
plugins: [
|
||||
plugin(function ({ addVariant }) {
|
||||
addVariant('light', '&:is(.light)')
|
||||
}),
|
||||
],
|
||||
}),
|
||||
})
|
||||
|
||||
expect(compiler.build(['dark:underline', 'light:underline'])).toMatchInlineSnapshot(`
|
||||
".dark\\:underline {
|
||||
&:is(.my-dark) {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
}
|
||||
.light\\:underline {
|
||||
&:is(.my-light) {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
|
|
|||
47
packages/tailwindcss/src/compat/merge-into-theme.test.ts
Normal file
47
packages/tailwindcss/src/compat/merge-into-theme.test.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import { test } from 'vitest'
|
||||
import { Theme } from '../theme'
|
||||
import { mergeIntoTheme } from './merge-into-theme'
|
||||
|
||||
test('Config values can be merged into the theme', ({ expect }) => {
|
||||
let theme = new Theme()
|
||||
|
||||
mergeIntoTheme(theme, {
|
||||
colors: {
|
||||
primary: '#c0ffee',
|
||||
red: {
|
||||
500: 'red',
|
||||
},
|
||||
},
|
||||
|
||||
fontSize: {
|
||||
sm: '0.875rem',
|
||||
base: [
|
||||
'1rem',
|
||||
{
|
||||
lineHeight: '1.5',
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
expect(theme.resolve('primary', ['--colors'] as any[])).toEqual('#c0ffee')
|
||||
expect(theme.resolve('red-500', ['--colors'] as any[])).toEqual('red')
|
||||
expect(theme.resolve('sm', ['--font-size'] as any[])).toEqual('0.875rem')
|
||||
expect(theme.resolve('base', ['--font-size'] as any[])).toEqual('1rem')
|
||||
expect(theme.get(['--font-size-base--line-height'] as any[])).toEqual('1.5')
|
||||
})
|
||||
|
||||
test('Existing values are not overwritten', ({ expect }) => {
|
||||
let theme = new Theme()
|
||||
theme.add('--colors-primary', '#c0ffee')
|
||||
|
||||
mergeIntoTheme(theme, {
|
||||
colors: {
|
||||
primary: '#c0ffee',
|
||||
secondary: '#bada55',
|
||||
},
|
||||
})
|
||||
|
||||
expect(theme.resolve('primary', ['--colors'] as any[])).toEqual('var(--colors-primary, #c0ffee)')
|
||||
expect(theme.resolve('secondary', ['--colors'] as any[])).toEqual('#bada55')
|
||||
})
|
||||
125
packages/tailwindcss/src/compat/merge-into-theme.ts
Normal file
125
packages/tailwindcss/src/compat/merge-into-theme.ts
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
import type { DesignSystem } from '../design-system'
|
||||
import { resolveConfig, type ConfigFile } from './config/resolve-config'
|
||||
import type { ResolvedConfig } from './config/types'
|
||||
|
||||
export function mergeIntoTheme(designSystem: DesignSystem, configs: ConfigFile[]) {
|
||||
let theme = resolveConfig(designSystem, configs).theme
|
||||
|
||||
for (let [path, value] of themeableValues(theme)) {
|
||||
let name = keyPathToCssProperty(path)
|
||||
|
||||
designSystem.theme.add(`--${name}`, value as any, {
|
||||
isInline: true,
|
||||
isReference: false,
|
||||
})
|
||||
}
|
||||
|
||||
return theme
|
||||
}
|
||||
|
||||
function themeableValues(config: ResolvedConfig['theme']): [string[], unknown][] {
|
||||
let toAdd: [string[], unknown][] = []
|
||||
|
||||
walk(config as any, [], (value, path) => {
|
||||
if (isValidThemePrimitive(value)) {
|
||||
toAdd.push([path, value])
|
||||
|
||||
return WalkAction.Skip
|
||||
}
|
||||
|
||||
if (isValidThemeTuple(value)) {
|
||||
toAdd.push([path, value[0]])
|
||||
|
||||
for (let key of Reflect.ownKeys(value[1]) as string[]) {
|
||||
toAdd.push([[...path, `-${key}`], value[1][key]])
|
||||
}
|
||||
|
||||
return WalkAction.Skip
|
||||
}
|
||||
})
|
||||
|
||||
return toAdd
|
||||
}
|
||||
|
||||
function keyPathToCssProperty(path: string[]) {
|
||||
return (
|
||||
path
|
||||
// [1] should move into the nested object tuple. To create the CSS variable
|
||||
// name for this, we replace it with an empty string that will result in two
|
||||
// subsequent dashes when joined.
|
||||
.map((path) => (path === '1' ? '' : path))
|
||||
|
||||
// Resolve the key path to a CSS variable segment
|
||||
.map((part) =>
|
||||
part
|
||||
.replaceAll('.', '_')
|
||||
.replace(/([a-z])([A-Z])/g, (_, a, b) => `${a}-${b.toLowerCase()}`),
|
||||
)
|
||||
|
||||
// Remove the `DEFAULT` key at the end of a path
|
||||
// We're reading from CSS anyway so it'll be a string
|
||||
.filter((part, index) => part !== 'DEFAULT' || index !== path.length - 1)
|
||||
.join('-')
|
||||
)
|
||||
}
|
||||
|
||||
function isValidThemePrimitive(value: unknown) {
|
||||
return typeof value === 'number' || typeof value === 'string'
|
||||
}
|
||||
|
||||
function isValidThemeTuple(value: unknown): value is [string, Record<string, string | number>] {
|
||||
// Check for tuple values of the form
|
||||
// `[string, Record<string, string | number>]`
|
||||
if (!Array.isArray(value)) return false
|
||||
if (value.length !== 2) return false
|
||||
|
||||
// A string or number as the "value"
|
||||
if (typeof value[0] !== 'string' && typeof value[0] !== 'number') return false
|
||||
|
||||
// An object as the nested theme values
|
||||
if (value[1] === undefined || value[1] === null) return false
|
||||
if (typeof value[1] !== 'object') return false
|
||||
|
||||
for (let key of Reflect.ownKeys(value[1])) {
|
||||
if (typeof key !== 'string') return false
|
||||
if (typeof value[1][key] !== 'string' && typeof value[1][key] !== 'number') return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
enum WalkAction {
|
||||
/** Continue walking, which is the default */
|
||||
Continue,
|
||||
|
||||
/** Skip visiting the children of this node */
|
||||
Skip,
|
||||
|
||||
/** Stop the walk entirely */
|
||||
Stop,
|
||||
}
|
||||
|
||||
function walk(
|
||||
obj: Record<string, unknown>,
|
||||
path: string[] = [],
|
||||
callback: (value: unknown, path: string[]) => WalkAction | void,
|
||||
) {
|
||||
for (let key of Reflect.ownKeys(obj) as string[]) {
|
||||
let value = obj[key]
|
||||
|
||||
if (value === undefined || value === null) {
|
||||
continue
|
||||
}
|
||||
|
||||
let keyPath = [...path, key]
|
||||
|
||||
let result = callback(value, keyPath) ?? WalkAction.Continue
|
||||
|
||||
if (result === WalkAction.Skip) continue
|
||||
if (result === WalkAction.Stop) break
|
||||
|
||||
if (!Array.isArray(value) && typeof value !== 'object') continue
|
||||
|
||||
walk(value as any, keyPath, callback)
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ 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 { mergeIntoTheme } from './compat/merge-into-theme'
|
||||
import type { DesignSystem } from './design-system'
|
||||
import { createThemeFn } from './theme-fn'
|
||||
import { withAlpha, withNegative } from './utilities'
|
||||
|
|
@ -351,10 +352,11 @@ export function registerPlugins(
|
|||
ast: AstNode[],
|
||||
configs: ConfigFile[],
|
||||
) {
|
||||
let userConfig = [{ config: { plugins } }, ...configs]
|
||||
|
||||
let resolvedConfig = resolveConfig(designSystem, [
|
||||
{ config: createCompatConfig(designSystem.theme) },
|
||||
...configs,
|
||||
{ config: { plugins } },
|
||||
...userConfig,
|
||||
{ config: { plugins: [darkModePlugin] } },
|
||||
])
|
||||
|
||||
|
|
@ -364,6 +366,10 @@ export function registerPlugins(
|
|||
handler(pluginApi)
|
||||
}
|
||||
|
||||
// Theme keys don't need to read from the compat config because core plugins
|
||||
// _already_ look through "fallback" theme keys
|
||||
mergeIntoTheme(designSystem, userConfig)
|
||||
|
||||
return {
|
||||
pluginApi,
|
||||
resolvedConfig,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue