tailwindcss 0.0.0-insiders.f667746 → 0.0.0-insiders.f69a46b

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.
@@ -82,6 +82,9 @@ function _interop_require_wildcard(obj, nodeInterop) {
82
82
  return newObj;
83
83
  }
84
84
  let variantPlugins = {
85
+ childVariant: ({ addVariant })=>{
86
+ addVariant('*', '& > *');
87
+ },
85
88
  pseudoElementVariants: ({ addVariant })=>{
86
89
  addVariant('first-letter', '&::first-letter');
87
90
  addVariant('first-line', '&::first-line');
@@ -265,15 +268,15 @@ let variantPlugins = {
265
268
  }
266
269
  },
267
270
  directionVariants: ({ addVariant })=>{
268
- addVariant('ltr', ':is([dir="ltr"] &)');
269
- addVariant('rtl', ':is([dir="rtl"] &)');
271
+ addVariant('ltr', '&:where([dir="ltr"], [dir="ltr"] *)');
272
+ addVariant('rtl', '&:where([dir="rtl"], [dir="rtl"] *)');
270
273
  },
271
274
  reducedMotionVariants: ({ addVariant })=>{
272
275
  addVariant('motion-safe', '@media (prefers-reduced-motion: no-preference)');
273
276
  addVariant('motion-reduce', '@media (prefers-reduced-motion: reduce)');
274
277
  },
275
278
  darkVariants: ({ config, addVariant })=>{
276
- let [mode, className = '.dark'] = [].concat(config('darkMode', 'media'));
279
+ let [mode, selector = '.dark'] = [].concat(config('darkMode', 'media'));
277
280
  if (mode === false) {
278
281
  mode = 'media';
279
282
  _log.default.warn('darkmode-false', [
@@ -282,10 +285,48 @@ let variantPlugins = {
282
285
  'https://tailwindcss.com/docs/upgrade-guide#remove-dark-mode-configuration'
283
286
  ]);
284
287
  }
285
- if (mode === 'class') {
286
- addVariant('dark', `:is(${className} &)`);
288
+ if (mode === 'variant') {
289
+ let formats;
290
+ if (Array.isArray(selector)) {
291
+ formats = selector;
292
+ } else if (typeof selector === 'function') {
293
+ formats = selector;
294
+ } else if (typeof selector === 'string') {
295
+ formats = [
296
+ selector
297
+ ];
298
+ }
299
+ // TODO: We could also add these warnings if the user passes a function that returns string | string[]
300
+ // But this is an advanced enough use case that it's probably not necessary
301
+ if (Array.isArray(formats)) {
302
+ for (let format of formats){
303
+ if (format === '.dark') {
304
+ mode = false;
305
+ _log.default.warn('darkmode-variant-without-selector', [
306
+ 'When using `variant` for `darkMode`, you must provide a selector.',
307
+ 'Example: `darkMode: ["variant", ".your-selector &"]`'
308
+ ]);
309
+ } else if (!format.includes('&')) {
310
+ mode = false;
311
+ _log.default.warn('darkmode-variant-without-ampersand', [
312
+ 'When using `variant` for `darkMode`, your selector must contain `&`.',
313
+ 'Example `darkMode: ["variant", ".your-selector &"]`'
314
+ ]);
315
+ }
316
+ }
317
+ }
318
+ selector = formats;
319
+ }
320
+ if (mode === 'selector') {
321
+ // New preferred behavior
322
+ addVariant('dark', `&:where(${selector}, ${selector} *)`);
287
323
  } else if (mode === 'media') {
288
324
  addVariant('dark', '@media (prefers-color-scheme: dark)');
325
+ } else if (mode === 'variant') {
326
+ addVariant('dark', selector);
327
+ } else if (mode === 'class') {
328
+ // Old behavior
329
+ addVariant('dark', `:is(${selector} &)`);
289
330
  }
290
331
  },
291
332
  printVariant: ({ addVariant })=>{
@@ -195,9 +195,9 @@ select {
195
195
  */
196
196
 
197
197
  button,
198
- [type='button'],
199
- [type='reset'],
200
- [type='submit'] {
198
+ input:where([type='button']),
199
+ input:where([type='reset']),
200
+ input:where([type='submit']) {
201
201
  -webkit-appearance: button; /* 1 */
202
202
  background-color: transparent; /* 2 */
203
203
  background-image: none; /* 2 */
@@ -21,7 +21,6 @@ const _fs = /*#__PURE__*/ _interop_require_default(require("fs"));
21
21
  const _path = /*#__PURE__*/ _interop_require_default(require("path"));
22
22
  const _isglob = /*#__PURE__*/ _interop_require_default(require("is-glob"));
23
23
  const _fastglob = /*#__PURE__*/ _interop_require_default(require("fast-glob"));
24
- const _normalizepath = /*#__PURE__*/ _interop_require_default(require("normalize-path"));
25
24
  const _parseGlob = require("../util/parseGlob");
26
25
  const _sharedState = require("./sharedState");
27
26
  const _oxide = require("@tailwindcss/oxide");
@@ -30,6 +29,37 @@ function _interop_require_default(obj) {
30
29
  default: obj
31
30
  };
32
31
  }
32
+ /*!
33
+ * Modified version of normalize-path, original license below
34
+ *
35
+ * normalize-path <https://github.com/jonschlinkert/normalize-path>
36
+ *
37
+ * Copyright (c) 2014-2018, Jon Schlinkert.
38
+ * Released under the MIT License.
39
+ */ function normalizePath(path) {
40
+ if (typeof path !== 'string') {
41
+ throw new TypeError('expected path to be a string');
42
+ }
43
+ if (path === '\\' || path === '/') return '/';
44
+ var len = path.length;
45
+ if (len <= 1) return path;
46
+ // ensure that win32 namespaces has two leading slashes, so that the path is
47
+ // handled properly by the win32 version of path.parse() after being normalized
48
+ // https://msdn.microsoft.com/library/windows/desktop/aa365247(v=vs.85).aspx#namespaces
49
+ var prefix = '';
50
+ if (len > 4 && path[3] === '\\') {
51
+ var ch = path[2];
52
+ if ((ch === '?' || ch === '.') && path.slice(0, 2) === '\\\\') {
53
+ path = path.slice(2);
54
+ prefix = '//';
55
+ }
56
+ }
57
+ // Modified part: instead of purely splitting on `\\` and `/`, we split on
58
+ // `/` and `\\` that is _not_ followed by any of the following characters: ()[]
59
+ // This is to ensure that we keep the escaping of brackets and parentheses
60
+ let segs = path.split(/[/\\]+(?![\(\)\[\]])/);
61
+ return prefix + segs.join('/');
62
+ }
33
63
  /** @typedef {import('../../types/config.js').RawFile} RawFile */ /** @typedef {import('../../types/config.js').FilePath} FilePath */ /*
34
64
  * @param {import('tailwindcss').Config} tailwindConfig
35
65
  * @param {{skip:string[]}} options
@@ -59,7 +89,7 @@ function parseCandidateFiles(context, tailwindConfig) {
59
89
  });
60
90
  // Normalize the file globs
61
91
  files = files.filter((filePath)=>typeof filePath === 'string');
62
- files = files.map(_normalizepath.default);
92
+ files = files.map(normalizePath);
63
93
  // Split into included and excluded globs
64
94
  let tasks = _fastglob.default.generateTasks(files);
65
95
  /** @type {ContentPath[]} */ let included = [];
@@ -86,6 +116,9 @@ function parseCandidateFiles(context, tailwindConfig) {
86
116
  * @param {boolean} ignore
87
117
  * @returns {ContentPath}
88
118
  */ function parseFilePath(filePath, ignore) {
119
+ // Escape special characters in the file path such as: ()[]
120
+ // But only if the special character isn't already escaped
121
+ filePath = filePath.replace(RegExp("(?<!\\\\)([\\[\\]\\(\\)])", "g"), '\\$1');
89
122
  let contentPath = {
90
123
  original: filePath,
91
124
  base: filePath,
@@ -107,7 +140,7 @@ function parseCandidateFiles(context, tailwindConfig) {
107
140
  // Afaik, this technically shouldn't be needed but there's probably
108
141
  // some internal, direct path matching with a normalized path in
109
142
  // a package which can't handle mixed directory separators
110
- let base = (0, _normalizepath.default)(contentPath.base);
143
+ let base = normalizePath(contentPath.base);
111
144
  // If the user's file path contains any special characters (like parens) for instance fast-glob
112
145
  // is like "OOOH SHINY" and treats them as such. So we have to escape the base path to fix this
113
146
  base = _fastglob.default.escapePath(base);
@@ -82,7 +82,11 @@ function* buildRegExps(context) {
82
82
  // Utilities
83
83
  _regex.pattern([
84
84
  // Utility Name / Group Name
85
- /-?(?:\w+)/,
85
+ _regex.any([
86
+ /-?(?:\w+)/,
87
+ // This is here to make sure @container supports everything that other utilities do
88
+ /@(?:\w+)/
89
+ ]),
86
90
  // Normal/Arbitrary values
87
91
  _regex.optional(_regex.any([
88
92
  _regex.pattern([
@@ -163,6 +163,9 @@ function applyImportant(matches, classCandidate) {
163
163
  return matches;
164
164
  }
165
165
  let result = [];
166
+ function isInKeyframes(rule) {
167
+ return rule.parent && rule.parent.type === 'atrule' && rule.parent.name === 'keyframes';
168
+ }
166
169
  for (let [meta, rule] of matches){
167
170
  let container = _postcss.default.root({
168
171
  nodes: [
@@ -170,6 +173,11 @@ function applyImportant(matches, classCandidate) {
170
173
  ]
171
174
  });
172
175
  container.walkRules((r)=>{
176
+ // Declarations inside keyframes cannot be marked as important
177
+ // They will be ignored by the browser
178
+ if (isInKeyframes(r)) {
179
+ return;
180
+ }
173
181
  let ast = (0, _postcssselectorparser.default)().astSync(r.selector);
174
182
  // Remove extraneous selectors that do not include the base candidate
175
183
  ast.each((sel)=>(0, _formatVariantSelector.eliminateIrrelevantSelectors)(sel, classCandidate));
@@ -504,7 +512,7 @@ function extractArbitraryProperty(classCandidate, context) {
504
512
  if (!isParsableCssValue(property, normalized)) {
505
513
  return null;
506
514
  }
507
- let sort = context.offsets.arbitraryProperty();
515
+ let sort = context.offsets.arbitraryProperty(classCandidate);
508
516
  return [
509
517
  [
510
518
  {
@@ -25,7 +25,7 @@ function _interop_require_default(obj) {
25
25
  }
26
26
  let jiti = null;
27
27
  function useCustomJiti(_jiti) {
28
- jiti = _jiti;
28
+ jiti = _jiti();
29
29
  }
30
30
  function lazyJiti() {
31
31
  return jiti !== null && jiti !== void 0 ? jiti : jiti = (0, _jiti.default)(__filename, {
@@ -28,15 +28,19 @@ class Offsets {
28
28
  variants: 0n,
29
29
  parallelIndex: 0n,
30
30
  index: this.offsets[layer]++,
31
+ propertyOffset: 0n,
32
+ property: '',
31
33
  options: []
32
34
  };
33
35
  }
34
36
  /**
37
+ * @param {string} name
35
38
  * @returns {RuleOffset}
36
- */ arbitraryProperty() {
39
+ */ arbitraryProperty(name) {
37
40
  return {
38
41
  ...this.create('utilities'),
39
- arbitrary: 1n
42
+ arbitrary: 1n,
43
+ property: name
40
44
  };
41
45
  }
42
46
  /**
@@ -176,6 +180,10 @@ class Offsets {
176
180
  if (a.arbitrary !== b.arbitrary) {
177
181
  return a.arbitrary - b.arbitrary;
178
182
  }
183
+ // Always sort arbitrary properties alphabetically
184
+ if (a.propertyOffset !== b.propertyOffset) {
185
+ return a.propertyOffset - b.propertyOffset;
186
+ }
179
187
  // Sort utilities, components, etc… in the order they were registered
180
188
  return a.index - b.index;
181
189
  }
@@ -229,8 +237,49 @@ class Offsets {
229
237
  * @template T
230
238
  * @param {[RuleOffset, T][]} list
231
239
  * @returns {[RuleOffset, T][]}
240
+ */ sortArbitraryProperties(list) {
241
+ // Collect all known arbitrary properties
242
+ let known = new Set();
243
+ for (let [offset] of list){
244
+ if (offset.arbitrary === 1n) {
245
+ known.add(offset.property);
246
+ }
247
+ }
248
+ // No arbitrary properties? Nothing to do.
249
+ if (known.size === 0) {
250
+ return list;
251
+ }
252
+ // Sort the properties alphabetically
253
+ let properties = Array.from(known).sort();
254
+ // Create a map from the property name to its offset
255
+ let offsets = new Map();
256
+ let offset = 1n;
257
+ for (let property of properties){
258
+ offsets.set(property, offset++);
259
+ }
260
+ // Apply the sorted offsets to the list
261
+ return list.map((item)=>{
262
+ let [offset, rule] = item;
263
+ var _offsets_get;
264
+ offset = {
265
+ ...offset,
266
+ propertyOffset: (_offsets_get = offsets.get(offset.property)) !== null && _offsets_get !== void 0 ? _offsets_get : 0n
267
+ };
268
+ return [
269
+ offset,
270
+ rule
271
+ ];
272
+ });
273
+ }
274
+ /**
275
+ * @template T
276
+ * @param {[RuleOffset, T][]} list
277
+ * @returns {[RuleOffset, T][]}
232
278
  */ sort(list) {
279
+ // Sort arbitrary variants so they're in alphabetical order
233
280
  list = this.remapArbitraryVariantOffsets(list);
281
+ // Sort arbitrary properties so they're in alphabetical order
282
+ list = this.sortArbitraryProperties(list);
234
283
  return list.sort(([a], [b])=>(0, _bigSign.default)(this.compare(a, b)));
235
284
  }
236
285
  constructor(){
@@ -111,7 +111,9 @@ function resolveDefaultsAtRules({ tailwindConfig }) {
111
111
  // we consider them separately because merging the declarations into
112
112
  // a single rule will cause browsers that do not understand the
113
113
  // vendor prefix to throw out the whole rule
114
- let selectorGroupName = selector.includes(':-') || selector.includes('::-') ? selector : '__DEFAULT__';
114
+ // Additionally if a selector contains `:has` we also consider
115
+ // it separately because FF only recently gained support for it
116
+ let selectorGroupName = selector.includes(':-') || selector.includes('::-') || selector.includes(':has') ? selector : '__DEFAULT__';
115
117
  var _selectorGroups_get;
116
118
  let selectors = (_selectorGroups_get = selectorGroups.get(selectorGroupName)) !== null && _selectorGroups_get !== void 0 ? _selectorGroups_get : new Set();
117
119
  selectorGroups.set(selectorGroupName, selectors);
@@ -742,6 +742,7 @@ function resolvePlugins(context, root) {
742
742
  // TODO: This is a workaround for backwards compatibility, since custom variants
743
743
  // were historically sorted before screen/stackable variants.
744
744
  let beforeVariants = [
745
+ _corePlugins.variantPlugins['childVariant'],
745
746
  _corePlugins.variantPlugins['pseudoElementVariants'],
746
747
  _corePlugins.variantPlugins['pseudoClassVariants'],
747
748
  _corePlugins.variantPlugins['hasVariants'],
@@ -750,15 +751,31 @@ function resolvePlugins(context, root) {
750
751
  ];
751
752
  let afterVariants = [
752
753
  _corePlugins.variantPlugins['supportsVariants'],
753
- _corePlugins.variantPlugins['directionVariants'],
754
754
  _corePlugins.variantPlugins['reducedMotionVariants'],
755
755
  _corePlugins.variantPlugins['prefersContrastVariants'],
756
- _corePlugins.variantPlugins['forcedColorsVariants'],
757
- _corePlugins.variantPlugins['darkVariants'],
758
- _corePlugins.variantPlugins['printVariant'],
759
756
  _corePlugins.variantPlugins['screenVariants'],
760
- _corePlugins.variantPlugins['orientationVariants']
757
+ _corePlugins.variantPlugins['orientationVariants'],
758
+ _corePlugins.variantPlugins['directionVariants'],
759
+ _corePlugins.variantPlugins['darkVariants'],
760
+ _corePlugins.variantPlugins['forcedColorsVariants'],
761
+ _corePlugins.variantPlugins['printVariant']
761
762
  ];
763
+ // This is a compatibility fix for the pre 3.4 dark mode behavior
764
+ // `class` retains the old behavior, but `selector` keeps the new behavior
765
+ let isLegacyDarkMode = context.tailwindConfig.darkMode === 'class' || Array.isArray(context.tailwindConfig.darkMode) && context.tailwindConfig.darkMode[0] === 'class';
766
+ if (isLegacyDarkMode) {
767
+ afterVariants = [
768
+ _corePlugins.variantPlugins['supportsVariants'],
769
+ _corePlugins.variantPlugins['reducedMotionVariants'],
770
+ _corePlugins.variantPlugins['prefersContrastVariants'],
771
+ _corePlugins.variantPlugins['darkVariants'],
772
+ _corePlugins.variantPlugins['screenVariants'],
773
+ _corePlugins.variantPlugins['orientationVariants'],
774
+ _corePlugins.variantPlugins['directionVariants'],
775
+ _corePlugins.variantPlugins['forcedColorsVariants'],
776
+ _corePlugins.variantPlugins['printVariant']
777
+ ];
778
+ }
762
779
  return [
763
780
  ...corePluginList,
764
781
  ...beforeVariants,
@@ -141,7 +141,13 @@ function normalize(value, context = null, isRoot = true) {
141
141
  'keyboard-inset-bottom',
142
142
  'keyboard-inset-left',
143
143
  'keyboard-inset-width',
144
- 'keyboard-inset-height'
144
+ 'keyboard-inset-height',
145
+ 'radial-gradient',
146
+ 'linear-gradient',
147
+ 'conic-gradient',
148
+ 'repeating-radial-gradient',
149
+ 'repeating-linear-gradient',
150
+ 'repeating-conic-gradient'
145
151
  ];
146
152
  return value.replace(/(calc|min|max|clamp)\(.+\)/g, (match)=>{
147
153
  let result = '';
@@ -187,6 +193,10 @@ function normalize(value, context = null, isRoot = true) {
187
193
  result += consumeUntil([
188
194
  ')'
189
195
  ]);
196
+ } else if (peek('[')) {
197
+ result += consumeUntil([
198
+ ']'
199
+ ]);
190
200
  } else if ([
191
201
  '+',
192
202
  '-',
@@ -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": "0.0.0-insiders.f667746",
3
+ "version": "0.0.0-insiders.f69a46b",
4
4
  "description": "A utility-first CSS framework for rapidly building custom user interfaces.",
5
5
  "license": "MIT",
6
6
  "main": "lib/index.js",
@@ -44,47 +44,47 @@
44
44
  ],
45
45
  "devDependencies": {
46
46
  "@swc/cli": "^0.1.63",
47
- "@swc/core": "^1.3.95",
47
+ "@swc/core": "^1.3.101",
48
48
  "@swc/jest": "^0.2.29",
49
49
  "@swc/register": "^0.1.10",
50
50
  "concurrently": "^8.0.1",
51
- "eslint": "^8.52.0",
52
- "eslint-config-prettier": "^9.0.0",
51
+ "eslint": "^8.56.0",
52
+ "eslint-config-prettier": "^9.1.0",
53
53
  "eslint-plugin-prettier": "^4.2.1",
54
54
  "jest": "^29.7.0",
55
55
  "jest-diff": "^29.7.0",
56
56
  "prettier": "^2.8.8",
57
57
  "rimraf": "^5.0.0",
58
58
  "source-map-js": "^1.0.2",
59
- "turbo": "^1.10.16"
59
+ "turbo": "^1.12.3"
60
60
  },
61
61
  "dependencies": {
62
62
  "@alloc/quick-lru": "^5.2.0",
63
- "@tailwindcss/oxide": "0.0.0-insiders.f667746",
63
+ "@tailwindcss/oxide": "0.0.0-insiders.f69a46b",
64
64
  "arg": "^5.0.2",
65
- "browserslist": "^4.22.1",
66
- "chokidar": "^3.5.3",
65
+ "browserslist": "^4.22.3",
66
+ "chokidar": "^3.6.0",
67
67
  "didyoumean": "^1.2.2",
68
68
  "dlv": "^1.1.3",
69
69
  "fast-glob": "^3.3.2",
70
70
  "glob-parent": "^6.0.2",
71
71
  "is-glob": "^4.0.3",
72
72
  "jiti": "^1.21.0",
73
- "lightningcss": "^1.22.1",
73
+ "lightningcss": "^1.23.0",
74
74
  "lilconfig": "^3.0.0",
75
75
  "micromatch": "^4.0.5",
76
76
  "normalize-path": "^3.0.0",
77
77
  "object-hash": "^3.0.0",
78
78
  "picocolors": "^1.0.0",
79
- "postcss": "^8.4.31",
79
+ "postcss": "^8.4.35",
80
80
  "postcss-import": "^15.1.0",
81
81
  "postcss-js": "^4.0.1",
82
82
  "postcss-load-config": "^4.0.2",
83
83
  "postcss-nested": "^6.0.1",
84
- "postcss-selector-parser": "^6.0.12",
84
+ "postcss-selector-parser": "^6.0.15",
85
85
  "postcss-value-parser": "^4.2.0",
86
86
  "resolve": "^1.22.8",
87
- "sucrase": "^3.34.0"
87
+ "sucrase": "^3.35.0"
88
88
  },
89
89
  "browserslist": [
90
90
  "defaults and supports css-variables and supports css-matches-pseudo",
@@ -24,6 +24,9 @@ import { normalize } from './util/dataTypes'
24
24
  import { INTERNAL_FEATURES } from './lib/setupContextUtils'
25
25
 
26
26
  export let variantPlugins = {
27
+ childVariant: ({ addVariant }) => {
28
+ addVariant('*', '& > *')
29
+ },
27
30
  pseudoElementVariants: ({ addVariant }) => {
28
31
  addVariant('first-letter', '&::first-letter')
29
32
  addVariant('first-line', '&::first-line')
@@ -203,8 +206,8 @@ export let variantPlugins = {
203
206
  },
204
207
 
205
208
  directionVariants: ({ addVariant }) => {
206
- addVariant('ltr', ':is([dir="ltr"] &)')
207
- addVariant('rtl', ':is([dir="rtl"] &)')
209
+ addVariant('ltr', '&:where([dir="ltr"], [dir="ltr"] *)')
210
+ addVariant('rtl', '&:where([dir="rtl"], [dir="rtl"] *)')
208
211
  },
209
212
 
210
213
  reducedMotionVariants: ({ addVariant }) => {
@@ -213,7 +216,7 @@ export let variantPlugins = {
213
216
  },
214
217
 
215
218
  darkVariants: ({ config, addVariant }) => {
216
- let [mode, className = '.dark'] = [].concat(config('darkMode', 'media'))
219
+ let [mode, selector = '.dark'] = [].concat(config('darkMode', 'media'))
217
220
 
218
221
  if (mode === false) {
219
222
  mode = 'media'
@@ -224,10 +227,49 @@ export let variantPlugins = {
224
227
  ])
225
228
  }
226
229
 
227
- if (mode === 'class') {
228
- addVariant('dark', `:is(${className} &)`)
230
+ if (mode === 'variant') {
231
+ let formats
232
+ if (Array.isArray(selector)) {
233
+ formats = selector
234
+ } else if (typeof selector === 'function') {
235
+ formats = selector
236
+ } else if (typeof selector === 'string') {
237
+ formats = [selector]
238
+ }
239
+
240
+ // TODO: We could also add these warnings if the user passes a function that returns string | string[]
241
+ // But this is an advanced enough use case that it's probably not necessary
242
+ if (Array.isArray(formats)) {
243
+ for (let format of formats) {
244
+ if (format === '.dark') {
245
+ mode = false
246
+ log.warn('darkmode-variant-without-selector', [
247
+ 'When using `variant` for `darkMode`, you must provide a selector.',
248
+ 'Example: `darkMode: ["variant", ".your-selector &"]`',
249
+ ])
250
+ } else if (!format.includes('&')) {
251
+ mode = false
252
+ log.warn('darkmode-variant-without-ampersand', [
253
+ 'When using `variant` for `darkMode`, your selector must contain `&`.',
254
+ 'Example `darkMode: ["variant", ".your-selector &"]`',
255
+ ])
256
+ }
257
+ }
258
+ }
259
+
260
+ selector = formats
261
+ }
262
+
263
+ if (mode === 'selector') {
264
+ // New preferred behavior
265
+ addVariant('dark', `&:where(${selector}, ${selector} *)`)
229
266
  } else if (mode === 'media') {
230
267
  addVariant('dark', '@media (prefers-color-scheme: dark)')
268
+ } else if (mode === 'variant') {
269
+ addVariant('dark', selector)
270
+ } else if (mode === 'class') {
271
+ // Old behavior
272
+ addVariant('dark', `:is(${selector} &)`)
231
273
  }
232
274
  },
233
275
 
@@ -195,9 +195,9 @@ select {
195
195
  */
196
196
 
197
197
  button,
198
- [type='button'],
199
- [type='reset'],
200
- [type='submit'] {
198
+ input:where([type='button']),
199
+ input:where([type='reset']),
200
+ input:where([type='submit']) {
201
201
  -webkit-appearance: button; /* 1 */
202
202
  background-color: transparent; /* 2 */
203
203
  background-image: none; /* 2 */
@@ -4,11 +4,48 @@ import fs from 'fs'
4
4
  import path from 'path'
5
5
  import isGlob from 'is-glob'
6
6
  import fastGlob from 'fast-glob'
7
- import normalizePath from 'normalize-path'
8
7
  import { parseGlob } from '../util/parseGlob'
9
8
  import { env } from './sharedState'
10
9
  import { resolveContentPaths } from '@tailwindcss/oxide'
11
10
 
11
+ /*!
12
+ * Modified version of normalize-path, original license below
13
+ *
14
+ * normalize-path <https://github.com/jonschlinkert/normalize-path>
15
+ *
16
+ * Copyright (c) 2014-2018, Jon Schlinkert.
17
+ * Released under the MIT License.
18
+ */
19
+
20
+ function normalizePath(path) {
21
+ if (typeof path !== 'string') {
22
+ throw new TypeError('expected path to be a string')
23
+ }
24
+
25
+ if (path === '\\' || path === '/') return '/'
26
+
27
+ var len = path.length
28
+ if (len <= 1) return path
29
+
30
+ // ensure that win32 namespaces has two leading slashes, so that the path is
31
+ // handled properly by the win32 version of path.parse() after being normalized
32
+ // https://msdn.microsoft.com/library/windows/desktop/aa365247(v=vs.85).aspx#namespaces
33
+ var prefix = ''
34
+ if (len > 4 && path[3] === '\\') {
35
+ var ch = path[2]
36
+ if ((ch === '?' || ch === '.') && path.slice(0, 2) === '\\\\') {
37
+ path = path.slice(2)
38
+ prefix = '//'
39
+ }
40
+ }
41
+
42
+ // Modified part: instead of purely splitting on `\\` and `/`, we split on
43
+ // `/` and `\\` that is _not_ followed by any of the following characters: ()[]
44
+ // This is to ensure that we keep the escaping of brackets and parentheses
45
+ let segs = path.split(/[/\\]+(?![\(\)\[\]])/)
46
+ return prefix + segs.join('/')
47
+ }
48
+
12
49
  /** @typedef {import('../../types/config.js').RawFile} RawFile */
13
50
  /** @typedef {import('../../types/config.js').FilePath} FilePath */
14
51
 
@@ -105,6 +142,10 @@ export function parseCandidateFiles(context, tailwindConfig) {
105
142
  * @returns {ContentPath}
106
143
  */
107
144
  function parseFilePath(filePath, ignore) {
145
+ // Escape special characters in the file path such as: ()[]
146
+ // But only if the special character isn't already escaped
147
+ filePath = filePath.replace(/(?<!\\)([\[\]\(\)])/g, '\\$1')
148
+
108
149
  let contentPath = {
109
150
  original: filePath,
110
151
  base: filePath,
@@ -40,7 +40,12 @@ function* buildRegExps(context) {
40
40
  // Utilities
41
41
  regex.pattern([
42
42
  // Utility Name / Group Name
43
- /-?(?:\w+)/,
43
+ regex.any([
44
+ /-?(?:\w+)/,
45
+
46
+ // This is here to make sure @container supports everything that other utilities do
47
+ /@(?:\w+)/,
48
+ ]),
44
49
 
45
50
  // Normal/Arbitrary values
46
51
  regex.optional(
@@ -118,10 +118,20 @@ function applyImportant(matches, classCandidate) {
118
118
 
119
119
  let result = []
120
120
 
121
+ function isInKeyframes(rule) {
122
+ return rule.parent && rule.parent.type === 'atrule' && rule.parent.name === 'keyframes'
123
+ }
124
+
121
125
  for (let [meta, rule] of matches) {
122
126
  let container = postcss.root({ nodes: [rule.clone()] })
123
127
 
124
128
  container.walkRules((r) => {
129
+ // Declarations inside keyframes cannot be marked as important
130
+ // They will be ignored by the browser
131
+ if (isInKeyframes(r)) {
132
+ return
133
+ }
134
+
125
135
  let ast = selectorParser().astSync(r.selector)
126
136
 
127
137
  // Remove extraneous selectors that do not include the base candidate
@@ -497,7 +507,7 @@ function extractArbitraryProperty(classCandidate, context) {
497
507
  return null
498
508
  }
499
509
 
500
- let sort = context.offsets.arbitraryProperty()
510
+ let sort = context.offsets.arbitraryProperty(classCandidate)
501
511
 
502
512
  return [
503
513
  [
@@ -8,8 +8,8 @@ let jiti: ReturnType<typeof jitiFactory> | null = null
8
8
  // @internal
9
9
  // This WILL be removed in some future release
10
10
  // If you rely on this your stuff WILL break
11
- export function useCustomJiti(_jiti: ReturnType<typeof jitiFactory>) {
12
- jiti = _jiti
11
+ export function useCustomJiti(_jiti: () => ReturnType<typeof jitiFactory>) {
12
+ jiti = _jiti()
13
13
  }
14
14
 
15
15
  function lazyJiti() {
@@ -24,6 +24,8 @@ import { remapBitfield } from './remap-bitfield.js'
24
24
  * @property {bigint} variants Dynamic size. 1 bit per registered variant. 0n means no variants
25
25
  * @property {bigint} parallelIndex Rule index for the parallel variant. 0 if not applicable.
26
26
  * @property {bigint} index Index of the rule / utility in its given *parent* layer. Monotonically increasing.
27
+ * @property {bigint} propertyOffset Offset for the arbitrary property. Only valid after sorting.
28
+ * @property {string} property Name/Value of the arbitrary property.
27
29
  * @property {VariantOption[]} options Some information on how we can sort arbitrary variants
28
30
  */
29
31
 
@@ -88,17 +90,21 @@ export class Offsets {
88
90
  variants: 0n,
89
91
  parallelIndex: 0n,
90
92
  index: this.offsets[layer]++,
93
+ propertyOffset: 0n,
94
+ property: '',
91
95
  options: [],
92
96
  }
93
97
  }
94
98
 
95
99
  /**
100
+ * @param {string} name
96
101
  * @returns {RuleOffset}
97
102
  */
98
- arbitraryProperty() {
103
+ arbitraryProperty(name) {
99
104
  return {
100
105
  ...this.create('utilities'),
101
106
  arbitrary: 1n,
107
+ property: name,
102
108
  }
103
109
  }
104
110
 
@@ -262,6 +268,11 @@ export class Offsets {
262
268
  return a.arbitrary - b.arbitrary
263
269
  }
264
270
 
271
+ // Always sort arbitrary properties alphabetically
272
+ if (a.propertyOffset !== b.propertyOffset) {
273
+ return a.propertyOffset - b.propertyOffset
274
+ }
275
+
265
276
  // Sort utilities, components, etc… in the order they were registered
266
277
  return a.index - b.index
267
278
  }
@@ -320,14 +331,62 @@ export class Offsets {
320
331
  })
321
332
  }
322
333
 
334
+ /**
335
+ * @template T
336
+ * @param {[RuleOffset, T][]} list
337
+ * @returns {[RuleOffset, T][]}
338
+ */
339
+ sortArbitraryProperties(list) {
340
+ // Collect all known arbitrary properties
341
+ let known = new Set()
342
+
343
+ for (let [offset] of list) {
344
+ if (offset.arbitrary === 1n) {
345
+ known.add(offset.property)
346
+ }
347
+ }
348
+
349
+ // No arbitrary properties? Nothing to do.
350
+ if (known.size === 0) {
351
+ return list
352
+ }
353
+
354
+ // Sort the properties alphabetically
355
+ let properties = Array.from(known).sort()
356
+
357
+ // Create a map from the property name to its offset
358
+ let offsets = new Map()
359
+
360
+ let offset = 1n
361
+ for (let property of properties) {
362
+ offsets.set(property, offset++)
363
+ }
364
+
365
+ // Apply the sorted offsets to the list
366
+ return list.map((item) => {
367
+ let [offset, rule] = item
368
+
369
+ offset = {
370
+ ...offset,
371
+ propertyOffset: offsets.get(offset.property) ?? 0n,
372
+ }
373
+
374
+ return [offset, rule]
375
+ })
376
+ }
377
+
323
378
  /**
324
379
  * @template T
325
380
  * @param {[RuleOffset, T][]} list
326
381
  * @returns {[RuleOffset, T][]}
327
382
  */
328
383
  sort(list) {
384
+ // Sort arbitrary variants so they're in alphabetical order
329
385
  list = this.remapArbitraryVariantOffsets(list)
330
386
 
387
+ // Sort arbitrary properties so they're in alphabetical order
388
+ list = this.sortArbitraryProperties(list)
389
+
331
390
  return list.sort(([a], [b]) => bigSign(this.compare(a, b)))
332
391
  }
333
392
  }
@@ -104,8 +104,12 @@ export default function resolveDefaultsAtRules({ tailwindConfig }) {
104
104
  // we consider them separately because merging the declarations into
105
105
  // a single rule will cause browsers that do not understand the
106
106
  // vendor prefix to throw out the whole rule
107
+ // Additionally if a selector contains `:has` we also consider
108
+ // it separately because FF only recently gained support for it
107
109
  let selectorGroupName =
108
- selector.includes(':-') || selector.includes('::-') ? selector : '__DEFAULT__'
110
+ selector.includes(':-') || selector.includes('::-') || selector.includes(':has')
111
+ ? selector
112
+ : '__DEFAULT__'
109
113
 
110
114
  let selectors = selectorGroups.get(selectorGroupName) ?? new Set()
111
115
  selectorGroups.set(selectorGroupName, selectors)
@@ -746,6 +746,7 @@ function resolvePlugins(context, root) {
746
746
  // TODO: This is a workaround for backwards compatibility, since custom variants
747
747
  // were historically sorted before screen/stackable variants.
748
748
  let beforeVariants = [
749
+ variantPlugins['childVariant'],
749
750
  variantPlugins['pseudoElementVariants'],
750
751
  variantPlugins['pseudoClassVariants'],
751
752
  variantPlugins['hasVariants'],
@@ -754,16 +755,37 @@ function resolvePlugins(context, root) {
754
755
  ]
755
756
  let afterVariants = [
756
757
  variantPlugins['supportsVariants'],
757
- variantPlugins['directionVariants'],
758
758
  variantPlugins['reducedMotionVariants'],
759
759
  variantPlugins['prefersContrastVariants'],
760
- variantPlugins['forcedColorsVariants'],
761
- variantPlugins['darkVariants'],
762
- variantPlugins['printVariant'],
763
760
  variantPlugins['screenVariants'],
764
761
  variantPlugins['orientationVariants'],
762
+ variantPlugins['directionVariants'],
763
+ variantPlugins['darkVariants'],
764
+ variantPlugins['forcedColorsVariants'],
765
+ variantPlugins['printVariant'],
765
766
  ]
766
767
 
768
+ // This is a compatibility fix for the pre 3.4 dark mode behavior
769
+ // `class` retains the old behavior, but `selector` keeps the new behavior
770
+ let isLegacyDarkMode =
771
+ context.tailwindConfig.darkMode === 'class' ||
772
+ (Array.isArray(context.tailwindConfig.darkMode) &&
773
+ context.tailwindConfig.darkMode[0] === 'class')
774
+
775
+ if (isLegacyDarkMode) {
776
+ afterVariants = [
777
+ variantPlugins['supportsVariants'],
778
+ variantPlugins['reducedMotionVariants'],
779
+ variantPlugins['prefersContrastVariants'],
780
+ variantPlugins['darkVariants'],
781
+ variantPlugins['screenVariants'],
782
+ variantPlugins['orientationVariants'],
783
+ variantPlugins['directionVariants'],
784
+ variantPlugins['forcedColorsVariants'],
785
+ variantPlugins['printVariant'],
786
+ ]
787
+ }
788
+
767
789
  return [...corePluginList, ...beforeVariants, ...userPlugins, ...afterVariants, ...layerPlugins]
768
790
  }
769
791
 
@@ -107,6 +107,13 @@ function normalizeMathOperatorSpacing(value) {
107
107
  'keyboard-inset-left',
108
108
  'keyboard-inset-width',
109
109
  'keyboard-inset-height',
110
+
111
+ 'radial-gradient',
112
+ 'linear-gradient',
113
+ 'conic-gradient',
114
+ 'repeating-radial-gradient',
115
+ 'repeating-linear-gradient',
116
+ 'repeating-conic-gradient',
110
117
  ]
111
118
 
112
119
  return value.replace(/(calc|min|max|clamp)\(.+\)/g, (match) => {
@@ -162,6 +169,11 @@ function normalizeMathOperatorSpacing(value) {
162
169
  result += consumeUntil([')'])
163
170
  }
164
171
 
172
+ // Don't break CSS grid track names
173
+ else if (peek('[')) {
174
+ result += consumeUntil([']'])
175
+ }
176
+
165
177
  // Handle operators
166
178
  else if (
167
179
  ['+', '-', '*', '/'].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
@@ -75,6 +75,13 @@ type DarkModeConfig =
75
75
  | 'class'
76
76
  // Use the `class` strategy with a custom class instead of `.dark`.
77
77
  | ['class', string]
78
+ // Use the `selector` strategy — same as `class` but uses `:where()` for more predicable behavior
79
+ | 'selector'
80
+ // Use the `selector` strategy with a custom selector instead of `.dark`.
81
+ | ['selector', string]
82
+ // Use the `variant` strategy, which allows you to completely customize the selector
83
+ // It takes a string or an array of strings, which are passed directly to `addVariant()`
84
+ | ['variant', string | string[]]
78
85
 
79
86
  type Screen = { raw: string } | { min: string } | { max: string } | { min: string; max: string }
80
87
  type ScreensConfig = string[] | KeyValuePair<string, string | Screen | Screen[]>