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:
Philipp Spiess 2024-10-16 16:06:09 +02:00 committed by GitHub
parent 0e262a13e6
commit bf179916bf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 362 additions and 121 deletions

View File

@ -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

View File

@ -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]

View File

@ -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'
}

View File

@ -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 = [

View File

@ -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)

View File

@ -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())

View File

@ -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

View File

@ -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;
}
}
"
`)
})

View File

@ -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)))
}
}
}

View 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;
}
}
"
`)
})

View 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
}

View File

@ -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']),
)
})

View File

@ -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
}

View File

@ -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));
}
}
"
`)
})

View File

@ -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
}

View File

@ -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 {
}
"
`)
})

View File

@ -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;

View File

@ -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(' '),

View File

@ -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}`