tailwindcss 3.4.0 → 3.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ - Nothing yet!
11
+
12
+ ## [3.4.1] - 2014-01-05
13
+
14
+ ### Fixed
15
+
16
+ - Don't remove keyframe stops when using important utilities ([#12639](https://github.com/tailwindlabs/tailwindcss/pull/12639))
17
+ - Don't add spaces to gradients and grid track names when followed by `calc()` ([#12704](https://github.com/tailwindlabs/tailwindcss/pull/12704))
18
+ - Restore old behavior for `class` dark mode strategy ([#12717](https://github.com/tailwindlabs/tailwindcss/pull/12717))
19
+
20
+ ### Added
21
+
22
+ - Add new `selector` and `variant` strategies for dark mode ([#12717](https://github.com/tailwindlabs/tailwindcss/pull/12717))
23
+
24
+ ### Changed
25
+
26
+ - Support `rtl` and `ltr` variants on same element as `dir` attribute ([#12717](https://github.com/tailwindlabs/tailwindcss/pull/12717))
27
+
10
28
  ## [3.4.0] - 2023-12-19
11
29
 
12
30
  ### Added
@@ -2333,7 +2351,8 @@ No release notes
2333
2351
 
2334
2352
  - Everything!
2335
2353
 
2336
- [unreleased]: https://github.com/tailwindlabs/tailwindcss/compare/v3.4.0...HEAD
2354
+ [unreleased]: https://github.com/tailwindlabs/tailwindcss/compare/v3.4.1...HEAD
2355
+ [3.4.1]: https://github.com/tailwindlabs/tailwindcss/compare/v3.4.0...v3.4.1
2337
2356
  [3.4.0]: https://github.com/tailwindlabs/tailwindcss/compare/v3.3.7...v3.4.0
2338
2357
  [3.3.7]: https://github.com/tailwindlabs/tailwindcss/compare/v3.3.6...v3.3.7
2339
2358
  [3.3.6]: https://github.com/tailwindlabs/tailwindcss/compare/v3.3.5...v3.3.6
@@ -267,15 +267,15 @@ let variantPlugins = {
267
267
  }
268
268
  },
269
269
  directionVariants: ({ addVariant })=>{
270
- addVariant("ltr", ':is(:where([dir="ltr"]) &)');
271
- addVariant("rtl", ':is(:where([dir="rtl"]) &)');
270
+ addVariant("ltr", '&:where([dir="ltr"], [dir="ltr"] *)');
271
+ addVariant("rtl", '&:where([dir="rtl"], [dir="rtl"] *)');
272
272
  },
273
273
  reducedMotionVariants: ({ addVariant })=>{
274
274
  addVariant("motion-safe", "@media (prefers-reduced-motion: no-preference)");
275
275
  addVariant("motion-reduce", "@media (prefers-reduced-motion: reduce)");
276
276
  },
277
277
  darkVariants: ({ config , addVariant })=>{
278
- let [mode, className = ".dark"] = [].concat(config("darkMode", "media"));
278
+ let [mode, selector = ".dark"] = [].concat(config("darkMode", "media"));
279
279
  if (mode === false) {
280
280
  mode = "media";
281
281
  _log.default.warn("darkmode-false", [
@@ -284,10 +284,48 @@ let variantPlugins = {
284
284
  "https://tailwindcss.com/docs/upgrade-guide#remove-dark-mode-configuration"
285
285
  ]);
286
286
  }
287
- if (mode === "class") {
288
- addVariant("dark", `:is(:where(${className}) &)`);
287
+ if (mode === "variant") {
288
+ let formats;
289
+ if (Array.isArray(selector)) {
290
+ formats = selector;
291
+ } else if (typeof selector === "function") {
292
+ formats = selector;
293
+ } else if (typeof selector === "string") {
294
+ formats = [
295
+ selector
296
+ ];
297
+ }
298
+ // TODO: We could also add these warnings if the user passes a function that returns string | string[]
299
+ // But this is an advanced enough use case that it's probably not necessary
300
+ if (Array.isArray(formats)) {
301
+ for (let format of formats){
302
+ if (format === ".dark") {
303
+ mode = false;
304
+ _log.default.warn("darkmode-variant-without-selector", [
305
+ "When using `variant` for `darkMode`, you must provide a selector.",
306
+ 'Example: `darkMode: ["variant", ".your-selector &"]`'
307
+ ]);
308
+ } else if (!format.includes("&")) {
309
+ mode = false;
310
+ _log.default.warn("darkmode-variant-without-ampersand", [
311
+ "When using `variant` for `darkMode`, your selector must contain `&`.",
312
+ 'Example `darkMode: ["variant", ".your-selector &"]`'
313
+ ]);
314
+ }
315
+ }
316
+ }
317
+ selector = formats;
318
+ }
319
+ if (mode === "selector") {
320
+ // New preferred behavior
321
+ addVariant("dark", `&:where(${selector}, ${selector} *)`);
289
322
  } else if (mode === "media") {
290
323
  addVariant("dark", "@media (prefers-color-scheme: dark)");
324
+ } else if (mode === "variant") {
325
+ addVariant("dark", selector);
326
+ } else if (mode === "class") {
327
+ // Old behavior
328
+ addVariant("dark", `:is(${selector} &)`);
291
329
  }
292
330
  },
293
331
  printVariant: ({ addVariant })=>{
@@ -162,6 +162,9 @@ function applyImportant(matches, classCandidate) {
162
162
  return matches;
163
163
  }
164
164
  let result = [];
165
+ function isInKeyframes(rule) {
166
+ return rule.parent && rule.parent.type === "atrule" && rule.parent.name === "keyframes";
167
+ }
165
168
  for (let [meta, rule] of matches){
166
169
  let container = _postcss.default.root({
167
170
  nodes: [
@@ -169,6 +172,11 @@ function applyImportant(matches, classCandidate) {
169
172
  ]
170
173
  });
171
174
  container.walkRules((r)=>{
175
+ // Declarations inside keyframes cannot be marked as important
176
+ // They will be ignored by the browser
177
+ if (isInKeyframes(r)) {
178
+ return;
179
+ }
172
180
  let ast = (0, _postcssselectorparser.default)().astSync(r.selector);
173
181
  // Remove extraneous selectors that do not include the base candidate
174
182
  ast.each((sel)=>(0, _formatVariantSelector.eliminateIrrelevantSelectors)(sel, classCandidate));
@@ -762,13 +762,29 @@ function resolvePlugins(context, root) {
762
762
  _corePlugins.variantPlugins["supportsVariants"],
763
763
  _corePlugins.variantPlugins["reducedMotionVariants"],
764
764
  _corePlugins.variantPlugins["prefersContrastVariants"],
765
- _corePlugins.variantPlugins["printVariant"],
766
765
  _corePlugins.variantPlugins["screenVariants"],
767
766
  _corePlugins.variantPlugins["orientationVariants"],
768
767
  _corePlugins.variantPlugins["directionVariants"],
769
768
  _corePlugins.variantPlugins["darkVariants"],
770
- _corePlugins.variantPlugins["forcedColorsVariants"]
769
+ _corePlugins.variantPlugins["forcedColorsVariants"],
770
+ _corePlugins.variantPlugins["printVariant"]
771
771
  ];
772
+ // This is a compatibility fix for the pre 3.4 dark mode behavior
773
+ // `class` retains the old behavior, but `selector` keeps the new behavior
774
+ let isLegacyDarkMode = context.tailwindConfig.darkMode === "class" || Array.isArray(context.tailwindConfig.darkMode) && context.tailwindConfig.darkMode[0] === "class";
775
+ if (isLegacyDarkMode) {
776
+ afterVariants = [
777
+ _corePlugins.variantPlugins["supportsVariants"],
778
+ _corePlugins.variantPlugins["reducedMotionVariants"],
779
+ _corePlugins.variantPlugins["prefersContrastVariants"],
780
+ _corePlugins.variantPlugins["darkVariants"],
781
+ _corePlugins.variantPlugins["screenVariants"],
782
+ _corePlugins.variantPlugins["orientationVariants"],
783
+ _corePlugins.variantPlugins["directionVariants"],
784
+ _corePlugins.variantPlugins["forcedColorsVariants"],
785
+ _corePlugins.variantPlugins["printVariant"]
786
+ ];
787
+ }
772
788
  return [
773
789
  ...corePluginList,
774
790
  ...beforeVariants,
@@ -140,7 +140,13 @@ function normalize(value, context = null, isRoot = true) {
140
140
  "keyboard-inset-bottom",
141
141
  "keyboard-inset-left",
142
142
  "keyboard-inset-width",
143
- "keyboard-inset-height"
143
+ "keyboard-inset-height",
144
+ "radial-gradient",
145
+ "linear-gradient",
146
+ "conic-gradient",
147
+ "repeating-radial-gradient",
148
+ "repeating-linear-gradient",
149
+ "repeating-conic-gradient"
144
150
  ];
145
151
  return value.replace(/(calc|min|max|clamp)\(.+\)/g, (match)=>{
146
152
  let result = "";
@@ -186,6 +192,10 @@ function normalize(value, context = null, isRoot = true) {
186
192
  result += consumeUntil([
187
193
  ")"
188
194
  ]);
195
+ } else if (peek("[")) {
196
+ result += consumeUntil([
197
+ "]"
198
+ ]);
189
199
  } else if ([
190
200
  "+",
191
201
  "-",
@@ -114,6 +114,9 @@ let elementProperties = {
114
114
  "terminal",
115
115
  "jumpable"
116
116
  ],
117
+ ":where": [],
118
+ ":is": [],
119
+ ":has": [],
117
120
  // The default value is used when the pseudo-element is not recognized
118
121
  // Because it's not recognized, we don't know if it's terminal or not
119
122
  // So we assume it can be moved AND can have user-action pseudo classes attached to it
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tailwindcss",
3
- "version": "3.4.0",
3
+ "version": "3.4.1",
4
4
  "description": "A utility-first CSS framework for rapidly building custom user interfaces.",
5
5
  "license": "MIT",
6
6
  "main": "lib/index.js",
@@ -207,8 +207,8 @@ export let variantPlugins = {
207
207
  },
208
208
 
209
209
  directionVariants: ({ addVariant }) => {
210
- addVariant('ltr', ':is(:where([dir="ltr"]) &)')
211
- addVariant('rtl', ':is(:where([dir="rtl"]) &)')
210
+ addVariant('ltr', '&:where([dir="ltr"], [dir="ltr"] *)')
211
+ addVariant('rtl', '&:where([dir="rtl"], [dir="rtl"] *)')
212
212
  },
213
213
 
214
214
  reducedMotionVariants: ({ addVariant }) => {
@@ -217,7 +217,7 @@ export let variantPlugins = {
217
217
  },
218
218
 
219
219
  darkVariants: ({ config, addVariant }) => {
220
- let [mode, className = '.dark'] = [].concat(config('darkMode', 'media'))
220
+ let [mode, selector = '.dark'] = [].concat(config('darkMode', 'media'))
221
221
 
222
222
  if (mode === false) {
223
223
  mode = 'media'
@@ -228,10 +228,49 @@ export let variantPlugins = {
228
228
  ])
229
229
  }
230
230
 
231
- if (mode === 'class') {
232
- addVariant('dark', `:is(:where(${className}) &)`)
231
+ if (mode === 'variant') {
232
+ let formats
233
+ if (Array.isArray(selector)) {
234
+ formats = selector
235
+ } else if (typeof selector === 'function') {
236
+ formats = selector
237
+ } else if (typeof selector === 'string') {
238
+ formats = [selector]
239
+ }
240
+
241
+ // TODO: We could also add these warnings if the user passes a function that returns string | string[]
242
+ // But this is an advanced enough use case that it's probably not necessary
243
+ if (Array.isArray(formats)) {
244
+ for (let format of formats) {
245
+ if (format === '.dark') {
246
+ mode = false
247
+ log.warn('darkmode-variant-without-selector', [
248
+ 'When using `variant` for `darkMode`, you must provide a selector.',
249
+ 'Example: `darkMode: ["variant", ".your-selector &"]`',
250
+ ])
251
+ } else if (!format.includes('&')) {
252
+ mode = false
253
+ log.warn('darkmode-variant-without-ampersand', [
254
+ 'When using `variant` for `darkMode`, your selector must contain `&`.',
255
+ 'Example `darkMode: ["variant", ".your-selector &"]`',
256
+ ])
257
+ }
258
+ }
259
+ }
260
+
261
+ selector = formats
262
+ }
263
+
264
+ if (mode === 'selector') {
265
+ // New preferred behavior
266
+ addVariant('dark', `&:where(${selector}, ${selector} *)`)
233
267
  } else if (mode === 'media') {
234
268
  addVariant('dark', '@media (prefers-color-scheme: dark)')
269
+ } else if (mode === 'variant') {
270
+ addVariant('dark', selector)
271
+ } else if (mode === 'class') {
272
+ // Old behavior
273
+ addVariant('dark', `:is(${selector} &)`)
235
274
  }
236
275
  },
237
276
 
@@ -119,10 +119,20 @@ function applyImportant(matches, classCandidate) {
119
119
 
120
120
  let result = []
121
121
 
122
+ function isInKeyframes(rule) {
123
+ return rule.parent && rule.parent.type === 'atrule' && rule.parent.name === 'keyframes'
124
+ }
125
+
122
126
  for (let [meta, rule] of matches) {
123
127
  let container = postcss.root({ nodes: [rule.clone()] })
124
128
 
125
129
  container.walkRules((r) => {
130
+ // Declarations inside keyframes cannot be marked as important
131
+ // They will be ignored by the browser
132
+ if (isInKeyframes(r)) {
133
+ return
134
+ }
135
+
126
136
  let ast = selectorParser().astSync(r.selector)
127
137
 
128
138
  // Remove extraneous selectors that do not include the base candidate
@@ -767,14 +767,35 @@ function resolvePlugins(context, root) {
767
767
  variantPlugins['supportsVariants'],
768
768
  variantPlugins['reducedMotionVariants'],
769
769
  variantPlugins['prefersContrastVariants'],
770
- variantPlugins['printVariant'],
771
770
  variantPlugins['screenVariants'],
772
771
  variantPlugins['orientationVariants'],
773
772
  variantPlugins['directionVariants'],
774
773
  variantPlugins['darkVariants'],
775
774
  variantPlugins['forcedColorsVariants'],
775
+ variantPlugins['printVariant'],
776
776
  ]
777
777
 
778
+ // This is a compatibility fix for the pre 3.4 dark mode behavior
779
+ // `class` retains the old behavior, but `selector` keeps the new behavior
780
+ let isLegacyDarkMode =
781
+ context.tailwindConfig.darkMode === 'class' ||
782
+ (Array.isArray(context.tailwindConfig.darkMode) &&
783
+ context.tailwindConfig.darkMode[0] === 'class')
784
+
785
+ if (isLegacyDarkMode) {
786
+ afterVariants = [
787
+ variantPlugins['supportsVariants'],
788
+ variantPlugins['reducedMotionVariants'],
789
+ variantPlugins['prefersContrastVariants'],
790
+ variantPlugins['darkVariants'],
791
+ variantPlugins['screenVariants'],
792
+ variantPlugins['orientationVariants'],
793
+ variantPlugins['directionVariants'],
794
+ variantPlugins['forcedColorsVariants'],
795
+ variantPlugins['printVariant'],
796
+ ]
797
+ }
798
+
778
799
  return [...corePluginList, ...beforeVariants, ...userPlugins, ...afterVariants, ...layerPlugins]
779
800
  }
780
801
 
@@ -106,6 +106,13 @@ function normalizeMathOperatorSpacing(value) {
106
106
  'keyboard-inset-left',
107
107
  'keyboard-inset-width',
108
108
  'keyboard-inset-height',
109
+
110
+ 'radial-gradient',
111
+ 'linear-gradient',
112
+ 'conic-gradient',
113
+ 'repeating-radial-gradient',
114
+ 'repeating-linear-gradient',
115
+ 'repeating-conic-gradient',
109
116
  ]
110
117
 
111
118
  return value.replace(/(calc|min|max|clamp)\(.+\)/g, (match) => {
@@ -161,6 +168,11 @@ function normalizeMathOperatorSpacing(value) {
161
168
  result += consumeUntil([')'])
162
169
  }
163
170
 
171
+ // Don't break CSS grid track names
172
+ else if (peek('[')) {
173
+ result += consumeUntil([']'])
174
+ }
175
+
164
176
  // Handle operators
165
177
  else if (
166
178
  ['+', '-', '*', '/'].includes(char) &&
@@ -60,6 +60,10 @@ let elementProperties = {
60
60
  ':first-letter': ['terminal', 'jumpable'],
61
61
  ':first-line': ['terminal', 'jumpable'],
62
62
 
63
+ ':where': [],
64
+ ':is': [],
65
+ ':has': [],
66
+
63
67
  // The default value is used when the pseudo-element is not recognized
64
68
  // Because it's not recognized, we don't know if it's terminal or not
65
69
  // So we assume it can be moved AND can have user-action pseudo classes attached to it
package/types/config.d.ts CHANGED
@@ -74,6 +74,13 @@ type DarkModeConfig =
74
74
  | 'class'
75
75
  // Use the `class` strategy with a custom class instead of `.dark`.
76
76
  | ['class', string]
77
+ // Use the `selector` strategy — same as `class` but uses `:where()` for more predicable behavior
78
+ | 'selector'
79
+ // Use the `selector` strategy with a custom selector instead of `.dark`.
80
+ | ['selector', string]
81
+ // Use the `variant` strategy, which allows you to completely customize the selector
82
+ // It takes a string or an array of strings, which are passed directly to `addVariant()`
83
+ | ['variant', string | string[]]
77
84
 
78
85
  type Screen = { raw: string } | { min: string } | { max: string } | { min: string; max: string }
79
86
  type ScreensConfig = string[] | KeyValuePair<string, string | Screen | Screen[]>