Skip to content

Commit a4f8a36

Browse files
authored
Don't unset keys like --font-weight-* when unsetting --font-* (#14906)
This PR fixes an issue where unsetting `--font-*` would unset `--font-weight-*` and `--font-size-*`. ```css @theme { --font-weight-bold: bold; --font-size-sm: 14px; --font-sans: sans-serif; --font-serif: serif; } @theme { --font-*: initial; --font-body: Inter; } ``` Up until now this was sort of intended/desired behavior but with recent changes there are now more overlapping theme keys (`--inset-*` and `--inset-shadow-*` as well for example), and we don't want to make it impossible to unset _just_ the default `font-family` values. This PR also simplifies how we were handling making sure that the `inset-*` utility ignored `--inset-shadow-*` variables since it's all really the same problem. This does mean we need to maintain a list of known theme keys so we know when there is a conflict between two keys, which is kind of unfortunate because up until now this was a totally dynamic thing. End users can still add whatever custom stuff they want under `@theme` but we don't really know about those namespaces since we're maintaining a static list so we can't resolve conflicts there. I'm confident there are ways we could solve this if it actually becomes a problem, so content to push forward without solving it right now and just deal with it if/when it actually arises, because it just might not. --------- Co-authored-by: Adam Wathan <[email protected]>
1 parent 15fc7f4 commit a4f8a36

File tree

6 files changed

+255
-115
lines changed

6 files changed

+255
-115
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2929
- Ensure adjacent rules are merged together after handling nesting when generating optimized CSS ([#14873](https://github.com/tailwindlabs/tailwindcss/pull/14873))
3030
- Rebase `url()` inside imported CSS files when using Vite ([#14877](https://github.com/tailwindlabs/tailwindcss/pull/14877))
3131
- 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))
32+
- Don't unset keys like `--inset-shadow-*` when unsetting keys like `--inset-*` ([#14906](https://github.com/tailwindlabs/tailwindcss/pull/14906))
3233
- _Upgrade (experimental)_: Install `@tailwindcss/postcss` next to `tailwindcss` ([#14830](https://github.com/tailwindlabs/tailwindcss/pull/14830))
3334
- _Upgrade (experimental)_: Remove whitespace around `,` separator when print arbitrary values ([#14838](https://github.com/tailwindlabs/tailwindcss/pull/14838))
3435
- _Upgrade (experimental)_: Fix crash during upgrade when content globs escape root of project ([#14896](https://github.com/tailwindlabs/tailwindcss/pull/14896))

packages/tailwindcss/src/index.test.ts

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1174,6 +1174,179 @@ describe('Parsing themes values from CSS', () => {
11741174
`)
11751175
})
11761176

1177+
test('unsetting `--font-*` does not unset `--font-weight-*` or `--font-size-*`', async () => {
1178+
expect(
1179+
await compileCss(
1180+
css`
1181+
@theme {
1182+
--font-weight-bold: bold;
1183+
--font-size-sm: 14px;
1184+
--font-sans: sans-serif;
1185+
--font-serif: serif;
1186+
}
1187+
@theme {
1188+
--font-*: initial;
1189+
--font-body: Inter;
1190+
}
1191+
@tailwind utilities;
1192+
`,
1193+
['font-bold', 'text-sm', 'font-sans', 'font-serif', 'font-body'],
1194+
),
1195+
).toMatchInlineSnapshot(`
1196+
":root {
1197+
--font-weight-bold: bold;
1198+
--font-size-sm: 14px;
1199+
--font-body: Inter;
1200+
}
1201+
1202+
.font-body {
1203+
font-family: var(--font-body);
1204+
}
1205+
1206+
.text-sm {
1207+
font-size: var(--font-size-sm);
1208+
}
1209+
1210+
.font-bold {
1211+
--tw-font-weight: var(--font-weight-bold);
1212+
font-weight: var(--font-weight-bold);
1213+
}
1214+
1215+
@supports (-moz-orient: inline) {
1216+
@layer base {
1217+
*, :before, :after, ::backdrop {
1218+
--tw-font-weight: initial;
1219+
}
1220+
}
1221+
}
1222+
1223+
@property --tw-font-weight {
1224+
syntax: "*";
1225+
inherits: false
1226+
}"
1227+
`)
1228+
})
1229+
1230+
test('unsetting `--inset-*` does not unset `--inset-shadow-*`', async () => {
1231+
expect(
1232+
await compileCss(
1233+
css`
1234+
@theme {
1235+
--inset-shadow-sm: inset 0 2px 4px rgb(0 0 0 / 0.05);
1236+
--inset-lg: 100px;
1237+
--inset-sm: 10px;
1238+
}
1239+
@theme {
1240+
--inset-*: initial;
1241+
--inset-md: 50px;
1242+
}
1243+
@tailwind utilities;
1244+
`,
1245+
['inset-shadow-sm', 'inset-ring-thick', 'inset-lg', 'inset-sm', 'inset-md'],
1246+
),
1247+
).toMatchInlineSnapshot(`
1248+
":root {
1249+
--inset-shadow-sm: inset 0 2px 4px #0000000d;
1250+
--inset-md: 50px;
1251+
}
1252+
1253+
.inset-md {
1254+
inset: var(--inset-md);
1255+
}
1256+
1257+
.inset-shadow-sm {
1258+
--tw-inset-shadow: inset 0 2px 4px var(--tw-inset-shadow-color, #0000000d);
1259+
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
1260+
}
1261+
1262+
@supports (-moz-orient: inline) {
1263+
@layer base {
1264+
*, :before, :after, ::backdrop {
1265+
--tw-shadow: 0 0 #0000;
1266+
--tw-shadow-color: initial;
1267+
--tw-inset-shadow: 0 0 #0000;
1268+
--tw-inset-shadow-color: initial;
1269+
--tw-ring-color: initial;
1270+
--tw-ring-shadow: 0 0 #0000;
1271+
--tw-inset-ring-color: initial;
1272+
--tw-inset-ring-shadow: 0 0 #0000;
1273+
--tw-ring-inset: initial;
1274+
--tw-ring-offset-width: 0px;
1275+
--tw-ring-offset-color: #fff;
1276+
--tw-ring-offset-shadow: 0 0 #0000;
1277+
}
1278+
}
1279+
}
1280+
1281+
@property --tw-shadow {
1282+
syntax: "*";
1283+
inherits: false;
1284+
initial-value: 0 0 #0000;
1285+
}
1286+
1287+
@property --tw-shadow-color {
1288+
syntax: "*";
1289+
inherits: false
1290+
}
1291+
1292+
@property --tw-inset-shadow {
1293+
syntax: "*";
1294+
inherits: false;
1295+
initial-value: 0 0 #0000;
1296+
}
1297+
1298+
@property --tw-inset-shadow-color {
1299+
syntax: "*";
1300+
inherits: false
1301+
}
1302+
1303+
@property --tw-ring-color {
1304+
syntax: "*";
1305+
inherits: false
1306+
}
1307+
1308+
@property --tw-ring-shadow {
1309+
syntax: "*";
1310+
inherits: false;
1311+
initial-value: 0 0 #0000;
1312+
}
1313+
1314+
@property --tw-inset-ring-color {
1315+
syntax: "*";
1316+
inherits: false
1317+
}
1318+
1319+
@property --tw-inset-ring-shadow {
1320+
syntax: "*";
1321+
inherits: false;
1322+
initial-value: 0 0 #0000;
1323+
}
1324+
1325+
@property --tw-ring-inset {
1326+
syntax: "*";
1327+
inherits: false
1328+
}
1329+
1330+
@property --tw-ring-offset-width {
1331+
syntax: "<length>";
1332+
inherits: false;
1333+
initial-value: 0;
1334+
}
1335+
1336+
@property --tw-ring-offset-color {
1337+
syntax: "*";
1338+
inherits: false;
1339+
initial-value: #fff;
1340+
}
1341+
1342+
@property --tw-ring-offset-shadow {
1343+
syntax: "*";
1344+
inherits: false;
1345+
initial-value: 0 0 #0000;
1346+
}"
1347+
`)
1348+
})
1349+
11771350
test('unused keyframes are removed when an animation is unset', async () => {
11781351
expect(
11791352
await compileCss(

packages/tailwindcss/src/intellisense.test.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ function loadDesignSystem() {
1818
theme.add('--opacity-background', '0.3')
1919
theme.add('--drop-shadow-sm', '0 1px 1px rgb(0 0 0 / 0.05)')
2020
theme.add('--inset-shadow-sm', 'inset 0 1px 1px rgb(0 0 0 / 0.05)')
21-
theme.add('--inset-ring-big', '100px')
2221
return buildDesignSystem(theme)
2322
}
2423

packages/tailwindcss/src/theme.ts

Lines changed: 32 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,16 @@ export const enum ThemeOptions {
88
DEFAULT = 1 << 2,
99
}
1010

11-
function isIgnoredThemeKey(themeKey: ThemeKey, ignoredThemeKeys: ThemeKey[]) {
12-
return ignoredThemeKeys.some(
11+
// In the future we may want to replace this with just a `Set` of known theme
12+
// keys and let the computer sort out which keys should ignored which other keys
13+
// based on overlapping prefixes.
14+
const ignoredThemeKeyMap = new Map([
15+
['--font', ['--font-weight', '--font-size']],
16+
['--inset', ['--inset-shadow', '--inset-ring']],
17+
])
18+
19+
function isIgnoredThemeKey(themeKey: ThemeKey, namespace: ThemeKey) {
20+
return (ignoredThemeKeyMap.get(namespace) ?? []).some(
1321
(ignoredThemeKey) => themeKey === ignoredThemeKey || themeKey.startsWith(`${ignoredThemeKey}-`),
1422
)
1523
}
@@ -50,22 +58,22 @@ export class Theme {
5058
}
5159
}
5260

53-
keysInNamespaces(themeKeys: ThemeKey[], ignoredThemeKeys: ThemeKey[] = []): string[] {
61+
keysInNamespaces(themeKeys: ThemeKey[]): string[] {
5462
let keys: string[] = []
5563

56-
for (let prefix of themeKeys) {
57-
let namespace = `${prefix}-`
64+
for (let namespace of themeKeys) {
65+
let prefix = `${namespace}-`
5866

5967
for (let key of this.values.keys()) {
60-
if (!key.startsWith(namespace)) continue
68+
if (!key.startsWith(prefix)) continue
6169

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

64-
if (isIgnoredThemeKey(key as ThemeKey, ignoredThemeKeys)) {
72+
if (isIgnoredThemeKey(key as ThemeKey, namespace)) {
6573
continue
6674
}
6775

68-
keys.push(key.slice(namespace.length))
76+
keys.push(key.slice(prefix.length))
6977
}
7078
}
7179

@@ -106,32 +114,33 @@ export class Theme {
106114
}
107115

108116
clearNamespace(namespace: string, clearOptions: ThemeOptions) {
109-
for (let key of this.values.keys()) {
117+
let ignored = ignoredThemeKeyMap.get(namespace) ?? []
118+
119+
outer: for (let key of this.values.keys()) {
110120
if (key.startsWith(namespace)) {
111121
if (clearOptions !== ThemeOptions.NONE) {
112122
let options = this.getOptions(key)
113123
if ((options & clearOptions) !== clearOptions) {
114124
continue
115125
}
116126
}
127+
for (let ignoredNamespace of ignored) {
128+
if (key.startsWith(ignoredNamespace)) continue outer
129+
}
117130
this.values.delete(key)
118131
}
119132
}
120133
}
121134

122-
#resolveKey(
123-
candidateValue: string | null,
124-
themeKeys: ThemeKey[],
125-
ignoredThemeKeys: ThemeKey[] = [],
126-
): string | null {
127-
for (let key of themeKeys) {
135+
#resolveKey(candidateValue: string | null, themeKeys: ThemeKey[]): string | null {
136+
for (let namespace of themeKeys) {
128137
let themeKey =
129138
candidateValue !== null
130-
? (escape(`${key}-${candidateValue.replaceAll('.', '_')}`) as ThemeKey)
131-
: key
139+
? (escape(`${namespace}-${candidateValue.replaceAll('.', '_')}`) as ThemeKey)
140+
: namespace
132141

133142
if (!this.values.has(themeKey)) continue
134-
if (isIgnoredThemeKey(themeKey, ignoredThemeKeys)) continue
143+
if (isIgnoredThemeKey(themeKey, namespace)) continue
135144

136145
return themeKey
137146
}
@@ -147,12 +156,8 @@ export class Theme {
147156
return `var(${this.#prefixKey(themeKey)})`
148157
}
149158

150-
resolve(
151-
candidateValue: string | null,
152-
themeKeys: ThemeKey[],
153-
ignoredThemeKeys: ThemeKey[] = [],
154-
): string | null {
155-
let themeKey = this.#resolveKey(candidateValue, themeKeys, ignoredThemeKeys)
159+
resolve(candidateValue: string | null, themeKeys: ThemeKey[]): string | null {
160+
let themeKey = this.#resolveKey(candidateValue, themeKeys)
156161

157162
if (!themeKey) return null
158163

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

168-
resolveValue(
169-
candidateValue: string | null,
170-
themeKeys: ThemeKey[],
171-
ignoredThemeKeys: ThemeKey[] = [],
172-
): string | null {
173-
let themeKey = this.#resolveKey(candidateValue, themeKeys, ignoredThemeKeys)
173+
resolveValue(candidateValue: string | null, themeKeys: ThemeKey[]): string | null {
174+
let themeKey = this.#resolveKey(candidateValue, themeKeys)
174175

175176
if (!themeKey) return null
176177

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

188188
if (!themeKey) return null
189189

0 commit comments

Comments
 (0)