mirror of
https://github.com/tailwindlabs/tailwindcss.git
synced 2025-12-08 21:36:08 +00:00
Extract more backwards compatibility logic to compatibility layer (#14365)
I noticed a lot more backwards compatibility concerns had started leaking into core, especially around the `theme` function, so did a bit of work to try and pull that stuff out and into the compatibility layer. Now the core version of `theme` only handles CSS variables (like `--color-red-500`) and has no knowledge of the dot notation or how to upgrade it. Instead, we unconditionally override that function in the compatibility layer with a light version that _does_ know how to do the dot notation upgrade, and override that again with the very heavy/slow version that handles JS config objects only if plugins/JS configs are actually used. I've also renamed `registerPlugins` to `applyCompatibilityHooks` because the name was definitely a bit out of date given how much work it's doing now, and now call it unconditionally from core, leaving that function to do any conditional optimizations itself internally. Next steps I think would be to split up `plugin-api.ts` a bit and maybe make `applyCompatibilityHooks` its own file, and move both of those files into the `compat` folder so everything is truly isolated there. My goal with this stuff is that if/when we ever decide to drop backwards compatibility with these features in the future (maybe v5), that all we have to do is delete the one line of code that calls `applyCompatibilityHooks` in `index.ts`, and delete the `compat` folder and we're done. I could be convinced that this isn't a worthwhile goal if we feel it's making the codebase needlessly complex, so open to that discussion as well. --------- Co-authored-by: Adam Wathan <4323180+adamwathan@users.noreply.github.com> Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
This commit is contained in:
parent
8c6c291869
commit
8b0fff6edd
@ -63,7 +63,7 @@ export function applyConfigToTheme(designSystem: DesignSystem, configs: ConfigFi
|
||||
{
|
||||
let fontFamily = resolveThemeValue(theme.fontFamily.mono)
|
||||
if (fontFamily && designSystem.theme.hasDefault('--font-family-mono')) {
|
||||
designSystem.theme.add('--default-mono-font-family', 'theme(fontFamily.mono)', options)
|
||||
designSystem.theme.add('--default-mono-font-family', fontFamily, options)
|
||||
designSystem.theme.add(
|
||||
'--default-mono-font-feature-settings',
|
||||
resolveThemeValue(theme.fontFamily.mono, 'fontFeatureSettings') ?? 'normal',
|
||||
|
||||
@ -4,7 +4,7 @@ import { type ValueAstNode } from './value-parser'
|
||||
|
||||
export const THEME_FUNCTION_INVOCATION = 'theme('
|
||||
|
||||
type ResolveThemeValue = (path: string) => unknown
|
||||
type ResolveThemeValue = (path: string) => string | undefined
|
||||
|
||||
export function substituteFunctions(ast: AstNode[], resolveThemeValue: ResolveThemeValue) {
|
||||
walk(ast, (node) => {
|
||||
@ -39,7 +39,7 @@ export function substituteFunctionsInValue(
|
||||
if (node.kind === 'function' && node.value === 'theme') {
|
||||
if (node.nodes.length < 1) {
|
||||
throw new Error(
|
||||
'Expected `theme()` function call to have a path. For example: `theme(colors.red.500)`.',
|
||||
'Expected `theme()` function call to have a path. For example: `theme(--color-red-500)`.',
|
||||
)
|
||||
}
|
||||
|
||||
@ -55,7 +55,7 @@ export function substituteFunctionsInValue(
|
||||
// comma (`,`), spaces alone should be merged into the previous word to
|
||||
// avoid splitting in this case:
|
||||
//
|
||||
// theme(colors.red.500 / 75%) theme(colors.red.500 / 75%, foo, bar)
|
||||
// theme(--color-red-500 / 75%) theme(--color-red-500 / 75%, foo, bar)
|
||||
//
|
||||
// We only need to do this for the first node, as the fallback values are
|
||||
// passed through as-is.
|
||||
@ -83,20 +83,7 @@ function cssThemeFn(
|
||||
path: string,
|
||||
fallbackValues: ValueAstNode[],
|
||||
): ValueAstNode[] {
|
||||
let resolvedValue: string | null = null
|
||||
let themeValue = resolveThemeValue(path)
|
||||
|
||||
if (Array.isArray(themeValue) && themeValue.length === 2) {
|
||||
// When a tuple is returned, return the first element
|
||||
resolvedValue = themeValue[0]
|
||||
} else if (Array.isArray(themeValue)) {
|
||||
// Arrays get serialized into a comma-separated lists
|
||||
resolvedValue = themeValue.join(', ')
|
||||
} else if (typeof themeValue === 'string') {
|
||||
// Otherwise only allow string values here, objects (and namespace maps)
|
||||
// are treated as non-resolved values for the CSS `theme()` function.
|
||||
resolvedValue = themeValue
|
||||
}
|
||||
let resolvedValue = resolveThemeValue(path)
|
||||
|
||||
if (!resolvedValue && fallbackValues.length > 0) {
|
||||
return fallbackValues
|
||||
|
||||
@ -3,9 +3,8 @@ import { parseCandidate, parseVariant, type Candidate } from './candidate'
|
||||
import { compileAstNodes, compileCandidates } from './compile'
|
||||
import { getClassList, getVariants, type ClassEntry, type VariantEntry } from './intellisense'
|
||||
import { getClassOrder } from './sort'
|
||||
import type { Theme } from './theme'
|
||||
import { resolveThemeValue } from './theme-fn'
|
||||
import { Utilities, createUtilities } from './utilities'
|
||||
import type { Theme, ThemeKey } from './theme'
|
||||
import { Utilities, createUtilities, withAlpha } from './utilities'
|
||||
import { DefaultMap } from './utils/default-map'
|
||||
import { Variants, createVariants } from './variants'
|
||||
|
||||
@ -24,7 +23,7 @@ export type DesignSystem = {
|
||||
compileAstNodes(candidate: Candidate): ReturnType<typeof compileAstNodes>
|
||||
|
||||
getUsedVariants(): ReturnType<typeof parseVariant>[]
|
||||
resolveThemeValue(path: string, defaultValue?: string): string | undefined
|
||||
resolveThemeValue(path: string): string | undefined
|
||||
}
|
||||
|
||||
export function buildDesignSystem(theme: Theme): DesignSystem {
|
||||
@ -82,8 +81,24 @@ export function buildDesignSystem(theme: Theme): DesignSystem {
|
||||
return Array.from(parsedVariants.values())
|
||||
},
|
||||
|
||||
resolveThemeValue(path: string, defaultValue?: string) {
|
||||
return resolveThemeValue(theme, path, defaultValue)
|
||||
resolveThemeValue(path: `${ThemeKey}` | `${ThemeKey}${string}`) {
|
||||
// Extract an eventual modifier from the path. e.g.:
|
||||
// - "--color-red-500 / 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() as ThemeKey
|
||||
}
|
||||
|
||||
let themeValue = theme.get([path]) ?? undefined
|
||||
|
||||
// Apply the opacity modifier if present
|
||||
if (modifier && themeValue) {
|
||||
return withAlpha(themeValue, modifier)
|
||||
}
|
||||
|
||||
return themeValue
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@ -6,7 +6,7 @@ import { compileCandidates } from './compile'
|
||||
import { substituteFunctions, THEME_FUNCTION_INVOCATION } from './css-functions'
|
||||
import * as CSS from './css-parser'
|
||||
import { buildDesignSystem, type DesignSystem } from './design-system'
|
||||
import { registerPlugins, type CssPluginOptions, type Plugin } from './plugin-api'
|
||||
import { applyCompatibilityHooks, type CssPluginOptions, type Plugin } from './plugin-api'
|
||||
import { Theme, ThemeOptions } from './theme'
|
||||
import { segment } from './utils/segment'
|
||||
export type Config = UserConfig
|
||||
@ -306,24 +306,19 @@ async function parseCss(
|
||||
|
||||
let designSystem = buildDesignSystem(theme)
|
||||
|
||||
let configs = await Promise.all(
|
||||
configPaths.map(async (configPath) => ({
|
||||
path: configPath,
|
||||
config: await loadConfig(configPath),
|
||||
})),
|
||||
)
|
||||
|
||||
let plugins = await Promise.all(
|
||||
pluginPaths.map(async ([pluginPath, pluginOptions]) => ({
|
||||
path: pluginPath,
|
||||
plugin: await loadPlugin(pluginPath),
|
||||
options: pluginOptions,
|
||||
})),
|
||||
)
|
||||
|
||||
if (plugins.length || configs.length) {
|
||||
registerPlugins(plugins, designSystem, ast, configs, globs)
|
||||
}
|
||||
// Apply hooks from backwards compatibility layer. This function takes a lot
|
||||
// of random arguments because it really just needs access to "the world" to
|
||||
// do whatever ungodly things it needs to do to make things backwards
|
||||
// compatible without polluting core.
|
||||
await applyCompatibilityHooks({
|
||||
designSystem,
|
||||
ast,
|
||||
pluginPaths,
|
||||
loadPlugin,
|
||||
configPaths,
|
||||
loadConfig,
|
||||
globs,
|
||||
})
|
||||
|
||||
for (let customVariant of customVariants) {
|
||||
customVariant(designSystem)
|
||||
@ -376,11 +371,7 @@ async function parseCss(
|
||||
substituteAtApply(ast, designSystem)
|
||||
}
|
||||
|
||||
// Replace `theme()` function calls with the actual theme variables. Plugins
|
||||
// could register new rules that include functions, and JS config files could
|
||||
// also contain functions or plugins that use functions so we need to evaluate
|
||||
// functions if either of those are present.
|
||||
if (plugins.length > 0 || configs.length > 0 || css.includes(THEME_FUNCTION_INVOCATION)) {
|
||||
if (css.includes(THEME_FUNCTION_INVOCATION)) {
|
||||
substituteFunctions(ast, designSystem.resolveThemeValue)
|
||||
}
|
||||
|
||||
|
||||
@ -3,11 +3,13 @@ import { decl, rule, type AstNode } from './ast'
|
||||
import type { Candidate, NamedUtilityValue } from './candidate'
|
||||
import { applyConfigToTheme } from './compat/apply-config-to-theme'
|
||||
import { createCompatConfig } from './compat/config/create-compat-config'
|
||||
import { resolveConfig, type ConfigFile } from './compat/config/resolve-config'
|
||||
import { resolveConfig } from './compat/config/resolve-config'
|
||||
import type { ResolvedConfig, UserConfig } from './compat/config/types'
|
||||
import { darkModePlugin } from './compat/dark-mode'
|
||||
import { createThemeFn } from './compat/plugin-functions'
|
||||
import { substituteFunctions } from './css-functions'
|
||||
import type { DesignSystem } from './design-system'
|
||||
import type { Theme, ThemeKey } from './theme'
|
||||
import { withAlpha, withNegative } from './utilities'
|
||||
import { inferDataType } from './utils/infer-data-type'
|
||||
import { segment } from './utils/segment'
|
||||
@ -73,7 +75,9 @@ function buildPluginApi(
|
||||
): PluginAPI {
|
||||
let api: PluginAPI = {
|
||||
addBase(css) {
|
||||
ast.push(rule('@layer base', objectToAst(css)))
|
||||
let baseNodes = objectToAst(css)
|
||||
substituteFunctions(baseNodes, api.theme)
|
||||
ast.push(rule('@layer base', baseNodes))
|
||||
},
|
||||
|
||||
addVariant(name, variant) {
|
||||
@ -349,19 +353,71 @@ function objectToAst(rules: CssInJs | CssInJs[]): AstNode[] {
|
||||
type Primitive = string | number | boolean | null
|
||||
export type CssPluginOptions = Record<string, Primitive | Primitive[]>
|
||||
|
||||
interface PluginDetail {
|
||||
path: string
|
||||
plugin: Plugin
|
||||
options: CssPluginOptions | null
|
||||
}
|
||||
export async function applyCompatibilityHooks({
|
||||
designSystem,
|
||||
ast,
|
||||
pluginPaths,
|
||||
loadPlugin,
|
||||
configPaths,
|
||||
loadConfig,
|
||||
globs,
|
||||
}: {
|
||||
designSystem: DesignSystem
|
||||
ast: AstNode[]
|
||||
pluginPaths: [string, CssPluginOptions | null][]
|
||||
loadPlugin: (path: string) => Promise<Plugin>
|
||||
configPaths: string[]
|
||||
loadConfig: (path: string) => Promise<UserConfig>
|
||||
globs: { origin?: string; pattern: string }[]
|
||||
}) {
|
||||
// Override `resolveThemeValue` with a version that is backwards compatible
|
||||
// with dot notation paths like `colors.red.500`. We could do this by default
|
||||
// in `resolveThemeValue` but handling it here keeps all backwards
|
||||
// compatibility concerns localized to our compatibility layer.
|
||||
let resolveThemeVariableValue = designSystem.resolveThemeValue
|
||||
|
||||
designSystem.resolveThemeValue = function resolveThemeValue(path: string) {
|
||||
if (path.startsWith('--')) {
|
||||
return resolveThemeVariableValue(path)
|
||||
}
|
||||
|
||||
// Extract an eventual modifier from the path. e.g.:
|
||||
// - "colors.red.500 / 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() as ThemeKey
|
||||
}
|
||||
|
||||
let themeValue = lookupThemeValue(designSystem.theme, path)
|
||||
|
||||
// Apply the opacity modifier if present
|
||||
if (modifier && themeValue) {
|
||||
return withAlpha(themeValue, modifier)
|
||||
}
|
||||
|
||||
return themeValue
|
||||
}
|
||||
|
||||
// If there are no plugins or configs registered, we don't need to register
|
||||
// any additional backwards compatibility hooks.
|
||||
if (!pluginPaths.length && !configPaths.length) return
|
||||
|
||||
let configs = await Promise.all(
|
||||
configPaths.map(async (configPath) => ({
|
||||
path: configPath,
|
||||
config: await loadConfig(configPath),
|
||||
})),
|
||||
)
|
||||
let pluginDetails = await Promise.all(
|
||||
pluginPaths.map(async ([pluginPath, pluginOptions]) => ({
|
||||
path: pluginPath,
|
||||
plugin: await loadPlugin(pluginPath),
|
||||
options: pluginOptions,
|
||||
})),
|
||||
)
|
||||
|
||||
export function registerPlugins(
|
||||
pluginDetails: PluginDetail[],
|
||||
designSystem: DesignSystem,
|
||||
ast: AstNode[],
|
||||
configs: ConfigFile[],
|
||||
globs: { origin?: string; pattern: string }[],
|
||||
) {
|
||||
let plugins = pluginDetails.map((detail) => {
|
||||
if (!detail.options) {
|
||||
return detail.plugin
|
||||
@ -393,8 +449,27 @@ export function registerPlugins(
|
||||
// core utilities already read from.
|
||||
applyConfigToTheme(designSystem, userConfig)
|
||||
|
||||
// Replace `resolveThemeValue` with a version that is backwards compatible
|
||||
// with dot-notation but also aware of any JS theme configurations registered
|
||||
// by plugins or JS config files. This is significantly slower than just
|
||||
// upgrading dot-notation keys so we only use this version if plugins or
|
||||
// config files are actually being used. In the future we may want to optimize
|
||||
// this further by only doing this if plugins or config files _actually_
|
||||
// registered JS config objects.
|
||||
designSystem.resolveThemeValue = function resolveThemeValue(path: string, defaultValue?: string) {
|
||||
return pluginApi.theme(path, defaultValue)
|
||||
let resolvedValue = pluginApi.theme(path, defaultValue)
|
||||
|
||||
if (Array.isArray(resolvedValue) && resolvedValue.length === 2) {
|
||||
// When a tuple is returned, return the first element
|
||||
return resolvedValue[0]
|
||||
} else if (Array.isArray(resolvedValue)) {
|
||||
// Arrays get serialized into a comma-separated lists
|
||||
return resolvedValue.join(', ')
|
||||
} else if (typeof resolvedValue === 'string') {
|
||||
// Otherwise only allow string values here, objects (and namespace maps)
|
||||
// are treated as non-resolved values for the CSS `theme()` function.
|
||||
return resolvedValue
|
||||
}
|
||||
}
|
||||
|
||||
for (let file of resolvedConfig.content.files) {
|
||||
@ -407,3 +482,99 @@ export function registerPlugins(
|
||||
globs.push({ origin: file.base, pattern: file.pattern })
|
||||
}
|
||||
}
|
||||
|
||||
function toThemeKey(keypath: string[]) {
|
||||
return (
|
||||
keypath
|
||||
// [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 !== keypath.length - 1)
|
||||
.join('-')
|
||||
)
|
||||
}
|
||||
|
||||
function lookupThemeValue(theme: Theme, path: string) {
|
||||
let baseThemeKey = '--' + toThemeKey(toKeyPath(path))
|
||||
|
||||
let resolvedValue = theme.get([baseThemeKey as ThemeKey])
|
||||
|
||||
if (resolvedValue !== null) {
|
||||
return resolvedValue
|
||||
}
|
||||
|
||||
for (let [givenKey, upgradeKey] of Object.entries(themeUpgradeKeys)) {
|
||||
if (!baseThemeKey.startsWith(givenKey)) continue
|
||||
|
||||
let upgradedKey = upgradeKey + baseThemeKey.slice(givenKey.length)
|
||||
let resolvedValue = theme.get([upgradedKey as ThemeKey])
|
||||
|
||||
if (resolvedValue !== null) {
|
||||
return resolvedValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let themeUpgradeKeys = {
|
||||
'--colors': '--color',
|
||||
'--accent-color': '--color',
|
||||
'--backdrop-blur': '--blur',
|
||||
'--backdrop-brightness': '--brightness',
|
||||
'--backdrop-contrast': '--contrast',
|
||||
'--backdrop-grayscale': '--grayscale',
|
||||
'--backdrop-hue-rotate': '--hueRotate',
|
||||
'--backdrop-invert': '--invert',
|
||||
'--backdrop-opacity': '--opacity',
|
||||
'--backdrop-saturate': '--saturate',
|
||||
'--backdrop-sepia': '--sepia',
|
||||
'--background-color': '--color',
|
||||
'--background-opacity': '--opacity',
|
||||
'--border-color': '--color',
|
||||
'--border-opacity': '--opacity',
|
||||
'--border-spacing': '--spacing',
|
||||
'--box-shadow-color': '--color',
|
||||
'--caret-color': '--color',
|
||||
'--divide-color': '--borderColor',
|
||||
'--divide-opacity': '--borderOpacity',
|
||||
'--divide-width': '--borderWidth',
|
||||
'--fill': '--color',
|
||||
'--flex-basis': '--spacing',
|
||||
'--gap': '--spacing',
|
||||
'--gradient-color-stops': '--color',
|
||||
'--height': '--spacing',
|
||||
'--inset': '--spacing',
|
||||
'--margin': '--spacing',
|
||||
'--max-height': '--spacing',
|
||||
'--max-width': '--spacing',
|
||||
'--min-height': '--spacing',
|
||||
'--min-width': '--spacing',
|
||||
'--outline-color': '--color',
|
||||
'--padding': '--spacing',
|
||||
'--placeholder-color': '--color',
|
||||
'--placeholder-opacity': '--opacity',
|
||||
'--ring-color': '--color',
|
||||
'--ring-offset-color': '--color',
|
||||
'--ring-opacity': '--opacity',
|
||||
'--scroll-margin': '--spacing',
|
||||
'--scroll-padding': '--spacing',
|
||||
'--space': '--spacing',
|
||||
'--stroke': '--color',
|
||||
'--text-color': '--color',
|
||||
'--text-decoration-color': '--color',
|
||||
'--text-indent': '--spacing',
|
||||
'--text-opacity': '--opacity',
|
||||
'--translate': '--spacing',
|
||||
'--size': '--spacing',
|
||||
'--width': '--spacing',
|
||||
}
|
||||
|
||||
@ -1,129 +0,0 @@
|
||||
import type { Theme, ThemeKey } from './theme'
|
||||
import { withAlpha } from './utilities'
|
||||
import { toKeyPath } from './utils/to-key-path'
|
||||
|
||||
/**
|
||||
* Looks up a value in the CSS theme
|
||||
*/
|
||||
export function resolveThemeValue(theme: Theme, path: string, defaultValue?: string) {
|
||||
// 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 themeValue = lookupThemeValue(theme, path, defaultValue)
|
||||
|
||||
// Apply the opacity modifier if present
|
||||
if (modifier && typeof themeValue === 'string') {
|
||||
return withAlpha(themeValue, modifier)
|
||||
}
|
||||
|
||||
return themeValue
|
||||
}
|
||||
|
||||
function toThemeKey(keypath: string[]) {
|
||||
return (
|
||||
keypath
|
||||
// [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 !== keypath.length - 1)
|
||||
.join('-')
|
||||
)
|
||||
}
|
||||
|
||||
function lookupThemeValue(theme: Theme, path: string, defaultValue?: string) {
|
||||
if (path.startsWith('--')) {
|
||||
return theme.get([path as any]) ?? defaultValue
|
||||
}
|
||||
|
||||
let baseThemeKey = '--' + toThemeKey(toKeyPath(path))
|
||||
|
||||
let resolvedValue = theme.get([baseThemeKey as ThemeKey])
|
||||
|
||||
if (resolvedValue !== null) {
|
||||
return resolvedValue
|
||||
}
|
||||
|
||||
for (let [givenKey, upgradeKey] of Object.entries(themeUpgradeKeys)) {
|
||||
if (!baseThemeKey.startsWith(givenKey)) continue
|
||||
|
||||
let upgradedKey = upgradeKey + baseThemeKey.slice(givenKey.length)
|
||||
let resolvedValue = theme.get([upgradedKey as ThemeKey])
|
||||
|
||||
if (resolvedValue !== null) {
|
||||
return resolvedValue
|
||||
}
|
||||
}
|
||||
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
let themeUpgradeKeys = {
|
||||
'--colors': '--color',
|
||||
'--accent-color': '--color',
|
||||
'--backdrop-blur': '--blur',
|
||||
'--backdrop-brightness': '--brightness',
|
||||
'--backdrop-contrast': '--contrast',
|
||||
'--backdrop-grayscale': '--grayscale',
|
||||
'--backdrop-hue-rotate': '--hueRotate',
|
||||
'--backdrop-invert': '--invert',
|
||||
'--backdrop-opacity': '--opacity',
|
||||
'--backdrop-saturate': '--saturate',
|
||||
'--backdrop-sepia': '--sepia',
|
||||
'--background-color': '--color',
|
||||
'--background-opacity': '--opacity',
|
||||
'--border-color': '--color',
|
||||
'--border-opacity': '--opacity',
|
||||
'--border-spacing': '--spacing',
|
||||
'--box-shadow-color': '--color',
|
||||
'--caret-color': '--color',
|
||||
'--divide-color': '--borderColor',
|
||||
'--divide-opacity': '--borderOpacity',
|
||||
'--divide-width': '--borderWidth',
|
||||
'--fill': '--color',
|
||||
'--flex-basis': '--spacing',
|
||||
'--gap': '--spacing',
|
||||
'--gradient-color-stops': '--color',
|
||||
'--height': '--spacing',
|
||||
'--inset': '--spacing',
|
||||
'--margin': '--spacing',
|
||||
'--max-height': '--spacing',
|
||||
'--max-width': '--spacing',
|
||||
'--min-height': '--spacing',
|
||||
'--min-width': '--spacing',
|
||||
'--outline-color': '--color',
|
||||
'--padding': '--spacing',
|
||||
'--placeholder-color': '--color',
|
||||
'--placeholder-opacity': '--opacity',
|
||||
'--ring-color': '--color',
|
||||
'--ring-offset-color': '--color',
|
||||
'--ring-opacity': '--opacity',
|
||||
'--scroll-margin': '--spacing',
|
||||
'--scroll-padding': '--spacing',
|
||||
'--space': '--spacing',
|
||||
'--stroke': '--color',
|
||||
'--text-color': '--color',
|
||||
'--text-decoration-color': '--color',
|
||||
'--text-indent': '--spacing',
|
||||
'--text-opacity': '--opacity',
|
||||
'--translate': '--spacing',
|
||||
'--size': '--spacing',
|
||||
'--width': '--spacing',
|
||||
}
|
||||
@ -54,7 +54,7 @@ export class Theme {
|
||||
return keys
|
||||
}
|
||||
|
||||
get(themeKeys: (ThemeKey | `${ThemeKey}-${string}`)[]): string | null {
|
||||
get(themeKeys: ThemeKey[]): string | null {
|
||||
for (let key of themeKeys) {
|
||||
let value = this.values.get(key)
|
||||
if (value) {
|
||||
@ -179,130 +179,4 @@ export class Theme {
|
||||
}
|
||||
}
|
||||
|
||||
export type ThemeKey =
|
||||
| '--accent-color'
|
||||
| '--animate'
|
||||
| '--aspect-ratio'
|
||||
| '--backdrop-blur'
|
||||
| '--backdrop-brightness'
|
||||
| '--backdrop-contrast'
|
||||
| '--backdrop-grayscale'
|
||||
| '--backdrop-hue-rotate'
|
||||
| '--backdrop-invert'
|
||||
| '--backdrop-opacity'
|
||||
| '--backdrop-saturate'
|
||||
| '--backdrop-sepia'
|
||||
| '--background-color'
|
||||
| '--background-image'
|
||||
| '--blur'
|
||||
| '--border-color'
|
||||
| '--border-spacing'
|
||||
| '--border-width'
|
||||
| '--box-shadow-color'
|
||||
| '--breakpoint'
|
||||
| '--brightness'
|
||||
| '--caret-color'
|
||||
| '--color'
|
||||
| '--columns'
|
||||
| '--contrast'
|
||||
| '--cursor'
|
||||
| '--default-border-width'
|
||||
| '--default-ring-color'
|
||||
| '--default-ring-width'
|
||||
| '--default-transition-timing-function'
|
||||
| '--default-transition-duration'
|
||||
| '--divide-width'
|
||||
| '--divide-color'
|
||||
| '--drop-shadow'
|
||||
| '--fill'
|
||||
| '--flex-basis'
|
||||
| '--font-family'
|
||||
| '--font-size'
|
||||
| '--font-weight'
|
||||
| '--gap'
|
||||
| '--gradient-color-stop-positions'
|
||||
| '--grayscale'
|
||||
| '--grid-auto-columns'
|
||||
| '--grid-auto-rows'
|
||||
| '--grid-column'
|
||||
| '--grid-column-end'
|
||||
| '--grid-column-start'
|
||||
| '--grid-row'
|
||||
| '--grid-row-end'
|
||||
| '--grid-row-start'
|
||||
| '--grid-template-columns'
|
||||
| '--grid-template-rows'
|
||||
| '--height'
|
||||
| '--hue-rotate'
|
||||
| '--inset'
|
||||
| '--inset-shadow'
|
||||
| '--invert'
|
||||
| '--letter-spacing'
|
||||
| '--line-height'
|
||||
| '--line-clamp'
|
||||
| '--list-style-image'
|
||||
| '--list-style-type'
|
||||
| '--margin'
|
||||
| '--max-height'
|
||||
| '--max-width'
|
||||
| '--min-height'
|
||||
| '--min-width'
|
||||
| '--object-position'
|
||||
| '--opacity'
|
||||
| '--order'
|
||||
| '--outline-color'
|
||||
| '--outline-width'
|
||||
| '--outline-offset'
|
||||
| '--padding'
|
||||
| '--placeholder-color'
|
||||
| '--perspective'
|
||||
| '--perspective-origin'
|
||||
| '--radius'
|
||||
| '--ring-color'
|
||||
| '--ring-offset-color'
|
||||
| '--ring-offset-width'
|
||||
| '--ring-width'
|
||||
| '--rotate'
|
||||
| '--saturate'
|
||||
| '--scale'
|
||||
| '--scroll-margin'
|
||||
| '--scroll-padding'
|
||||
| '--sepia'
|
||||
| '--shadow'
|
||||
| '--size'
|
||||
| '--skew'
|
||||
| '--space'
|
||||
| '--spacing'
|
||||
| '--stroke'
|
||||
| '--stroke-width'
|
||||
| '--text-color'
|
||||
| '--text-decoration-color'
|
||||
| '--text-decoration-thickness'
|
||||
| '--text-indent'
|
||||
| '--text-underline-offset'
|
||||
| '--transform-origin'
|
||||
| '--transition-delay'
|
||||
| '--transition-duration'
|
||||
| '--transition-property'
|
||||
| '--transition-timing-function'
|
||||
| '--translate'
|
||||
| '--width'
|
||||
| '--z-index'
|
||||
| `--default-${string}`
|
||||
|
||||
export type ColorThemeKey =
|
||||
| '--color'
|
||||
| '--accent-color'
|
||||
| '--background-color'
|
||||
| '--border-color'
|
||||
| '--box-shadow-color'
|
||||
| '--caret-color'
|
||||
| '--divide-color'
|
||||
| '--fill'
|
||||
| '--outline-color'
|
||||
| '--placeholder-color'
|
||||
| '--ring-color'
|
||||
| '--ring-offset-color'
|
||||
| '--stroke'
|
||||
| '--text-color'
|
||||
| '--text-decoration-color'
|
||||
export type ThemeKey = `--${string}`
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { decl, rule, type AstNode, type Rule } from './ast'
|
||||
import type { Candidate, CandidateModifier, NamedUtilityValue } from './candidate'
|
||||
import type { ColorThemeKey, Theme, ThemeKey } from './theme'
|
||||
import type { Theme, ThemeKey } from './theme'
|
||||
import { DefaultMap } from './utils/default-map'
|
||||
import { inferDataType } from './utils/infer-data-type'
|
||||
import { replaceShadowColors } from './utils/replace-shadow-colors'
|
||||
@ -161,7 +161,7 @@ export function withNegative(
|
||||
* The values `inherit`, `transparent` and `current` are special-cased as they
|
||||
* are universal and don't need to be resolved from the theme.
|
||||
*/
|
||||
function resolveThemeColor<T extends ColorThemeKey>(
|
||||
function resolveThemeColor<T extends ThemeKey>(
|
||||
candidate: Extract<Candidate, { kind: 'functional' }>,
|
||||
theme: Theme,
|
||||
themeKeys: T[],
|
||||
@ -324,7 +324,7 @@ export function createUtilities(theme: Theme) {
|
||||
}
|
||||
|
||||
type ColorUtilityDescription = {
|
||||
themeKeys: ColorThemeKey[]
|
||||
themeKeys: ThemeKey[]
|
||||
handle: (value: string) => AstNode[] | undefined
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user