@tenphi/tasty 0.0.0-snapshot.167ce4e → 0.0.0-snapshot.17c4f74

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.
Files changed (63) hide show
  1. package/dist/async-storage-B7_o6FKt.js.map +1 -1
  2. package/dist/{collector-WUJKpM4q.js → collector-C-keQH9m.js} +24 -11
  3. package/dist/collector-C-keQH9m.js.map +1 -0
  4. package/dist/{collector-LuU1vZ68.d.ts → collector-osfWTeRd.d.ts} +12 -2
  5. package/dist/{config-raGoEeGs.js → config-BBiyxMCe.js} +1588 -1256
  6. package/dist/config-BBiyxMCe.js.map +1 -0
  7. package/dist/{config-vuCRkBWX.d.ts → config-BoZDUHW5.d.ts} +65 -4
  8. package/dist/context-CkSg-kDT.js.map +1 -1
  9. package/dist/core/index.d.ts +5 -5
  10. package/dist/core/index.js +6 -6
  11. package/dist/{core-CrPLif0D.js → core-BO4319td.js} +18 -12
  12. package/dist/core-BO4319td.js.map +1 -0
  13. package/dist/{css-writer-BaR8ywQm.js → css-writer-BWvwQzz0.js} +28 -6
  14. package/dist/css-writer-BWvwQzz0.js.map +1 -0
  15. package/dist/format-global-rules-Dbc_1tc3.js.map +1 -1
  16. package/dist/{format-rules-B_Cuw1ZS.js → format-rules-BSjeH4Z7.js} +2 -2
  17. package/dist/{format-rules-B_Cuw1ZS.js.map → format-rules-BSjeH4Z7.js.map} +1 -1
  18. package/dist/{hydrate-DmVyww8Y.js → hydrate-CcvrP4qJ.js} +2 -2
  19. package/dist/{hydrate-DmVyww8Y.js.map → hydrate-CcvrP4qJ.js.map} +1 -1
  20. package/dist/{index-ZRxZWzlj.d.ts → index-B_k47mc_.d.ts} +74 -21
  21. package/dist/{index-dUtwpOux.d.ts → index-tcHuMPFt.d.ts} +22 -2
  22. package/dist/index.d.ts +5 -5
  23. package/dist/index.js +10 -10
  24. package/dist/index.js.map +1 -1
  25. package/dist/{keyframes-C9OD_9bX.js → keyframes-BUQhdOSJ.js} +2 -2
  26. package/dist/{keyframes-C9OD_9bX.js.map → keyframes-BUQhdOSJ.js.map} +1 -1
  27. package/dist/{merge-styles-CtDJMhpJ.d.ts → merge-styles-BMWcH6MF.d.ts} +2 -2
  28. package/dist/{merge-styles-B57eQpFZ.js → merge-styles-Cd2vBl9b.js} +2 -2
  29. package/dist/{merge-styles-B57eQpFZ.js.map → merge-styles-Cd2vBl9b.js.map} +1 -1
  30. package/dist/{resolve-recipes-J9mdpVSZ.js → resolve-recipes-C1nrvnYh.js} +3 -3
  31. package/dist/{resolve-recipes-J9mdpVSZ.js.map → resolve-recipes-C1nrvnYh.js.map} +1 -1
  32. package/dist/ssr/astro-client.js +1 -1
  33. package/dist/ssr/astro-client.js.map +1 -1
  34. package/dist/ssr/astro-middleware.js.map +1 -1
  35. package/dist/ssr/astro.js +3 -3
  36. package/dist/ssr/astro.js.map +1 -1
  37. package/dist/ssr/index.d.ts +1 -1
  38. package/dist/ssr/index.js +3 -3
  39. package/dist/ssr/next.d.ts +1 -1
  40. package/dist/ssr/next.js +4 -4
  41. package/dist/ssr/next.js.map +1 -1
  42. package/dist/static/index.d.ts +2 -2
  43. package/dist/static/index.js +1 -1
  44. package/dist/static/index.js.map +1 -1
  45. package/dist/static/inject.js.map +1 -1
  46. package/dist/zero/babel.d.ts +1 -1
  47. package/dist/zero/babel.js +16 -8
  48. package/dist/zero/babel.js.map +1 -1
  49. package/dist/zero/index.d.ts +1 -1
  50. package/dist/zero/index.js +1 -1
  51. package/dist/zero/next.js.map +1 -1
  52. package/docs/configuration.md +44 -0
  53. package/docs/methodology.md +26 -0
  54. package/docs/pipeline.md +40 -14
  55. package/docs/react-api.md +24 -0
  56. package/docs/ssr.md +5 -3
  57. package/docs/styles.md +9 -7
  58. package/docs/tasty-static.md +15 -0
  59. package/package.json +8 -8
  60. package/dist/collector-WUJKpM4q.js.map +0 -1
  61. package/dist/config-raGoEeGs.js.map +0 -1
  62. package/dist/core-CrPLif0D.js.map +0 -1
  63. package/dist/css-writer-BaR8ywQm.js.map +0 -1
@@ -57,6 +57,7 @@ function canonicalFuncName(lowered) {
57
57
  //#endregion
58
58
  //#region src/parser/lru.ts
59
59
  var Lru = class {
60
+ limit;
60
61
  map = /* @__PURE__ */ new Map();
61
62
  head = null;
62
63
  tail = null;
@@ -1374,6 +1375,93 @@ function isDevEnv() {
1374
1375
  return nodeEnv !== "test" && nodeEnv !== "production";
1375
1376
  }
1376
1377
  //#endregion
1378
+ //#region src/utils/name-prefix.ts
1379
+ /**
1380
+ * Name prefix utilities for generated identifiers.
1381
+ *
1382
+ * Tasty generates three kinds of identifiers from content hashes:
1383
+ * - class names (used in DOM `class` attribute)
1384
+ * - keyframe names (used in CSS `animation`)
1385
+ * - counter-style names (used in CSS `list-style-type`)
1386
+ *
1387
+ * All three derive from a single configurable prefix so that an app
1388
+ * can namespace every identifier under one string. Discriminator letters
1389
+ * (`k`, `c`) keep the three kinds visually distinct in devtools — they
1390
+ * are not required for correctness (CSS keeps these in separate
1391
+ * namespaces), only for readability.
1392
+ *
1393
+ * The runtime / SSR / RSC paths must agree on the prefix; otherwise the
1394
+ * client-side hash for a given style will not match the server-rendered
1395
+ * class and hydration breaks. The zero-runtime build path uses a
1396
+ * different default (`'ts'`) so its classes can't collide with runtime
1397
+ * (`'t'`) classes when both are loaded on the same page.
1398
+ */
1399
+ /** Default prefix used by the runtime / SSR / RSC paths. */
1400
+ const DEFAULT_NAME_PREFIX = "t";
1401
+ /** Default prefix used by the zero-runtime (`tastyStatic`) build path. */
1402
+ const DEFAULT_ZERO_NAME_PREFIX = "ts";
1403
+ /**
1404
+ * Allowed shape: starts with a letter or underscore, then letters/
1405
+ * digits/underscore/hyphen. Length capped at 32 to keep generated
1406
+ * names sane. Matches the CSS identifier rules for the common case
1407
+ * while keeping the surface conservative.
1408
+ */
1409
+ const NAME_PREFIX_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_-]{0,31}$/;
1410
+ /**
1411
+ * Validate a `namePrefix` value.
1412
+ * Throws a TypeError with a descriptive message on invalid input so
1413
+ * misconfiguration fails loudly at `configure()` time rather than
1414
+ * surfacing later as broken hydration.
1415
+ */
1416
+ function validateNamePrefix(prefix) {
1417
+ if (typeof prefix !== "string") throw new TypeError(`[Tasty] namePrefix must be a string, got ${typeof prefix}.`);
1418
+ if (!NAME_PREFIX_PATTERN.test(prefix)) throw new TypeError(`[Tasty] namePrefix "${prefix}" is invalid. It must start with a letter (a-z, A-Z) or "_", contain only letters, digits, "_" or "-", and be 1-32 characters long. Examples: "t", "ts", "myapp-", "_foo".`);
1419
+ }
1420
+ /**
1421
+ * Build a class name: `${prefix}${hash}`.
1422
+ * The hash is appended verbatim — supply a separator inside the prefix
1423
+ * itself if you want one (e.g. `'myapp-'`).
1424
+ */
1425
+ function makeClassName(prefix, hash) {
1426
+ return `${prefix}${hash}`;
1427
+ }
1428
+ /**
1429
+ * Build a keyframe name: `${prefix}k${suffix}`.
1430
+ * The `k` discriminator keeps keyframe names visually distinct from
1431
+ * class names sharing the same prefix. `suffix` is typically a content
1432
+ * hash but may be a counter for ad-hoc allocation.
1433
+ */
1434
+ function makeKeyframeName(prefix, suffix) {
1435
+ return `${prefix}k${suffix}`;
1436
+ }
1437
+ /**
1438
+ * Build a counter-style name: `${prefix}c${suffix}`.
1439
+ * The `c` discriminator keeps counter-style names visually distinct
1440
+ * from class names sharing the same prefix.
1441
+ */
1442
+ function makeCounterStyleName(prefix, suffix) {
1443
+ return `${prefix}c${suffix}`;
1444
+ }
1445
+ /** Escape a string for safe inclusion in a regex literal. */
1446
+ function escapeRegex(str) {
1447
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1448
+ }
1449
+ /**
1450
+ * Regex matching any tasty class for the given prefix.
1451
+ * Used by the runtime GC's DOM scan and class-allocation bookkeeping.
1452
+ */
1453
+ function tastyClassRegex(prefix) {
1454
+ return new RegExp(`^${escapeRegex(prefix)}[a-z0-9]+$`);
1455
+ }
1456
+ /**
1457
+ * Global regex extracting tasty class names from RSC-inlined CSS.
1458
+ * Looks for the doubled-specificity pattern `.cls.cls` that
1459
+ * `formatRules()` always emits, which makes extraction reliable.
1460
+ */
1461
+ function rscClassRegexGlobal(prefix) {
1462
+ return new RegExp(`\\.(${escapeRegex(prefix)}[a-z0-9]+)\\.\\1`, "g");
1463
+ }
1464
+ //#endregion
1377
1465
  //#region src/parser/types.ts
1378
1466
  let Bucket = /* @__PURE__ */ function(Bucket) {
1379
1467
  Bucket[Bucket["Color"] = 0] = "Color";
@@ -1788,6 +1876,7 @@ function scan(src, cb) {
1788
1876
  //#endregion
1789
1877
  //#region src/parser/parser.ts
1790
1878
  var StyleParser = class {
1879
+ opts;
1791
1880
  cache;
1792
1881
  constructor(opts = {}) {
1793
1882
  this.opts = opts;
@@ -3234,10 +3323,13 @@ function resolveFontFamily(font, fontFamily) {
3234
3323
  /**
3235
3324
  * Handles typography preset and individual font properties.
3236
3325
  *
3237
- * Preset syntax uses `/` to separate name from modifier:
3326
+ * Preset syntax uses `/` to separate the name from one or more
3327
+ * space-separated modifiers:
3238
3328
  * - `preset="h1"` — name only
3239
3329
  * - `preset="h2 / strong"` — name + modifier
3330
+ * - `preset="h2 / strong italic"` — name + multiple modifiers
3240
3331
  * - `preset="bold"` — modifier-only shorthand (name defaults to `inherit`)
3332
+ * - `preset="bold italic"` — modifier-only shorthand with multiple modifiers
3241
3333
  *
3242
3334
  * When `preset` is defined, it sets up CSS custom properties for typography.
3243
3335
  * Individual font props can be used with or without `preset`:
@@ -3257,14 +3349,17 @@ function presetStyle({ preset, fontSize, lineHeight, textTransform, letterSpacin
3257
3349
  const { parts } = parseStyle(preset === true ? "" : String(preset)).groups[0] ?? { parts: [] };
3258
3350
  const namePart = parts[0];
3259
3351
  const modPart = parts[1];
3352
+ const nameTokens = namePart?.all ?? [];
3353
+ const isModOnly = nameTokens.length > 0 && nameTokens.every((t) => PRESET_MODIFIERS.has(t));
3260
3354
  const nameToken = namePart?.mods[0] ?? namePart?.values[0] ?? "";
3261
- const isModOnly = PRESET_MODIFIERS.has(nameToken);
3262
3355
  const name = isModOnly ? "inherit" : nameToken || "inherit";
3263
- const modifier = isModOnly ? nameToken : modPart?.mods[0] ?? "";
3264
- const isStrong = modifier === "strong" || modifier === "bold";
3265
- const isItalic = modifier === "italic";
3266
- const isIcon = modifier === "icon";
3267
- const isTight = modifier === "tight";
3356
+ const modTokens = isModOnly ? nameTokens : modPart?.all ?? [];
3357
+ const activeMods = /* @__PURE__ */ new Set();
3358
+ for (const tok of modTokens) if (PRESET_MODIFIERS.has(tok)) activeMods.add(tok);
3359
+ const isStrong = activeMods.has("strong") || activeMods.has("bold");
3360
+ const isItalic = activeMods.has("italic");
3361
+ const isIcon = activeMods.has("icon");
3362
+ const isTight = activeMods.has("tight");
3268
3363
  if (fontSize == null) setCSSValue(styles, "font-size", name, { cssOnly: true });
3269
3364
  if (lineHeight == null) setCSSValue(styles, "line-height", name, { cssOnly: true });
3270
3365
  if (letterSpacing == null) setCSSValue(styles, "letter-spacing", name, { cssOnly: true });
@@ -4129,7 +4224,7 @@ var SheetManager = class {
4129
4224
  }
4130
4225
  }
4131
4226
  if (!anyInserted) {}
4132
- } else console.warn("[tasty] Browser rejected CSS rule:", fullRule, e);
4227
+ } else if (!(fullRule.startsWith("@property ") && !this.engineSupportsAtProperty(registry, styleSheet))) console.warn("[tasty] Browser rejected CSS rule:", fullRule, e);
4133
4228
  }
4134
4229
  } else if (styleElement) {
4135
4230
  const atomicRuleIndex = this.findAvailableRuleIndex(targetSheet);
@@ -4281,6 +4376,37 @@ var SheetManager = class {
4281
4376
  return sheet.ruleCount;
4282
4377
  }
4283
4378
  /**
4379
+ * Probe whether the underlying CSS engine supports `@property` at-rules.
4380
+ * Result is cached per registry on `registry.atPropertySupported`.
4381
+ *
4382
+ * The probe inserts and immediately deletes a minimal known-valid rule
4383
+ * (`@property --__tasty_probe__ { syntax: "*"; inherits: true; }`).
4384
+ * Engines that lack `@property` support (jsdom, happy-dom) reject any
4385
+ * `@property` rule including this one, so a probe failure is a reliable
4386
+ * signal that further `@property` rejections are environmental noise and
4387
+ * not user-authored bugs.
4388
+ *
4389
+ * The probe is intentionally a separate operation from the user's failing
4390
+ * insertion: we don't want to leak `--__tasty_probe__` into the sheet, so
4391
+ * on success we delete the probe rule immediately, leaving `ruleCount`
4392
+ * and `cssRules.length` unchanged.
4393
+ */
4394
+ engineSupportsAtProperty(registry, styleSheet) {
4395
+ if (registry.atPropertySupported !== void 0) return registry.atPropertySupported;
4396
+ const probeRule = "@property --__tasty_probe__ { syntax: \"*\"; inherits: true; }";
4397
+ try {
4398
+ const probeIdx = styleSheet.cssRules.length;
4399
+ styleSheet.insertRule(probeRule, probeIdx);
4400
+ try {
4401
+ styleSheet.deleteRule(probeIdx);
4402
+ } catch {}
4403
+ registry.atPropertySupported = true;
4404
+ } catch {
4405
+ registry.atPropertySupported = false;
4406
+ }
4407
+ return registry.atPropertySupported;
4408
+ }
4409
+ /**
4284
4410
  * Force cleanup of unused styles
4285
4411
  */
4286
4412
  forceCleanup(registry) {
@@ -4801,18 +4927,10 @@ function formatCounterStyleRule(name, descriptors) {
4801
4927
  //#endregion
4802
4928
  //#region src/injector/injector.ts
4803
4929
  /**
4804
- * Generate a deterministic class name from a cache key using content hash.
4805
- * The same cache key always produces the same class name across environments.
4806
- */
4807
- function generateClassName(cacheKey) {
4808
- return `t${hashString(cacheKey)}`;
4809
- }
4810
- const RSC_CLASS_RE = /\.(t[a-z0-9]+)\.\1/g;
4811
- /**
4812
4930
  * Extract class names from `<style data-tasty-rsc>` tags.
4813
4931
  * The doubled-specificity pattern `.tXXX.tXXX` makes extraction reliable.
4814
4932
  */
4815
- function extractRSCClassNames() {
4933
+ function extractRSCClassNames(rscClassRegex) {
4816
4934
  if (typeof document === "undefined") return [];
4817
4935
  const styles = document.querySelectorAll("style[data-tasty-rsc]");
4818
4936
  if (styles.length === 0) return [];
@@ -4821,8 +4939,8 @@ function extractRSCClassNames() {
4821
4939
  const text = style.textContent;
4822
4940
  if (!text) continue;
4823
4941
  let match;
4824
- RSC_CLASS_RE.lastIndex = 0;
4825
- while ((match = RSC_CLASS_RE.exec(text)) !== null) classSet.add(match[1]);
4942
+ rscClassRegex.lastIndex = 0;
4943
+ while ((match = rscClassRegex.exec(text)) !== null) classSet.add(match[1]);
4826
4944
  }
4827
4945
  return Array.from(classSet);
4828
4946
  }
@@ -4836,7 +4954,7 @@ function extractRSCClassNames() {
4836
4954
  * Called inside `inject()` / `allocateClassName()` to pick up
4837
4955
  * class names rendered on the server (including during SPA navigation).
4838
4956
  */
4839
- function syncServerClasses(registry) {
4957
+ function syncServerClasses(registry, rscClassRegex) {
4840
4958
  if (typeof window === "undefined") return;
4841
4959
  const classes = window.__TASTY__;
4842
4960
  if (classes && classes.length > registry.serverClassSyncIndex) {
@@ -4845,7 +4963,7 @@ function syncServerClasses(registry) {
4845
4963
  }
4846
4964
  if (!registry.rscStylesScanned) {
4847
4965
  registry.rscStylesScanned = true;
4848
- for (const cls of extractRSCClassNames()) registerHydratedClass(registry, cls);
4966
+ for (const cls of extractRSCClassNames(rscClassRegex)) registerHydratedClass(registry, cls);
4849
4967
  }
4850
4968
  }
4851
4969
  function registerHydratedClass(registry, className) {
@@ -4857,25 +4975,40 @@ function registerHydratedClass(registry, className) {
4857
4975
  });
4858
4976
  registry.refCounts.set(className, 0);
4859
4977
  }
4860
- var StyleInjector = class StyleInjector {
4978
+ var StyleInjector = class {
4861
4979
  sheetManager;
4862
4980
  config;
4863
4981
  globalRuleCounter = 0;
4864
4982
  pendingGCHandle = null;
4983
+ namePrefix;
4984
+ classRegex;
4985
+ rscClassRegex;
4865
4986
  /** @internal — exposed for debug utilities only */
4866
4987
  get _sheetManager() {
4867
4988
  return this.sheetManager;
4868
4989
  }
4869
4990
  constructor(config = {}) {
4991
+ if (config.namePrefix !== void 0) validateNamePrefix(config.namePrefix);
4870
4992
  this.config = config;
4871
4993
  this.sheetManager = new SheetManager(config);
4994
+ this.namePrefix = config.namePrefix ?? "t";
4995
+ this.classRegex = tastyClassRegex(this.namePrefix);
4996
+ this.rscClassRegex = rscClassRegexGlobal(this.namePrefix);
4997
+ }
4998
+ /**
4999
+ * Generate a deterministic class name from a cache key using content hash.
5000
+ * The same cache key always produces the same class name across environments
5001
+ * with the same `namePrefix`.
5002
+ */
5003
+ generateClassName(cacheKey) {
5004
+ return makeClassName(this.namePrefix, hashString(cacheKey));
4872
5005
  }
4873
5006
  /**
4874
5007
  * Check if `className` was hydrated from server-rendered styles and,
4875
5008
  * if so, wire the cacheKey mapping. Returns true on hit.
4876
5009
  */
4877
5010
  tryHydratedHit(registry, cacheKey, className) {
4878
- syncServerClasses(registry);
5011
+ syncServerClasses(registry, this.rscClassRegex);
4879
5012
  const rule = registry.rules.get(className);
4880
5013
  if (rule && rule.ruleIndex === -2 && rule.sheetIndex === -2) {
4881
5014
  registry.cacheKeyToClassName.set(cacheKey, className);
@@ -4894,7 +5027,7 @@ var StyleInjector = class StyleInjector {
4894
5027
  className: registry.cacheKeyToClassName.get(cacheKey),
4895
5028
  isNewAllocation: false
4896
5029
  };
4897
- const className = generateClassName(cacheKey);
5030
+ const className = this.generateClassName(cacheKey);
4898
5031
  if (this.tryHydratedHit(registry, cacheKey, className)) return {
4899
5032
  className,
4900
5033
  isNewAllocation: false
@@ -4946,7 +5079,7 @@ var StyleInjector = class StyleInjector {
4946
5079
  };
4947
5080
  }
4948
5081
  } else if (cacheKey) {
4949
- className = generateClassName(cacheKey);
5082
+ className = this.generateClassName(cacheKey);
4950
5083
  if (this.tryHydratedHit(registry, cacheKey, className)) {
4951
5084
  registry.refCounts.set(className, (registry.refCounts.get(className) || 0) + 1);
4952
5085
  if (registry.metrics) registry.metrics.hits++;
@@ -4955,7 +5088,10 @@ var StyleInjector = class StyleInjector {
4955
5088
  dispose: () => this.dispose(className, registry)
4956
5089
  };
4957
5090
  }
4958
- } else className = `t${hashString(rules.map((r) => `${r.selector}\0${r.declarations}`).join("\n"))}`;
5091
+ } else {
5092
+ const parts = rules.map((r) => `${r.selector}\0${r.declarations}`);
5093
+ className = makeClassName(this.namePrefix, hashString(parts.join("\n")));
5094
+ }
4959
5095
  const rulesToInsert = rules.map((rule) => {
4960
5096
  let newSelector = rule.selector;
4961
5097
  if (rule.needsClassName) {
@@ -5173,8 +5309,23 @@ var StyleInjector = class StyleInjector {
5173
5309
  }
5174
5310
  const cssName = effectiveResult.cssName;
5175
5311
  const definition = effectiveResult.definition;
5176
- const normalizedDef = normalizePropertyDefinition(definition);
5177
- if (registry.injectedProperties.get(cssName) !== void 0) return;
5312
+ this.insertPropertyRule(registry, root, cssName, definition, name);
5313
+ if (effectiveResult.isColor) {
5314
+ const companionCssName = `${cssName}-${getColorSpaceSuffix()}`;
5315
+ const companionDefinition = {
5316
+ syntax: getComponentPropertySyntax(),
5317
+ inherits: definition.inherits,
5318
+ initialValue: colorInitialValueToComponents(definition.initialValue)
5319
+ };
5320
+ this.insertPropertyRule(registry, root, companionCssName, companionDefinition, `${name}:components`);
5321
+ }
5322
+ }
5323
+ /**
5324
+ * Build and insert a single `@property` rule into the given registry.
5325
+ * No-op if the property was already injected.
5326
+ */
5327
+ insertPropertyRule(registry, root, cssName, definition, cacheKey) {
5328
+ if (registry.injectedProperties.has(cssName)) return;
5178
5329
  const parts = [];
5179
5330
  if (definition.syntax != null) {
5180
5331
  let syntax = String(definition.syntax).trim();
@@ -5194,8 +5345,8 @@ var StyleInjector = class StyleInjector {
5194
5345
  selector: `@property ${cssName}`,
5195
5346
  declarations
5196
5347
  };
5197
- if (!this.sheetManager.insertGlobalRule(registry, [rule], `property:${name}`, root)) return;
5198
- registry.injectedProperties.set(cssName, normalizedDef);
5348
+ registry.injectedProperties.set(cssName, normalizePropertyDefinition(definition));
5349
+ this.sheetManager.insertGlobalRule(registry, [rule], `property:${cacheKey}`, root);
5199
5350
  }
5200
5351
  /**
5201
5352
  * Check whether a given @property name was already injected by this injector.
@@ -5275,12 +5426,12 @@ var StyleInjector = class StyleInjector {
5275
5426
  let actualName;
5276
5427
  if (providedName) {
5277
5428
  const existingContentForName = registry.keyframesNameToContent.get(providedName);
5278
- if (existingContentForName && existingContentForName !== contentHash) actualName = `${providedName}-k${registry.keyframesCounter++}`;
5429
+ if (existingContentForName && existingContentForName !== contentHash) actualName = `${providedName}-${makeKeyframeName(this.namePrefix, String(registry.keyframesCounter++))}`;
5279
5430
  else {
5280
5431
  actualName = providedName;
5281
5432
  registry.keyframesNameToContent.set(providedName, contentHash);
5282
5433
  }
5283
- } else actualName = `k${registry.keyframesCounter++}`;
5434
+ } else actualName = makeKeyframeName(this.namePrefix, String(registry.keyframesCounter++));
5284
5435
  const result = this.sheetManager.insertKeyframes(registry, steps, actualName, root);
5285
5436
  if (!result) return {
5286
5437
  toString: () => "",
@@ -5329,7 +5480,6 @@ var StyleInjector = class StyleInjector {
5329
5480
  }
5330
5481
  }
5331
5482
  }
5332
- static TASTY_CLASS_RE = /^t[a-z0-9]+$/;
5333
5483
  /**
5334
5484
  * Record a render-time usage hit for one or more classNames.
5335
5485
  * Handles space-separated multi-chunk classNames.
@@ -5345,7 +5495,7 @@ var StyleInjector = class StyleInjector {
5345
5495
  const now = Date.now();
5346
5496
  const parts = className.indexOf(" ") === -1 ? [className] : className.split(" ");
5347
5497
  for (const cls of parts) {
5348
- if (!StyleInjector.TASTY_CLASS_RE.test(cls)) continue;
5498
+ if (!this.classRegex.test(cls)) continue;
5349
5499
  if (!registry.rules.has(cls)) continue;
5350
5500
  const entry = registry.usageMap.get(cls);
5351
5501
  if (entry) entry.lastTouchedAt = now;
@@ -5398,7 +5548,7 @@ var StyleInjector = class StyleInjector {
5398
5548
  if (registry.usageMap.size - activeCount <= capacity) return 0;
5399
5549
  }
5400
5550
  const liveClasses = /* @__PURE__ */ new Set();
5401
- for (const el of root.querySelectorAll("[class]")) for (const token of el.classList) if (StyleInjector.TASTY_CLASS_RE.test(token)) liveClasses.add(token);
5551
+ for (const el of root.querySelectorAll("[class]")) for (const token of el.classList) if (this.classRegex.test(token)) liveClasses.add(token);
5402
5552
  let swept = 0;
5403
5553
  if (force) for (const [className] of registry.usageMap) {
5404
5554
  if (liveClasses.has(className)) continue;
@@ -6055,9 +6205,13 @@ function simplifyInner(node) {
6055
6205
  return node;
6056
6206
  }
6057
6207
  if (node.kind === "compound") {
6208
+ const key = getConditionUniqueId(node);
6209
+ const cached = simplifyCache.get(key);
6210
+ if (cached) return cached;
6058
6211
  const simplifiedChildren = node.children.map((c) => simplifyInner(c));
6059
- if (node.operator === "AND") return simplifyAnd(simplifiedChildren);
6060
- else return simplifyOr(simplifiedChildren);
6212
+ const result = node.operator === "AND" ? simplifyAnd(simplifiedChildren) : simplifyOr(simplifiedChildren);
6213
+ simplifyCache.set(key, result);
6214
+ return result;
6061
6215
  }
6062
6216
  return node;
6063
6217
  }
@@ -6075,13 +6229,23 @@ function simplifyAnd(children) {
6075
6229
  if (hasRangeContradiction(terms)) return falseCondition();
6076
6230
  if (hasAttributeConflict(terms)) return falseCondition();
6077
6231
  if (hasContainerStyleConflict(terms)) return falseCondition();
6078
- terms = removeImpliedNegations(terms);
6079
- terms = deduplicateTerms(terms);
6080
- terms = mergeRanges(terms);
6081
- terms = sortTerms(terms);
6082
- terms = applyAbsorptionAnd(terms);
6083
- terms = applyConsensusAnd(terms);
6084
- terms = pruneContradictedOrBranches(terms);
6232
+ const MAX_SIMPLIFY_PASSES = 4;
6233
+ for (let pass = 0; pass < MAX_SIMPLIFY_PASSES; pass++) {
6234
+ const lengthBefore = terms.length;
6235
+ const keyBefore = simplifyTermsKey(terms);
6236
+ terms = removeImpliedNegations(terms);
6237
+ terms = deduplicateTerms(terms);
6238
+ terms = mergeRanges(terms);
6239
+ terms = sortTerms(terms);
6240
+ terms = applyAbsorptionAnd(terms);
6241
+ terms = applyConsensusAnd(terms);
6242
+ terms = pruneContradictedOrBranches(terms);
6243
+ if (terms.length === 0) break;
6244
+ if (terms.length === 1) break;
6245
+ if (hasContradiction(terms)) return falseCondition();
6246
+ if (hasAttributeConflict(terms)) return falseCondition();
6247
+ if (terms.length === lengthBefore && simplifyTermsKey(terms) === keyBefore) break;
6248
+ }
6085
6249
  if (terms.length === 0) return trueCondition();
6086
6250
  if (terms.length === 1) return terms[0];
6087
6251
  return {
@@ -6396,6 +6560,13 @@ function removeImpliedNegations(terms) {
6396
6560
  const isImplied = buildImpliedNegationCheck(terms);
6397
6561
  return terms.filter((t) => !isImplied(t));
6398
6562
  }
6563
+ /**
6564
+ * Build a stable, order-insensitive key for a list of AND terms.
6565
+ * Used to detect fixpoint convergence in `simplifyAnd`.
6566
+ */
6567
+ function simplifyTermsKey(terms) {
6568
+ return terms.map((t) => getConditionUniqueId(t)).sort().join("&");
6569
+ }
6399
6570
  function deduplicateTerms(terms) {
6400
6571
  const seen = /* @__PURE__ */ new Set();
6401
6572
  const result = [];
@@ -6778,1061 +6949,677 @@ function pruneContradictedOrBranches(terms) {
6778
6949
  return flattened;
6779
6950
  }
6780
6951
  //#endregion
6781
- //#region src/pipeline/exclusive.ts
6952
+ //#region src/pipeline/materialize-contradictions.ts
6782
6953
  /**
6783
- * Build exclusive conditions for a list of parsed style entries.
6784
- *
6785
- * The entries should be ordered by priority (highest priority first).
6786
- *
6787
- * For each entry, we compute:
6788
- * exclusiveCondition = condition & !prior[0] & !prior[1] & ...
6789
- *
6790
- * This ensures exactly one condition matches at any time.
6791
- *
6792
- * Example:
6793
- * Input (ordered highest to lowest priority):
6794
- * A: value1 (priority 2)
6795
- * B: value2 (priority 1)
6796
- * C: value3 (priority 0)
6797
- *
6798
- * Output:
6799
- * A: A
6800
- * B: B & !A
6801
- * C: C & !A & !B
6802
- *
6803
- * @param entries Parsed style entries ordered by priority (highest first)
6804
- * @returns Entries with exclusive conditions, filtered to remove impossible ones
6954
+ * Generic deduplication by a key extraction function.
6955
+ * Preserves insertion order, keeping the first occurrence of each key.
6805
6956
  */
6806
- function buildExclusiveConditions(entries) {
6957
+ function dedupeByKey(items, getKey) {
6958
+ const seen = /* @__PURE__ */ new Set();
6807
6959
  const result = [];
6808
- const priorConditions = [];
6809
- for (const entry of entries) {
6810
- let exclusive = entry.condition;
6811
- for (const prior of priorConditions) if (prior.kind !== "true") exclusive = and(exclusive, not(prior));
6812
- const simplified = simplifyCondition(exclusive);
6813
- if (simplified.kind === "false") continue;
6814
- result.push({
6815
- ...entry,
6816
- exclusiveCondition: simplified
6817
- });
6818
- if (entry.condition.kind !== "true") priorConditions.push(entry.condition);
6960
+ for (const item of items) {
6961
+ const key = getKey(item);
6962
+ if (!seen.has(key)) {
6963
+ seen.add(key);
6964
+ result.push(item);
6965
+ }
6819
6966
  }
6820
6967
  return result;
6821
6968
  }
6969
+ function dedupeMediaConditions(conditions) {
6970
+ return dedupeByKey(conditions, (c) => `${c.subtype}|${c.condition}|${c.negated}`);
6971
+ }
6972
+ function dedupeContainerConditions(conditions) {
6973
+ return dedupeByKey(conditions, (c) => `${c.name ?? ""}|${c.condition}|${c.negated}`);
6974
+ }
6975
+ function dedupeSupportsConditions(conditions) {
6976
+ return dedupeByKey(conditions, (c) => `${c.subtype}|${c.condition}|${c.negated}`);
6977
+ }
6822
6978
  /**
6823
- * Parse style entries from a value mapping object.
6824
- *
6825
- * @param styleKey The style key (e.g., 'padding')
6826
- * @param valueMap The value mapping { '': '2x', 'compact': '1x', '@media(w < 768px)': '0.5x' }
6827
- * @param parseCondition Function to parse state keys into conditions
6828
- * @returns Parsed entries ordered by priority (highest first)
6979
+ * Check if supports conditions contain contradictions
6980
+ * e.g., @supports(display: grid) AND NOT @supports(display: grid)
6829
6981
  */
6830
- function parseStyleEntries(styleKey, valueMap, parseCondition) {
6831
- const entries = [];
6832
- Object.keys(valueMap).forEach((stateKey, index) => {
6833
- const value = valueMap[stateKey];
6834
- const condition = stateKey === "" ? trueCondition() : parseCondition(stateKey);
6835
- entries.push({
6836
- styleKey,
6837
- stateKey,
6838
- value,
6839
- condition,
6840
- priority: index
6841
- });
6842
- });
6843
- entries.reverse();
6844
- return entries;
6982
+ function hasSupportsContradiction(conditions) {
6983
+ const conditionMap = /* @__PURE__ */ new Map();
6984
+ for (const cond of conditions) {
6985
+ const key = `${cond.subtype}|${cond.condition}`;
6986
+ const existing = conditionMap.get(key);
6987
+ if (existing !== void 0 && existing !== !cond.negated) return true;
6988
+ conditionMap.set(key, !cond.negated);
6989
+ }
6990
+ return false;
6845
6991
  }
6846
6992
  /**
6847
- * Merge parsed entries that share the same value.
6848
- *
6849
- * When multiple **non-default** state keys map to the same value, their
6850
- * conditions can be combined with OR and treated as a single entry.
6851
- * This must happen **before** exclusive expansion and OR branch splitting
6852
- * to avoid combinatorial explosion and duplicate CSS output.
6853
- *
6854
- * Default (TRUE) entries are **never** merged with non-default entries.
6855
- * Merging `TRUE | X` collapses to `TRUE`, destroying the non-default
6856
- * condition's participation in exclusive building. That causes
6857
- * intermediate-priority states to lose their `:not(X)` negation,
6858
- * breaking mutual exclusivity when X and an intermediate state are
6859
- * both active. Stage 6 `mergeByValue` handles combining rules with
6860
- * identical CSS output after exclusive conditions are correctly built.
6993
+ * Check if a set of media conditions contains contradictions
6994
+ * e.g., (prefers-color-scheme: light) AND NOT (prefers-color-scheme: light)
6995
+ * or (width >= 900px) AND (width < 600px)
6861
6996
  *
6862
- * Example: `{ '@dark': 'red', '@dark & @hc': 'red' }` merges into a
6863
- * single entry with condition `@dark | (@dark & @hc)` = `@dark`.
6997
+ * Uses parsed media conditions for efficient analysis without regex parsing.
6998
+ */
6999
+ function hasMediaContradiction(conditions) {
7000
+ const featureConditions = /* @__PURE__ */ new Map();
7001
+ const typeConditions = /* @__PURE__ */ new Map();
7002
+ const dimensionConditions = /* @__PURE__ */ new Map();
7003
+ const dimensionsByDim = /* @__PURE__ */ new Map();
7004
+ for (const cond of conditions) if (cond.subtype === "type") {
7005
+ const key = cond.mediaType || "all";
7006
+ const existing = typeConditions.get(key);
7007
+ if (existing !== void 0 && existing !== !cond.negated) return true;
7008
+ typeConditions.set(key, !cond.negated);
7009
+ } else if (cond.subtype === "feature") {
7010
+ const key = cond.condition;
7011
+ const existing = featureConditions.get(key);
7012
+ if (existing !== void 0 && existing !== !cond.negated) return true;
7013
+ featureConditions.set(key, !cond.negated);
7014
+ } else if (cond.subtype === "dimension") {
7015
+ const condKey = cond.condition;
7016
+ const existing = dimensionConditions.get(condKey);
7017
+ if (existing !== void 0 && existing !== !cond.negated) return true;
7018
+ dimensionConditions.set(condKey, !cond.negated);
7019
+ if (!cond.negated) {
7020
+ const dim = cond.dimension || "width";
7021
+ let bounds = dimensionsByDim.get(dim);
7022
+ if (!bounds) {
7023
+ bounds = {
7024
+ lowerBound: null,
7025
+ upperBound: null
7026
+ };
7027
+ dimensionsByDim.set(dim, bounds);
7028
+ }
7029
+ if (cond.lowerBound?.valueNumeric != null) {
7030
+ const value = cond.lowerBound.valueNumeric;
7031
+ if (bounds.lowerBound === null || value > bounds.lowerBound) bounds.lowerBound = value;
7032
+ }
7033
+ if (cond.upperBound?.valueNumeric != null) {
7034
+ const value = cond.upperBound.valueNumeric;
7035
+ if (bounds.upperBound === null || value < bounds.upperBound) bounds.upperBound = value;
7036
+ }
7037
+ if (bounds.lowerBound !== null && bounds.upperBound !== null && bounds.lowerBound >= bounds.upperBound) return true;
7038
+ }
7039
+ }
7040
+ return false;
7041
+ }
7042
+ /**
7043
+ * Check if container conditions contain contradictions in style queries
7044
+ * e.g., style(--variant: danger) and style(--variant: success) together
7045
+ * Same property with different values = always false
6864
7046
  *
6865
- * Entries are ordered highest-priority-first. The merged entry keeps the
6866
- * highest priority of the group.
7047
+ * Uses parsed container conditions for efficient analysis without regex parsing.
6867
7048
  */
6868
- function mergeEntriesByValue(entries) {
6869
- if (entries.length <= 1) return entries;
6870
- const groups = /* @__PURE__ */ new Map();
6871
- for (const entry of entries) {
6872
- const valueKey = serializeValue(entry.value);
6873
- const group = groups.get(valueKey);
6874
- if (group) {
6875
- group.entries.push(entry);
6876
- group.maxPriority = Math.max(group.maxPriority, entry.priority);
6877
- } else groups.set(valueKey, {
6878
- entries: [entry],
6879
- maxPriority: entry.priority
7049
+ function hasContainerStyleContradiction(conditions) {
7050
+ const styleQueries = /* @__PURE__ */ new Map();
7051
+ for (const cond of conditions) {
7052
+ if (cond.subtype !== "style" || !cond.property) continue;
7053
+ const property = cond.property;
7054
+ const value = cond.propertyValue;
7055
+ if (!styleQueries.has(property)) styleQueries.set(property, {
7056
+ hasExistence: false,
7057
+ values: /* @__PURE__ */ new Set(),
7058
+ hasNegatedExistence: false
6880
7059
  });
7060
+ const entry = styleQueries.get(property);
7061
+ if (cond.negated) {
7062
+ if (value === void 0) entry.hasNegatedExistence = true;
7063
+ } else if (value === void 0) entry.hasExistence = true;
7064
+ else entry.values.add(value);
6881
7065
  }
6882
- if (groups.size === entries.length) return entries;
6883
- const merged = [];
6884
- for (const [, group] of groups) {
6885
- if (group.entries.length === 1) {
6886
- merged.push(group.entries[0]);
6887
- continue;
6888
- }
6889
- const defaultEntries = group.entries.filter((e) => e.condition.kind === "true");
6890
- const nonDefaultEntries = group.entries.filter((e) => e.condition.kind !== "true");
6891
- for (const entry of defaultEntries) merged.push(entry);
6892
- if (nonDefaultEntries.length === 1) merged.push(nonDefaultEntries[0]);
6893
- else if (nonDefaultEntries.length >= 2) {
6894
- const combinedCondition = simplifyCondition(or(...nonDefaultEntries.map((e) => e.condition)));
6895
- const combinedStateKey = nonDefaultEntries.map((e) => e.stateKey).join(" | ");
6896
- merged.push({
6897
- styleKey: nonDefaultEntries[0].styleKey,
6898
- stateKey: combinedStateKey,
6899
- value: nonDefaultEntries[0].value,
6900
- condition: combinedCondition,
6901
- priority: group.maxPriority
6902
- });
6903
- }
7066
+ for (const [, entry] of styleQueries) {
7067
+ if (entry.hasExistence && entry.hasNegatedExistence) return true;
7068
+ if (entry.values.size > 1) return true;
7069
+ if (entry.hasNegatedExistence && entry.values.size > 0) return true;
6904
7070
  }
6905
- merged.sort((a, b) => b.priority - a.priority);
6906
- return merged;
6907
- }
6908
- function serializeValue(value) {
6909
- if (value === null || value === void 0) return "null";
6910
- if (typeof value === "string" || typeof value === "number") return String(value);
6911
- return JSON.stringify(value);
7071
+ return false;
6912
7072
  }
7073
+ //#endregion
7074
+ //#region src/pipeline/materialize.ts
6913
7075
  /**
6914
- * Eliminate redundant state dimensions from a value map.
6915
- *
6916
- * When a value map contains compound AND state keys (e.g. `@dark & @hc`),
6917
- * checks whether any state atom is a "don't-care" variable — i.e. the
6918
- * value is the same whether that atom is present or absent. Redundant
6919
- * atoms are removed from all keys and duplicate entries are collapsed.
6920
- *
6921
- * This runs **before** condition parsing so that downstream stages
6922
- * (`mergeEntriesByValue`, `buildExclusiveConditions`, materialization)
6923
- * never see the irrelevant dimension, producing simpler, smaller CSS.
6924
- *
6925
- * Only pure top-level AND combinations are eligible. Keys that contain
6926
- * `|`, `^`, or `,` at the top level are treated as opaque single atoms.
7076
+ * CSS Materialization
6927
7077
  *
6928
- * @example
6929
- * { '': A, '@dark': B, '@hc': A, '@dark & @hc': B }
6930
- * // @hc is redundant → { '': A, '@dark': B }
7078
+ * Converts condition trees into CSS selectors and at-rules.
7079
+ * This is the final stage that produces actual CSS output.
6931
7080
  */
6932
- function extractCompoundStates(valueMap) {
6933
- const keys = Object.keys(valueMap);
6934
- if (keys.length < 3 || !keys.some((k) => k.includes("&"))) return valueMap;
6935
- const entries = keys.map((key) => {
6936
- return {
6937
- atoms: splitTopLevelAnd(key) ?? [key],
6938
- value: valueMap[key]
6939
- };
6940
- });
6941
- const allAtoms = /* @__PURE__ */ new Set();
6942
- for (const e of entries) for (const a of e.atoms) allAtoms.add(a);
6943
- const redundant = /* @__PURE__ */ new Set();
6944
- for (const atom of allAtoms) if (isAtomRedundant(entries, atom)) redundant.add(atom);
6945
- if (redundant.size === 0) return valueMap;
6946
- const newMap = {};
6947
- for (const e of entries) {
6948
- const newKey = e.atoms.filter((a) => !redundant.has(a)).join(" & ");
6949
- if (!(newKey in newMap)) newMap[newKey] = e.value;
6950
- }
6951
- return newMap;
7081
+ const conditionCache = new Lru(3e3);
7082
+ /**
7083
+ * Convert a condition tree to CSS components
7084
+ */
7085
+ function conditionToCSS(node) {
7086
+ const key = getConditionUniqueId(node);
7087
+ const cached = conditionCache.get(key);
7088
+ if (cached) return cached;
7089
+ const result = conditionToCSSInner(node);
7090
+ conditionCache.set(key, result);
7091
+ return result;
7092
+ }
7093
+ function emptyVariant() {
7094
+ return {
7095
+ modifierConditions: [],
7096
+ pseudoConditions: [],
7097
+ selectorGroups: [],
7098
+ ownGroups: [],
7099
+ mediaConditions: [],
7100
+ containerConditions: [],
7101
+ supportsConditions: [],
7102
+ rootGroups: [],
7103
+ parentGroups: [],
7104
+ startingStyle: false
7105
+ };
7106
+ }
7107
+ function conditionToCSSInner(node) {
7108
+ if (node.kind === "true") return {
7109
+ variants: [emptyVariant()],
7110
+ isImpossible: false
7111
+ };
7112
+ if (node.kind === "false") return {
7113
+ variants: [],
7114
+ isImpossible: true
7115
+ };
7116
+ if (node.kind === "state") return stateToCSS(node);
7117
+ if (node.kind === "compound") if (node.operator === "AND") return andToCSS(node.children);
7118
+ else return orToCSS(node.children);
7119
+ return {
7120
+ variants: [emptyVariant()],
7121
+ isImpossible: false
7122
+ };
6952
7123
  }
6953
7124
  /**
6954
- * Split a state key by top-level `&` operators.
6955
- *
6956
- * Returns `null` if the key contains `|`, `^`, or `,` at the top level
6957
- * (making it ineligible for atom-level extraction).
6958
- * Returns `[]` for the empty string (default key).
7125
+ * Convert a state condition to CSS
6959
7126
  */
6960
- function splitTopLevelAnd(key) {
6961
- if (key === "") return [];
6962
- const parts = [];
6963
- let depth = 0;
6964
- let current = "";
6965
- for (const ch of key) {
6966
- if (ch === "(" || ch === "[") depth++;
6967
- else if (ch === ")" || ch === "]") depth--;
6968
- if (depth === 0) {
6969
- if (ch === "&") {
6970
- const trimmed = current.trim();
6971
- if (trimmed) parts.push(trimmed);
6972
- current = "";
6973
- continue;
6974
- }
6975
- if (ch === "|" || ch === "^" || ch === ",") return null;
7127
+ function stateToCSS(state) {
7128
+ switch (state.type) {
7129
+ case "media": return {
7130
+ variants: mediaToParsed(state).map((mediaCond) => {
7131
+ const v = emptyVariant();
7132
+ v.mediaConditions.push(mediaCond);
7133
+ return v;
7134
+ }),
7135
+ isImpossible: false
7136
+ };
7137
+ case "root": return innerConditionToVariants(state.innerCondition, state.negated ?? false, "rootGroups");
7138
+ case "parent": return parentConditionToVariants(state.innerCondition, state.negated ?? false, state.direct);
7139
+ case "own": return innerConditionToVariants(state.innerCondition, state.negated ?? false, "ownGroups");
7140
+ case "modifier": {
7141
+ const v = emptyVariant();
7142
+ v.modifierConditions.push(modifierToParsed(state));
7143
+ return {
7144
+ variants: [v],
7145
+ isImpossible: false
7146
+ };
7147
+ }
7148
+ case "pseudo": {
7149
+ const v = emptyVariant();
7150
+ v.pseudoConditions.push(pseudoToParsed(state));
7151
+ return {
7152
+ variants: [v],
7153
+ isImpossible: false
7154
+ };
7155
+ }
7156
+ case "container": {
7157
+ const v = emptyVariant();
7158
+ v.containerConditions.push(containerToParsed(state));
7159
+ return {
7160
+ variants: [v],
7161
+ isImpossible: false
7162
+ };
7163
+ }
7164
+ case "supports": {
7165
+ const v = emptyVariant();
7166
+ v.supportsConditions.push(supportsToParsed(state));
7167
+ return {
7168
+ variants: [v],
7169
+ isImpossible: false
7170
+ };
7171
+ }
7172
+ case "starting": {
7173
+ const v = emptyVariant();
7174
+ v.startingStyle = !state.negated;
7175
+ return {
7176
+ variants: [v],
7177
+ isImpossible: false
7178
+ };
6976
7179
  }
6977
- current += ch;
6978
7180
  }
6979
- const trimmed = current.trim();
6980
- if (trimmed) parts.push(trimmed);
6981
- return parts;
6982
7181
  }
6983
7182
  /**
6984
- * An atom is redundant when every entry that contains it has a matching
6985
- * partner (same remaining atoms, atom absent) with the same value.
7183
+ * Convert modifier condition to parsed structure
6986
7184
  */
6987
- function isAtomRedundant(entries, atom) {
6988
- const withAtom = entries.filter((e) => e.atoms.includes(atom));
6989
- if (withAtom.length === 0) return false;
6990
- for (const wa of withAtom) {
6991
- const remaining = wa.atoms.filter((a) => a !== atom);
6992
- const pair = entries.find((e) => !e.atoms.includes(atom) && e.atoms.length === remaining.length && remaining.every((r) => e.atoms.includes(r)));
6993
- if (!pair) return false;
6994
- if (serializeValue(wa.value) !== serializeValue(pair.value)) return false;
6995
- }
6996
- return true;
7185
+ function modifierToParsed(state) {
7186
+ return {
7187
+ attribute: state.attribute,
7188
+ value: state.value,
7189
+ operator: state.operator,
7190
+ negated: state.negated ?? false
7191
+ };
6997
7192
  }
6998
7193
  /**
6999
- * Check if a value is a style value mapping (object with state keys)
7194
+ * Convert parsed modifier to CSS selector string (for final output)
7000
7195
  */
7001
- function isValueMapping(value) {
7002
- return value !== null && typeof value === "object" && !Array.isArray(value) && !(value instanceof Date);
7196
+ function modifierToCSS(mod) {
7197
+ let selector;
7198
+ if (mod.value !== void 0) {
7199
+ const op = mod.operator || "=";
7200
+ selector = `[${mod.attribute}${op}"${mod.value}"]`;
7201
+ } else selector = `[${mod.attribute}]`;
7202
+ if (mod.negated) return `:not(${selector})`;
7203
+ return selector;
7003
7204
  }
7004
7205
  /**
7005
- * Expand OR conditions in parsed entries into multiple exclusive entries.
7006
- *
7007
- * For an entry with condition `A | B | C`, this creates 3 entries:
7008
- * - condition: A
7009
- * - condition: B & !A
7010
- * - condition: C & !A & !B
7011
- *
7012
- * This ensures OR branches are mutually exclusive BEFORE the main
7013
- * exclusive condition building pass.
7014
- *
7015
- * @param entries Parsed entries (may contain OR conditions)
7016
- * @returns Expanded entries with OR branches made exclusive
7206
+ * Convert pseudo condition to parsed structure
7017
7207
  */
7018
- function expandOrConditions(entries) {
7019
- const result = [];
7020
- for (const entry of entries) {
7021
- const expanded = expandSingleEntry(entry);
7022
- result.push(...expanded);
7023
- }
7024
- return result;
7208
+ function pseudoToParsed(state) {
7209
+ return {
7210
+ pseudo: state.pseudo,
7211
+ negated: state.negated ?? false
7212
+ };
7025
7213
  }
7026
7214
  /**
7027
- * Expand a single entry's OR condition into multiple exclusive entries.
7215
+ * Convert parsed pseudo to CSS selector string (for final output).
7028
7216
  *
7029
- * Note: branches are NOT sorted by at-rule context here (unlike the
7030
- * `expandExclusiveOrs` pass below). User-authored ORs in state keys aren't
7031
- * the product of De Morgan negation, so each branch is expected to render
7032
- * independently in its own scope and at-rule sort isn't load-bearing.
7033
- * The post-build pass needs the sort because it has to preserve at-rule
7034
- * wrapping across branches that came from negating a compound at-rule.
7217
+ * :not() is normalized to negated :is() at parse time, so pseudo.pseudo
7218
+ * never starts with ':not(' here. When negated:
7219
+ * - :is(X) :not(X) (unwrap :is)
7220
+ * - :where(X) :not(X) (unwrap :where)
7221
+ * - :has(X) :not(:has(X))
7222
+ * - other :not(other)
7223
+ *
7224
+ * When not negated, single-argument :is()/:where() is unwrapped when the
7225
+ * inner content is a simple compound selector that can safely append to
7226
+ * the base selector (this happens after double-negation of :not()).
7035
7227
  */
7036
- function expandSingleEntry(entry) {
7037
- const orBranches = collectOrBranches(entry.condition);
7038
- if (orBranches.length <= 1) return [entry];
7039
- const result = [];
7040
- const priorBranches = [];
7041
- for (let i = 0; i < orBranches.length; i++) {
7042
- const branch = orBranches[i];
7043
- let exclusiveBranch = branch;
7044
- for (const prior of priorBranches) exclusiveBranch = and(exclusiveBranch, not(prior));
7045
- const simplified = simplifyCondition(exclusiveBranch);
7046
- if (simplified.kind === "false") {
7047
- priorBranches.push(branch);
7048
- continue;
7049
- }
7050
- result.push({
7051
- ...entry,
7052
- stateKey: `${entry.stateKey}[${i}]`,
7053
- condition: simplified
7054
- });
7055
- priorBranches.push(branch);
7228
+ function pseudoToCSS(pseudo) {
7229
+ const p = pseudo.pseudo;
7230
+ if (pseudo.negated) {
7231
+ if (p.startsWith(":is(") || p.startsWith(":where(")) return `:not(${p.slice(p.indexOf("(") + 1, -1)})`;
7232
+ return `:not(${p})`;
7056
7233
  }
7057
- return result;
7234
+ if ((p.startsWith(":is(") || p.startsWith(":where(")) && !p.includes(",")) {
7235
+ const inner = p.slice(p.indexOf("(") + 1, -1);
7236
+ const ch = inner[0];
7237
+ if ((ch === ":" || ch === "." || ch === "[" || ch === "#") && !/\s/.test(inner)) return inner;
7238
+ }
7239
+ return p;
7058
7240
  }
7059
7241
  /**
7060
- * Collect top-level OR branches from a condition.
7061
- *
7062
- * For `A | B | C`, returns [A, B, C]
7063
- * For `A & B`, returns [A & B] (single branch)
7064
- * For `A | (B & C)`, returns [A, B & C]
7242
+ * Convert media condition to parsed structure(s)
7243
+ * Returns an array because negated ranges produce OR branches (two separate conditions)
7065
7244
  */
7066
- function collectOrBranches(condition) {
7067
- if (condition.kind === "true" || condition.kind === "false") return [condition];
7068
- if (isCompoundCondition(condition) && condition.operator === "OR") {
7069
- const branches = [];
7070
- for (const child of condition.children) branches.push(...collectOrBranches(child));
7071
- return branches;
7072
- }
7073
- return [condition];
7245
+ function mediaToParsed(state) {
7246
+ if (state.subtype === "type") {
7247
+ const mediaType = state.mediaType || "all";
7248
+ return [{
7249
+ subtype: "type",
7250
+ negated: state.negated ?? false,
7251
+ condition: mediaType,
7252
+ mediaType: state.mediaType
7253
+ }];
7254
+ } else if (state.subtype === "feature") {
7255
+ let condition;
7256
+ if (state.featureValue) condition = `(${state.feature}: ${state.featureValue})`;
7257
+ else condition = `(${state.feature})`;
7258
+ return [{
7259
+ subtype: "feature",
7260
+ negated: state.negated ?? false,
7261
+ condition,
7262
+ feature: state.feature,
7263
+ featureValue: state.featureValue
7264
+ }];
7265
+ } else return dimensionToMediaParsed(state.dimension || "width", state.lowerBound, state.upperBound, state.negated ?? false);
7074
7266
  }
7075
7267
  /**
7076
- * Expand OR conditions in exclusive entries AFTER buildExclusiveConditions.
7077
- *
7078
- * This handles ORs that arise from De Morgan expansion during negation:
7079
- * !(A & B) = !A | !B
7080
- *
7081
- * These ORs need to be made exclusive to avoid overlapping CSS rules:
7082
- * !A | !B → !A | (A & !B)
7083
- *
7084
- * This is logically equivalent but ensures each branch has proper context.
7085
- *
7086
- * Example:
7087
- * Input: { "": V1, "@supports(...) & :has()": V2 }
7088
- * V2's exclusive = @supports & :has
7089
- * V1's exclusive = !(@supports & :has) = !@supports | !:has
7090
- *
7091
- * Without this fix: V1 gets two rules:
7092
- * - @supports (not ...) → V1 ✓
7093
- * - :not(:has()) → V1 ✗ (missing @supports context!)
7094
- *
7095
- * With this fix: V1 gets two exclusive rules:
7096
- * - @supports (not ...) → V1 ✓
7097
- * - @supports (...) { :not(:has()) } → V1 ✓ (proper context!)
7268
+ * Convert dimension bounds to parsed media condition(s)
7269
+ * Uses CSS Media Queries Level 4 `not (condition)` syntax for negation.
7098
7270
  */
7099
- function expandExclusiveOrs(entries) {
7100
- const result = [];
7101
- for (const entry of entries) {
7102
- const expanded = expandExclusiveConditionOrs(entry);
7103
- result.push(...expanded);
7104
- }
7105
- return result;
7271
+ function dimensionToMediaParsed(dimension, lowerBound, upperBound, negated) {
7272
+ let condition;
7273
+ if (lowerBound && upperBound) {
7274
+ const lowerOp = lowerBound.inclusive ? "<=" : "<";
7275
+ const upperOp = upperBound.inclusive ? "<=" : "<";
7276
+ condition = `(${lowerBound.value} ${lowerOp} ${dimension} ${upperOp} ${upperBound.value})`;
7277
+ } else if (upperBound) condition = `(${dimension} ${upperBound.inclusive ? "<=" : "<"} ${upperBound.value})`;
7278
+ else if (lowerBound) condition = `(${dimension} ${lowerBound.inclusive ? ">=" : ">"} ${lowerBound.value})`;
7279
+ else condition = `(${dimension})`;
7280
+ return [{
7281
+ subtype: "dimension",
7282
+ negated: negated ?? false,
7283
+ condition,
7284
+ dimension,
7285
+ lowerBound,
7286
+ upperBound
7287
+ }];
7106
7288
  }
7107
7289
  /**
7108
- * Check if a condition involves at-rules (media, container, supports, starting)
7290
+ * Convert container condition to parsed structure
7291
+ * This enables structured analysis for contradiction detection and condition combining
7109
7292
  */
7110
- function hasAtRuleContext(node) {
7111
- if (node.kind === "true" || node.kind === "false") return false;
7112
- if (node.kind === "state") return node.type === "media" || node.type === "container" || node.type === "supports" || node.type === "starting";
7113
- if (node.kind === "compound") return node.children.some(hasAtRuleContext);
7114
- return false;
7293
+ function containerToParsed(state) {
7294
+ let condition;
7295
+ if (state.subtype === "style") if (state.propertyValue) condition = `style(--${state.property}: ${state.propertyValue})`;
7296
+ else condition = `style(--${state.property})`;
7297
+ else if (state.subtype === "raw") condition = state.rawCondition;
7298
+ else condition = dimensionToContainerCondition(state.dimension || "width", state.lowerBound, state.upperBound);
7299
+ return {
7300
+ name: state.containerName,
7301
+ condition,
7302
+ negated: state.negated ?? false,
7303
+ subtype: state.subtype,
7304
+ property: state.property,
7305
+ propertyValue: state.propertyValue
7306
+ };
7115
7307
  }
7116
7308
  /**
7117
- * Sort OR branches to prioritize at-rule conditions first.
7118
- *
7119
- * This is critical for correct CSS generation. For `!A | !B` where A is at-rule
7120
- * and B is modifier, we want:
7121
- * - Branch 0: !A (at-rule negation - covers "no @supports/media" case)
7122
- * - Branch 1: A & !B (modifier negation with at-rule context)
7123
- *
7124
- * If we process in wrong order (!B first), we'd get:
7125
- * - Branch 0: !B (modifier negation WITHOUT at-rule context - WRONG!)
7126
- * - Branch 1: B & !A (at-rule negation with modifier - incomplete coverage)
7309
+ * Convert dimension bounds to container query condition (single string)
7310
+ * Container queries support "not (condition)", so no need to invert manually
7127
7311
  */
7128
- function sortOrBranchesForExpansion(branches) {
7129
- return [...branches].sort((a, b) => {
7130
- const aHasAtRule = hasAtRuleContext(a);
7131
- const bHasAtRule = hasAtRuleContext(b);
7132
- if (aHasAtRule && !bHasAtRule) return -1;
7133
- if (!aHasAtRule && bHasAtRule) return 1;
7134
- return 0;
7135
- });
7312
+ function dimensionToContainerCondition(dimension, lowerBound, upperBound) {
7313
+ if (lowerBound && upperBound) {
7314
+ const lowerOp = lowerBound.inclusive ? "<=" : "<";
7315
+ const upperOp = upperBound.inclusive ? "<=" : "<";
7316
+ return `(${lowerBound.value} ${lowerOp} ${dimension} ${upperOp} ${upperBound.value})`;
7317
+ } else if (upperBound) return `(${dimension} ${upperBound.inclusive ? "<=" : "<"} ${upperBound.value})`;
7318
+ else if (lowerBound) return `(${dimension} ${lowerBound.inclusive ? ">=" : ">"} ${lowerBound.value})`;
7319
+ return "(width)";
7136
7320
  }
7137
7321
  /**
7138
- * Expand ORs in a single entry's exclusive condition
7322
+ * Convert supports condition to parsed structure
7139
7323
  */
7140
- function expandExclusiveConditionOrs(entry) {
7141
- let orBranches = collectOrBranches(entry.exclusiveCondition);
7142
- if (orBranches.length <= 1) return [entry];
7143
- orBranches = sortOrBranchesForExpansion(orBranches);
7144
- const result = [];
7145
- const priorBranches = [];
7146
- for (let i = 0; i < orBranches.length; i++) {
7147
- const branch = orBranches[i];
7148
- let exclusiveBranch = branch;
7149
- for (const prior of priorBranches) exclusiveBranch = and(exclusiveBranch, not(prior));
7150
- const simplified = simplifyCondition(exclusiveBranch);
7151
- if (simplified.kind === "false") {
7152
- priorBranches.push(branch);
7153
- continue;
7154
- }
7155
- result.push({
7156
- ...entry,
7157
- stateKey: `${entry.stateKey}[or:${i}]`,
7158
- exclusiveCondition: simplified
7159
- });
7160
- priorBranches.push(branch);
7161
- }
7162
- return result;
7324
+ function supportsToParsed(state) {
7325
+ return {
7326
+ subtype: state.subtype,
7327
+ condition: state.condition,
7328
+ negated: state.negated ?? false
7329
+ };
7163
7330
  }
7164
- //#endregion
7165
- //#region src/pipeline/materialize-contradictions.ts
7166
7331
  /**
7167
- * Generic deduplication by a key extraction function.
7168
- * Preserves insertion order, keeping the first occurrence of each key.
7332
+ * Collect all modifier and pseudo conditions from a variant as a flat array.
7169
7333
  */
7170
- function dedupeByKey(items, getKey) {
7171
- const seen = /* @__PURE__ */ new Set();
7172
- const result = [];
7173
- for (const item of items) {
7174
- const key = getKey(item);
7175
- if (!seen.has(key)) {
7176
- seen.add(key);
7177
- result.push(item);
7178
- }
7179
- }
7180
- return result;
7181
- }
7182
- function dedupeMediaConditions(conditions) {
7183
- return dedupeByKey(conditions, (c) => `${c.subtype}|${c.condition}|${c.negated}`);
7184
- }
7185
- function dedupeContainerConditions(conditions) {
7186
- return dedupeByKey(conditions, (c) => `${c.name ?? ""}|${c.condition}|${c.negated}`);
7187
- }
7188
- function dedupeSupportsConditions(conditions) {
7189
- return dedupeByKey(conditions, (c) => `${c.subtype}|${c.condition}|${c.negated}`);
7334
+ function collectSelectorConditions(variant) {
7335
+ return [...variant.modifierConditions, ...variant.pseudoConditions];
7190
7336
  }
7191
7337
  /**
7192
- * Check if supports conditions contain contradictions
7193
- * e.g., @supports(display: grid) AND NOT @supports(display: grid)
7338
+ * Convert an inner condition tree into a single SelectorVariant with
7339
+ * one SelectorGroup whose branches represent the inner OR alternatives.
7340
+ * Shared by @root() and @own().
7341
+ *
7342
+ * Both positive and negated cases produce one variant with one group.
7343
+ * Negation simply sets the `negated` flag, which swaps :is() for :not()
7344
+ * in the final CSS output — no De Morgan transformation is needed.
7345
+ *
7346
+ * This mirrors parentConditionToVariants: OR branches are kept inside
7347
+ * a single group and rendered as comma-separated arguments in
7348
+ * :is()/:not(), e.g. :root:is([a], [b]) or [el]:not([a], [b]).
7194
7349
  */
7195
- function hasSupportsContradiction(conditions) {
7196
- const conditionMap = /* @__PURE__ */ new Map();
7197
- for (const cond of conditions) {
7198
- const key = `${cond.subtype}|${cond.condition}`;
7199
- const existing = conditionMap.get(key);
7200
- if (existing !== void 0 && existing !== !cond.negated) return true;
7201
- conditionMap.set(key, !cond.negated);
7350
+ function innerConditionToVariants(innerCondition, negated, target) {
7351
+ const innerCSS = conditionToCSS(innerCondition);
7352
+ if (innerCSS.isImpossible || innerCSS.variants.length === 0) return {
7353
+ variants: [],
7354
+ isImpossible: true
7355
+ };
7356
+ const branches = [];
7357
+ for (const innerVariant of innerCSS.variants) {
7358
+ const conditions = collectSelectorConditions(innerVariant);
7359
+ if (conditions.length > 0) branches.push(conditions);
7202
7360
  }
7203
- return false;
7361
+ if (branches.length === 0) return {
7362
+ variants: [emptyVariant()],
7363
+ isImpossible: false
7364
+ };
7365
+ const v = emptyVariant();
7366
+ v[target].push({
7367
+ branches,
7368
+ negated
7369
+ });
7370
+ return {
7371
+ variants: [v],
7372
+ isImpossible: false
7373
+ };
7204
7374
  }
7205
7375
  /**
7206
- * Check if a set of media conditions contains contradictions
7207
- * e.g., (prefers-color-scheme: light) AND NOT (prefers-color-scheme: light)
7208
- * or (width >= 900px) AND (width < 600px)
7376
+ * Convert a @parent() inner condition into a single SelectorVariant with
7377
+ * one ParentGroup whose branches represent the inner OR alternatives.
7209
7378
  *
7210
- * Uses parsed media conditions for efficient analysis without regex parsing.
7379
+ * Both positive and negated cases produce one variant with one group.
7380
+ * Negation simply sets the `negated` flag, which swaps :is() for :not()
7381
+ * in the final CSS output — no structural transformation is needed.
7211
7382
  */
7212
- function hasMediaContradiction(conditions) {
7213
- const featureConditions = /* @__PURE__ */ new Map();
7214
- const typeConditions = /* @__PURE__ */ new Map();
7215
- const dimensionConditions = /* @__PURE__ */ new Map();
7216
- const dimensionsByDim = /* @__PURE__ */ new Map();
7217
- for (const cond of conditions) if (cond.subtype === "type") {
7218
- const key = cond.mediaType || "all";
7219
- const existing = typeConditions.get(key);
7220
- if (existing !== void 0 && existing !== !cond.negated) return true;
7221
- typeConditions.set(key, !cond.negated);
7222
- } else if (cond.subtype === "feature") {
7223
- const key = cond.condition;
7224
- const existing = featureConditions.get(key);
7225
- if (existing !== void 0 && existing !== !cond.negated) return true;
7226
- featureConditions.set(key, !cond.negated);
7227
- } else if (cond.subtype === "dimension") {
7228
- const condKey = cond.condition;
7229
- const existing = dimensionConditions.get(condKey);
7230
- if (existing !== void 0 && existing !== !cond.negated) return true;
7231
- dimensionConditions.set(condKey, !cond.negated);
7232
- if (!cond.negated) {
7233
- const dim = cond.dimension || "width";
7234
- let bounds = dimensionsByDim.get(dim);
7235
- if (!bounds) {
7236
- bounds = {
7237
- lowerBound: null,
7238
- upperBound: null
7239
- };
7240
- dimensionsByDim.set(dim, bounds);
7241
- }
7242
- if (cond.lowerBound?.valueNumeric != null) {
7243
- const value = cond.lowerBound.valueNumeric;
7244
- if (bounds.lowerBound === null || value > bounds.lowerBound) bounds.lowerBound = value;
7245
- }
7246
- if (cond.upperBound?.valueNumeric != null) {
7247
- const value = cond.upperBound.valueNumeric;
7248
- if (bounds.upperBound === null || value < bounds.upperBound) bounds.upperBound = value;
7249
- }
7250
- if (bounds.lowerBound !== null && bounds.upperBound !== null && bounds.lowerBound >= bounds.upperBound) return true;
7251
- }
7252
- }
7253
- return false;
7254
- }
7255
- /**
7256
- * Check if container conditions contain contradictions in style queries
7257
- * e.g., style(--variant: danger) and style(--variant: success) together
7258
- * Same property with different values = always false
7259
- *
7260
- * Uses parsed container conditions for efficient analysis without regex parsing.
7261
- */
7262
- function hasContainerStyleContradiction(conditions) {
7263
- const styleQueries = /* @__PURE__ */ new Map();
7264
- for (const cond of conditions) {
7265
- if (cond.subtype !== "style" || !cond.property) continue;
7266
- const property = cond.property;
7267
- const value = cond.propertyValue;
7268
- if (!styleQueries.has(property)) styleQueries.set(property, {
7269
- hasExistence: false,
7270
- values: /* @__PURE__ */ new Set(),
7271
- hasNegatedExistence: false
7272
- });
7273
- const entry = styleQueries.get(property);
7274
- if (cond.negated) {
7275
- if (value === void 0) entry.hasNegatedExistence = true;
7276
- } else if (value === void 0) entry.hasExistence = true;
7277
- else entry.values.add(value);
7278
- }
7279
- for (const [, entry] of styleQueries) {
7280
- if (entry.hasExistence && entry.hasNegatedExistence) return true;
7281
- if (entry.values.size > 1) return true;
7282
- if (entry.hasNegatedExistence && entry.values.size > 0) return true;
7283
- }
7284
- return false;
7285
- }
7286
- //#endregion
7287
- //#region src/pipeline/materialize.ts
7288
- /**
7289
- * CSS Materialization
7290
- *
7291
- * Converts condition trees into CSS selectors and at-rules.
7292
- * This is the final stage that produces actual CSS output.
7293
- */
7294
- const conditionCache = new Lru(3e3);
7295
- /**
7296
- * Convert a condition tree to CSS components
7297
- */
7298
- function conditionToCSS(node) {
7299
- const key = getConditionUniqueId(node);
7300
- const cached = conditionCache.get(key);
7301
- if (cached) return cached;
7302
- const result = conditionToCSSInner(node);
7303
- conditionCache.set(key, result);
7304
- return result;
7305
- }
7306
- function emptyVariant() {
7307
- return {
7308
- modifierConditions: [],
7309
- pseudoConditions: [],
7310
- selectorGroups: [],
7311
- ownGroups: [],
7312
- mediaConditions: [],
7313
- containerConditions: [],
7314
- supportsConditions: [],
7315
- rootGroups: [],
7316
- parentGroups: [],
7317
- startingStyle: false
7383
+ function parentConditionToVariants(innerCondition, negated, direct) {
7384
+ const innerCSS = conditionToCSS(innerCondition);
7385
+ if (innerCSS.isImpossible || innerCSS.variants.length === 0) return {
7386
+ variants: [],
7387
+ isImpossible: true
7318
7388
  };
7319
- }
7320
- function conditionToCSSInner(node) {
7321
- if (node.kind === "true") return {
7389
+ const branches = [];
7390
+ for (const innerVariant of innerCSS.variants) {
7391
+ const conditions = collectSelectorConditions(innerVariant);
7392
+ if (conditions.length > 0) branches.push(conditions);
7393
+ }
7394
+ if (branches.length === 0) return {
7322
7395
  variants: [emptyVariant()],
7323
7396
  isImpossible: false
7324
7397
  };
7325
- if (node.kind === "false") return {
7326
- variants: [],
7327
- isImpossible: true
7328
- };
7329
- if (node.kind === "state") return stateToCSS(node);
7330
- if (node.kind === "compound") if (node.operator === "AND") return andToCSS(node.children);
7331
- else return orToCSS(node.children);
7398
+ const v = emptyVariant();
7399
+ v.parentGroups.push({
7400
+ branches,
7401
+ direct,
7402
+ negated
7403
+ });
7332
7404
  return {
7333
- variants: [emptyVariant()],
7405
+ variants: [v],
7334
7406
  isImpossible: false
7335
7407
  };
7336
7408
  }
7337
7409
  /**
7338
- * Convert a state condition to CSS
7410
+ * Sort key for canonical condition output within selectors.
7411
+ *
7412
+ * Priority order:
7413
+ * 0: Boolean attribute selectors ([data-hovered])
7414
+ * 1: Value attribute selectors ([data-size="small"])
7415
+ * 2: Negated boolean attributes (:not([data-disabled]))
7416
+ * 3: Negated value attributes (:not([data-size="small"]))
7417
+ * 4: Pseudo-classes (:hover, :focus)
7418
+ * 5: Negated pseudo-classes (:not(:disabled))
7419
+ *
7420
+ * Secondary sort: alphabetical by attribute name / pseudo string.
7339
7421
  */
7340
- function stateToCSS(state) {
7341
- switch (state.type) {
7342
- case "media": return {
7343
- variants: mediaToParsed(state).map((mediaCond) => {
7344
- const v = emptyVariant();
7345
- v.mediaConditions.push(mediaCond);
7346
- return v;
7347
- }),
7348
- isImpossible: false
7349
- };
7350
- case "root": return innerConditionToVariants(state.innerCondition, state.negated ?? false, "rootGroups");
7351
- case "parent": return parentConditionToVariants(state.innerCondition, state.negated ?? false, state.direct);
7352
- case "own": return innerConditionToVariants(state.innerCondition, state.negated ?? false, "ownGroups");
7353
- case "modifier": {
7354
- const v = emptyVariant();
7355
- v.modifierConditions.push(modifierToParsed(state));
7356
- return {
7357
- variants: [v],
7358
- isImpossible: false
7359
- };
7360
- }
7361
- case "pseudo": {
7362
- const v = emptyVariant();
7363
- v.pseudoConditions.push(pseudoToParsed(state));
7364
- return {
7365
- variants: [v],
7366
- isImpossible: false
7367
- };
7368
- }
7369
- case "container": {
7370
- const v = emptyVariant();
7371
- v.containerConditions.push(containerToParsed(state));
7372
- return {
7373
- variants: [v],
7374
- isImpossible: false
7375
- };
7376
- }
7377
- case "supports": {
7378
- const v = emptyVariant();
7379
- v.supportsConditions.push(supportsToParsed(state));
7380
- return {
7381
- variants: [v],
7382
- isImpossible: false
7383
- };
7384
- }
7385
- case "starting": {
7386
- const v = emptyVariant();
7387
- v.startingStyle = !state.negated;
7388
- return {
7389
- variants: [v],
7390
- isImpossible: false
7391
- };
7392
- }
7422
+ function conditionSortKey(cond) {
7423
+ if ("attribute" in cond) {
7424
+ const hasValue = cond.value !== void 0 ? 1 : 0;
7425
+ return `${(cond.negated ? 2 : 0) + hasValue}|${cond.attribute}|${cond.value ?? ""}`;
7393
7426
  }
7427
+ return `${cond.negated ? 5 : 4}|${cond.pseudo}`;
7428
+ }
7429
+ function sortConditions(conditions) {
7430
+ return conditions.toSorted((a, b) => conditionSortKey(a).localeCompare(conditionSortKey(b)));
7431
+ }
7432
+ function branchToCSS(branch) {
7433
+ let parts = "";
7434
+ for (const cond of sortConditions(branch)) parts += selectorConditionToCSS(cond);
7435
+ return parts;
7394
7436
  }
7395
7437
  /**
7396
- * Convert modifier condition to parsed structure
7438
+ * Wrap serialized selector arguments in :is() or :not().
7439
+ * Arguments are sorted for canonical output.
7397
7440
  */
7398
- function modifierToParsed(state) {
7399
- return {
7400
- attribute: state.attribute,
7401
- value: state.value,
7402
- operator: state.operator,
7403
- negated: state.negated ?? false
7404
- };
7441
+ function wrapInIsOrNot(args, negated) {
7442
+ return `${negated ? ":not" : ":is"}(${args.sort().join(", ")})`;
7405
7443
  }
7406
7444
  /**
7407
- * Convert parsed modifier to CSS selector string (for final output)
7445
+ * Convert a selector group to a CSS selector fragment.
7446
+ *
7447
+ * Single-branch groups are unwrapped (no :is() wrapper).
7448
+ * Multi-branch groups use :is() or :not().
7449
+ * Negation swaps :is() for :not().
7408
7450
  */
7409
- function modifierToCSS(mod) {
7410
- let selector;
7411
- if (mod.value !== void 0) {
7412
- const op = mod.operator || "=";
7413
- selector = `[${mod.attribute}${op}"${mod.value}"]`;
7414
- } else selector = `[${mod.attribute}]`;
7415
- if (mod.negated) return `:not(${selector})`;
7416
- return selector;
7451
+ function selectorGroupToCSS(group) {
7452
+ if (group.branches.length === 0) return "";
7453
+ if (group.branches.length === 1) {
7454
+ const parts = branchToCSS(group.branches[0]);
7455
+ if (group.negated) return `:not(${parts})`;
7456
+ return parts;
7457
+ }
7458
+ return wrapInIsOrNot(group.branches.map(branchToCSS), group.negated);
7417
7459
  }
7418
7460
  /**
7419
- * Convert pseudo condition to parsed structure
7461
+ * Collect facts about modifier conditions for subsumption analysis.
7462
+ * Tracks negated boolean attrs (:not([attr])) and positive exact values ([attr="X"]).
7420
7463
  */
7421
- function pseudoToParsed(state) {
7464
+ function collectSubsumptionFacts(modifiers) {
7465
+ const negatedBooleanAttrs = /* @__PURE__ */ new Set();
7466
+ const positiveExactValuesByAttr = /* @__PURE__ */ new Map();
7467
+ for (const mod of modifiers) {
7468
+ if (mod.negated && mod.value === void 0) negatedBooleanAttrs.add(mod.attribute);
7469
+ if (!mod.negated && mod.value !== void 0 && (mod.operator ?? "=") === "=") {
7470
+ let vals = positiveExactValuesByAttr.get(mod.attribute);
7471
+ if (!vals) {
7472
+ vals = /* @__PURE__ */ new Set();
7473
+ positiveExactValuesByAttr.set(mod.attribute, vals);
7474
+ }
7475
+ vals.add(mod.value);
7476
+ }
7477
+ }
7422
7478
  return {
7423
- pseudo: state.pseudo,
7424
- negated: state.negated ?? false
7479
+ negatedBooleanAttrs,
7480
+ positiveExactValuesByAttr
7425
7481
  };
7426
7482
  }
7427
7483
  /**
7428
- * Convert parsed pseudo to CSS selector string (for final output).
7429
- *
7430
- * :not() is normalized to negated :is() at parse time, so pseudo.pseudo
7431
- * never starts with ':not(' here. When negated:
7432
- * - :is(X) → :not(X) (unwrap :is)
7433
- * - :where(X) → :not(X) (unwrap :where)
7434
- * - :has(X) → :not(:has(X))
7435
- * - other → :not(other)
7484
+ * Check if a negated-value modifier is subsumed by stronger facts:
7485
+ * - :not([attr]) subsumes :not([attr="val"])
7486
+ * - [attr="X"] implies :not([attr="Y"]) is redundant (single exact value)
7436
7487
  *
7437
- * When not negated, single-argument :is()/:where() is unwrapped when the
7438
- * inner content is a simple compound selector that can safely append to
7439
- * the base selector (this happens after double-negation of :not()).
7488
+ * Only applies to exact-match (=) operators; substring operators don't
7489
+ * imply exclusivity between values.
7440
7490
  */
7441
- function pseudoToCSS(pseudo) {
7442
- const p = pseudo.pseudo;
7443
- if (pseudo.negated) {
7444
- if (p.startsWith(":is(") || p.startsWith(":where(")) return `:not(${p.slice(p.indexOf("(") + 1, -1)})`;
7445
- return `:not(${p})`;
7491
+ function isSubsumedNegatedModifier(mod, facts) {
7492
+ if (!mod.negated || mod.value === void 0) return false;
7493
+ if (facts.negatedBooleanAttrs.has(mod.attribute)) return true;
7494
+ if ((mod.operator ?? "=") === "=") {
7495
+ const posVals = facts.positiveExactValuesByAttr.get(mod.attribute);
7496
+ if (posVals && posVals.size === 1 && !posVals.has(mod.value)) return true;
7446
7497
  }
7447
- if ((p.startsWith(":is(") || p.startsWith(":where(")) && !p.includes(",")) {
7448
- const inner = p.slice(p.indexOf("(") + 1, -1);
7449
- const ch = inner[0];
7450
- if ((ch === ":" || ch === "." || ch === "[" || ch === "#") && !/\s/.test(inner)) return inner;
7498
+ return false;
7499
+ }
7500
+ /**
7501
+ * Remove redundant single-condition groups that are subsumed by stronger
7502
+ * groups on the same attribute. O(n) — only inspects single-branch,
7503
+ * single-condition groups.
7504
+ */
7505
+ function optimizeGroups(groups) {
7506
+ if (groups.length <= 1) return groups;
7507
+ const seen = /* @__PURE__ */ new Set();
7508
+ const result = [];
7509
+ for (const g of groups) {
7510
+ const key = getSelectorGroupKey(g);
7511
+ if (!seen.has(key)) {
7512
+ seen.add(key);
7513
+ result.push(g);
7514
+ }
7451
7515
  }
7452
- return p;
7516
+ if (result.length <= 1) return result;
7517
+ const effectiveModifiers = [];
7518
+ for (const g of result) {
7519
+ if (g.branches.length !== 1 || g.branches[0].length !== 1) continue;
7520
+ const cond = g.branches[0][0];
7521
+ if (!("attribute" in cond)) continue;
7522
+ effectiveModifiers.push({
7523
+ ...cond,
7524
+ negated: g.negated !== cond.negated
7525
+ });
7526
+ }
7527
+ const facts = collectSubsumptionFacts(effectiveModifiers);
7528
+ if (facts.negatedBooleanAttrs.size === 0 && facts.positiveExactValuesByAttr.size === 0) return result;
7529
+ return result.filter((g) => {
7530
+ if (g.branches.length !== 1 || g.branches[0].length !== 1) return true;
7531
+ const cond = g.branches[0][0];
7532
+ if (!("attribute" in cond) || !g.negated || cond.negated || cond.value === void 0) return true;
7533
+ return !isSubsumedNegatedModifier({
7534
+ ...cond,
7535
+ negated: true
7536
+ }, facts);
7537
+ });
7453
7538
  }
7454
7539
  /**
7455
- * Convert media condition to parsed structure(s)
7456
- * Returns an array because negated ranges produce OR branches (two separate conditions)
7540
+ * Convert root groups to CSS selector prefix (for final output)
7457
7541
  */
7458
- function mediaToParsed(state) {
7459
- if (state.subtype === "type") {
7460
- const mediaType = state.mediaType || "all";
7461
- return [{
7462
- subtype: "type",
7463
- negated: state.negated ?? false,
7464
- condition: mediaType,
7465
- mediaType: state.mediaType
7466
- }];
7467
- } else if (state.subtype === "feature") {
7468
- let condition;
7469
- if (state.featureValue) condition = `(${state.feature}: ${state.featureValue})`;
7470
- else condition = `(${state.feature})`;
7471
- return [{
7472
- subtype: "feature",
7473
- negated: state.negated ?? false,
7474
- condition,
7475
- feature: state.feature,
7476
- featureValue: state.featureValue
7477
- }];
7478
- } else return dimensionToMediaParsed(state.dimension || "width", state.lowerBound, state.upperBound, state.negated ?? false);
7542
+ function rootGroupsToCSS(groups) {
7543
+ if (groups.length === 0) return void 0;
7544
+ const optimized = optimizeGroups(groups);
7545
+ if (optimized.length === 0) return void 0;
7546
+ let prefix = ":root";
7547
+ for (const group of optimized) prefix += selectorGroupToCSS(group);
7548
+ return prefix;
7479
7549
  }
7480
7550
  /**
7481
- * Convert dimension bounds to parsed media condition(s)
7482
- * Uses CSS Media Queries Level 4 `not (condition)` syntax for negation.
7551
+ * Convert parent groups to CSS selector fragments (for final output).
7552
+ * Each group produces its own :is()/:not() wrapper with a combinator
7553
+ * suffix (` *` or ` > *`) appended to each branch.
7483
7554
  */
7484
- function dimensionToMediaParsed(dimension, lowerBound, upperBound, negated) {
7485
- let condition;
7486
- if (lowerBound && upperBound) {
7487
- const lowerOp = lowerBound.inclusive ? "<=" : "<";
7488
- const upperOp = upperBound.inclusive ? "<=" : "<";
7489
- condition = `(${lowerBound.value} ${lowerOp} ${dimension} ${upperOp} ${upperBound.value})`;
7490
- } else if (upperBound) condition = `(${dimension} ${upperBound.inclusive ? "<=" : "<"} ${upperBound.value})`;
7491
- else if (lowerBound) condition = `(${dimension} ${lowerBound.inclusive ? ">=" : ">"} ${lowerBound.value})`;
7492
- else condition = `(${dimension})`;
7493
- return [{
7494
- subtype: "dimension",
7495
- negated: negated ?? false,
7496
- condition,
7497
- dimension,
7498
- lowerBound,
7499
- upperBound
7500
- }];
7555
+ function parentGroupsToCSS(groups) {
7556
+ let result = "";
7557
+ for (const group of groups) {
7558
+ const combinator = group.direct ? " > *" : " *";
7559
+ const args = group.branches.map((branch) => branchToCSS(branch) + combinator);
7560
+ result += wrapInIsOrNot(args, group.negated);
7561
+ }
7562
+ return result;
7501
7563
  }
7502
7564
  /**
7503
- * Convert container condition to parsed structure
7504
- * This enables structured analysis for contradiction detection and condition combining
7565
+ * Convert a modifier or pseudo condition to a CSS selector fragment
7505
7566
  */
7506
- function containerToParsed(state) {
7507
- let condition;
7508
- if (state.subtype === "style") if (state.propertyValue) condition = `style(--${state.property}: ${state.propertyValue})`;
7509
- else condition = `style(--${state.property})`;
7510
- else if (state.subtype === "raw") condition = state.rawCondition;
7511
- else condition = dimensionToContainerCondition(state.dimension || "width", state.lowerBound, state.upperBound);
7512
- return {
7513
- name: state.containerName,
7514
- condition,
7515
- negated: state.negated ?? false,
7516
- subtype: state.subtype,
7517
- property: state.property,
7518
- propertyValue: state.propertyValue
7519
- };
7567
+ function selectorConditionToCSS(cond) {
7568
+ if ("attribute" in cond) return modifierToCSS(cond);
7569
+ return pseudoToCSS(cond);
7520
7570
  }
7521
7571
  /**
7522
- * Convert dimension bounds to container query condition (single string)
7523
- * Container queries support "not (condition)", so no need to invert manually
7572
+ * Get unique key for a modifier condition
7524
7573
  */
7525
- function dimensionToContainerCondition(dimension, lowerBound, upperBound) {
7526
- if (lowerBound && upperBound) {
7527
- const lowerOp = lowerBound.inclusive ? "<=" : "<";
7528
- const upperOp = upperBound.inclusive ? "<=" : "<";
7529
- return `(${lowerBound.value} ${lowerOp} ${dimension} ${upperOp} ${upperBound.value})`;
7530
- } else if (upperBound) return `(${dimension} ${upperBound.inclusive ? "<=" : "<"} ${upperBound.value})`;
7531
- else if (lowerBound) return `(${dimension} ${lowerBound.inclusive ? ">=" : ">"} ${lowerBound.value})`;
7532
- return "(width)";
7574
+ function getModifierKey(mod) {
7575
+ const base = mod.value ? `${mod.attribute}${mod.operator || "="}${mod.value}` : mod.attribute;
7576
+ return mod.negated ? `!${base}` : base;
7533
7577
  }
7534
7578
  /**
7535
- * Convert supports condition to parsed structure
7579
+ * Get unique key for a pseudo condition
7536
7580
  */
7537
- function supportsToParsed(state) {
7538
- return {
7539
- subtype: state.subtype,
7540
- condition: state.condition,
7541
- negated: state.negated ?? false
7542
- };
7581
+ function getPseudoKey(pseudo) {
7582
+ return pseudo.negated ? `!${pseudo.pseudo}` : pseudo.pseudo;
7543
7583
  }
7544
7584
  /**
7545
- * Collect all modifier and pseudo conditions from a variant as a flat array.
7585
+ * Get unique key for any selector condition (modifier or pseudo)
7546
7586
  */
7547
- function collectSelectorConditions(variant) {
7548
- return [...variant.modifierConditions, ...variant.pseudoConditions];
7587
+ function getSelectorConditionKey(cond) {
7588
+ return "attribute" in cond ? `mod:${getModifierKey(cond)}` : `pseudo:${getPseudoKey(cond)}`;
7549
7589
  }
7550
7590
  /**
7551
- * Convert an inner condition tree into a single SelectorVariant with
7552
- * one SelectorGroup whose branches represent the inner OR alternatives.
7553
- * Shared by @root() and @own().
7554
- *
7555
- * Both positive and negated cases produce one variant with one group.
7556
- * Negation simply sets the `negated` flag, which swaps :is() for :not()
7557
- * in the final CSS output — no De Morgan transformation is needed.
7558
- *
7559
- * This mirrors parentConditionToVariants: OR branches are kept inside
7560
- * a single group and rendered as comma-separated arguments in
7561
- * :is()/:not(), e.g. :root:is([a], [b]) or [el]:not([a], [b]).
7591
+ * Deduplicate selector conditions (modifiers or pseudos).
7592
+ * Shared by root, parent, and own conditions.
7562
7593
  */
7563
- function innerConditionToVariants(innerCondition, negated, target) {
7564
- const innerCSS = conditionToCSS(innerCondition);
7565
- if (innerCSS.isImpossible || innerCSS.variants.length === 0) return {
7566
- variants: [],
7567
- isImpossible: true
7568
- };
7569
- const branches = [];
7570
- for (const innerVariant of innerCSS.variants) {
7571
- const conditions = collectSelectorConditions(innerVariant);
7572
- if (conditions.length > 0) branches.push(conditions);
7594
+ function dedupeSelectorConditions(conditions) {
7595
+ const seen = /* @__PURE__ */ new Set();
7596
+ const result = [];
7597
+ for (const c of conditions) {
7598
+ const key = getSelectorConditionKey(c);
7599
+ if (!seen.has(key)) {
7600
+ seen.add(key);
7601
+ result.push(c);
7602
+ }
7573
7603
  }
7574
- if (branches.length === 0) return {
7575
- variants: [emptyVariant()],
7576
- isImpossible: false
7577
- };
7578
- const v = emptyVariant();
7579
- v[target].push({
7580
- branches,
7581
- negated
7604
+ const facts = collectSubsumptionFacts(result.filter((c) => "attribute" in c));
7605
+ if (facts.negatedBooleanAttrs.size === 0 && facts.positiveExactValuesByAttr.size === 0) return result;
7606
+ return result.filter((c) => {
7607
+ if (!("attribute" in c)) return true;
7608
+ if (isSubsumedNegatedModifier(c, facts)) return false;
7609
+ if (!c.negated && c.value === void 0 && facts.positiveExactValuesByAttr.has(c.attribute)) return false;
7610
+ return true;
7582
7611
  });
7583
- return {
7584
- variants: [v],
7585
- isImpossible: false
7586
- };
7587
7612
  }
7588
7613
  /**
7589
- * Convert a @parent() inner condition into a single SelectorVariant with
7590
- * one ParentGroup whose branches represent the inner OR alternatives.
7591
- *
7592
- * Both positive and negated cases produce one variant with one group.
7593
- * Negation simply sets the `negated` flag, which swaps :is() for :not()
7594
- * in the final CSS output — no structural transformation is needed.
7614
+ * Check for modifier contradiction: same attribute with opposite negation
7595
7615
  */
7596
- function parentConditionToVariants(innerCondition, negated, direct) {
7597
- const innerCSS = conditionToCSS(innerCondition);
7598
- if (innerCSS.isImpossible || innerCSS.variants.length === 0) return {
7599
- variants: [],
7600
- isImpossible: true
7601
- };
7602
- const branches = [];
7603
- for (const innerVariant of innerCSS.variants) {
7604
- const conditions = collectSelectorConditions(innerVariant);
7605
- if (conditions.length > 0) branches.push(conditions);
7606
- }
7607
- if (branches.length === 0) return {
7608
- variants: [emptyVariant()],
7609
- isImpossible: false
7610
- };
7611
- const v = emptyVariant();
7612
- v.parentGroups.push({
7613
- branches,
7614
- direct,
7615
- negated
7616
- });
7617
- return {
7618
- variants: [v],
7619
- isImpossible: false
7620
- };
7621
- }
7622
- /**
7623
- * Sort key for canonical condition output within selectors.
7624
- *
7625
- * Priority order:
7626
- * 0: Boolean attribute selectors ([data-hovered])
7627
- * 1: Value attribute selectors ([data-size="small"])
7628
- * 2: Negated boolean attributes (:not([data-disabled]))
7629
- * 3: Negated value attributes (:not([data-size="small"]))
7630
- * 4: Pseudo-classes (:hover, :focus)
7631
- * 5: Negated pseudo-classes (:not(:disabled))
7632
- *
7633
- * Secondary sort: alphabetical by attribute name / pseudo string.
7634
- */
7635
- function conditionSortKey(cond) {
7636
- if ("attribute" in cond) {
7637
- const hasValue = cond.value !== void 0 ? 1 : 0;
7638
- return `${(cond.negated ? 2 : 0) + hasValue}|${cond.attribute}|${cond.value ?? ""}`;
7639
- }
7640
- return `${cond.negated ? 5 : 4}|${cond.pseudo}`;
7641
- }
7642
- function sortConditions(conditions) {
7643
- return conditions.toSorted((a, b) => conditionSortKey(a).localeCompare(conditionSortKey(b)));
7644
- }
7645
- function branchToCSS(branch) {
7646
- let parts = "";
7647
- for (const cond of sortConditions(branch)) parts += selectorConditionToCSS(cond);
7648
- return parts;
7649
- }
7650
- /**
7651
- * Wrap serialized selector arguments in :is() or :not().
7652
- * Arguments are sorted for canonical output.
7653
- */
7654
- function wrapInIsOrNot(args, negated) {
7655
- return `${negated ? ":not" : ":is"}(${args.sort().join(", ")})`;
7656
- }
7657
- /**
7658
- * Convert a selector group to a CSS selector fragment.
7659
- *
7660
- * Single-branch groups are unwrapped (no :is() wrapper).
7661
- * Multi-branch groups use :is() or :not().
7662
- * Negation swaps :is() for :not().
7663
- */
7664
- function selectorGroupToCSS(group) {
7665
- if (group.branches.length === 0) return "";
7666
- if (group.branches.length === 1) {
7667
- const parts = branchToCSS(group.branches[0]);
7668
- if (group.negated) return `:not(${parts})`;
7669
- return parts;
7670
- }
7671
- return wrapInIsOrNot(group.branches.map(branchToCSS), group.negated);
7672
- }
7673
- /**
7674
- * Collect facts about modifier conditions for subsumption analysis.
7675
- * Tracks negated boolean attrs (:not([attr])) and positive exact values ([attr="X"]).
7676
- */
7677
- function collectSubsumptionFacts(modifiers) {
7678
- const negatedBooleanAttrs = /* @__PURE__ */ new Set();
7679
- const positiveExactValuesByAttr = /* @__PURE__ */ new Map();
7680
- for (const mod of modifiers) {
7681
- if (mod.negated && mod.value === void 0) negatedBooleanAttrs.add(mod.attribute);
7682
- if (!mod.negated && mod.value !== void 0 && (mod.operator ?? "=") === "=") {
7683
- let vals = positiveExactValuesByAttr.get(mod.attribute);
7684
- if (!vals) {
7685
- vals = /* @__PURE__ */ new Set();
7686
- positiveExactValuesByAttr.set(mod.attribute, vals);
7687
- }
7688
- vals.add(mod.value);
7689
- }
7690
- }
7691
- return {
7692
- negatedBooleanAttrs,
7693
- positiveExactValuesByAttr
7694
- };
7695
- }
7696
- /**
7697
- * Check if a negated-value modifier is subsumed by stronger facts:
7698
- * - :not([attr]) subsumes :not([attr="val"])
7699
- * - [attr="X"] implies :not([attr="Y"]) is redundant (single exact value)
7700
- *
7701
- * Only applies to exact-match (=) operators; substring operators don't
7702
- * imply exclusivity between values.
7703
- */
7704
- function isSubsumedNegatedModifier(mod, facts) {
7705
- if (!mod.negated || mod.value === void 0) return false;
7706
- if (facts.negatedBooleanAttrs.has(mod.attribute)) return true;
7707
- if ((mod.operator ?? "=") === "=") {
7708
- const posVals = facts.positiveExactValuesByAttr.get(mod.attribute);
7709
- if (posVals && posVals.size === 1 && !posVals.has(mod.value)) return true;
7710
- }
7711
- return false;
7712
- }
7713
- /**
7714
- * Remove redundant single-condition groups that are subsumed by stronger
7715
- * groups on the same attribute. O(n) — only inspects single-branch,
7716
- * single-condition groups.
7717
- */
7718
- function optimizeGroups(groups) {
7719
- if (groups.length <= 1) return groups;
7720
- const seen = /* @__PURE__ */ new Set();
7721
- const result = [];
7722
- for (const g of groups) {
7723
- const key = getSelectorGroupKey(g);
7724
- if (!seen.has(key)) {
7725
- seen.add(key);
7726
- result.push(g);
7727
- }
7728
- }
7729
- if (result.length <= 1) return result;
7730
- const effectiveModifiers = [];
7731
- for (const g of result) {
7732
- if (g.branches.length !== 1 || g.branches[0].length !== 1) continue;
7733
- const cond = g.branches[0][0];
7734
- if (!("attribute" in cond)) continue;
7735
- effectiveModifiers.push({
7736
- ...cond,
7737
- negated: g.negated !== cond.negated
7738
- });
7739
- }
7740
- const facts = collectSubsumptionFacts(effectiveModifiers);
7741
- if (facts.negatedBooleanAttrs.size === 0 && facts.positiveExactValuesByAttr.size === 0) return result;
7742
- return result.filter((g) => {
7743
- if (g.branches.length !== 1 || g.branches[0].length !== 1) return true;
7744
- const cond = g.branches[0][0];
7745
- if (!("attribute" in cond) || !g.negated || cond.negated || cond.value === void 0) return true;
7746
- return !isSubsumedNegatedModifier({
7747
- ...cond,
7748
- negated: true
7749
- }, facts);
7750
- });
7751
- }
7752
- /**
7753
- * Convert root groups to CSS selector prefix (for final output)
7754
- */
7755
- function rootGroupsToCSS(groups) {
7756
- if (groups.length === 0) return void 0;
7757
- const optimized = optimizeGroups(groups);
7758
- if (optimized.length === 0) return void 0;
7759
- let prefix = ":root";
7760
- for (const group of optimized) prefix += selectorGroupToCSS(group);
7761
- return prefix;
7762
- }
7763
- /**
7764
- * Convert parent groups to CSS selector fragments (for final output).
7765
- * Each group produces its own :is()/:not() wrapper with a combinator
7766
- * suffix (` *` or ` > *`) appended to each branch.
7767
- */
7768
- function parentGroupsToCSS(groups) {
7769
- let result = "";
7770
- for (const group of groups) {
7771
- const combinator = group.direct ? " > *" : " *";
7772
- const args = group.branches.map((branch) => branchToCSS(branch) + combinator);
7773
- result += wrapInIsOrNot(args, group.negated);
7774
- }
7775
- return result;
7776
- }
7777
- /**
7778
- * Convert a modifier or pseudo condition to a CSS selector fragment
7779
- */
7780
- function selectorConditionToCSS(cond) {
7781
- if ("attribute" in cond) return modifierToCSS(cond);
7782
- return pseudoToCSS(cond);
7783
- }
7784
- /**
7785
- * Get unique key for a modifier condition
7786
- */
7787
- function getModifierKey(mod) {
7788
- const base = mod.value ? `${mod.attribute}${mod.operator || "="}${mod.value}` : mod.attribute;
7789
- return mod.negated ? `!${base}` : base;
7790
- }
7791
- /**
7792
- * Get unique key for a pseudo condition
7793
- */
7794
- function getPseudoKey(pseudo) {
7795
- return pseudo.negated ? `!${pseudo.pseudo}` : pseudo.pseudo;
7796
- }
7797
- /**
7798
- * Get unique key for any selector condition (modifier or pseudo)
7799
- */
7800
- function getSelectorConditionKey(cond) {
7801
- return "attribute" in cond ? `mod:${getModifierKey(cond)}` : `pseudo:${getPseudoKey(cond)}`;
7802
- }
7803
- /**
7804
- * Deduplicate selector conditions (modifiers or pseudos).
7805
- * Shared by root, parent, and own conditions.
7806
- */
7807
- function dedupeSelectorConditions(conditions) {
7808
- const seen = /* @__PURE__ */ new Set();
7809
- const result = [];
7810
- for (const c of conditions) {
7811
- const key = getSelectorConditionKey(c);
7812
- if (!seen.has(key)) {
7813
- seen.add(key);
7814
- result.push(c);
7815
- }
7816
- }
7817
- const facts = collectSubsumptionFacts(result.filter((c) => "attribute" in c));
7818
- if (facts.negatedBooleanAttrs.size === 0 && facts.positiveExactValuesByAttr.size === 0) return result;
7819
- return result.filter((c) => {
7820
- if (!("attribute" in c)) return true;
7821
- if (isSubsumedNegatedModifier(c, facts)) return false;
7822
- if (!c.negated && c.value === void 0 && facts.positiveExactValuesByAttr.has(c.attribute)) return false;
7823
- return true;
7824
- });
7825
- }
7826
- /**
7827
- * Check for modifier contradiction: same attribute with opposite negation
7828
- */
7829
- function hasModifierContradiction(conditions) {
7830
- const byKey = /* @__PURE__ */ new Map();
7831
- for (const mod of conditions) {
7832
- const baseKey = mod.value ? `${mod.attribute}${mod.operator || "="}${mod.value}` : mod.attribute;
7833
- const existing = byKey.get(baseKey);
7834
- if (existing !== void 0 && existing !== !mod.negated) return true;
7835
- byKey.set(baseKey, !mod.negated);
7616
+ function hasModifierContradiction(conditions) {
7617
+ const byKey = /* @__PURE__ */ new Map();
7618
+ for (const mod of conditions) {
7619
+ const baseKey = mod.value ? `${mod.attribute}${mod.operator || "="}${mod.value}` : mod.attribute;
7620
+ const existing = byKey.get(baseKey);
7621
+ if (existing !== void 0 && existing !== !mod.negated) return true;
7622
+ byKey.set(baseKey, !mod.negated);
7836
7623
  }
7837
7624
  return false;
7838
7625
  }
@@ -8039,328 +7826,827 @@ function getParentGroupKey(g) {
8039
7826
  /**
8040
7827
  * Deduplicate variants
8041
7828
  *
8042
- * Removes:
8043
- * 1. Exact duplicates (same key)
8044
- * 2. Superset variants (more restrictive selectors that are redundant)
7829
+ * Removes:
7830
+ * 1. Exact duplicates (same key)
7831
+ * 2. Superset variants (more restrictive selectors that are redundant)
7832
+ */
7833
+ function dedupeVariants(variants) {
7834
+ if (variants.length <= 1) return variants;
7835
+ const seen = /* @__PURE__ */ new Set();
7836
+ const result = [];
7837
+ for (const v of variants) {
7838
+ const key = getVariantKey(v);
7839
+ if (!seen.has(key)) {
7840
+ seen.add(key);
7841
+ result.push(v);
7842
+ }
7843
+ }
7844
+ if (result.length <= 1) return result;
7845
+ result.sort((a, b) => variantConditionCount(a) - variantConditionCount(b));
7846
+ const filtered = [];
7847
+ for (const candidate of result) {
7848
+ let isRedundant = false;
7849
+ for (const kept of filtered) if (isVariantSuperset(candidate, kept)) {
7850
+ isRedundant = true;
7851
+ break;
7852
+ }
7853
+ if (!isRedundant) filtered.push(candidate);
7854
+ }
7855
+ return filtered;
7856
+ }
7857
+ /**
7858
+ * Combine AND conditions into CSS
7859
+ *
7860
+ * AND of conditions means cartesian product of variants:
7861
+ * (A1 | A2) & (B1 | B2) = A1&B1 | A1&B2 | A2&B1 | A2&B2
7862
+ *
7863
+ * Variants that result in contradictions (e.g., conflicting media rules)
7864
+ * are filtered out.
7865
+ */
7866
+ function andToCSS(children) {
7867
+ const exclusiveChildren = makeOrBranchesExclusive(children);
7868
+ let currentVariants = [emptyVariant()];
7869
+ for (const child of exclusiveChildren) {
7870
+ const childCSS = conditionToCSSInner(child);
7871
+ if (childCSS.isImpossible || childCSS.variants.length === 0) return {
7872
+ variants: [],
7873
+ isImpossible: true
7874
+ };
7875
+ const newVariants = [];
7876
+ for (const current of currentVariants) for (const childVariant of childCSS.variants) {
7877
+ const merged = mergeVariants(current, childVariant);
7878
+ if (merged !== null) newVariants.push(merged);
7879
+ }
7880
+ if (newVariants.length === 0) return {
7881
+ variants: [],
7882
+ isImpossible: true
7883
+ };
7884
+ currentVariants = dedupeVariants(newVariants);
7885
+ }
7886
+ return {
7887
+ variants: currentVariants,
7888
+ isImpossible: false
7889
+ };
7890
+ }
7891
+ /**
7892
+ * Make OR branches within AND children mutually exclusive.
7893
+ *
7894
+ * For an AND child that is OR(A, B), transforms it to OR(A, B & !A)
7895
+ * so that when andToCSS does a Cartesian product, the resulting
7896
+ * CSS variants don't overlap.
7897
+ *
7898
+ * Only transforms OR children whose branches actually produce
7899
+ * different at-rule contexts when materialized. This avoids
7900
+ * breaking cases where contradiction detection in the Cartesian
7901
+ * product naturally handles deduplication.
7902
+ */
7903
+ function makeOrBranchesExclusive(children) {
7904
+ return children.map((child) => {
7905
+ if (!isCompoundCondition(child) || child.operator !== "OR") return child;
7906
+ if (child.children.length <= 1) return child;
7907
+ if (!branchesProduceDifferentContexts(child.children)) return child;
7908
+ const exclusiveBranches = [];
7909
+ const priorBranches = [];
7910
+ for (const branch of child.children) {
7911
+ if (priorBranches.length === 0) exclusiveBranches.push(branch);
7912
+ else {
7913
+ let exclusive = branch;
7914
+ for (const prior of priorBranches) exclusive = and(exclusive, not(prior));
7915
+ const simplified = simplifyCondition(exclusive);
7916
+ if (simplified.kind !== "false") exclusiveBranches.push(simplified);
7917
+ }
7918
+ priorBranches.push(branch);
7919
+ }
7920
+ if (exclusiveBranches.length === 0) return child;
7921
+ if (exclusiveBranches.length === 1) return exclusiveBranches[0];
7922
+ return {
7923
+ kind: "compound",
7924
+ operator: "OR",
7925
+ children: exclusiveBranches
7926
+ };
7927
+ });
7928
+ }
7929
+ /**
7930
+ * Check if OR branches produce different at-rule contexts when
7931
+ * materialized. If so, the Cartesian product in andToCSS will
7932
+ * create overlapping CSS variants that need exclusive expansion.
7933
+ *
7934
+ * Exported so Stage 2a (`expandOrConditions` in `exclusive.ts`) can
7935
+ * reuse the same heuristic and skip OR expansion when every branch
7936
+ * lives in the same at-rule/root/parent/own context — pure-selector
7937
+ * ORs are better collapsed into `:is(...)` at materialization time
7938
+ * than expanded into mutually-exclusive `A | (B & !A) | …` cascades.
7939
+ */
7940
+ function branchesProduceDifferentContexts(branches) {
7941
+ const contextKeys = /* @__PURE__ */ new Set();
7942
+ for (const branch of branches) {
7943
+ const css = conditionToCSSInner(branch);
7944
+ if (css.isImpossible) continue;
7945
+ for (const v of css.variants) contextKeys.add(getVariantContextKey(v));
7946
+ }
7947
+ return contextKeys.size > 1;
7948
+ }
7949
+ /**
7950
+ * Combine OR conditions into CSS
7951
+ *
7952
+ * OR in CSS means multiple selector variants (DNF).
7953
+ * After deduplication, variants that differ only in their base
7954
+ * modifier/pseudo conditions are merged into :is() groups.
7955
+ *
7956
+ * Note: OR exclusivity is handled at the pipeline level (expandOrConditions),
7957
+ * so here we just collect all variants. Any remaining ORs in the condition
7958
+ * tree (e.g., from De Morgan expansion) are handled as simple alternatives.
7959
+ */
7960
+ /**
7961
+ * Serialize a single negated own-element selector leaf (modifier or pseudo)
7962
+ * to its *positive* selector string, for use inside a combined `:not(...)`.
7963
+ *
7964
+ * Returns `null` when the node is not a negated, own-element selector leaf
7965
+ * (e.g. positive, or a media/container/parent/root/own/supports/starting
7966
+ * wrapper, or a compound), which means it cannot participate in De Morgan
7967
+ * recombination.
7968
+ */
7969
+ function negatedSelectorLeafToPositiveSelector(node) {
7970
+ if (node.kind !== "state" || !node.negated) return null;
7971
+ if (node.type === "modifier") return modifierToCSS({
7972
+ attribute: node.attribute,
7973
+ value: node.value,
7974
+ operator: node.operator,
7975
+ negated: false
7976
+ });
7977
+ if (node.type === "pseudo") {
7978
+ const p = node.pseudo;
7979
+ if ((p.startsWith(":is(") || p.startsWith(":where(")) && !p.includes(",")) {
7980
+ const inner = p.slice(p.indexOf("(") + 1, -1);
7981
+ if (!/\s/.test(inner)) return inner;
7982
+ return `:is(${inner})`;
7983
+ }
7984
+ return p;
7985
+ }
7986
+ return null;
7987
+ }
7988
+ /**
7989
+ * De Morgan recombination for an OR whose every branch is a negated
7990
+ * own-element selector leaf:
7991
+ *
7992
+ * OR(¬a, ¬b, ¬c) ≡ ¬(a ∧ b ∧ c) → single `:not(a b c)`
7993
+ *
7994
+ * This keeps the catch-all/default exclusive condition (which has no
7995
+ * positive terms to prune against) from exploding into a Cartesian product
7996
+ * of OR branches at `andToCSS`. Returns `null` when recombination does not
7997
+ * apply, so genuine unions (e.g. `:hover | :focus`) fall through to the
7998
+ * normal per-branch materialization.
7999
+ */
8000
+ function tryRecombineNegatedSelectorOr(children) {
8001
+ if (children.length < 2) return null;
8002
+ const positiveSelectors = [];
8003
+ for (const child of children) {
8004
+ const sel = negatedSelectorLeafToPositiveSelector(child);
8005
+ if (sel === null) return null;
8006
+ positiveSelectors.push(sel);
8007
+ }
8008
+ positiveSelectors.sort();
8009
+ const compound = positiveSelectors.join("");
8010
+ const v = emptyVariant();
8011
+ v.pseudoConditions.push({
8012
+ pseudo: `:is(${compound})`,
8013
+ negated: true
8014
+ });
8015
+ return {
8016
+ variants: [v],
8017
+ isImpossible: false
8018
+ };
8019
+ }
8020
+ function orToCSS(children) {
8021
+ const recombined = tryRecombineNegatedSelectorOr(children);
8022
+ if (recombined !== null) return recombined;
8023
+ const allVariants = [];
8024
+ for (const child of children) {
8025
+ const childCSS = conditionToCSSInner(child);
8026
+ if (childCSS.isImpossible) continue;
8027
+ allVariants.push(...childCSS.variants);
8028
+ }
8029
+ if (allVariants.length === 0) return {
8030
+ variants: [],
8031
+ isImpossible: true
8032
+ };
8033
+ return {
8034
+ variants: dedupeVariants(allVariants),
8035
+ isImpossible: false
8036
+ };
8037
+ }
8038
+ /**
8039
+ * Find keys present in ALL condition arrays.
8040
+ */
8041
+ function findCommonKeys(conditionSets, getKey) {
8042
+ if (conditionSets.length === 0) return /* @__PURE__ */ new Set();
8043
+ const common = new Set(conditionSets[0].map(getKey));
8044
+ for (let i = 1; i < conditionSets.length; i++) {
8045
+ const keys = new Set(conditionSets[i].map(getKey));
8046
+ for (const key of common) if (!keys.has(key)) common.delete(key);
8047
+ }
8048
+ return common;
8049
+ }
8050
+ /**
8051
+ * Merge OR variants that share the same "context" (at-rules, root, parent,
8052
+ * own, starting) into a single variant with a SelectorGroup.
8053
+ *
8054
+ * Variants with no modifier/pseudo conditions are kept separate (they match
8055
+ * unconditionally and can't be expressed inside :is()).
8056
+ */
8057
+ function mergeVariantsIntoSelectorGroups(variants) {
8058
+ if (variants.length <= 1) return variants;
8059
+ const groups = /* @__PURE__ */ new Map();
8060
+ for (const v of variants) {
8061
+ const key = getVariantContextKey(v);
8062
+ const group = groups.get(key);
8063
+ if (group) group.push(v);
8064
+ else groups.set(key, [v]);
8065
+ }
8066
+ const result = [];
8067
+ for (const group of groups.values()) {
8068
+ if (group.length === 1) {
8069
+ result.push(group[0]);
8070
+ continue;
8071
+ }
8072
+ const withSelectors = [];
8073
+ const withoutSelectors = [];
8074
+ for (const v of group) if (v.modifierConditions.length === 0 && v.pseudoConditions.length === 0) withoutSelectors.push(v);
8075
+ else withSelectors.push(v);
8076
+ result.push(...withoutSelectors);
8077
+ if (withSelectors.length <= 1) {
8078
+ result.push(...withSelectors);
8079
+ continue;
8080
+ }
8081
+ result.push(factorAndGroup(withSelectors));
8082
+ }
8083
+ return result;
8084
+ }
8085
+ /**
8086
+ * Factor common modifier/pseudo conditions out of variants and create
8087
+ * a single variant with a SelectorGroup for the remaining (differing)
8088
+ * conditions.
8089
+ *
8090
+ * Precondition: all variants must share the same context key (identical
8091
+ * at-rules, root/parent/own/selector groups, startingStyle).
8092
+ */
8093
+ function factorAndGroup(variants) {
8094
+ {
8095
+ const key0 = getVariantContextKey(variants[0]);
8096
+ for (let i = 1; i < variants.length; i++) {
8097
+ const keyI = getVariantContextKey(variants[i]);
8098
+ if (keyI !== key0) throw new Error(`factorAndGroup: context key mismatch at index ${i}.\n expected: ${key0}\n got: ${keyI}`);
8099
+ }
8100
+ }
8101
+ const commonModKeys = findCommonKeys(variants.map((v) => v.modifierConditions), getModifierKey);
8102
+ const commonPseudoKeys = findCommonKeys(variants.map((v) => v.pseudoConditions), getPseudoKey);
8103
+ const commonModifiers = variants[0].modifierConditions.filter((m) => commonModKeys.has(getModifierKey(m)));
8104
+ const commonPseudos = variants[0].pseudoConditions.filter((p) => commonPseudoKeys.has(getPseudoKey(p)));
8105
+ const branches = [];
8106
+ let hasEmptyBranch = false;
8107
+ for (const v of variants) {
8108
+ const branch = [];
8109
+ for (const mod of v.modifierConditions) if (!commonModKeys.has(getModifierKey(mod))) branch.push(mod);
8110
+ for (const pseudo of v.pseudoConditions) if (!commonPseudoKeys.has(getPseudoKey(pseudo))) branch.push(pseudo);
8111
+ if (branch.length > 0) branches.push(branch);
8112
+ else hasEmptyBranch = true;
8113
+ }
8114
+ if (hasEmptyBranch) return {
8115
+ ...variants[0],
8116
+ modifierConditions: commonModifiers,
8117
+ pseudoConditions: commonPseudos
8118
+ };
8119
+ const factoredGroups = tryFactorIntoDimensions(branches);
8120
+ if (factoredGroups) return {
8121
+ modifierConditions: commonModifiers,
8122
+ pseudoConditions: commonPseudos,
8123
+ selectorGroups: [...variants[0].selectorGroups, ...factoredGroups],
8124
+ ownGroups: [...variants[0].ownGroups],
8125
+ mediaConditions: [...variants[0].mediaConditions],
8126
+ containerConditions: [...variants[0].containerConditions],
8127
+ supportsConditions: [...variants[0].supportsConditions],
8128
+ rootGroups: [...variants[0].rootGroups],
8129
+ parentGroups: [...variants[0].parentGroups],
8130
+ startingStyle: variants[0].startingStyle
8131
+ };
8132
+ return {
8133
+ modifierConditions: commonModifiers,
8134
+ pseudoConditions: commonPseudos,
8135
+ selectorGroups: [...variants[0].selectorGroups, {
8136
+ branches,
8137
+ negated: false
8138
+ }],
8139
+ ownGroups: [...variants[0].ownGroups],
8140
+ mediaConditions: [...variants[0].mediaConditions],
8141
+ containerConditions: [...variants[0].containerConditions],
8142
+ supportsConditions: [...variants[0].supportsConditions],
8143
+ rootGroups: [...variants[0].rootGroups],
8144
+ parentGroups: [...variants[0].parentGroups],
8145
+ startingStyle: variants[0].startingStyle
8146
+ };
8147
+ }
8148
+ /**
8149
+ * Detect when branches form a complete Cartesian product of independent
8150
+ * modifier attribute dimensions and return one SelectorGroup per dimension.
8151
+ *
8152
+ * Example: 4 branches for 2 attributes × 2 values each →
8153
+ * :is(A1, A2):is(B1, B2) instead of :is(A1B1, A1B2, A2B1, A2B2)
8154
+ */
8155
+ function tryFactorIntoDimensions(branches) {
8156
+ if (branches.length < 4) return null;
8157
+ const dimensions = /* @__PURE__ */ new Map();
8158
+ for (const branch of branches) for (const cond of branch) {
8159
+ if (!("attribute" in cond)) return null;
8160
+ if (!dimensions.has(cond.attribute)) dimensions.set(cond.attribute, /* @__PURE__ */ new Map());
8161
+ dimensions.get(cond.attribute).set(getModifierKey(cond), cond);
8162
+ }
8163
+ if (dimensions.size < 2) return null;
8164
+ for (const branch of branches) {
8165
+ const seen = /* @__PURE__ */ new Set();
8166
+ for (const cond of branch) {
8167
+ const attr = cond.attribute;
8168
+ if (seen.has(attr)) return null;
8169
+ seen.add(attr);
8170
+ }
8171
+ if (seen.size !== dimensions.size) return null;
8172
+ }
8173
+ let expectedCount = 1;
8174
+ for (const vals of dimensions.values()) expectedCount *= vals.size;
8175
+ if (branches.length !== expectedCount) return null;
8176
+ return [...dimensions.values()].map((vals) => ({
8177
+ branches: [...vals.values()].map((cond) => [cond]),
8178
+ negated: false
8179
+ }));
8180
+ }
8181
+ /**
8182
+ * Build at-rules array from a variant
8183
+ */
8184
+ function buildAtRulesFromVariant(variant) {
8185
+ const atRules = [];
8186
+ if (variant.mediaConditions.length > 0) {
8187
+ const conditionParts = variant.mediaConditions.map((c) => {
8188
+ if (c.subtype === "type") return c.negated ? `not ${c.condition}` : c.condition;
8189
+ else return c.negated ? `(not ${c.condition})` : c.condition;
8190
+ });
8191
+ atRules.push(`@media ${conditionParts.sort().join(" and ")}`);
8192
+ }
8193
+ if (variant.containerConditions.length > 0) {
8194
+ const byName = /* @__PURE__ */ new Map();
8195
+ for (const cond of variant.containerConditions) {
8196
+ const group = byName.get(cond.name) || [];
8197
+ group.push(cond);
8198
+ byName.set(cond.name, group);
8199
+ }
8200
+ for (const [name, conditions] of byName) {
8201
+ const conditionParts = conditions.map((c) => c.negated ? `(not ${c.condition})` : c.condition);
8202
+ const namePrefix = name ? `${name} ` : "";
8203
+ atRules.push(`@container ${namePrefix}${conditionParts.join(" and ")}`);
8204
+ }
8205
+ }
8206
+ if (variant.supportsConditions.length > 0) {
8207
+ const conditionParts = variant.supportsConditions.map((c) => {
8208
+ if (c.subtype === "selector") {
8209
+ const selectorCond = `selector(${c.condition})`;
8210
+ return c.negated ? `(not ${selectorCond})` : selectorCond;
8211
+ } else {
8212
+ const featureCond = `(${c.condition})`;
8213
+ return c.negated ? `(not ${featureCond})` : featureCond;
8214
+ }
8215
+ });
8216
+ atRules.push(`@supports ${conditionParts.join(" and ")}`);
8217
+ }
8218
+ return atRules;
8219
+ }
8220
+ //#endregion
8221
+ //#region src/pipeline/exclusive.ts
8222
+ /**
8223
+ * Build exclusive conditions for a list of parsed style entries.
8224
+ *
8225
+ * The entries should be ordered by priority (highest priority first).
8226
+ *
8227
+ * For each entry, we compute:
8228
+ * exclusiveCondition = condition & !prior[0] & !prior[1] & ...
8229
+ *
8230
+ * This ensures exactly one condition matches at any time.
8231
+ *
8232
+ * Example:
8233
+ * Input (ordered highest to lowest priority):
8234
+ * A: value1 (priority 2)
8235
+ * B: value2 (priority 1)
8236
+ * C: value3 (priority 0)
8237
+ *
8238
+ * Output:
8239
+ * A: A
8240
+ * B: B & !A
8241
+ * C: C & !A & !B
8242
+ *
8243
+ * @param entries Parsed style entries ordered by priority (highest first)
8244
+ * @returns Entries with exclusive conditions, filtered to remove impossible ones
8245
+ */
8246
+ function buildExclusiveConditions(entries) {
8247
+ const result = [];
8248
+ const priorConditions = [];
8249
+ for (const entry of entries) {
8250
+ let exclusive = entry.condition;
8251
+ for (const prior of priorConditions) {
8252
+ if (prior.kind === "true") continue;
8253
+ if (entry.condition.kind !== "true" && simplifyCondition(and(entry.condition, prior)).kind === "false") continue;
8254
+ exclusive = and(exclusive, not(prior));
8255
+ }
8256
+ const simplified = simplifyCondition(exclusive);
8257
+ if (simplified.kind === "false") continue;
8258
+ result.push({
8259
+ ...entry,
8260
+ exclusiveCondition: simplified
8261
+ });
8262
+ if (entry.condition.kind !== "true") priorConditions.push(entry.condition);
8263
+ }
8264
+ return result;
8265
+ }
8266
+ /**
8267
+ * Parse style entries from a value mapping object.
8268
+ *
8269
+ * @param styleKey The style key (e.g., 'padding')
8270
+ * @param valueMap The value mapping { '': '2x', 'compact': '1x', '@media(w < 768px)': '0.5x' }
8271
+ * @param parseCondition Function to parse state keys into conditions
8272
+ * @returns Parsed entries ordered by priority (highest first)
8273
+ */
8274
+ function parseStyleEntries(styleKey, valueMap, parseCondition) {
8275
+ const entries = [];
8276
+ Object.keys(valueMap).forEach((stateKey, index) => {
8277
+ const value = valueMap[stateKey];
8278
+ const condition = stateKey === "" ? trueCondition() : parseCondition(stateKey);
8279
+ entries.push({
8280
+ styleKey,
8281
+ stateKey,
8282
+ value,
8283
+ condition,
8284
+ priority: index
8285
+ });
8286
+ });
8287
+ entries.reverse();
8288
+ return entries;
8289
+ }
8290
+ /**
8291
+ * Merge parsed entries that share the same value.
8292
+ *
8293
+ * When multiple **non-default** state keys map to the same value, their
8294
+ * conditions can be combined with OR and treated as a single entry.
8295
+ * This must happen **before** exclusive expansion and OR branch splitting
8296
+ * to avoid combinatorial explosion and duplicate CSS output.
8297
+ *
8298
+ * **Merging must preserve the authored cascade.** Merging two same-value
8299
+ * entries with priorities `p_h > p_l` lifts the lower-priority entry up
8300
+ * to `p_h` and changes the "blocker" for intermediate-priority entries
8301
+ * from `!C_h` to `!(C_h | C_l) = !C_h & !C_l`. The added `!C_l`
8302
+ * constraint can incorrectly block an intermediate entry that should
8303
+ * have won.
8304
+ *
8305
+ * Two same-value entries with conditions `C_h` (higher priority) and
8306
+ * `C_l` (lower priority) are safe to merge iff for every entry
8307
+ * `e_m` strictly between them in priority with a different value,
8308
+ *
8309
+ * simplify(C_m & C_l & !C_h) = FALSE
8310
+ *
8311
+ * i.e. there is no scenario where the intermediate state could have
8312
+ * matched (`C_m`), the lower-priority same-value entry would also have
8313
+ * matched (`C_l`), and the higher-priority entry would not (`!C_h`).
8314
+ * In such scenarios the intermediate is supposed to win; the merge
8315
+ * would block it by introducing `!C_l`.
8316
+ *
8317
+ * Example (UNSAFE — must not merge):
8318
+ * `{ hovered: 'red', pressed: 'blue', disabled: 'red' }`.
8319
+ * C_h = disabled, C_l = hovered, C_m = pressed. `pressed & hovered &
8320
+ * !disabled` is satisfiable (three independent modifiers), so the
8321
+ * intermediate `pressed` would lose to a merged red rule when both
8322
+ * `pressed` and `hovered` are active — breaking the cascade
8323
+ * `disabled > pressed > hovered`.
8324
+ *
8325
+ * Example (SAFE — still merges):
8326
+ * `{ '': light, '@dark': dark, '@hc': hc, '@dark & @hc': dark }`.
8327
+ * C_h = `@dark & @hc`, C_l = `@dark`, C_m = `@hc`.
8328
+ * `@hc & @dark & !(@dark & @hc) = @hc & @dark & (!@dark | !@hc)`
8329
+ * simplifies to FALSE, so merging the two darks into one `@dark` rule
8330
+ * at the higher priority does not affect the `@hc` rule.
8331
+ *
8332
+ * Default (TRUE) entries are never merged with non-default entries.
8333
+ * Merging `TRUE | X` collapses to `TRUE`, destroying the non-default
8334
+ * condition's participation in exclusive building. Stage 6
8335
+ * `mergeByValue` handles combining rules with identical CSS output
8336
+ * after exclusive conditions are correctly built.
8337
+ *
8338
+ * The merged entry keeps the highest priority of the merged entries.
8045
8339
  */
8046
- function dedupeVariants(variants) {
8047
- if (variants.length <= 1) return variants;
8048
- const seen = /* @__PURE__ */ new Set();
8049
- const result = [];
8050
- for (const v of variants) {
8051
- const key = getVariantKey(v);
8052
- if (!seen.has(key)) {
8053
- seen.add(key);
8054
- result.push(v);
8340
+ function mergeEntriesByValue(entries) {
8341
+ if (entries.length <= 1) return entries;
8342
+ const merged = [];
8343
+ for (const entry of entries) {
8344
+ if (entry.condition.kind === "true") {
8345
+ merged.push(entry);
8346
+ continue;
8055
8347
  }
8056
- }
8057
- if (result.length <= 1) return result;
8058
- result.sort((a, b) => variantConditionCount(a) - variantConditionCount(b));
8059
- const filtered = [];
8060
- for (const candidate of result) {
8061
- let isRedundant = false;
8062
- for (const kept of filtered) if (isVariantSuperset(candidate, kept)) {
8063
- isRedundant = true;
8064
- break;
8348
+ const valueKey = serializeValue(entry.value);
8349
+ let mergeIdx = -1;
8350
+ for (let j = merged.length - 1; j >= 0; j--) {
8351
+ const prev = merged[j];
8352
+ if (prev.condition.kind === "true") continue;
8353
+ if (serializeValue(prev.value) !== valueKey) continue;
8354
+ let safe = true;
8355
+ for (let k = j + 1; k < merged.length; k++) {
8356
+ const inter = merged[k];
8357
+ if (inter.condition.kind === "true") continue;
8358
+ if (serializeValue(inter.value) === valueKey) continue;
8359
+ if (simplifyCondition(and(inter.condition, entry.condition, not(prev.condition))).kind !== "false") {
8360
+ safe = false;
8361
+ break;
8362
+ }
8363
+ }
8364
+ if (safe) {
8365
+ mergeIdx = j;
8366
+ break;
8367
+ }
8065
8368
  }
8066
- if (!isRedundant) filtered.push(candidate);
8369
+ if (mergeIdx >= 0) {
8370
+ const prev = merged[mergeIdx];
8371
+ const newCondition = simplifyCondition(or(prev.condition, entry.condition));
8372
+ merged[mergeIdx] = {
8373
+ styleKey: prev.styleKey,
8374
+ stateKey: `${prev.stateKey} | ${entry.stateKey}`,
8375
+ value: prev.value,
8376
+ condition: newCondition,
8377
+ priority: Math.max(prev.priority, entry.priority)
8378
+ };
8379
+ } else merged.push(entry);
8067
8380
  }
8068
- return filtered;
8381
+ return merged;
8382
+ }
8383
+ function serializeValue(value) {
8384
+ if (value === null || value === void 0) return "null";
8385
+ if (typeof value === "string" || typeof value === "number") return String(value);
8386
+ return JSON.stringify(value);
8069
8387
  }
8070
8388
  /**
8071
- * Combine AND conditions into CSS
8389
+ * Eliminate redundant state dimensions from a value map.
8072
8390
  *
8073
- * AND of conditions means cartesian product of variants:
8074
- * (A1 | A2) & (B1 | B2) = A1&B1 | A1&B2 | A2&B1 | A2&B2
8391
+ * When a value map contains compound AND state keys (e.g. `@dark & @hc`),
8392
+ * checks whether any state atom is a "don't-care" variable i.e. the
8393
+ * value is the same whether that atom is present or absent. Redundant
8394
+ * atoms are removed from all keys and duplicate entries are collapsed.
8075
8395
  *
8076
- * Variants that result in contradictions (e.g., conflicting media rules)
8077
- * are filtered out.
8396
+ * This runs **before** condition parsing so that downstream stages
8397
+ * (`mergeEntriesByValue`, `buildExclusiveConditions`, materialization)
8398
+ * never see the irrelevant dimension, producing simpler, smaller CSS.
8399
+ *
8400
+ * Only pure top-level AND combinations are eligible. Keys that contain
8401
+ * `|`, `^`, or `,` at the top level are treated as opaque single atoms.
8402
+ *
8403
+ * @example
8404
+ * { '': A, '@dark': B, '@hc': A, '@dark & @hc': B }
8405
+ * // @hc is redundant → { '': A, '@dark': B }
8078
8406
  */
8079
- function andToCSS(children) {
8080
- const exclusiveChildren = makeOrBranchesExclusive(children);
8081
- let currentVariants = [emptyVariant()];
8082
- for (const child of exclusiveChildren) {
8083
- const childCSS = conditionToCSSInner(child);
8084
- if (childCSS.isImpossible || childCSS.variants.length === 0) return {
8085
- variants: [],
8086
- isImpossible: true
8087
- };
8088
- const newVariants = [];
8089
- for (const current of currentVariants) for (const childVariant of childCSS.variants) {
8090
- const merged = mergeVariants(current, childVariant);
8091
- if (merged !== null) newVariants.push(merged);
8092
- }
8093
- if (newVariants.length === 0) return {
8094
- variants: [],
8095
- isImpossible: true
8407
+ function extractCompoundStates(valueMap) {
8408
+ const keys = Object.keys(valueMap);
8409
+ if (keys.length < 3 || !keys.some((k) => k.includes("&"))) return valueMap;
8410
+ const entries = keys.map((key) => {
8411
+ return {
8412
+ atoms: splitTopLevelAnd(key) ?? [key],
8413
+ value: valueMap[key]
8096
8414
  };
8097
- currentVariants = dedupeVariants(newVariants);
8415
+ });
8416
+ const allAtoms = /* @__PURE__ */ new Set();
8417
+ for (const e of entries) for (const a of e.atoms) allAtoms.add(a);
8418
+ const redundant = /* @__PURE__ */ new Set();
8419
+ for (const atom of allAtoms) if (isAtomRedundant(entries, atom)) redundant.add(atom);
8420
+ if (redundant.size === 0) return valueMap;
8421
+ const newMap = {};
8422
+ for (const e of entries) {
8423
+ const newKey = e.atoms.filter((a) => !redundant.has(a)).join(" & ");
8424
+ if (!(newKey in newMap)) newMap[newKey] = e.value;
8098
8425
  }
8099
- return {
8100
- variants: currentVariants,
8101
- isImpossible: false
8102
- };
8426
+ return newMap;
8103
8427
  }
8104
8428
  /**
8105
- * Make OR branches within AND children mutually exclusive.
8106
- *
8107
- * For an AND child that is OR(A, B), transforms it to OR(A, B & !A)
8108
- * so that when andToCSS does a Cartesian product, the resulting
8109
- * CSS variants don't overlap.
8429
+ * Split a state key by top-level `&` operators.
8110
8430
  *
8111
- * Only transforms OR children whose branches actually produce
8112
- * different at-rule contexts when materialized. This avoids
8113
- * breaking cases where contradiction detection in the Cartesian
8114
- * product naturally handles deduplication.
8431
+ * Returns `null` if the key contains `|`, `^`, or `,` at the top level
8432
+ * (making it ineligible for atom-level extraction).
8433
+ * Returns `[]` for the empty string (default key).
8115
8434
  */
8116
- function makeOrBranchesExclusive(children) {
8117
- return children.map((child) => {
8118
- if (!isCompoundCondition(child) || child.operator !== "OR") return child;
8119
- if (child.children.length <= 1) return child;
8120
- if (!branchesProduceDifferentContexts(child.children)) return child;
8121
- const exclusiveBranches = [];
8122
- const priorBranches = [];
8123
- for (const branch of child.children) {
8124
- if (priorBranches.length === 0) exclusiveBranches.push(branch);
8125
- else {
8126
- let exclusive = branch;
8127
- for (const prior of priorBranches) exclusive = and(exclusive, not(prior));
8128
- const simplified = simplifyCondition(exclusive);
8129
- if (simplified.kind !== "false") exclusiveBranches.push(simplified);
8435
+ function splitTopLevelAnd(key) {
8436
+ if (key === "") return [];
8437
+ const parts = [];
8438
+ let depth = 0;
8439
+ let current = "";
8440
+ for (const ch of key) {
8441
+ if (ch === "(" || ch === "[") depth++;
8442
+ else if (ch === ")" || ch === "]") depth--;
8443
+ if (depth === 0) {
8444
+ if (ch === "&") {
8445
+ const trimmed = current.trim();
8446
+ if (trimmed) parts.push(trimmed);
8447
+ current = "";
8448
+ continue;
8130
8449
  }
8131
- priorBranches.push(branch);
8450
+ if (ch === "|" || ch === "^" || ch === ",") return null;
8132
8451
  }
8133
- if (exclusiveBranches.length === 0) return child;
8134
- if (exclusiveBranches.length === 1) return exclusiveBranches[0];
8135
- return {
8136
- kind: "compound",
8137
- operator: "OR",
8138
- children: exclusiveBranches
8139
- };
8140
- });
8452
+ current += ch;
8453
+ }
8454
+ const trimmed = current.trim();
8455
+ if (trimmed) parts.push(trimmed);
8456
+ return parts;
8141
8457
  }
8142
8458
  /**
8143
- * Check if OR branches produce different at-rule contexts when
8144
- * materialized. If so, the Cartesian product in andToCSS will
8145
- * create overlapping CSS variants that need exclusive expansion.
8459
+ * An atom is redundant when every entry that contains it has a matching
8460
+ * partner (same remaining atoms, atom absent) with the same value.
8146
8461
  */
8147
- function branchesProduceDifferentContexts(branches) {
8148
- const contextKeys = /* @__PURE__ */ new Set();
8149
- for (const branch of branches) {
8150
- const css = conditionToCSSInner(branch);
8151
- if (css.isImpossible) continue;
8152
- for (const v of css.variants) contextKeys.add(getVariantContextKey(v));
8462
+ function isAtomRedundant(entries, atom) {
8463
+ const withAtom = entries.filter((e) => e.atoms.includes(atom));
8464
+ if (withAtom.length === 0) return false;
8465
+ for (const wa of withAtom) {
8466
+ const remaining = wa.atoms.filter((a) => a !== atom);
8467
+ const pair = entries.find((e) => !e.atoms.includes(atom) && e.atoms.length === remaining.length && remaining.every((r) => e.atoms.includes(r)));
8468
+ if (!pair) return false;
8469
+ if (serializeValue(wa.value) !== serializeValue(pair.value)) return false;
8153
8470
  }
8154
- return contextKeys.size > 1;
8471
+ return true;
8155
8472
  }
8156
8473
  /**
8157
- * Combine OR conditions into CSS
8474
+ * Check if a value is a style value mapping (object with state keys)
8475
+ */
8476
+ function isValueMapping(value) {
8477
+ return value !== null && typeof value === "object" && !Array.isArray(value) && !(value instanceof Date);
8478
+ }
8479
+ /**
8480
+ * Expand OR conditions in parsed entries into multiple exclusive entries.
8158
8481
  *
8159
- * OR in CSS means multiple selector variants (DNF).
8160
- * After deduplication, variants that differ only in their base
8161
- * modifier/pseudo conditions are merged into :is() groups.
8482
+ * For an entry with condition `A | B | C`, this creates 3 entries:
8483
+ * - condition: A
8484
+ * - condition: B & !A
8485
+ * - condition: C & !A & !B
8162
8486
  *
8163
- * Note: OR exclusivity is handled at the pipeline level (expandOrConditions),
8164
- * so here we just collect all variants. Any remaining ORs in the condition
8165
- * tree (e.g., from De Morgan expansion) are handled as simple alternatives.
8487
+ * This ensures OR branches are mutually exclusive BEFORE the main
8488
+ * exclusive condition building pass.
8489
+ *
8490
+ * @param entries Parsed entries (may contain OR conditions)
8491
+ * @returns Expanded entries with OR branches made exclusive
8166
8492
  */
8167
- function orToCSS(children) {
8168
- const allVariants = [];
8169
- for (const child of children) {
8170
- const childCSS = conditionToCSSInner(child);
8171
- if (childCSS.isImpossible) continue;
8172
- allVariants.push(...childCSS.variants);
8493
+ function expandOrConditions(entries) {
8494
+ const result = [];
8495
+ for (const entry of entries) {
8496
+ const expanded = expandSingleEntry(entry);
8497
+ result.push(...expanded);
8173
8498
  }
8174
- if (allVariants.length === 0) return {
8175
- variants: [],
8176
- isImpossible: true
8177
- };
8178
- return {
8179
- variants: dedupeVariants(allVariants),
8180
- isImpossible: false
8181
- };
8499
+ return result;
8500
+ }
8501
+ /**
8502
+ * Expand a single entry's OR condition into multiple exclusive entries.
8503
+ *
8504
+ * Note: branches are NOT sorted by at-rule context here (unlike the
8505
+ * `expandExclusiveOrs` pass below). User-authored ORs in state keys aren't
8506
+ * the product of De Morgan negation, so each branch is expected to render
8507
+ * independently in its own scope and at-rule sort isn't load-bearing.
8508
+ * The post-build pass needs the sort because it has to preserve at-rule
8509
+ * wrapping across branches that came from negating a compound at-rule.
8510
+ *
8511
+ * Skip optimisation: when every branch renders into the same at-rule /
8512
+ * root / parent / own context (see "Key Design Decision #2" in
8513
+ * `docs/pipeline.md`), forcing mutual exclusivity here produces dead
8514
+ * `B & !A`-style branches that materialization later folds back into
8515
+ * `:is(A, B)`. Bail out and let `materialize.ts` collapse the OR via
8516
+ * `mergeVariantsIntoSelectorGroups`. Cross-entry exclusivity is still
8517
+ * enforced by `buildExclusiveConditions`; the post-build `expandExclusiveOrs`
8518
+ * pass still handles De Morgan ORs whose branches actually differ in
8519
+ * context.
8520
+ */
8521
+ function expandSingleEntry(entry) {
8522
+ const orBranches = collectOrBranches(entry.condition);
8523
+ if (orBranches.length <= 1) return [entry];
8524
+ if (!branchesProduceDifferentContexts(orBranches)) return [entry];
8525
+ const result = [];
8526
+ const priorBranches = [];
8527
+ for (let i = 0; i < orBranches.length; i++) {
8528
+ const branch = orBranches[i];
8529
+ let exclusiveBranch = branch;
8530
+ for (const prior of priorBranches) exclusiveBranch = and(exclusiveBranch, not(prior));
8531
+ const simplified = simplifyCondition(exclusiveBranch);
8532
+ if (simplified.kind === "false") {
8533
+ priorBranches.push(branch);
8534
+ continue;
8535
+ }
8536
+ result.push({
8537
+ ...entry,
8538
+ stateKey: `${entry.stateKey}[${i}]`,
8539
+ condition: simplified
8540
+ });
8541
+ priorBranches.push(branch);
8542
+ }
8543
+ return result;
8182
8544
  }
8183
8545
  /**
8184
- * Find keys present in ALL condition arrays.
8546
+ * Collect top-level OR branches from a condition.
8547
+ *
8548
+ * For `A | B | C`, returns [A, B, C]
8549
+ * For `A & B`, returns [A & B] (single branch)
8550
+ * For `A | (B & C)`, returns [A, B & C]
8185
8551
  */
8186
- function findCommonKeys(conditionSets, getKey) {
8187
- if (conditionSets.length === 0) return /* @__PURE__ */ new Set();
8188
- const common = new Set(conditionSets[0].map(getKey));
8189
- for (let i = 1; i < conditionSets.length; i++) {
8190
- const keys = new Set(conditionSets[i].map(getKey));
8191
- for (const key of common) if (!keys.has(key)) common.delete(key);
8552
+ function collectOrBranches(condition) {
8553
+ if (condition.kind === "true" || condition.kind === "false") return [condition];
8554
+ if (isCompoundCondition(condition) && condition.operator === "OR") {
8555
+ const branches = [];
8556
+ for (const child of condition.children) branches.push(...collectOrBranches(child));
8557
+ return branches;
8192
8558
  }
8193
- return common;
8559
+ return [condition];
8194
8560
  }
8195
8561
  /**
8196
- * Merge OR variants that share the same "context" (at-rules, root, parent,
8197
- * own, starting) into a single variant with a SelectorGroup.
8562
+ * Expand OR conditions in exclusive entries AFTER buildExclusiveConditions.
8198
8563
  *
8199
- * Variants with no modifier/pseudo conditions are kept separate (they match
8200
- * unconditionally and can't be expressed inside :is()).
8564
+ * This handles ORs that arise from De Morgan expansion during negation:
8565
+ * !(A & B) = !A | !B
8566
+ *
8567
+ * These ORs need to be made exclusive to avoid overlapping CSS rules:
8568
+ * !A | !B → !A | (A & !B)
8569
+ *
8570
+ * This is logically equivalent but ensures each branch has proper context.
8571
+ *
8572
+ * Example:
8573
+ * Input: { "": V1, "@supports(...) & :has()": V2 }
8574
+ * V2's exclusive = @supports & :has
8575
+ * V1's exclusive = !(@supports & :has) = !@supports | !:has
8576
+ *
8577
+ * Without this fix: V1 gets two rules:
8578
+ * - @supports (not ...) → V1 ✓
8579
+ * - :not(:has()) → V1 ✗ (missing @supports context!)
8580
+ *
8581
+ * With this fix: V1 gets two exclusive rules:
8582
+ * - @supports (not ...) → V1 ✓
8583
+ * - @supports (...) { :not(:has()) } → V1 ✓ (proper context!)
8201
8584
  */
8202
- function mergeVariantsIntoSelectorGroups(variants) {
8203
- if (variants.length <= 1) return variants;
8204
- const groups = /* @__PURE__ */ new Map();
8205
- for (const v of variants) {
8206
- const key = getVariantContextKey(v);
8207
- const group = groups.get(key);
8208
- if (group) group.push(v);
8209
- else groups.set(key, [v]);
8210
- }
8585
+ function expandExclusiveOrs(entries) {
8211
8586
  const result = [];
8212
- for (const group of groups.values()) {
8213
- if (group.length === 1) {
8214
- result.push(group[0]);
8215
- continue;
8216
- }
8217
- const withSelectors = [];
8218
- const withoutSelectors = [];
8219
- for (const v of group) if (v.modifierConditions.length === 0 && v.pseudoConditions.length === 0) withoutSelectors.push(v);
8220
- else withSelectors.push(v);
8221
- result.push(...withoutSelectors);
8222
- if (withSelectors.length <= 1) {
8223
- result.push(...withSelectors);
8224
- continue;
8225
- }
8226
- result.push(factorAndGroup(withSelectors));
8587
+ for (const entry of entries) {
8588
+ const expanded = expandExclusiveConditionOrs(entry);
8589
+ result.push(...expanded);
8227
8590
  }
8228
8591
  return result;
8229
8592
  }
8230
8593
  /**
8231
- * Factor common modifier/pseudo conditions out of variants and create
8232
- * a single variant with a SelectorGroup for the remaining (differing)
8233
- * conditions.
8234
- *
8235
- * Precondition: all variants must share the same context key (identical
8236
- * at-rules, root/parent/own/selector groups, startingStyle).
8594
+ * Check if a condition involves at-rules (media, container, supports, starting)
8237
8595
  */
8238
- function factorAndGroup(variants) {
8239
- {
8240
- const key0 = getVariantContextKey(variants[0]);
8241
- for (let i = 1; i < variants.length; i++) {
8242
- const keyI = getVariantContextKey(variants[i]);
8243
- if (keyI !== key0) throw new Error(`factorAndGroup: context key mismatch at index ${i}.\n expected: ${key0}\n got: ${keyI}`);
8244
- }
8245
- }
8246
- const commonModKeys = findCommonKeys(variants.map((v) => v.modifierConditions), getModifierKey);
8247
- const commonPseudoKeys = findCommonKeys(variants.map((v) => v.pseudoConditions), getPseudoKey);
8248
- const commonModifiers = variants[0].modifierConditions.filter((m) => commonModKeys.has(getModifierKey(m)));
8249
- const commonPseudos = variants[0].pseudoConditions.filter((p) => commonPseudoKeys.has(getPseudoKey(p)));
8250
- const branches = [];
8251
- let hasEmptyBranch = false;
8252
- for (const v of variants) {
8253
- const branch = [];
8254
- for (const mod of v.modifierConditions) if (!commonModKeys.has(getModifierKey(mod))) branch.push(mod);
8255
- for (const pseudo of v.pseudoConditions) if (!commonPseudoKeys.has(getPseudoKey(pseudo))) branch.push(pseudo);
8256
- if (branch.length > 0) branches.push(branch);
8257
- else hasEmptyBranch = true;
8258
- }
8259
- if (hasEmptyBranch) return {
8260
- ...variants[0],
8261
- modifierConditions: commonModifiers,
8262
- pseudoConditions: commonPseudos
8263
- };
8264
- const factoredGroups = tryFactorIntoDimensions(branches);
8265
- if (factoredGroups) return {
8266
- modifierConditions: commonModifiers,
8267
- pseudoConditions: commonPseudos,
8268
- selectorGroups: [...variants[0].selectorGroups, ...factoredGroups],
8269
- ownGroups: [...variants[0].ownGroups],
8270
- mediaConditions: [...variants[0].mediaConditions],
8271
- containerConditions: [...variants[0].containerConditions],
8272
- supportsConditions: [...variants[0].supportsConditions],
8273
- rootGroups: [...variants[0].rootGroups],
8274
- parentGroups: [...variants[0].parentGroups],
8275
- startingStyle: variants[0].startingStyle
8276
- };
8277
- return {
8278
- modifierConditions: commonModifiers,
8279
- pseudoConditions: commonPseudos,
8280
- selectorGroups: [...variants[0].selectorGroups, {
8281
- branches,
8282
- negated: false
8283
- }],
8284
- ownGroups: [...variants[0].ownGroups],
8285
- mediaConditions: [...variants[0].mediaConditions],
8286
- containerConditions: [...variants[0].containerConditions],
8287
- supportsConditions: [...variants[0].supportsConditions],
8288
- rootGroups: [...variants[0].rootGroups],
8289
- parentGroups: [...variants[0].parentGroups],
8290
- startingStyle: variants[0].startingStyle
8291
- };
8596
+ function hasAtRuleContext(node) {
8597
+ if (node.kind === "true" || node.kind === "false") return false;
8598
+ if (node.kind === "state") return node.type === "media" || node.type === "container" || node.type === "supports" || node.type === "starting";
8599
+ if (node.kind === "compound") return node.children.some(hasAtRuleContext);
8600
+ return false;
8292
8601
  }
8293
8602
  /**
8294
- * Detect when branches form a complete Cartesian product of independent
8295
- * modifier attribute dimensions and return one SelectorGroup per dimension.
8603
+ * Sort OR branches to prioritize at-rule conditions first.
8296
8604
  *
8297
- * Example: 4 branches for 2 attributes × 2 values each
8298
- * :is(A1, A2):is(B1, B2) instead of :is(A1B1, A1B2, A2B1, A2B2)
8605
+ * This is critical for correct CSS generation. For `!A | !B` where A is at-rule
8606
+ * and B is modifier, we want:
8607
+ * - Branch 0: !A (at-rule negation - covers "no @supports/media" case)
8608
+ * - Branch 1: A & !B (modifier negation with at-rule context)
8609
+ *
8610
+ * If we process in wrong order (!B first), we'd get:
8611
+ * - Branch 0: !B (modifier negation WITHOUT at-rule context - WRONG!)
8612
+ * - Branch 1: B & !A (at-rule negation with modifier - incomplete coverage)
8299
8613
  */
8300
- function tryFactorIntoDimensions(branches) {
8301
- if (branches.length < 4) return null;
8302
- const dimensions = /* @__PURE__ */ new Map();
8303
- for (const branch of branches) for (const cond of branch) {
8304
- if (!("attribute" in cond)) return null;
8305
- if (!dimensions.has(cond.attribute)) dimensions.set(cond.attribute, /* @__PURE__ */ new Map());
8306
- dimensions.get(cond.attribute).set(getModifierKey(cond), cond);
8307
- }
8308
- if (dimensions.size < 2) return null;
8309
- for (const branch of branches) {
8310
- const seen = /* @__PURE__ */ new Set();
8311
- for (const cond of branch) {
8312
- const attr = cond.attribute;
8313
- if (seen.has(attr)) return null;
8314
- seen.add(attr);
8315
- }
8316
- if (seen.size !== dimensions.size) return null;
8317
- }
8318
- let expectedCount = 1;
8319
- for (const vals of dimensions.values()) expectedCount *= vals.size;
8320
- if (branches.length !== expectedCount) return null;
8321
- return [...dimensions.values()].map((vals) => ({
8322
- branches: [...vals.values()].map((cond) => [cond]),
8323
- negated: false
8324
- }));
8614
+ function sortOrBranchesForExpansion(branches) {
8615
+ return [...branches].sort((a, b) => {
8616
+ const aHasAtRule = hasAtRuleContext(a);
8617
+ const bHasAtRule = hasAtRuleContext(b);
8618
+ if (aHasAtRule && !bHasAtRule) return -1;
8619
+ if (!aHasAtRule && bHasAtRule) return 1;
8620
+ return 0;
8621
+ });
8325
8622
  }
8326
8623
  /**
8327
- * Build at-rules array from a variant
8624
+ * Expand ORs in a single entry's exclusive condition
8328
8625
  */
8329
- function buildAtRulesFromVariant(variant) {
8330
- const atRules = [];
8331
- if (variant.mediaConditions.length > 0) {
8332
- const conditionParts = variant.mediaConditions.map((c) => {
8333
- if (c.subtype === "type") return c.negated ? `not ${c.condition}` : c.condition;
8334
- else return c.negated ? `(not ${c.condition})` : c.condition;
8335
- });
8336
- atRules.push(`@media ${conditionParts.sort().join(" and ")}`);
8337
- }
8338
- if (variant.containerConditions.length > 0) {
8339
- const byName = /* @__PURE__ */ new Map();
8340
- for (const cond of variant.containerConditions) {
8341
- const group = byName.get(cond.name) || [];
8342
- group.push(cond);
8343
- byName.set(cond.name, group);
8344
- }
8345
- for (const [name, conditions] of byName) {
8346
- const conditionParts = conditions.map((c) => c.negated ? `(not ${c.condition})` : c.condition);
8347
- const namePrefix = name ? `${name} ` : "";
8348
- atRules.push(`@container ${namePrefix}${conditionParts.join(" and ")}`);
8626
+ function expandExclusiveConditionOrs(entry) {
8627
+ let orBranches = collectOrBranches(entry.exclusiveCondition);
8628
+ if (orBranches.length <= 1) return [entry];
8629
+ if (!branchesProduceDifferentContexts(orBranches)) return [entry];
8630
+ orBranches = sortOrBranchesForExpansion(orBranches);
8631
+ const result = [];
8632
+ const priorBranches = [];
8633
+ for (let i = 0; i < orBranches.length; i++) {
8634
+ const branch = orBranches[i];
8635
+ let exclusiveBranch = branch;
8636
+ for (const prior of priorBranches) exclusiveBranch = and(exclusiveBranch, not(prior));
8637
+ const simplified = simplifyCondition(exclusiveBranch);
8638
+ if (simplified.kind === "false") {
8639
+ priorBranches.push(branch);
8640
+ continue;
8349
8641
  }
8350
- }
8351
- if (variant.supportsConditions.length > 0) {
8352
- const conditionParts = variant.supportsConditions.map((c) => {
8353
- if (c.subtype === "selector") {
8354
- const selectorCond = `selector(${c.condition})`;
8355
- return c.negated ? `(not ${selectorCond})` : selectorCond;
8356
- } else {
8357
- const featureCond = `(${c.condition})`;
8358
- return c.negated ? `(not ${featureCond})` : featureCond;
8359
- }
8642
+ result.push({
8643
+ ...entry,
8644
+ stateKey: `${entry.stateKey}[or:${i}]`,
8645
+ exclusiveCondition: simplified
8360
8646
  });
8361
- atRules.push(`@supports ${conditionParts.join(" and ")}`);
8647
+ priorBranches.push(branch);
8362
8648
  }
8363
- return atRules;
8649
+ return result;
8364
8650
  }
8365
8651
  //#endregion
8366
8652
  //#region src/utils/case-converter.ts
@@ -8427,6 +8713,15 @@ function emitWarning(code, message) {
8427
8713
  const MAX_XOR_CHAIN_LENGTH = 4;
8428
8714
  const parseCache = new Lru(5e3);
8429
8715
  /**
8716
+ * Chrome-internal pseudo-classes (e.g. `:-internal-autofill-selected`,
8717
+ * `:-internal-autofill-previewed`) cannot be targeted from user CSS and
8718
+ * may invalidate the surrounding rule in Safari even when wrapped in
8719
+ * forgiving `:is(...)`. The regex matches both bare uses and references
8720
+ * inside enhanced pseudo arguments like `:is(:-webkit-autofill,
8721
+ * :-internal-autofill-selected)`.
8722
+ */
8723
+ const INTERNAL_PSEUDO_PATTERN = /:-internal-[a-z0-9-]+/g;
8724
+ /**
8430
8725
  * Pattern for tokenizing state notation.
8431
8726
  * Matches: operators, parentheses, @-prefixed states, value mods, boolean mods,
8432
8727
  * pseudo-classes, class selectors, and attribute selectors.
@@ -8619,7 +8914,15 @@ var Parser = class {
8619
8914
  return createPseudoCondition(value, false, value);
8620
8915
  }
8621
8916
  if (value.startsWith(".")) return createPseudoCondition(value, false, value);
8622
- if (value.startsWith("[")) return createPseudoCondition(value, false, value);
8917
+ if (value.startsWith("[")) {
8918
+ const attrMatch = /^\[\s*([a-zA-Z_][\w-]*)\s*(?:(=|\^=|\$=|\*=)\s*(?:"([^"]*)"|'([^']*)'|([^\]\s]+)))?\s*\]$/.exec(value);
8919
+ if (attrMatch) {
8920
+ const [, attribute, operator, dq, sq, bare] = attrMatch;
8921
+ if (operator === void 0) return createModifierCondition(attribute, void 0, "=", false, value);
8922
+ return createModifierCondition(attribute, dq ?? sq ?? bare, operator, false, value);
8923
+ }
8924
+ return createPseudoCondition(value, false, value);
8925
+ }
8623
8926
  if (value.includes("=")) return this.parseValueModifier(value);
8624
8927
  return this.parseBooleanModifier(value);
8625
8928
  }
@@ -8844,6 +9147,14 @@ function parseStateKey(stateKey, options = {}) {
8844
9147
  const cacheKey = trimmed + "\0" + (options.isSubElement ? "1" : "0") + "\0" + localStatesKey;
8845
9148
  const cached = parseCache.get(cacheKey);
8846
9149
  if (cached) return cached;
9150
+ if (isDevEnv()) {
9151
+ INTERNAL_PSEUDO_PATTERN.lastIndex = 0;
9152
+ const internalMatches = trimmed.match(INTERNAL_PSEUDO_PATTERN);
9153
+ if (internalMatches && internalMatches.length > 0) {
9154
+ const unique = Array.from(new Set(internalMatches));
9155
+ emitWarning("INTERNAL_PSEUDO_USED", `State key "${trimmed}" references internal pseudo-class${unique.length > 1 ? "es" : ""} ${unique.map((p) => `\`${p}\``).join(", ")}. These are unmatchable from user CSS and can invalidate the surrounding rule in Safari (even inside \`:is(...)\`). Use \`:-webkit-autofill | :autofill\` instead for autofill states.`);
9156
+ }
9157
+ }
8847
9158
  const result = new Parser(tokenize(trimmed), options).parse();
8848
9159
  parseCache.set(cacheKey, result);
8849
9160
  return result;
@@ -8913,7 +9224,15 @@ function runPipeline(styles, parserContext) {
8913
9224
  function processStyles(styles, selectorSuffix, parserContext, allRules) {
8914
9225
  const keys = Object.keys(styles);
8915
9226
  const selectorKeys = keys.filter((key) => isSelector(key));
8916
- const styleKeys = keys.filter((key) => !isSelector(key) && !key.startsWith("@"));
9227
+ const styleKeys = [];
9228
+ for (const key of keys) {
9229
+ if (isSelector(key) || key.startsWith("@")) continue;
9230
+ if (key.startsWith(":")) {
9231
+ emitWarning("INVALID_TOP_LEVEL_PSEUDO_KEY", `Style key "${key}" starts with ':' which is not a valid Tasty style key. Use "&${key}" for nested-selector form, or move the state into a value map (e.g. \`{ color: { '${key}': value } }\`). The key has been ignored.`);
9232
+ continue;
9233
+ }
9234
+ styleKeys.push(key);
9235
+ }
8917
9236
  processNestedSelectors(styles, selectorKeys, selectorSuffix, parserContext, allRules);
8918
9237
  processHandlerQueue(buildHandlerQueue(styleKeys, styles), selectorSuffix, parserContext, allRules);
8919
9238
  }
@@ -9796,7 +10115,8 @@ function createDefaultConfig(isTest) {
9796
10115
  return {
9797
10116
  maxRulesPerSheet: 8192,
9798
10117
  forceTextInjection: isTest ?? false,
9799
- devMode: isDevEnv()
10118
+ devMode: isDevEnv(),
10119
+ namePrefix: "t"
9800
10120
  };
9801
10121
  }
9802
10122
  /**
@@ -10038,6 +10358,7 @@ function configure(config = {}) {
10038
10358
  warnOnce("configure-after-styles", "[Tasty] Cannot call configure() after styles have been generated.\nConfiguration must be done before the first render. The configuration will be ignored.");
10039
10359
  return;
10040
10360
  }
10361
+ if (config.namePrefix !== void 0) validateNamePrefix(config.namePrefix);
10041
10362
  let mergedStates = {};
10042
10363
  let mergedUnits = {};
10043
10364
  let mergedFuncs = {};
@@ -10189,6 +10510,17 @@ function getConfig() {
10189
10510
  return currentConfig;
10190
10511
  }
10191
10512
  /**
10513
+ * Get the configured prefix used for every generated identifier
10514
+ * (class names, keyframe names, counter-style names).
10515
+ *
10516
+ * Falls back to the default prefix (`'t'`) when `configure()` has not
10517
+ * been called yet — this matches the auto-configuration behavior used
10518
+ * by the rest of the system.
10519
+ */
10520
+ function getNamePrefix() {
10521
+ return currentConfig?.namePrefix ?? "t";
10522
+ }
10523
+ /**
10192
10524
  * Get the global injector instance.
10193
10525
  * Auto-configures with defaults if not already configured.
10194
10526
  */
@@ -10222,6 +10554,6 @@ function resetConfig() {
10222
10554
  delete storage[GLOBAL_INJECTOR_KEY];
10223
10555
  }
10224
10556
  //#endregion
10225
- export { parseStyle as $, extractLocalCounterStyle as A, deprecationWarning as B, camelToKebab as C, Lru as Ct, getGlobalPredefinedStates as D, extractPredefinedStateRefs as E, formatFontFaceRule as F, DIRECTIONS as G, createStyle as H, hasLocalFontFace as I, getGlobalFuncs as J, customFunc as K, SheetManager as L, hasLocalCounterStyle as M, extractLocalFontFace as N, setGlobalPredefinedStates as O, fontFaceContentHash as P, parseColor as Q, STYLE_HANDLER_MAP as R, parseStateKey as S, strToRgb as St, extractLocalPredefinedStates as T, PropertyTypeResolver as U, warn as V, CUSTOM_UNITS as W, getGlobalPredefinedTokens as X, getGlobalParser as Y, normalizeColorTokenValue as Z, resetConfig as _, getComponentPropertySyntax as _t, getGlobalCounterStyle as a, StyleParser as at, isSelector as b, hexToRgb as bt, getGlobalKeyframes as c, hashString as ct, hasGlobalKeyframes as d, hasLocalProperties as dt, resetGlobalPredefinedTokens as et, hasGlobalRecipes as f, parsePropertyToken as ft, markStylesGenerated as g, getColorSpaceSuffix as gt, isTestEnvironment as h, getColorSpaceFunc as ht, getGlobalConfigTokens as i, okhslPlugin as it, formatCounterStyleRule as j, StyleInjector as k, getGlobalRecipes as l, extractLocalProperties as lt, isConfigLocked as m, getColorSpaceComponents as mt, getConfig as n, stringifyStyles as nt, getGlobalFontFace as o, Bucket as ot, hasStylesGenerated as p, colorInitialValueToComponents as pt, filterMods as q, getEffectiveProperties as r, okhslFunc as rt, getGlobalInjector as s, isDevEnv as st, configure as t, setGlobalPredefinedTokens as tt, getGlobalStyles as u, getEffectiveDefinition as ut, generateTypographyTokens as v, getNamedColorHex as vt, createStateParserContext as w, renderStyles as x, hslToRgbValues as xt, hasPipelineCacheEntry as y, getRgbValuesFromRgbaString as yt, styleHandlers as z };
10557
+ export { parseColor as $, StyleInjector as A, strToRgb as At, styleHandlers as B, parseStateKey as C, getColorSpaceFunc as Ct, extractPredefinedStateRefs as D, getRgbValuesFromRgbaString as Dt, extractLocalPredefinedStates as E, getNamedColorHex as Et, fontFaceContentHash as F, CUSTOM_UNITS as G, warn as H, formatFontFaceRule as I, filterMods as J, DIRECTIONS as K, hasLocalFontFace as L, formatCounterStyleRule as M, hasLocalCounterStyle as N, getGlobalPredefinedStates as O, hexToRgb as Ot, extractLocalFontFace as P, normalizeColorTokenValue as Q, SheetManager as R, renderStyles as S, getColorSpaceComponents as St, createStateParserContext as T, getComponentPropertySyntax as Tt, createStyle as U, deprecationWarning as V, PropertyTypeResolver as W, getGlobalParser as X, getGlobalFuncs as Y, getGlobalPredefinedTokens as Z, markStylesGenerated as _, extractLocalProperties as _t, getGlobalCounterStyle as a, okhslPlugin as at, hasPipelineCacheEntry as b, parsePropertyToken as bt, getGlobalKeyframes as c, DEFAULT_NAME_PREFIX as ct, getNamePrefix as d, makeCounterStyleName as dt, parseStyle as et, hasGlobalKeyframes as f, makeKeyframeName as ft, isTestEnvironment as g, hashString as gt, isConfigLocked as h, isDevEnv as ht, getGlobalConfigTokens as i, okhslFunc as it, extractLocalCounterStyle as j, Lru as jt, setGlobalPredefinedStates as k, hslToRgbValues as kt, getGlobalRecipes as l, DEFAULT_ZERO_NAME_PREFIX as lt, hasStylesGenerated as m, validateNamePrefix as mt, getConfig as n, setGlobalPredefinedTokens as nt, getGlobalFontFace as o, StyleParser as ot, hasGlobalRecipes as p, tastyClassRegex as pt, customFunc as q, getEffectiveProperties as r, stringifyStyles as rt, getGlobalInjector as s, Bucket as st, configure as t, resetGlobalPredefinedTokens as tt, getGlobalStyles as u, makeClassName as ut, resetConfig as v, getEffectiveDefinition as vt, camelToKebab as w, getColorSpaceSuffix as wt, isSelector as x, colorInitialValueToComponents as xt, generateTypographyTokens as y, hasLocalProperties as yt, STYLE_HANDLER_MAP as z };
10226
10558
 
10227
- //# sourceMappingURL=config-raGoEeGs.js.map
10559
+ //# sourceMappingURL=config-BBiyxMCe.js.map