Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Ensure adjacent rules are merged together after handling nesting when generating optimized CSS ([#14873](https://github.com/tailwindlabs/tailwindcss/pull/14873))
- Rebase `url()` inside imported CSS files when using Vite ([#14877](https://github.com/tailwindlabs/tailwindcss/pull/14877))
- Ensure that CSS transforms from other Vite plugins correctly work in full builds (e.g. `:deep()` in Vue) ([#14871](https://github.com/tailwindlabs/tailwindcss/pull/14871))
- Don't unset keys like `--inset-shadow-*` when unsetting keys like `--inset-*` ([#14906](https://github.com/tailwindlabs/tailwindcss/pull/14906))
- _Upgrade (experimental)_: Install `@tailwindcss/postcss` next to `tailwindcss` ([#14830](https://github.com/tailwindlabs/tailwindcss/pull/14830))
- _Upgrade (experimental)_: Remove whitespace around `,` separator when print arbitrary values ([#14838](https://github.com/tailwindlabs/tailwindcss/pull/14838))
- _Upgrade (experimental)_: Fix crash during upgrade when content globs escape root of project ([#14896](https://github.com/tailwindlabs/tailwindcss/pull/14896))
Expand Down
173 changes: 173 additions & 0 deletions packages/tailwindcss/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1174,6 +1174,179 @@ describe('Parsing themes values from CSS', () => {
`)
})

test('unsetting `--font-*` does not unset `--font-weight-*` or `--font-size-*`', async () => {
expect(
await compileCss(
css`
@theme {
--font-weight-bold: bold;
--font-size-sm: 14px;
--font-sans: sans-serif;
--font-serif: serif;
}
@theme {
--font-*: initial;
--font-body: Inter;
}
@tailwind utilities;
`,
['font-bold', 'text-sm', 'font-sans', 'font-serif', 'font-body'],
),
).toMatchInlineSnapshot(`
":root {
--font-weight-bold: bold;
--font-size-sm: 14px;
--font-body: Inter;
}

.font-body {
font-family: var(--font-body);
}

.text-sm {
font-size: var(--font-size-sm);
}

.font-bold {
--tw-font-weight: var(--font-weight-bold);
font-weight: var(--font-weight-bold);
}

@supports (-moz-orient: inline) {
@layer base {
*, :before, :after, ::backdrop {
--tw-font-weight: initial;
}
}
}

@property --tw-font-weight {
syntax: "*";
inherits: false
}"
`)
})

test('unsetting `--inset-*` does not unset `--inset-shadow-*`', async () => {
expect(
await compileCss(
css`
@theme {
--inset-shadow-sm: inset 0 2px 4px rgb(0 0 0 / 0.05);
--inset-lg: 100px;
--inset-sm: 10px;
}
@theme {
--inset-*: initial;
--inset-md: 50px;
}
@tailwind utilities;
`,
['inset-shadow-sm', 'inset-ring-thick', 'inset-lg', 'inset-sm', 'inset-md'],
),
).toMatchInlineSnapshot(`
":root {
--inset-shadow-sm: inset 0 2px 4px #0000000d;
--inset-md: 50px;
}

.inset-md {
inset: var(--inset-md);
}

.inset-shadow-sm {
--tw-inset-shadow: inset 0 2px 4px var(--tw-inset-shadow-color, #0000000d);
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
}

@supports (-moz-orient: inline) {
@layer base {
*, :before, :after, ::backdrop {
--tw-shadow: 0 0 #0000;
--tw-shadow-color: initial;
--tw-inset-shadow: 0 0 #0000;
--tw-inset-shadow-color: initial;
--tw-ring-color: initial;
--tw-ring-shadow: 0 0 #0000;
--tw-inset-ring-color: initial;
--tw-inset-ring-shadow: 0 0 #0000;
--tw-ring-inset: initial;
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-offset-shadow: 0 0 #0000;
}
}
}

@property --tw-shadow {
syntax: "*";
inherits: false;
initial-value: 0 0 #0000;
}

@property --tw-shadow-color {
syntax: "*";
inherits: false
}

@property --tw-inset-shadow {
syntax: "*";
inherits: false;
initial-value: 0 0 #0000;
}

@property --tw-inset-shadow-color {
syntax: "*";
inherits: false
}

@property --tw-ring-color {
syntax: "*";
inherits: false
}

@property --tw-ring-shadow {
syntax: "*";
inherits: false;
initial-value: 0 0 #0000;
}

@property --tw-inset-ring-color {
syntax: "*";
inherits: false
}

@property --tw-inset-ring-shadow {
syntax: "*";
inherits: false;
initial-value: 0 0 #0000;
}

@property --tw-ring-inset {
syntax: "*";
inherits: false
}

@property --tw-ring-offset-width {
syntax: "<length>";
inherits: false;
initial-value: 0;
}

@property --tw-ring-offset-color {
syntax: "*";
inherits: false;
initial-value: #fff;
}

@property --tw-ring-offset-shadow {
syntax: "*";
inherits: false;
initial-value: 0 0 #0000;
}"
`)
})

test('unused keyframes are removed when an animation is unset', async () => {
expect(
await compileCss(
Expand Down
1 change: 0 additions & 1 deletion packages/tailwindcss/src/intellisense.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ function loadDesignSystem() {
theme.add('--opacity-background', '0.3')
theme.add('--drop-shadow-sm', '0 1px 1px rgb(0 0 0 / 0.05)')
theme.add('--inset-shadow-sm', 'inset 0 1px 1px rgb(0 0 0 / 0.05)')
theme.add('--inset-ring-big', '100px')
return buildDesignSystem(theme)
}

Expand Down
64 changes: 32 additions & 32 deletions packages/tailwindcss/src/theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,16 @@ export const enum ThemeOptions {
DEFAULT = 1 << 2,
}

function isIgnoredThemeKey(themeKey: ThemeKey, ignoredThemeKeys: ThemeKey[]) {
return ignoredThemeKeys.some(
// In the future we may want to replace this with just a `Set` of known theme
// keys and let the computer sort out which keys should ignored which other keys
// based on overlapping prefixes.
const ignoredThemeKeyMap = new Map([
['--font', ['--font-weight', '--font-size']],
['--inset', ['--inset-shadow', '--inset-ring']],
])

function isIgnoredThemeKey(themeKey: ThemeKey, namespace: ThemeKey) {
return (ignoredThemeKeyMap.get(namespace) ?? []).some(
(ignoredThemeKey) => themeKey === ignoredThemeKey || themeKey.startsWith(`${ignoredThemeKey}-`),
)
}
Expand Down Expand Up @@ -50,22 +58,22 @@ export class Theme {
}
}

keysInNamespaces(themeKeys: ThemeKey[], ignoredThemeKeys: ThemeKey[] = []): string[] {
keysInNamespaces(themeKeys: ThemeKey[]): string[] {
let keys: string[] = []

for (let prefix of themeKeys) {
let namespace = `${prefix}-`
for (let namespace of themeKeys) {
let prefix = `${namespace}-`

for (let key of this.values.keys()) {
if (!key.startsWith(namespace)) continue
if (!key.startsWith(prefix)) continue

if (key.indexOf('--', 2) !== -1) continue

if (isIgnoredThemeKey(key as ThemeKey, ignoredThemeKeys)) {
if (isIgnoredThemeKey(key as ThemeKey, namespace)) {
continue
}

keys.push(key.slice(namespace.length))
keys.push(key.slice(prefix.length))
}
}

Expand Down Expand Up @@ -106,32 +114,33 @@ export class Theme {
}

clearNamespace(namespace: string, clearOptions: ThemeOptions) {
for (let key of this.values.keys()) {
let ignored = ignoredThemeKeyMap.get(namespace) ?? []

outer: 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
}
}
for (let ignoredNamespace of ignored) {
if (key.startsWith(ignoredNamespace)) continue outer
}
this.values.delete(key)
}
}
}

#resolveKey(
candidateValue: string | null,
themeKeys: ThemeKey[],
ignoredThemeKeys: ThemeKey[] = [],
): string | null {
for (let key of themeKeys) {
#resolveKey(candidateValue: string | null, themeKeys: ThemeKey[]): string | null {
for (let namespace of themeKeys) {
let themeKey =
candidateValue !== null
? (escape(`${key}-${candidateValue.replaceAll('.', '_')}`) as ThemeKey)
: key
? (escape(`${namespace}-${candidateValue.replaceAll('.', '_')}`) as ThemeKey)
: namespace

if (!this.values.has(themeKey)) continue
if (isIgnoredThemeKey(themeKey, ignoredThemeKeys)) continue
if (isIgnoredThemeKey(themeKey, namespace)) continue

return themeKey
}
Expand All @@ -147,12 +156,8 @@ export class Theme {
return `var(${this.#prefixKey(themeKey)})`
}

resolve(
candidateValue: string | null,
themeKeys: ThemeKey[],
ignoredThemeKeys: ThemeKey[] = [],
): string | null {
let themeKey = this.#resolveKey(candidateValue, themeKeys, ignoredThemeKeys)
resolve(candidateValue: string | null, themeKeys: ThemeKey[]): string | null {
let themeKey = this.#resolveKey(candidateValue, themeKeys)

if (!themeKey) return null

Expand All @@ -165,12 +170,8 @@ export class Theme {
return this.#var(themeKey)
}

resolveValue(
candidateValue: string | null,
themeKeys: ThemeKey[],
ignoredThemeKeys: ThemeKey[] = [],
): string | null {
let themeKey = this.#resolveKey(candidateValue, themeKeys, ignoredThemeKeys)
resolveValue(candidateValue: string | null, themeKeys: ThemeKey[]): string | null {
let themeKey = this.#resolveKey(candidateValue, themeKeys)

if (!themeKey) return null

Expand All @@ -181,9 +182,8 @@ export class Theme {
candidateValue: string,
themeKeys: ThemeKey[],
nestedKeys: `--${string}`[] = [],
ignoredThemeKeys: ThemeKey[] = [],
): [string, Record<string, string>] | null {
let themeKey = this.#resolveKey(candidateValue, themeKeys, ignoredThemeKeys)
let themeKey = this.#resolveKey(candidateValue, themeKeys)

if (!themeKey) return null

Expand Down
Loading