mirror of
https://github.com/tailwindlabs/tailwindcss.git
synced 2025-12-08 21:36:08 +00:00
Reset default @theme values for non extend JS theme config (#14672)
Imagine the following setup:
```css
/* src/input.css */
@import "tailwindcss";
@config "../tailwind.config.ts";
@theme {
--color-red-500: #ef4444;
}
```
```ts
/* tailwind.config.ts */
export default {
theme: {
colors: {
red: {
600: '#dc2626'
}
},
extend: {
colors: {
400: '#f87171'
}
}
}
}
```
Since the theme object in the JS config contains `colors` in the
non-`extends` block, you would expect this to _not pull in all the
default colors imported via `@import "tailwindcss";`_. This, however,
wasn't the case right now since all theme options were purely _additive_
to the CSS.
This PR makes it so that non-`extend` theme keys _overwrite default CSS
theme values_. The emphasis is on `default` here since you still want to
be able to overwrite your options via `@theme {}` in user space.
This now generates the same CSS that our upgrade codemods would also
generate as this would apply the new CSS right after the `@import
"tailwindcss";` rule resulting in:
```css
@import "tailwindcss";
@theme {
--color-*: initial;
--color-red-400: #f87171;
--color-red-600: #dc2626;
}
@theme {
--color-red-500: #ef4444;
}
```
## Keyframes
This PR also adds a new core API to unset keyframes the same way. We
previously had no option of doing that but while working on the above
codemods we noticed that keyframes should behave the same way:
```css
@import "tailwindcss";
@theme {
--keyframes-*: initial;
@keyframes spin {
to {
transform: rotate(361deg);
}
}
}
```
To do this, the keyframes bookeeping was moved from the main Tailwind
CSS v4 file into the `Theme` class.
_I’m not sure super of the API yet but we would need a way for the
codemods to behave the same as out interop layer here. Option B is that
we don't reset keyframes the same way we reset other theme variables_.
This commit is contained in:
parent
0e262a13e6
commit
bf179916bf
@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Fixed
|
||||
|
||||
- Ensure `theme` values defined outside of `extend` in JS configuration files overwrite all existing values for that namespace ([#14672](https://github.com/tailwindlabs/tailwindcss/pull/14672))
|
||||
- _Upgrade (experimental)_: Speed up template migrations ([#14679](https://github.com/tailwindlabs/tailwindcss/pull/14679))
|
||||
|
||||
## [4.0.0-alpha.27] - 2024-10-15
|
||||
|
||||
@ -15,8 +15,8 @@ export function migrateMediaScreen({
|
||||
function migrate(root: Root) {
|
||||
if (!designSystem || !userConfig) return
|
||||
|
||||
let resolvedUserConfig = resolveConfig(designSystem, [{ base: '', config: userConfig }])
|
||||
let screens = resolvedUserConfig?.theme?.screens || {}
|
||||
let { resolvedConfig } = resolveConfig(designSystem, [{ base: '', config: userConfig }])
|
||||
let screens = resolvedConfig?.theme?.screens || {}
|
||||
|
||||
let mediaQueries = new DefaultMap<string, string | null>((name) => {
|
||||
let value = designSystem?.resolveThemeValue(`--breakpoint-${name}`) ?? screens?.[name]
|
||||
|
||||
@ -9,7 +9,7 @@ import {
|
||||
keyPathToCssProperty,
|
||||
themeableValues,
|
||||
} from '../../tailwindcss/src/compat/apply-config-to-theme'
|
||||
import { applyKeyframesToAst } from '../../tailwindcss/src/compat/apply-keyframes-to-ast'
|
||||
import { keyframesToRules } from '../../tailwindcss/src/compat/apply-keyframes-to-theme'
|
||||
import { deepMerge } from '../../tailwindcss/src/compat/config/deep-merge'
|
||||
import { mergeThemeExtension } from '../../tailwindcss/src/compat/config/resolve-config'
|
||||
import type { ThemeConfig } from '../../tailwindcss/src/compat/config/types'
|
||||
@ -249,7 +249,6 @@ function onlyUsesAllowedTopLevelKeys(theme: ThemeConfig): boolean {
|
||||
}
|
||||
|
||||
function keyframesToCss(keyframes: Record<string, unknown>): string {
|
||||
let ast: AstNode[] = []
|
||||
applyKeyframesToAst(ast, { theme: { keyframes } })
|
||||
let ast: AstNode[] = keyframesToRules({ theme: { keyframes } })
|
||||
return toCss(ast).trim() + '\n'
|
||||
}
|
||||
|
||||
@ -83,7 +83,7 @@ async function createResolvedUserConfig(fullConfigPath: string): Promise<Config>
|
||||
|
||||
return resolveConfig(noopDesignSystem, [
|
||||
{ base: dirname(fullConfigPath), config: unresolvedUserConfig },
|
||||
]) as any
|
||||
]).resolvedConfig as any
|
||||
}
|
||||
|
||||
const DEFAULT_CONFIG_FILES = [
|
||||
|
||||
@ -2,7 +2,7 @@ import { rule, toCss, walk, WalkAction, type AstNode } from '../ast'
|
||||
import type { DesignSystem } from '../design-system'
|
||||
import { segment } from '../utils/segment'
|
||||
import { applyConfigToTheme } from './apply-config-to-theme'
|
||||
import { applyKeyframesToAst } from './apply-keyframes-to-ast'
|
||||
import { applyKeyframesToTheme } from './apply-keyframes-to-theme'
|
||||
import { createCompatConfig } from './config/create-compat-config'
|
||||
import { resolveConfig } from './config/resolve-config'
|
||||
import type { UserConfig } from './config/types'
|
||||
@ -215,12 +215,15 @@ function upgradeToFullPluginSupport({
|
||||
|
||||
let userConfig = [...pluginConfigs, ...configs]
|
||||
|
||||
let resolvedConfig = resolveConfig(designSystem, [
|
||||
let { resolvedConfig } = resolveConfig(designSystem, [
|
||||
{ config: createCompatConfig(designSystem.theme), base },
|
||||
...userConfig,
|
||||
{ config: { plugins: [darkModePlugin] }, base },
|
||||
])
|
||||
let resolvedUserConfig = resolveConfig(designSystem, userConfig)
|
||||
let { resolvedConfig: resolvedUserConfig, replacedThemeKeys } = resolveConfig(
|
||||
designSystem,
|
||||
userConfig,
|
||||
)
|
||||
|
||||
let pluginApi = buildPluginApi(designSystem, ast, resolvedConfig)
|
||||
|
||||
@ -231,8 +234,8 @@ function upgradeToFullPluginSupport({
|
||||
// Merge the user-configured theme keys into the design system. The compat
|
||||
// config would otherwise expand into namespaces like `background-color` which
|
||||
// core utilities already read from.
|
||||
applyConfigToTheme(designSystem, resolvedUserConfig)
|
||||
applyKeyframesToAst(ast, resolvedUserConfig)
|
||||
applyConfigToTheme(designSystem, resolvedUserConfig, replacedThemeKeys)
|
||||
applyKeyframesToTheme(designSystem, resolvedUserConfig, replacedThemeKeys)
|
||||
|
||||
registerThemeVariantOverrides(resolvedUserConfig, designSystem)
|
||||
registerScreensConfig(resolvedUserConfig, designSystem)
|
||||
|
||||
@ -1,14 +1,14 @@
|
||||
import { expect, test } from 'vitest'
|
||||
import { buildDesignSystem } from '../design-system'
|
||||
import { Theme } from '../theme'
|
||||
import { Theme, ThemeOptions } from '../theme'
|
||||
import { applyConfigToTheme } from './apply-config-to-theme'
|
||||
import { resolveConfig } from './config/resolve-config'
|
||||
|
||||
test('Config values can be merged into the theme', () => {
|
||||
test('config values can be merged into the theme', () => {
|
||||
let theme = new Theme()
|
||||
let design = buildDesignSystem(theme)
|
||||
|
||||
let resolvedUserConfig = resolveConfig(design, [
|
||||
let { resolvedConfig, replacedThemeKeys } = resolveConfig(design, [
|
||||
{
|
||||
config: {
|
||||
theme: {
|
||||
@ -54,7 +54,7 @@ test('Config values can be merged into the theme', () => {
|
||||
base: '/root',
|
||||
},
|
||||
])
|
||||
applyConfigToTheme(design, resolvedUserConfig)
|
||||
applyConfigToTheme(design, resolvedConfig, replacedThemeKeys)
|
||||
|
||||
expect(theme.resolve('primary', ['--color'])).toEqual('#c0ffee')
|
||||
expect(theme.resolve('sm', ['--breakpoint'])).toEqual('1234px')
|
||||
@ -75,11 +75,60 @@ test('Config values can be merged into the theme', () => {
|
||||
])
|
||||
})
|
||||
|
||||
test('Invalid keys are not merged into the theme', () => {
|
||||
test('will reset default theme values with overwriting theme values', () => {
|
||||
let theme = new Theme()
|
||||
let design = buildDesignSystem(theme)
|
||||
|
||||
let resolvedUserConfig = resolveConfig(design, [
|
||||
theme.add('--color-blue-400', 'lightblue', ThemeOptions.DEFAULT)
|
||||
theme.add('--color-blue-500', 'blue', ThemeOptions.DEFAULT)
|
||||
theme.add('--color-red-400', '#f87171')
|
||||
theme.add('--color-red-500', '#ef4444')
|
||||
|
||||
let { resolvedConfig, replacedThemeKeys } = resolveConfig(design, [
|
||||
{
|
||||
config: {
|
||||
theme: {
|
||||
colors: {
|
||||
blue: {
|
||||
500: '#3b82f6',
|
||||
},
|
||||
red: {
|
||||
500: 'red',
|
||||
},
|
||||
},
|
||||
extend: {
|
||||
colors: {
|
||||
blue: {
|
||||
600: '#2563eb',
|
||||
},
|
||||
red: {
|
||||
600: '#dc2626',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
base: '/root',
|
||||
},
|
||||
])
|
||||
applyConfigToTheme(design, resolvedConfig, replacedThemeKeys)
|
||||
|
||||
expect(theme.namespace('--color')).toMatchInlineSnapshot(`
|
||||
Map {
|
||||
"red-400" => "#f87171",
|
||||
"red-500" => "#ef4444",
|
||||
"blue-500" => "#3b82f6",
|
||||
"blue-600" => "#2563eb",
|
||||
"red-600" => "#dc2626",
|
||||
}
|
||||
`)
|
||||
})
|
||||
|
||||
test('invalid keys are not merged into the theme', () => {
|
||||
let theme = new Theme()
|
||||
let design = buildDesignSystem(theme)
|
||||
|
||||
let { resolvedConfig, replacedThemeKeys } = resolveConfig(design, [
|
||||
{
|
||||
config: {
|
||||
theme: {
|
||||
@ -92,7 +141,7 @@ test('Invalid keys are not merged into the theme', () => {
|
||||
},
|
||||
])
|
||||
|
||||
applyConfigToTheme(design, resolvedUserConfig)
|
||||
applyConfigToTheme(design, resolvedConfig, replacedThemeKeys)
|
||||
|
||||
let entries = Array.from(theme.entries())
|
||||
|
||||
|
||||
@ -19,7 +19,18 @@ function resolveThemeValue(value: unknown, subValue: string | null = null): stri
|
||||
return null
|
||||
}
|
||||
|
||||
export function applyConfigToTheme(designSystem: DesignSystem, { theme }: ResolvedConfig) {
|
||||
export function applyConfigToTheme(
|
||||
designSystem: DesignSystem,
|
||||
{ theme }: ResolvedConfig,
|
||||
replacedThemeKeys: Set<string>,
|
||||
) {
|
||||
for (let resetThemeKey of replacedThemeKeys) {
|
||||
let name = keyPathToCssProperty([resetThemeKey])
|
||||
if (!name) continue
|
||||
|
||||
designSystem.theme.clearNamespace(`--${name}`, ThemeOptions.DEFAULT)
|
||||
}
|
||||
|
||||
for (let [path, value] of themeableValues(theme)) {
|
||||
if (typeof value !== 'string' && typeof value !== 'number') {
|
||||
continue
|
||||
|
||||
@ -1,54 +0,0 @@
|
||||
import { expect, test } from 'vitest'
|
||||
import { toCss, type AstNode } from '../ast'
|
||||
import { buildDesignSystem } from '../design-system'
|
||||
import { Theme } from '../theme'
|
||||
import { applyKeyframesToAst } from './apply-keyframes-to-ast'
|
||||
import { resolveConfig } from './config/resolve-config'
|
||||
|
||||
test('Config values can be merged into the theme', () => {
|
||||
let theme = new Theme()
|
||||
let design = buildDesignSystem(theme)
|
||||
|
||||
let ast: AstNode[] = []
|
||||
|
||||
let resolvedUserConfig = resolveConfig(design, [
|
||||
{
|
||||
config: {
|
||||
theme: {
|
||||
keyframes: {
|
||||
'fade-in': {
|
||||
'0%': { opacity: '0' },
|
||||
'100%': { opacity: '1' },
|
||||
},
|
||||
'fade-out': {
|
||||
'0%': { opacity: '1' },
|
||||
'100%': { opacity: '0' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
base: '/root',
|
||||
},
|
||||
])
|
||||
applyKeyframesToAst(ast, resolvedUserConfig)
|
||||
|
||||
expect(toCss(ast)).toMatchInlineSnapshot(`
|
||||
"@keyframes fade-in {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@keyframes fade-out {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
@ -1,11 +0,0 @@
|
||||
import { rule, type AstNode } from '../ast'
|
||||
import type { ResolvedConfig } from './config/types'
|
||||
import { objectToAst } from './plugin-api'
|
||||
|
||||
export function applyKeyframesToAst(ast: AstNode[], { theme }: Pick<ResolvedConfig, 'theme'>) {
|
||||
if ('keyframes' in theme) {
|
||||
for (let [name, keyframe] of Object.entries(theme.keyframes)) {
|
||||
ast.push(rule(`@keyframes ${name}`, objectToAst(keyframe as any)))
|
||||
}
|
||||
}
|
||||
}
|
||||
133
packages/tailwindcss/src/compat/apply-keyframes-to-theme.test.ts
Normal file
133
packages/tailwindcss/src/compat/apply-keyframes-to-theme.test.ts
Normal file
@ -0,0 +1,133 @@
|
||||
import { expect, test } from 'vitest'
|
||||
import { decl, rule, toCss } from '../ast'
|
||||
import { buildDesignSystem } from '../design-system'
|
||||
import { Theme } from '../theme'
|
||||
import { applyKeyframesToTheme } from './apply-keyframes-to-theme'
|
||||
import { resolveConfig } from './config/resolve-config'
|
||||
|
||||
test('keyframes can be merged into the theme', () => {
|
||||
let theme = new Theme()
|
||||
let design = buildDesignSystem(theme)
|
||||
|
||||
let { resolvedConfig, replacedThemeKeys } = resolveConfig(design, [
|
||||
{
|
||||
config: {
|
||||
theme: {
|
||||
extend: {
|
||||
keyframes: {
|
||||
'fade-in': {
|
||||
'0%': { opacity: '0' },
|
||||
'100%': { opacity: '1' },
|
||||
},
|
||||
'fade-out': {
|
||||
'0%': { opacity: '1' },
|
||||
'100%': { opacity: '0' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
base: '/root',
|
||||
},
|
||||
])
|
||||
applyKeyframesToTheme(design, resolvedConfig, replacedThemeKeys)
|
||||
|
||||
expect(toCss(design.theme.getKeyframes())).toMatchInlineSnapshot(`
|
||||
"@keyframes fade-in {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@keyframes fade-out {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
test('will append to the default keyframes with new keyframes', () => {
|
||||
let theme = new Theme()
|
||||
let design = buildDesignSystem(theme)
|
||||
|
||||
theme.addKeyframes(
|
||||
rule('@keyframes slide-in', [
|
||||
rule('from', [decl('opacity', 'translateX(0%)')]),
|
||||
rule('to', [decl('opacity', 'translateX(100%)')]),
|
||||
]),
|
||||
)
|
||||
theme.addKeyframes(
|
||||
rule('@keyframes slide-out', [
|
||||
rule('from', [decl('opacity', 'translateX(100%)')]),
|
||||
rule('to', [decl('opacity', 'translateX(0%)')]),
|
||||
]),
|
||||
)
|
||||
|
||||
let { resolvedConfig, replacedThemeKeys } = resolveConfig(design, [
|
||||
{
|
||||
config: {
|
||||
theme: {
|
||||
keyframes: {
|
||||
'fade-in': {
|
||||
'0%': { opacity: '0' },
|
||||
'100%': { opacity: '1' },
|
||||
},
|
||||
},
|
||||
extend: {
|
||||
keyframes: {
|
||||
'fade-out': {
|
||||
'0%': { opacity: '1' },
|
||||
'100%': { opacity: '0' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
base: '/root',
|
||||
},
|
||||
])
|
||||
applyKeyframesToTheme(design, resolvedConfig, replacedThemeKeys)
|
||||
|
||||
expect(toCss(design.theme.getKeyframes())).toMatchInlineSnapshot(`
|
||||
"@keyframes slide-in {
|
||||
from {
|
||||
opacity: translateX(0%);
|
||||
}
|
||||
to {
|
||||
opacity: translateX(100%);
|
||||
}
|
||||
}
|
||||
@keyframes slide-out {
|
||||
from {
|
||||
opacity: translateX(100%);
|
||||
}
|
||||
to {
|
||||
opacity: translateX(0%);
|
||||
}
|
||||
}
|
||||
@keyframes fade-in {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@keyframes fade-out {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
24
packages/tailwindcss/src/compat/apply-keyframes-to-theme.ts
Normal file
24
packages/tailwindcss/src/compat/apply-keyframes-to-theme.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { rule, type Rule } from '../ast'
|
||||
import type { DesignSystem } from '../design-system'
|
||||
import type { ResolvedConfig } from './config/types'
|
||||
import { objectToAst } from './plugin-api'
|
||||
|
||||
export function applyKeyframesToTheme(
|
||||
designSystem: DesignSystem,
|
||||
resolvedConfig: Pick<ResolvedConfig, 'theme'>,
|
||||
replacedThemeKeys: Set<string>,
|
||||
) {
|
||||
for (let rule of keyframesToRules(resolvedConfig)) {
|
||||
designSystem.theme.addKeyframes(rule)
|
||||
}
|
||||
}
|
||||
|
||||
export function keyframesToRules(resolvedConfig: Pick<ResolvedConfig, 'theme'>): Rule[] {
|
||||
let rules: Rule[] = []
|
||||
if ('keyframes' in resolvedConfig.theme) {
|
||||
for (let [name, keyframe] of Object.entries(resolvedConfig.theme.keyframes)) {
|
||||
rules.push(rule(`@keyframes ${name}`, objectToAst(keyframe as any)))
|
||||
}
|
||||
}
|
||||
return rules
|
||||
}
|
||||
@ -6,7 +6,7 @@ import { resolveConfig } from './resolve-config'
|
||||
test('top level theme keys are replaced', () => {
|
||||
let design = buildDesignSystem(new Theme())
|
||||
|
||||
let config = resolveConfig(design, [
|
||||
let { resolvedConfig, replacedThemeKeys } = resolveConfig(design, [
|
||||
{
|
||||
config: {
|
||||
theme: {
|
||||
@ -43,7 +43,7 @@ test('top level theme keys are replaced', () => {
|
||||
},
|
||||
])
|
||||
|
||||
expect(config).toMatchObject({
|
||||
expect(resolvedConfig).toMatchObject({
|
||||
theme: {
|
||||
colors: {
|
||||
blue: 'blue',
|
||||
@ -53,12 +53,13 @@ test('top level theme keys are replaced', () => {
|
||||
},
|
||||
},
|
||||
})
|
||||
expect(replacedThemeKeys).toEqual(new Set(['colors', 'fontFamily']))
|
||||
})
|
||||
|
||||
test('theme can be extended', () => {
|
||||
let design = buildDesignSystem(new Theme())
|
||||
|
||||
let config = resolveConfig(design, [
|
||||
let { resolvedConfig, replacedThemeKeys } = resolveConfig(design, [
|
||||
{
|
||||
config: {
|
||||
theme: {
|
||||
@ -87,7 +88,7 @@ test('theme can be extended', () => {
|
||||
},
|
||||
])
|
||||
|
||||
expect(config).toMatchObject({
|
||||
expect(resolvedConfig).toMatchObject({
|
||||
theme: {
|
||||
colors: {
|
||||
red: 'red',
|
||||
@ -98,6 +99,7 @@ test('theme can be extended', () => {
|
||||
},
|
||||
},
|
||||
})
|
||||
expect(replacedThemeKeys).toEqual(new Set(['colors', 'fontFamily']))
|
||||
})
|
||||
|
||||
test('theme keys can reference other theme keys using the theme function regardless of order', ({
|
||||
@ -105,7 +107,7 @@ test('theme keys can reference other theme keys using the theme function regardl
|
||||
}) => {
|
||||
let design = buildDesignSystem(new Theme())
|
||||
|
||||
let config = resolveConfig(design, [
|
||||
let { resolvedConfig, replacedThemeKeys } = resolveConfig(design, [
|
||||
{
|
||||
config: {
|
||||
theme: {
|
||||
@ -146,7 +148,7 @@ test('theme keys can reference other theme keys using the theme function regardl
|
||||
},
|
||||
])
|
||||
|
||||
expect(config).toMatchObject({
|
||||
expect(resolvedConfig).toMatchObject({
|
||||
theme: {
|
||||
colors: {
|
||||
red: 'red',
|
||||
@ -170,6 +172,7 @@ test('theme keys can reference other theme keys using the theme function regardl
|
||||
},
|
||||
},
|
||||
})
|
||||
expect(replacedThemeKeys).toEqual(new Set(['colors', 'placeholderColor']))
|
||||
})
|
||||
|
||||
test('theme keys can read from the CSS theme', () => {
|
||||
@ -178,7 +181,7 @@ test('theme keys can read from the CSS theme', () => {
|
||||
|
||||
let design = buildDesignSystem(theme)
|
||||
|
||||
let config = resolveConfig(design, [
|
||||
let { resolvedConfig, replacedThemeKeys } = resolveConfig(design, [
|
||||
{
|
||||
config: {
|
||||
theme: {
|
||||
@ -212,7 +215,7 @@ test('theme keys can read from the CSS theme', () => {
|
||||
},
|
||||
])
|
||||
|
||||
expect(config).toMatchObject({
|
||||
expect(resolvedConfig).toMatchObject({
|
||||
theme: {
|
||||
colors: {
|
||||
red: 'red',
|
||||
@ -247,4 +250,7 @@ test('theme keys can read from the CSS theme', () => {
|
||||
},
|
||||
},
|
||||
})
|
||||
expect(replacedThemeKeys).toEqual(
|
||||
new Set(['colors', 'accentColor', 'placeholderColor', 'caretColor', 'transitionColor']),
|
||||
)
|
||||
})
|
||||
|
||||
@ -39,7 +39,10 @@ let minimal: ResolvedConfig = {
|
||||
},
|
||||
}
|
||||
|
||||
export function resolveConfig(design: DesignSystem, files: ConfigFile[]): ResolvedConfig {
|
||||
export function resolveConfig(
|
||||
design: DesignSystem,
|
||||
files: ConfigFile[],
|
||||
): { resolvedConfig: ResolvedConfig; replacedThemeKeys: Set<string> } {
|
||||
let ctx: ResolutionContext = {
|
||||
design,
|
||||
configs: [],
|
||||
@ -78,13 +81,16 @@ export function resolveConfig(design: DesignSystem, files: ConfigFile[]): Resolv
|
||||
}
|
||||
|
||||
// Merge themes
|
||||
mergeTheme(ctx)
|
||||
let replacedThemeKeys = mergeTheme(ctx)
|
||||
|
||||
return {
|
||||
...ctx.result,
|
||||
content: ctx.content,
|
||||
theme: ctx.theme as ResolvedConfig['theme'],
|
||||
plugins: ctx.plugins,
|
||||
resolvedConfig: {
|
||||
...ctx.result,
|
||||
content: ctx.content,
|
||||
theme: ctx.theme as ResolvedConfig['theme'],
|
||||
plugins: ctx.plugins,
|
||||
},
|
||||
replacedThemeKeys,
|
||||
}
|
||||
}
|
||||
|
||||
@ -175,7 +181,9 @@ function extractConfigs(ctx: ResolutionContext, { config, base, path }: ConfigFi
|
||||
ctx.configs.push(config)
|
||||
}
|
||||
|
||||
function mergeTheme(ctx: ResolutionContext) {
|
||||
function mergeTheme(ctx: ResolutionContext): Set<string> {
|
||||
let replacedThemeKeys: Set<string> = new Set()
|
||||
|
||||
let themeFn = createThemeFn(ctx.design, () => ctx.theme, resolveValue)
|
||||
let theme = Object.assign(themeFn, {
|
||||
theme: themeFn,
|
||||
@ -194,6 +202,14 @@ function mergeTheme(ctx: ResolutionContext) {
|
||||
let theme = config.theme ?? {}
|
||||
let extend = theme.extend ?? {}
|
||||
|
||||
// Keep track of all theme keys that were reset
|
||||
for (let key in theme) {
|
||||
if (key === 'extend') {
|
||||
continue
|
||||
}
|
||||
replacedThemeKeys.add(key)
|
||||
}
|
||||
|
||||
// Shallow merge themes so latest "group" wins
|
||||
Object.assign(ctx.theme, theme)
|
||||
|
||||
@ -238,4 +254,6 @@ function mergeTheme(ctx: ResolutionContext) {
|
||||
ctx.theme.screens[key] = screen.min
|
||||
}
|
||||
}
|
||||
|
||||
return replacedThemeKeys
|
||||
}
|
||||
|
||||
@ -67,18 +67,6 @@ describe('theme', async () => {
|
||||
}
|
||||
}
|
||||
}
|
||||
@keyframes enter {
|
||||
from {
|
||||
opacity: var(--tw-enter-opacity, 1);
|
||||
transform: translate3d(var(--tw-enter-translate-x, 0), var(--tw-enter-translate-y, 0), 0) scale3d(var(--tw-enter-scale, 1), var(--tw-enter-scale, 1), var(--tw-enter-scale, 1)) rotate(var(--tw-enter-rotate, 0));
|
||||
}
|
||||
}
|
||||
@keyframes exit {
|
||||
to {
|
||||
opacity: var(--tw-exit-opacity, 1);
|
||||
transform: translate3d(var(--tw-exit-translate-x, 0), var(--tw-exit-translate-y, 0), 0) scale3d(var(--tw-exit-scale, 1), var(--tw-exit-scale, 1), var(--tw-exit-scale, 1)) rotate(var(--tw-exit-rotate, 0));
|
||||
}
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
@ -204,7 +204,7 @@ export function buildPluginApi(
|
||||
|
||||
for (let [name, css] of Object.entries(utils)) {
|
||||
if (name.startsWith('@keyframes ')) {
|
||||
ast.push(rule(name, objectToAst(css)))
|
||||
designSystem.theme.addKeyframes(rule(name, objectToAst(css)))
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
@ -283,10 +283,12 @@ test('JS config `screens` overwrite CSS `--breakpoint-*`', async () => {
|
||||
loadModule: async () => ({
|
||||
module: {
|
||||
theme: {
|
||||
screens: {
|
||||
mini: '40rem',
|
||||
midi: '48rem',
|
||||
maxi: '64rem',
|
||||
extend: {
|
||||
screens: {
|
||||
mini: '40rem',
|
||||
midi: '48rem',
|
||||
maxi: '64rem',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -615,3 +617,43 @@ describe('complex screen configs', () => {
|
||||
`)
|
||||
})
|
||||
})
|
||||
|
||||
test('JS config `screens` can overwrite default CSS `--breakpoint-*`', async () => {
|
||||
let input = css`
|
||||
@theme default {
|
||||
--breakpoint-sm: 40rem;
|
||||
--breakpoint-md: 48rem;
|
||||
--breakpoint-lg: 64rem;
|
||||
--breakpoint-xl: 80rem;
|
||||
--breakpoint-2xl: 96rem;
|
||||
}
|
||||
@config "./config.js";
|
||||
@tailwind utilities;
|
||||
`
|
||||
|
||||
let compiler = await compile(input, {
|
||||
loadModule: async () => ({
|
||||
module: {
|
||||
theme: {
|
||||
screens: {
|
||||
mini: '40rem',
|
||||
midi: '48rem',
|
||||
maxi: '64rem',
|
||||
},
|
||||
},
|
||||
},
|
||||
base: '/root',
|
||||
}),
|
||||
})
|
||||
|
||||
// Note: The `sm`, `md`, and other variants are still there because they are
|
||||
// created before the compat layer can intercept. We do not remove them
|
||||
// currently.
|
||||
expect(
|
||||
compiler.build(['min-sm:flex', 'min-md:flex', 'min-lg:flex', 'min-xl:flex', 'min-2xl:flex']),
|
||||
).toMatchInlineSnapshot(`
|
||||
":root {
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
@ -1011,10 +1011,20 @@ describe('Parsing themes values from CSS', () => {
|
||||
--color-blue: #00f;
|
||||
--font-size-sm: 13px;
|
||||
--font-size-md: 16px;
|
||||
|
||||
--animate-spin: spin 1s infinite linear;
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
@theme {
|
||||
--color-*: initial;
|
||||
--font-size-md: initial;
|
||||
--animate-*: initial;
|
||||
--keyframes-*: initial;
|
||||
}
|
||||
@theme {
|
||||
--color-green: #0f0;
|
||||
|
||||
@ -82,7 +82,6 @@ async function parseCss(
|
||||
let customVariants: ((designSystem: DesignSystem) => void)[] = []
|
||||
let customUtilities: ((designSystem: DesignSystem) => void)[] = []
|
||||
let firstThemeRule: Rule | null = null
|
||||
let keyframesRules: Rule[] = []
|
||||
let globs: { base: string; pattern: string }[] = []
|
||||
|
||||
walk(ast, (node, { parent, replaceWith, context }) => {
|
||||
@ -286,7 +285,7 @@ async function parseCss(
|
||||
// Collect `@keyframes` rules to re-insert with theme variables later,
|
||||
// since the `@theme` rule itself will be removed.
|
||||
if (child.kind === 'rule' && child.selector.startsWith('@keyframes ')) {
|
||||
keyframesRules.push(child)
|
||||
theme.addKeyframes(child)
|
||||
replaceWith([])
|
||||
return WalkAction.Skip
|
||||
}
|
||||
@ -350,6 +349,7 @@ async function parseCss(
|
||||
nodes.push(decl(key, value.value))
|
||||
}
|
||||
|
||||
let keyframesRules = theme.getKeyframes()
|
||||
if (keyframesRules.length > 0) {
|
||||
let animationParts = [...theme.namespace('--animate').values()].flatMap((animation) =>
|
||||
animation.split(' '),
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import type { Rule } from './ast'
|
||||
import { escape } from './utils/escape'
|
||||
|
||||
export const enum ThemeOptions {
|
||||
@ -10,7 +11,10 @@ export const enum ThemeOptions {
|
||||
export class Theme {
|
||||
public prefix: string | null = null
|
||||
|
||||
constructor(private values = new Map<string, { value: string; options: number }>()) {}
|
||||
constructor(
|
||||
private values = new Map<string, { value: string; options: ThemeOptions }>(),
|
||||
private keyframes = new Set<Rule>([]),
|
||||
) {}
|
||||
|
||||
add(key: string, value: string, options = ThemeOptions.NONE): void {
|
||||
if (key.endsWith('-*')) {
|
||||
@ -20,7 +24,11 @@ export class Theme {
|
||||
if (key === '--*') {
|
||||
this.values.clear()
|
||||
} else {
|
||||
this.#clearNamespace(key.slice(0, -2))
|
||||
this.clearNamespace(
|
||||
key.slice(0, -2),
|
||||
// `--${key}-*: initial;` should clear _all_ theme values
|
||||
ThemeOptions.NONE,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -89,9 +97,15 @@ export class Theme {
|
||||
return `--${this.prefix}-${key.slice(2)}`
|
||||
}
|
||||
|
||||
#clearNamespace(namespace: string) {
|
||||
clearNamespace(namespace: string, clearOptions: ThemeOptions) {
|
||||
for (let key of this.values.keys()) {
|
||||
if (key.startsWith(namespace)) {
|
||||
if (clearOptions !== ThemeOptions.NONE) {
|
||||
let options = this.getOptions(key)
|
||||
if ((options & clearOptions) !== clearOptions) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
this.values.delete(key)
|
||||
}
|
||||
}
|
||||
@ -189,6 +203,14 @@ export class Theme {
|
||||
|
||||
return values
|
||||
}
|
||||
|
||||
addKeyframes(value: Rule): void {
|
||||
this.keyframes.add(value)
|
||||
}
|
||||
|
||||
getKeyframes() {
|
||||
return Array.from(this.keyframes)
|
||||
}
|
||||
}
|
||||
|
||||
export type ThemeKey = `--${string}`
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user