@tenphi/tasty 0.0.0-snapshot.4d272f6 → 0.0.0-snapshot.4e9dfa4

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 (68) hide show
  1. package/README.md +10 -5
  2. package/dist/async-storage-B7_o6FKt.js.map +1 -1
  3. package/dist/{collector-DM5o7SIi.js → collector-Crs9kGJW.js} +24 -11
  4. package/dist/collector-Crs9kGJW.js.map +1 -0
  5. package/dist/{collector-DMoQRxPK.d.ts → collector-osfWTeRd.d.ts} +12 -2
  6. package/dist/{config-CtJeETqW.js → config-BaxtQMS3.js} +987 -651
  7. package/dist/config-BaxtQMS3.js.map +1 -0
  8. package/dist/{config-DdnOynVC.d.ts → config-BoZDUHW5.d.ts} +65 -4
  9. package/dist/context-CkSg-kDT.js.map +1 -1
  10. package/dist/core/index.d.ts +5 -5
  11. package/dist/core/index.js +6 -6
  12. package/dist/{core-CD2f33ch.js → core-CmxaoZ-N.js} +18 -12
  13. package/dist/core-CmxaoZ-N.js.map +1 -0
  14. package/dist/{css-writer-CUbBb8gL.js → css-writer-CPy_cbFJ.js} +28 -6
  15. package/dist/css-writer-CPy_cbFJ.js.map +1 -0
  16. package/dist/format-global-rules-Dbc_1tc3.js.map +1 -1
  17. package/dist/{format-rules-BFgS3646.js → format-rules-CPirO_Mj.js} +2 -2
  18. package/dist/{format-rules-BFgS3646.js.map → format-rules-CPirO_Mj.js.map} +1 -1
  19. package/dist/{hydrate-BZ1SUmKW.js → hydrate-CVn-A_3y.js} +2 -2
  20. package/dist/{hydrate-BZ1SUmKW.js.map → hydrate-CVn-A_3y.js.map} +1 -1
  21. package/dist/{index-C-4JKlJ1.d.ts → index-B_k47mc_.d.ts} +74 -21
  22. package/dist/{index-DTXQTYHU.d.ts → index-tcHuMPFt.d.ts} +24 -3
  23. package/dist/index.d.ts +5 -5
  24. package/dist/index.js +10 -10
  25. package/dist/index.js.map +1 -1
  26. package/dist/{keyframes-DNpu5N3R.js → keyframes-vpzoVdUR.js} +3 -2
  27. package/dist/{keyframes-DNpu5N3R.js.map → keyframes-vpzoVdUR.js.map} +1 -1
  28. package/dist/{merge-styles-Dwdz1IsM.d.ts → merge-styles-BMWcH6MF.d.ts} +2 -2
  29. package/dist/{merge-styles-QCiE0R2O.js → merge-styles-BzQutdAK.js} +2 -2
  30. package/dist/{merge-styles-QCiE0R2O.js.map → merge-styles-BzQutdAK.js.map} +1 -1
  31. package/dist/{resolve-recipes-CHSdSpso.js → resolve-recipes-DgH8A3Nn.js} +3 -3
  32. package/dist/{resolve-recipes-CHSdSpso.js.map → resolve-recipes-DgH8A3Nn.js.map} +1 -1
  33. package/dist/ssr/astro-client.js +1 -1
  34. package/dist/ssr/astro-client.js.map +1 -1
  35. package/dist/ssr/astro-middleware.js.map +1 -1
  36. package/dist/ssr/astro.d.ts +1 -12
  37. package/dist/ssr/astro.js +3 -3
  38. package/dist/ssr/astro.js.map +1 -1
  39. package/dist/ssr/index.d.ts +1 -1
  40. package/dist/ssr/index.js +3 -3
  41. package/dist/ssr/next.d.ts +1 -1
  42. package/dist/ssr/next.js +4 -4
  43. package/dist/ssr/next.js.map +1 -1
  44. package/dist/static/index.d.ts +2 -2
  45. package/dist/static/index.js +1 -1
  46. package/dist/static/index.js.map +1 -1
  47. package/dist/static/inject.js.map +1 -1
  48. package/dist/zero/babel.d.ts +1 -1
  49. package/dist/zero/babel.js +16 -8
  50. package/dist/zero/babel.js.map +1 -1
  51. package/dist/zero/index.d.ts +1 -1
  52. package/dist/zero/index.js +1 -1
  53. package/dist/zero/next.js.map +1 -1
  54. package/docs/configuration.md +44 -0
  55. package/docs/dsl.md +14 -12
  56. package/docs/injector.md +2 -2
  57. package/docs/methodology.md +26 -0
  58. package/docs/pipeline.md +40 -14
  59. package/docs/react-api.md +24 -0
  60. package/docs/ssr.md +5 -3
  61. package/docs/styles.md +9 -7
  62. package/docs/tasty-static.md +15 -0
  63. package/package.json +10 -9
  64. package/tasty.config.ts +1 -0
  65. package/dist/collector-DM5o7SIi.js.map +0 -1
  66. package/dist/config-CtJeETqW.js.map +0 -1
  67. package/dist/core-CD2f33ch.js.map +0 -1
  68. package/dist/css-writer-CUbBb8gL.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";
@@ -1439,7 +1527,7 @@ function resolveUntilStable(value, opts, recurse, maxIterations = 10) {
1439
1527
  function classify(raw, opts, recurse) {
1440
1528
  const token = raw.trim();
1441
1529
  if (!token) return {
1442
- bucket: Bucket.Mod,
1530
+ bucket: 2,
1443
1531
  processed: ""
1444
1532
  };
1445
1533
  {
@@ -1461,31 +1549,31 @@ function classify(raw, opts, recurse) {
1461
1549
  if (depth !== 0) {
1462
1550
  console.warn("tasty: skipped invalid function token with unmatched parentheses:", token);
1463
1551
  return {
1464
- bucket: Bucket.Mod,
1552
+ bucket: 2,
1465
1553
  processed: ""
1466
1554
  };
1467
1555
  }
1468
1556
  }
1469
1557
  if (token.startsWith("\"") && token.endsWith("\"") || token.startsWith("'") && token.endsWith("'")) return {
1470
- bucket: Bucket.Value,
1558
+ bucket: 1,
1471
1559
  processed: token
1472
1560
  };
1473
1561
  if (token.startsWith("$$")) {
1474
1562
  const name = token.slice(2);
1475
1563
  if (/^[a-z_][a-z0-9-_]*$/i.test(name)) return {
1476
- bucket: Bucket.Value,
1564
+ bucket: 1,
1477
1565
  processed: `--${name}`
1478
1566
  };
1479
1567
  }
1480
1568
  if (token.startsWith("##")) {
1481
1569
  const name = token.slice(2);
1482
1570
  if (/^[a-z_][a-z0-9-_]*$/i.test(name)) return {
1483
- bucket: Bucket.Value,
1571
+ bucket: 1,
1484
1572
  processed: `--${name}-color`
1485
1573
  };
1486
1574
  }
1487
1575
  if (token === "#current") return {
1488
- bucket: Bucket.Color,
1576
+ bucket: 0,
1489
1577
  processed: "currentcolor"
1490
1578
  };
1491
1579
  const currentAlphaMatch = token.match(/^#current\.(\$[a-z_][a-z0-9-_]*|[0-9]+)$/i);
@@ -1496,7 +1584,7 @@ function classify(raw, opts, recurse) {
1496
1584
  else if (rawAlpha === "0") percentage = "0%";
1497
1585
  else percentage = `${parseFloat("." + rawAlpha) * 100}%`;
1498
1586
  return {
1499
- bucket: Bucket.Color,
1587
+ bucket: 0,
1500
1588
  processed: `color-mix(in oklab, currentcolor ${percentage}, transparent)`
1501
1589
  };
1502
1590
  }
@@ -1569,7 +1657,7 @@ function classify(raw, opts, recurse) {
1569
1657
  const constructed = `${normalizedFunc}(${hasModernAlpha || hasLegacyAlpha ? normalizeArgs(hasModernAlpha ? args.slice(0, slashIdx).trim() : args.slice(0, lastTopLevelComma).trim()) : normalizeArgs(args)} / ${alpha})`;
1570
1658
  if (!COLOR_FUNCS.has(normalizedFunc) && opts.funcs && normalizedFunc in opts.funcs) return classify(constructed, opts, recurse);
1571
1659
  return {
1572
- bucket: Bucket.Color,
1660
+ bucket: 0,
1573
1661
  processed: constructed
1574
1662
  };
1575
1663
  }
@@ -1580,11 +1668,11 @@ function classify(raw, opts, recurse) {
1580
1668
  }
1581
1669
  }
1582
1670
  if (token.match(/^var\(--([a-z0-9-]+)-color\)$/)) return {
1583
- bucket: Bucket.Color,
1671
+ bucket: 0,
1584
1672
  processed: token
1585
1673
  };
1586
1674
  if (token.startsWith("url(")) return {
1587
- bucket: Bucket.Value,
1675
+ bucket: 1,
1588
1676
  processed: token
1589
1677
  };
1590
1678
  if (token[0] === "$") {
@@ -1593,7 +1681,7 @@ function classify(raw, opts, recurse) {
1593
1681
  const name = identMatch[1];
1594
1682
  const processed = `var(--${name})`;
1595
1683
  return {
1596
- bucket: name.endsWith("-color") ? Bucket.Color : Bucket.Value,
1684
+ bucket: name.endsWith("-color") ? 0 : 1,
1597
1685
  processed
1598
1686
  };
1599
1687
  }
@@ -1607,17 +1695,17 @@ function classify(raw, opts, recurse) {
1607
1695
  else if (rawAlpha === "0") alpha = "0";
1608
1696
  else alpha = `.${rawAlpha}`;
1609
1697
  return {
1610
- bucket: Bucket.Color,
1698
+ bucket: 0,
1611
1699
  processed: `${getColorSpaceFunc()}(var(--${base}-color-${getColorSpaceSuffix()}) / ${alpha})`
1612
1700
  };
1613
1701
  }
1614
1702
  const name = token.slice(1);
1615
1703
  if (RE_HEX.test(name)) return {
1616
- bucket: Bucket.Color,
1704
+ bucket: 0,
1617
1705
  processed: `var(--${name}-color, #${name})`
1618
1706
  };
1619
1707
  return {
1620
- bucket: Bucket.Color,
1708
+ bucket: 0,
1621
1709
  processed: `var(--${name}-color)`
1622
1710
  };
1623
1711
  }
@@ -1628,7 +1716,7 @@ function classify(raw, opts, recurse) {
1628
1716
  if (COLOR_FUNCS.has(fname)) {
1629
1717
  const argProcessed = recurse(inner).output.replace(/,\s+/g, ",");
1630
1718
  return {
1631
- bucket: Bucket.Color,
1719
+ bucket: 0,
1632
1720
  processed: `${canonicalFuncName(fname)}(${argProcessed})`
1633
1721
  };
1634
1722
  }
@@ -1641,7 +1729,7 @@ function classify(raw, opts, recurse) {
1641
1729
  }
1642
1730
  const argProcessed = recurse(inner).output;
1643
1731
  return {
1644
- bucket: Bucket.Value,
1732
+ bucket: 1,
1645
1733
  processed: `${canonicalFuncName(fname)}(${argProcessed})`
1646
1734
  };
1647
1735
  }
@@ -1649,10 +1737,9 @@ function classify(raw, opts, recurse) {
1649
1737
  const colorMatch = token.slice(1, -1).match(/^#([a-z0-9-]+)\s*,\s*(.*)$/i);
1650
1738
  if (colorMatch) {
1651
1739
  const [, name, fallback] = colorMatch;
1652
- const processedFallback = recurse(fallback).output;
1653
1740
  return {
1654
- bucket: Bucket.Color,
1655
- processed: `var(--${name}-color, ${processedFallback})`
1741
+ bucket: 0,
1742
+ processed: `var(--${name}-color, ${recurse(fallback).output})`
1656
1743
  };
1657
1744
  }
1658
1745
  }
@@ -1662,18 +1749,15 @@ function classify(raw, opts, recurse) {
1662
1749
  const [, name, fallback] = match;
1663
1750
  const processedFallback = recurse(fallback).output;
1664
1751
  return {
1665
- bucket: name.endsWith("-color") ? Bucket.Color : Bucket.Value,
1752
+ bucket: name.endsWith("-color") ? 0 : 1,
1666
1753
  processed: `var(--${name}, ${processedFallback})`
1667
1754
  };
1668
1755
  }
1669
1756
  }
1670
- if (token[0] === "(" && token[token.length - 1] === ")") {
1671
- const innerProcessed = recurse(token.slice(1, -1)).output;
1672
- return {
1673
- bucket: Bucket.Value,
1674
- processed: `calc(${innerProcessed})`
1675
- };
1676
- }
1757
+ if (token[0] === "(" && token[token.length - 1] === ")") return {
1758
+ bucket: 1,
1759
+ processed: `calc(${recurse(token.slice(1, -1)).output})`
1760
+ };
1677
1761
  const um = token.match(RE_UNIT_NUM);
1678
1762
  if (um) {
1679
1763
  const unit = um[1];
@@ -1683,47 +1767,43 @@ function classify(raw, opts, recurse) {
1683
1767
  const rawMatch = handler.match(RE_RAW_UNIT);
1684
1768
  if (rawMatch) {
1685
1769
  const [, baseNum, cssUnit] = rawMatch;
1686
- const resolved = resolveUntilStable(`${numericPart * parseFloat(baseNum)}${cssUnit}`, opts, recurse);
1687
1770
  return {
1688
- bucket: Bucket.Value,
1689
- processed: resolved
1771
+ bucket: 1,
1772
+ processed: resolveUntilStable(`${numericPart * parseFloat(baseNum)}${cssUnit}`, opts, recurse)
1690
1773
  };
1691
1774
  }
1692
1775
  const base = handler;
1693
1776
  if (numericPart === 1) return {
1694
- bucket: Bucket.Value,
1777
+ bucket: 1,
1695
1778
  processed: base
1696
1779
  };
1697
1780
  return {
1698
- bucket: Bucket.Value,
1781
+ bucket: 1,
1699
1782
  processed: `calc(${numericPart} * ${base})`
1700
1783
  };
1701
- } else {
1702
- const inner = handler(numericPart);
1703
- return {
1704
- bucket: Bucket.Value,
1705
- processed: inner
1706
- };
1707
- }
1784
+ } else return {
1785
+ bucket: 1,
1786
+ processed: handler(numericPart)
1787
+ };
1708
1788
  }
1709
1789
  if (/^[+-]?(?:\d*\.\d+|\d+)[a-z%]+$/.test(token)) return {
1710
- bucket: Bucket.Value,
1790
+ bucket: 1,
1711
1791
  processed: token
1712
1792
  };
1713
1793
  if (RE_NUMBER.test(token)) return {
1714
- bucket: Bucket.Value,
1794
+ bucket: 1,
1715
1795
  processed: token
1716
1796
  };
1717
1797
  if (VALUE_KEYWORDS.has(token)) return {
1718
- bucket: Bucket.Value,
1798
+ bucket: 1,
1719
1799
  processed: token
1720
1800
  };
1721
1801
  if (token === "transparent" || token === "currentcolor") return {
1722
- bucket: Bucket.Color,
1802
+ bucket: 0,
1723
1803
  processed: token
1724
1804
  };
1725
1805
  return {
1726
- bucket: Bucket.Mod,
1806
+ bucket: 2,
1727
1807
  processed: token
1728
1808
  };
1729
1809
  }
@@ -1796,6 +1876,7 @@ function scan(src, cb) {
1796
1876
  //#endregion
1797
1877
  //#region src/parser/parser.ts
1798
1878
  var StyleParser = class {
1879
+ opts;
1799
1880
  cache;
1800
1881
  constructor(opts = {}) {
1801
1882
  this.opts = opts;
@@ -1823,13 +1904,13 @@ var StyleParser = class {
1823
1904
  return;
1824
1905
  }
1825
1906
  switch (bucket) {
1826
- case Bucket.Color:
1907
+ case 0:
1827
1908
  currentPart.colors.push(processed);
1828
1909
  break;
1829
- case Bucket.Value:
1910
+ case 1:
1830
1911
  currentPart.values.push(processed);
1831
1912
  break;
1832
- case Bucket.Mod:
1913
+ case 2:
1833
1914
  currentPart.mods.push(processed);
1834
1915
  break;
1835
1916
  }
@@ -3242,10 +3323,13 @@ function resolveFontFamily(font, fontFamily) {
3242
3323
  /**
3243
3324
  * Handles typography preset and individual font properties.
3244
3325
  *
3245
- * Preset syntax uses `/` to separate name from modifier:
3326
+ * Preset syntax uses `/` to separate the name from one or more
3327
+ * space-separated modifiers:
3246
3328
  * - `preset="h1"` — name only
3247
3329
  * - `preset="h2 / strong"` — name + modifier
3330
+ * - `preset="h2 / strong italic"` — name + multiple modifiers
3248
3331
  * - `preset="bold"` — modifier-only shorthand (name defaults to `inherit`)
3332
+ * - `preset="bold italic"` — modifier-only shorthand with multiple modifiers
3249
3333
  *
3250
3334
  * When `preset` is defined, it sets up CSS custom properties for typography.
3251
3335
  * Individual font props can be used with or without `preset`:
@@ -3265,14 +3349,17 @@ function presetStyle({ preset, fontSize, lineHeight, textTransform, letterSpacin
3265
3349
  const { parts } = parseStyle(preset === true ? "" : String(preset)).groups[0] ?? { parts: [] };
3266
3350
  const namePart = parts[0];
3267
3351
  const modPart = parts[1];
3352
+ const nameTokens = namePart?.all ?? [];
3353
+ const isModOnly = nameTokens.length > 0 && nameTokens.every((t) => PRESET_MODIFIERS.has(t));
3268
3354
  const nameToken = namePart?.mods[0] ?? namePart?.values[0] ?? "";
3269
- const isModOnly = PRESET_MODIFIERS.has(nameToken);
3270
3355
  const name = isModOnly ? "inherit" : nameToken || "inherit";
3271
- const modifier = isModOnly ? nameToken : modPart?.mods[0] ?? "";
3272
- const isStrong = modifier === "strong" || modifier === "bold";
3273
- const isItalic = modifier === "italic";
3274
- const isIcon = modifier === "icon";
3275
- 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");
3276
3363
  if (fontSize == null) setCSSValue(styles, "font-size", name, { cssOnly: true });
3277
3364
  if (lineHeight == null) setCSSValue(styles, "line-height", name, { cssOnly: true });
3278
3365
  if (letterSpacing == null) setCSSValue(styles, "letter-spacing", name, { cssOnly: true });
@@ -4137,7 +4224,7 @@ var SheetManager = class {
4137
4224
  }
4138
4225
  }
4139
4226
  if (!anyInserted) {}
4140
- } 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);
4141
4228
  }
4142
4229
  } else if (styleElement) {
4143
4230
  const atomicRuleIndex = this.findAvailableRuleIndex(targetSheet);
@@ -4289,6 +4376,37 @@ var SheetManager = class {
4289
4376
  return sheet.ruleCount;
4290
4377
  }
4291
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
+ /**
4292
4410
  * Force cleanup of unused styles
4293
4411
  */
4294
4412
  forceCleanup(registry) {
@@ -4809,18 +4927,10 @@ function formatCounterStyleRule(name, descriptors) {
4809
4927
  //#endregion
4810
4928
  //#region src/injector/injector.ts
4811
4929
  /**
4812
- * Generate a deterministic class name from a cache key using content hash.
4813
- * The same cache key always produces the same class name across environments.
4814
- */
4815
- function generateClassName(cacheKey) {
4816
- return `t${hashString(cacheKey)}`;
4817
- }
4818
- const RSC_CLASS_RE = /\.(t[a-z0-9]+)\.\1/g;
4819
- /**
4820
4930
  * Extract class names from `<style data-tasty-rsc>` tags.
4821
4931
  * The doubled-specificity pattern `.tXXX.tXXX` makes extraction reliable.
4822
4932
  */
4823
- function extractRSCClassNames() {
4933
+ function extractRSCClassNames(rscClassRegex) {
4824
4934
  if (typeof document === "undefined") return [];
4825
4935
  const styles = document.querySelectorAll("style[data-tasty-rsc]");
4826
4936
  if (styles.length === 0) return [];
@@ -4829,8 +4939,8 @@ function extractRSCClassNames() {
4829
4939
  const text = style.textContent;
4830
4940
  if (!text) continue;
4831
4941
  let match;
4832
- RSC_CLASS_RE.lastIndex = 0;
4833
- 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]);
4834
4944
  }
4835
4945
  return Array.from(classSet);
4836
4946
  }
@@ -4844,7 +4954,7 @@ function extractRSCClassNames() {
4844
4954
  * Called inside `inject()` / `allocateClassName()` to pick up
4845
4955
  * class names rendered on the server (including during SPA navigation).
4846
4956
  */
4847
- function syncServerClasses(registry) {
4957
+ function syncServerClasses(registry, rscClassRegex) {
4848
4958
  if (typeof window === "undefined") return;
4849
4959
  const classes = window.__TASTY__;
4850
4960
  if (classes && classes.length > registry.serverClassSyncIndex) {
@@ -4853,7 +4963,7 @@ function syncServerClasses(registry) {
4853
4963
  }
4854
4964
  if (!registry.rscStylesScanned) {
4855
4965
  registry.rscStylesScanned = true;
4856
- for (const cls of extractRSCClassNames()) registerHydratedClass(registry, cls);
4966
+ for (const cls of extractRSCClassNames(rscClassRegex)) registerHydratedClass(registry, cls);
4857
4967
  }
4858
4968
  }
4859
4969
  function registerHydratedClass(registry, className) {
@@ -4865,25 +4975,40 @@ function registerHydratedClass(registry, className) {
4865
4975
  });
4866
4976
  registry.refCounts.set(className, 0);
4867
4977
  }
4868
- var StyleInjector = class StyleInjector {
4978
+ var StyleInjector = class {
4869
4979
  sheetManager;
4870
4980
  config;
4871
4981
  globalRuleCounter = 0;
4872
4982
  pendingGCHandle = null;
4983
+ namePrefix;
4984
+ classRegex;
4985
+ rscClassRegex;
4873
4986
  /** @internal — exposed for debug utilities only */
4874
4987
  get _sheetManager() {
4875
4988
  return this.sheetManager;
4876
4989
  }
4877
4990
  constructor(config = {}) {
4991
+ if (config.namePrefix !== void 0) validateNamePrefix(config.namePrefix);
4878
4992
  this.config = config;
4879
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));
4880
5005
  }
4881
5006
  /**
4882
5007
  * Check if `className` was hydrated from server-rendered styles and,
4883
5008
  * if so, wire the cacheKey mapping. Returns true on hit.
4884
5009
  */
4885
5010
  tryHydratedHit(registry, cacheKey, className) {
4886
- syncServerClasses(registry);
5011
+ syncServerClasses(registry, this.rscClassRegex);
4887
5012
  const rule = registry.rules.get(className);
4888
5013
  if (rule && rule.ruleIndex === -2 && rule.sheetIndex === -2) {
4889
5014
  registry.cacheKeyToClassName.set(cacheKey, className);
@@ -4902,7 +5027,7 @@ var StyleInjector = class StyleInjector {
4902
5027
  className: registry.cacheKeyToClassName.get(cacheKey),
4903
5028
  isNewAllocation: false
4904
5029
  };
4905
- const className = generateClassName(cacheKey);
5030
+ const className = this.generateClassName(cacheKey);
4906
5031
  if (this.tryHydratedHit(registry, cacheKey, className)) return {
4907
5032
  className,
4908
5033
  isNewAllocation: false
@@ -4954,7 +5079,7 @@ var StyleInjector = class StyleInjector {
4954
5079
  };
4955
5080
  }
4956
5081
  } else if (cacheKey) {
4957
- className = generateClassName(cacheKey);
5082
+ className = this.generateClassName(cacheKey);
4958
5083
  if (this.tryHydratedHit(registry, cacheKey, className)) {
4959
5084
  registry.refCounts.set(className, (registry.refCounts.get(className) || 0) + 1);
4960
5085
  if (registry.metrics) registry.metrics.hits++;
@@ -4963,7 +5088,10 @@ var StyleInjector = class StyleInjector {
4963
5088
  dispose: () => this.dispose(className, registry)
4964
5089
  };
4965
5090
  }
4966
- } 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
+ }
4967
5095
  const rulesToInsert = rules.map((rule) => {
4968
5096
  let newSelector = rule.selector;
4969
5097
  if (rule.needsClassName) {
@@ -5181,8 +5309,23 @@ var StyleInjector = class StyleInjector {
5181
5309
  }
5182
5310
  const cssName = effectiveResult.cssName;
5183
5311
  const definition = effectiveResult.definition;
5184
- const normalizedDef = normalizePropertyDefinition(definition);
5185
- 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;
5186
5329
  const parts = [];
5187
5330
  if (definition.syntax != null) {
5188
5331
  let syntax = String(definition.syntax).trim();
@@ -5202,8 +5345,8 @@ var StyleInjector = class StyleInjector {
5202
5345
  selector: `@property ${cssName}`,
5203
5346
  declarations
5204
5347
  };
5205
- if (!this.sheetManager.insertGlobalRule(registry, [rule], `property:${name}`, root)) return;
5206
- registry.injectedProperties.set(cssName, normalizedDef);
5348
+ registry.injectedProperties.set(cssName, normalizePropertyDefinition(definition));
5349
+ this.sheetManager.insertGlobalRule(registry, [rule], `property:${cacheKey}`, root);
5207
5350
  }
5208
5351
  /**
5209
5352
  * Check whether a given @property name was already injected by this injector.
@@ -5283,12 +5426,12 @@ var StyleInjector = class StyleInjector {
5283
5426
  let actualName;
5284
5427
  if (providedName) {
5285
5428
  const existingContentForName = registry.keyframesNameToContent.get(providedName);
5286
- if (existingContentForName && existingContentForName !== contentHash) actualName = `${providedName}-k${registry.keyframesCounter++}`;
5429
+ if (existingContentForName && existingContentForName !== contentHash) actualName = `${providedName}-${makeKeyframeName(this.namePrefix, String(registry.keyframesCounter++))}`;
5287
5430
  else {
5288
5431
  actualName = providedName;
5289
5432
  registry.keyframesNameToContent.set(providedName, contentHash);
5290
5433
  }
5291
- } else actualName = `k${registry.keyframesCounter++}`;
5434
+ } else actualName = makeKeyframeName(this.namePrefix, String(registry.keyframesCounter++));
5292
5435
  const result = this.sheetManager.insertKeyframes(registry, steps, actualName, root);
5293
5436
  if (!result) return {
5294
5437
  toString: () => "",
@@ -5337,7 +5480,6 @@ var StyleInjector = class StyleInjector {
5337
5480
  }
5338
5481
  }
5339
5482
  }
5340
- static TASTY_CLASS_RE = /^t[a-z0-9]+$/;
5341
5483
  /**
5342
5484
  * Record a render-time usage hit for one or more classNames.
5343
5485
  * Handles space-separated multi-chunk classNames.
@@ -5353,7 +5495,7 @@ var StyleInjector = class StyleInjector {
5353
5495
  const now = Date.now();
5354
5496
  const parts = className.indexOf(" ") === -1 ? [className] : className.split(" ");
5355
5497
  for (const cls of parts) {
5356
- if (!StyleInjector.TASTY_CLASS_RE.test(cls)) continue;
5498
+ if (!this.classRegex.test(cls)) continue;
5357
5499
  if (!registry.rules.has(cls)) continue;
5358
5500
  const entry = registry.usageMap.get(cls);
5359
5501
  if (entry) entry.lastTouchedAt = now;
@@ -5406,7 +5548,7 @@ var StyleInjector = class StyleInjector {
5406
5548
  if (registry.usageMap.size - activeCount <= capacity) return 0;
5407
5549
  }
5408
5550
  const liveClasses = /* @__PURE__ */ new Set();
5409
- 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);
5410
5552
  let swept = 0;
5411
5553
  if (force) for (const [className] of registry.usageMap) {
5412
5554
  if (liveClasses.has(className)) continue;
@@ -6089,6 +6231,7 @@ function simplifyAnd(children) {
6089
6231
  terms = sortTerms(terms);
6090
6232
  terms = applyAbsorptionAnd(terms);
6091
6233
  terms = applyConsensusAnd(terms);
6234
+ terms = pruneContradictedOrBranches(terms);
6092
6235
  if (terms.length === 0) return trueCondition();
6093
6236
  if (terms.length === 1) return terms[0];
6094
6237
  return {
@@ -6375,11 +6518,14 @@ function hasContainerStyleConflict(terms) {
6375
6518
  */
6376
6519
  function buildImpliedNegationCheck(terms) {
6377
6520
  const positiveValues = /* @__PURE__ */ new Map();
6521
+ const negatedBooleanAttrs = /* @__PURE__ */ new Set();
6378
6522
  for (const term of terms) {
6379
- if (term.kind !== "state" || term.negated) continue;
6380
- if (term.type === "container" && term.subtype === "style") {
6381
- if (term.propertyValue !== void 0) positiveValues.set(`c:${term.containerName || "_"}:${term.property}`, term.propertyValue);
6382
- } else if (term.type === "modifier" && term.value !== void 0) positiveValues.set(`m:${term.attribute}`, term.value);
6523
+ if (term.kind !== "state") continue;
6524
+ if (!term.negated) {
6525
+ if (term.type === "container" && term.subtype === "style") {
6526
+ if (term.propertyValue !== void 0) positiveValues.set(`c:${term.containerName || "_"}:${term.property}`, term.propertyValue);
6527
+ } else if (term.type === "modifier" && term.value !== void 0) positiveValues.set(`m:${term.attribute}`, term.value);
6528
+ } else if (term.type === "modifier" && term.value === void 0) negatedBooleanAttrs.add(term.attribute);
6383
6529
  }
6384
6530
  return (term) => {
6385
6531
  if (term.kind !== "state" || !term.negated) return false;
@@ -6389,6 +6535,7 @@ function buildImpliedNegationCheck(terms) {
6389
6535
  return pos !== void 0 && term.propertyValue !== pos;
6390
6536
  }
6391
6537
  if (term.type === "modifier" && term.value !== void 0) {
6538
+ if (negatedBooleanAttrs.has(term.attribute)) return true;
6392
6539
  const pos = positiveValues.get(`m:${term.attribute}`);
6393
6540
  return pos !== void 0 && term.value !== pos;
6394
6541
  }
@@ -6736,437 +6883,97 @@ function tryResolvePair(aChildren, bChildren) {
6736
6883
  if (common.length === 1) return common[0];
6737
6884
  return or(...common);
6738
6885
  }
6739
- //#endregion
6740
- //#region src/pipeline/exclusive.ts
6741
6886
  /**
6742
- * Build exclusive conditions for a list of parsed style entries.
6887
+ * For each OR child of an AND, prune branches that are impossible
6888
+ * given the other AND siblings.
6743
6889
  *
6744
- * The entries should be ordered by priority (highest priority first).
6745
- *
6746
- * For each entry, we compute:
6747
- * exclusiveCondition = condition & !prior[0] & !prior[1] & ...
6748
- *
6749
- * This ensures exactly one condition matches at any time.
6750
- *
6751
- * Example:
6752
- * Input (ordered highest to lowest priority):
6753
- * A: value1 (priority 2)
6754
- * B: value2 (priority 1)
6755
- * C: value3 (priority 0)
6756
- *
6757
- * Output:
6758
- * A: A
6759
- * B: B & !A
6760
- * C: C & !A & !B
6890
+ * Example: A & OR(!A & X, Y) → A & Y
6761
6891
  *
6762
- * @param entries Parsed style entries ordered by priority (highest first)
6763
- * @returns Entries with exclusive conditions, filtered to remove impossible ones
6892
+ * This catches dead OR branches left by exclusive negation (Stage 2b)
6893
+ * where `NOT(combined)` introduces branches contradicting flat siblings.
6764
6894
  */
6765
- function buildExclusiveConditions(entries) {
6895
+ function pruneContradictedOrBranches(terms) {
6896
+ if (!terms.some((t) => t.kind === "compound" && t.operator === "OR")) return terms;
6897
+ let changed = false;
6766
6898
  const result = [];
6767
- const priorConditions = [];
6768
- for (const entry of entries) {
6769
- let exclusive = entry.condition;
6770
- for (const prior of priorConditions) if (prior.kind !== "true") exclusive = and(exclusive, not(prior));
6771
- const simplified = simplifyCondition(exclusive);
6772
- if (simplified.kind === "false") continue;
6773
- result.push({
6774
- ...entry,
6775
- exclusiveCondition: simplified
6776
- });
6777
- if (entry.condition.kind !== "true") priorConditions.push(entry.condition);
6778
- }
6779
- return result;
6780
- }
6781
- /**
6782
- * Parse style entries from a value mapping object.
6783
- *
6784
- * @param styleKey The style key (e.g., 'padding')
6785
- * @param valueMap The value mapping { '': '2x', 'compact': '1x', '@media(w < 768px)': '0.5x' }
6786
- * @param parseCondition Function to parse state keys into conditions
6787
- * @returns Parsed entries ordered by priority (highest first)
6788
- */
6789
- function parseStyleEntries(styleKey, valueMap, parseCondition) {
6790
- const entries = [];
6791
- Object.keys(valueMap).forEach((stateKey, index) => {
6792
- const value = valueMap[stateKey];
6793
- const condition = stateKey === "" ? trueCondition() : parseCondition(stateKey);
6794
- entries.push({
6795
- styleKey,
6796
- stateKey,
6797
- value,
6798
- condition,
6799
- priority: index
6800
- });
6801
- });
6802
- entries.reverse();
6803
- return entries;
6804
- }
6805
- /**
6806
- * Merge parsed entries that share the same value.
6807
- *
6808
- * When multiple **non-default** state keys map to the same value, their
6809
- * conditions can be combined with OR and treated as a single entry.
6810
- * This must happen **before** exclusive expansion and OR branch splitting
6811
- * to avoid combinatorial explosion and duplicate CSS output.
6812
- *
6813
- * Default (TRUE) entries are **never** merged with non-default entries.
6814
- * Merging `TRUE | X` collapses to `TRUE`, destroying the non-default
6815
- * condition's participation in exclusive building. That causes
6816
- * intermediate-priority states to lose their `:not(X)` negation,
6817
- * breaking mutual exclusivity when X and an intermediate state are
6818
- * both active. Stage 6 `mergeByValue` handles combining rules with
6819
- * identical CSS output after exclusive conditions are correctly built.
6820
- *
6821
- * Example: `{ '@dark': 'red', '@dark & @hc': 'red' }` merges into a
6822
- * single entry with condition `@dark | (@dark & @hc)` = `@dark`.
6823
- *
6824
- * Entries are ordered highest-priority-first. The merged entry keeps the
6825
- * highest priority of the group.
6826
- */
6827
- function mergeEntriesByValue(entries) {
6828
- if (entries.length <= 1) return entries;
6829
- const groups = /* @__PURE__ */ new Map();
6830
- for (const entry of entries) {
6831
- const valueKey = serializeValue(entry.value);
6832
- const group = groups.get(valueKey);
6833
- if (group) {
6834
- group.entries.push(entry);
6835
- group.maxPriority = Math.max(group.maxPriority, entry.priority);
6836
- } else groups.set(valueKey, {
6837
- entries: [entry],
6838
- maxPriority: entry.priority
6839
- });
6840
- }
6841
- if (groups.size === entries.length) return entries;
6842
- const merged = [];
6843
- for (const [, group] of groups) {
6844
- if (group.entries.length === 1) {
6845
- merged.push(group.entries[0]);
6899
+ for (let i = 0; i < terms.length; i++) {
6900
+ const term = terms[i];
6901
+ if (term.kind !== "compound" || term.operator !== "OR") {
6902
+ result.push(term);
6846
6903
  continue;
6847
6904
  }
6848
- const defaultEntries = group.entries.filter((e) => e.condition.kind === "true");
6849
- const nonDefaultEntries = group.entries.filter((e) => e.condition.kind !== "true");
6850
- for (const entry of defaultEntries) merged.push(entry);
6851
- if (nonDefaultEntries.length === 1) merged.push(nonDefaultEntries[0]);
6852
- else if (nonDefaultEntries.length >= 2) {
6853
- const combinedCondition = simplifyCondition(or(...nonDefaultEntries.map((e) => e.condition)));
6854
- const combinedStateKey = nonDefaultEntries.map((e) => e.stateKey).join(" | ");
6855
- merged.push({
6856
- styleKey: nonDefaultEntries[0].styleKey,
6857
- stateKey: combinedStateKey,
6858
- value: nonDefaultEntries[0].value,
6859
- condition: combinedCondition,
6860
- priority: group.maxPriority
6905
+ const siblings = [...terms.slice(0, i), ...terms.slice(i + 1)];
6906
+ const surviving = [];
6907
+ for (const branch of term.children) if (simplifyInner({
6908
+ kind: "compound",
6909
+ operator: "AND",
6910
+ children: [branch, ...siblings]
6911
+ }).kind !== "false") surviving.push(branch);
6912
+ if (surviving.length === term.children.length) result.push(term);
6913
+ else if (surviving.length === 0) return [falseCondition()];
6914
+ else {
6915
+ changed = true;
6916
+ if (surviving.length === 1) result.push(surviving[0]);
6917
+ else result.push({
6918
+ kind: "compound",
6919
+ operator: "OR",
6920
+ children: surviving
6861
6921
  });
6862
6922
  }
6863
6923
  }
6864
- merged.sort((a, b) => b.priority - a.priority);
6865
- return merged;
6866
- }
6867
- function serializeValue(value) {
6868
- if (value === null || value === void 0) return "null";
6869
- if (typeof value === "string" || typeof value === "number") return String(value);
6870
- return JSON.stringify(value);
6924
+ if (!changed) return terms;
6925
+ const flattened = [];
6926
+ for (const term of result) if (term.kind === "compound" && term.operator === "AND") flattened.push(...term.children);
6927
+ else flattened.push(term);
6928
+ return flattened;
6871
6929
  }
6930
+ //#endregion
6931
+ //#region src/pipeline/materialize-contradictions.ts
6872
6932
  /**
6873
- * Eliminate redundant state dimensions from a value map.
6874
- *
6875
- * When a value map contains compound AND state keys (e.g. `@dark & @hc`),
6876
- * checks whether any state atom is a "don't-care" variable — i.e. the
6877
- * value is the same whether that atom is present or absent. Redundant
6878
- * atoms are removed from all keys and duplicate entries are collapsed.
6879
- *
6880
- * This runs **before** condition parsing so that downstream stages
6881
- * (`mergeEntriesByValue`, `buildExclusiveConditions`, materialization)
6882
- * never see the irrelevant dimension, producing simpler, smaller CSS.
6883
- *
6884
- * Only pure top-level AND combinations are eligible. Keys that contain
6885
- * `|`, `^`, or `,` at the top level are treated as opaque single atoms.
6886
- *
6887
- * @example
6888
- * { '': A, '@dark': B, '@hc': A, '@dark & @hc': B }
6889
- * // @hc is redundant → { '': A, '@dark': B }
6933
+ * Generic deduplication by a key extraction function.
6934
+ * Preserves insertion order, keeping the first occurrence of each key.
6890
6935
  */
6891
- function extractCompoundStates(valueMap) {
6892
- const keys = Object.keys(valueMap);
6893
- if (keys.length < 3 || !keys.some((k) => k.includes("&"))) return valueMap;
6894
- const entries = keys.map((key) => {
6895
- return {
6896
- atoms: splitTopLevelAnd(key) ?? [key],
6897
- value: valueMap[key]
6898
- };
6899
- });
6900
- const allAtoms = /* @__PURE__ */ new Set();
6901
- for (const e of entries) for (const a of e.atoms) allAtoms.add(a);
6902
- const redundant = /* @__PURE__ */ new Set();
6903
- for (const atom of allAtoms) if (isAtomRedundant(entries, atom)) redundant.add(atom);
6904
- if (redundant.size === 0) return valueMap;
6905
- const newMap = {};
6906
- for (const e of entries) {
6907
- const newKey = e.atoms.filter((a) => !redundant.has(a)).join(" & ");
6908
- if (!(newKey in newMap)) newMap[newKey] = e.value;
6936
+ function dedupeByKey(items, getKey) {
6937
+ const seen = /* @__PURE__ */ new Set();
6938
+ const result = [];
6939
+ for (const item of items) {
6940
+ const key = getKey(item);
6941
+ if (!seen.has(key)) {
6942
+ seen.add(key);
6943
+ result.push(item);
6944
+ }
6909
6945
  }
6910
- return newMap;
6946
+ return result;
6947
+ }
6948
+ function dedupeMediaConditions(conditions) {
6949
+ return dedupeByKey(conditions, (c) => `${c.subtype}|${c.condition}|${c.negated}`);
6950
+ }
6951
+ function dedupeContainerConditions(conditions) {
6952
+ return dedupeByKey(conditions, (c) => `${c.name ?? ""}|${c.condition}|${c.negated}`);
6953
+ }
6954
+ function dedupeSupportsConditions(conditions) {
6955
+ return dedupeByKey(conditions, (c) => `${c.subtype}|${c.condition}|${c.negated}`);
6911
6956
  }
6912
6957
  /**
6913
- * Split a state key by top-level `&` operators.
6914
- *
6915
- * Returns `null` if the key contains `|`, `^`, or `,` at the top level
6916
- * (making it ineligible for atom-level extraction).
6917
- * Returns `[]` for the empty string (default key).
6958
+ * Check if supports conditions contain contradictions
6959
+ * e.g., @supports(display: grid) AND NOT @supports(display: grid)
6918
6960
  */
6919
- function splitTopLevelAnd(key) {
6920
- if (key === "") return [];
6921
- const parts = [];
6922
- let depth = 0;
6923
- let current = "";
6924
- for (const ch of key) {
6925
- if (ch === "(" || ch === "[") depth++;
6926
- else if (ch === ")" || ch === "]") depth--;
6927
- if (depth === 0) {
6928
- if (ch === "&") {
6929
- const trimmed = current.trim();
6930
- if (trimmed) parts.push(trimmed);
6931
- current = "";
6932
- continue;
6933
- }
6934
- if (ch === "|" || ch === "^" || ch === ",") return null;
6935
- }
6936
- current += ch;
6961
+ function hasSupportsContradiction(conditions) {
6962
+ const conditionMap = /* @__PURE__ */ new Map();
6963
+ for (const cond of conditions) {
6964
+ const key = `${cond.subtype}|${cond.condition}`;
6965
+ const existing = conditionMap.get(key);
6966
+ if (existing !== void 0 && existing !== !cond.negated) return true;
6967
+ conditionMap.set(key, !cond.negated);
6937
6968
  }
6938
- const trimmed = current.trim();
6939
- if (trimmed) parts.push(trimmed);
6940
- return parts;
6969
+ return false;
6941
6970
  }
6942
6971
  /**
6943
- * An atom is redundant when every entry that contains it has a matching
6944
- * partner (same remaining atoms, atom absent) with the same value.
6945
- */
6946
- function isAtomRedundant(entries, atom) {
6947
- const withAtom = entries.filter((e) => e.atoms.includes(atom));
6948
- if (withAtom.length === 0) return false;
6949
- for (const wa of withAtom) {
6950
- const remaining = wa.atoms.filter((a) => a !== atom);
6951
- const pair = entries.find((e) => !e.atoms.includes(atom) && e.atoms.length === remaining.length && remaining.every((r) => e.atoms.includes(r)));
6952
- if (!pair) return false;
6953
- if (serializeValue(wa.value) !== serializeValue(pair.value)) return false;
6954
- }
6955
- return true;
6956
- }
6957
- /**
6958
- * Check if a value is a style value mapping (object with state keys)
6959
- */
6960
- function isValueMapping(value) {
6961
- return value !== null && typeof value === "object" && !Array.isArray(value) && !(value instanceof Date);
6962
- }
6963
- /**
6964
- * Expand OR conditions in parsed entries into multiple exclusive entries.
6965
- *
6966
- * For an entry with condition `A | B | C`, this creates 3 entries:
6967
- * - condition: A
6968
- * - condition: B & !A
6969
- * - condition: C & !A & !B
6970
- *
6971
- * This ensures OR branches are mutually exclusive BEFORE the main
6972
- * exclusive condition building pass.
6973
- *
6974
- * @param entries Parsed entries (may contain OR conditions)
6975
- * @returns Expanded entries with OR branches made exclusive
6976
- */
6977
- function expandOrConditions(entries) {
6978
- const result = [];
6979
- for (const entry of entries) {
6980
- const expanded = expandSingleEntry(entry);
6981
- result.push(...expanded);
6982
- }
6983
- return result;
6984
- }
6985
- /**
6986
- * Expand a single entry's OR condition into multiple exclusive entries.
6987
- *
6988
- * Note: branches are NOT sorted by at-rule context here (unlike the
6989
- * `expandExclusiveOrs` pass below). User-authored ORs in state keys aren't
6990
- * the product of De Morgan negation, so each branch is expected to render
6991
- * independently in its own scope and at-rule sort isn't load-bearing.
6992
- * The post-build pass needs the sort because it has to preserve at-rule
6993
- * wrapping across branches that came from negating a compound at-rule.
6994
- */
6995
- function expandSingleEntry(entry) {
6996
- const orBranches = collectOrBranches(entry.condition);
6997
- if (orBranches.length <= 1) return [entry];
6998
- const result = [];
6999
- const priorBranches = [];
7000
- for (let i = 0; i < orBranches.length; i++) {
7001
- const branch = orBranches[i];
7002
- let exclusiveBranch = branch;
7003
- for (const prior of priorBranches) exclusiveBranch = and(exclusiveBranch, not(prior));
7004
- const simplified = simplifyCondition(exclusiveBranch);
7005
- if (simplified.kind === "false") {
7006
- priorBranches.push(branch);
7007
- continue;
7008
- }
7009
- result.push({
7010
- ...entry,
7011
- stateKey: `${entry.stateKey}[${i}]`,
7012
- condition: simplified
7013
- });
7014
- priorBranches.push(branch);
7015
- }
7016
- return result;
7017
- }
7018
- /**
7019
- * Collect top-level OR branches from a condition.
7020
- *
7021
- * For `A | B | C`, returns [A, B, C]
7022
- * For `A & B`, returns [A & B] (single branch)
7023
- * For `A | (B & C)`, returns [A, B & C]
7024
- */
7025
- function collectOrBranches(condition) {
7026
- if (condition.kind === "true" || condition.kind === "false") return [condition];
7027
- if (isCompoundCondition(condition) && condition.operator === "OR") {
7028
- const branches = [];
7029
- for (const child of condition.children) branches.push(...collectOrBranches(child));
7030
- return branches;
7031
- }
7032
- return [condition];
7033
- }
7034
- /**
7035
- * Expand OR conditions in exclusive entries AFTER buildExclusiveConditions.
7036
- *
7037
- * This handles ORs that arise from De Morgan expansion during negation:
7038
- * !(A & B) = !A | !B
7039
- *
7040
- * These ORs need to be made exclusive to avoid overlapping CSS rules:
7041
- * !A | !B → !A | (A & !B)
7042
- *
7043
- * This is logically equivalent but ensures each branch has proper context.
7044
- *
7045
- * Example:
7046
- * Input: { "": V1, "@supports(...) & :has()": V2 }
7047
- * V2's exclusive = @supports & :has
7048
- * V1's exclusive = !(@supports & :has) = !@supports | !:has
7049
- *
7050
- * Without this fix: V1 gets two rules:
7051
- * - @supports (not ...) → V1 ✓
7052
- * - :not(:has()) → V1 ✗ (missing @supports context!)
7053
- *
7054
- * With this fix: V1 gets two exclusive rules:
7055
- * - @supports (not ...) → V1 ✓
7056
- * - @supports (...) { :not(:has()) } → V1 ✓ (proper context!)
7057
- */
7058
- function expandExclusiveOrs(entries) {
7059
- const result = [];
7060
- for (const entry of entries) {
7061
- const expanded = expandExclusiveConditionOrs(entry);
7062
- result.push(...expanded);
7063
- }
7064
- return result;
7065
- }
7066
- /**
7067
- * Check if a condition involves at-rules (media, container, supports, starting)
7068
- */
7069
- function hasAtRuleContext(node) {
7070
- if (node.kind === "true" || node.kind === "false") return false;
7071
- if (node.kind === "state") return node.type === "media" || node.type === "container" || node.type === "supports" || node.type === "starting";
7072
- if (node.kind === "compound") return node.children.some(hasAtRuleContext);
7073
- return false;
7074
- }
7075
- /**
7076
- * Sort OR branches to prioritize at-rule conditions first.
7077
- *
7078
- * This is critical for correct CSS generation. For `!A | !B` where A is at-rule
7079
- * and B is modifier, we want:
7080
- * - Branch 0: !A (at-rule negation - covers "no @supports/media" case)
7081
- * - Branch 1: A & !B (modifier negation with at-rule context)
7082
- *
7083
- * If we process in wrong order (!B first), we'd get:
7084
- * - Branch 0: !B (modifier negation WITHOUT at-rule context - WRONG!)
7085
- * - Branch 1: B & !A (at-rule negation with modifier - incomplete coverage)
7086
- */
7087
- function sortOrBranchesForExpansion(branches) {
7088
- return [...branches].sort((a, b) => {
7089
- const aHasAtRule = hasAtRuleContext(a);
7090
- const bHasAtRule = hasAtRuleContext(b);
7091
- if (aHasAtRule && !bHasAtRule) return -1;
7092
- if (!aHasAtRule && bHasAtRule) return 1;
7093
- return 0;
7094
- });
7095
- }
7096
- /**
7097
- * Expand ORs in a single entry's exclusive condition
7098
- */
7099
- function expandExclusiveConditionOrs(entry) {
7100
- let orBranches = collectOrBranches(entry.exclusiveCondition);
7101
- if (orBranches.length <= 1) return [entry];
7102
- orBranches = sortOrBranchesForExpansion(orBranches);
7103
- const result = [];
7104
- const priorBranches = [];
7105
- for (let i = 0; i < orBranches.length; i++) {
7106
- const branch = orBranches[i];
7107
- let exclusiveBranch = branch;
7108
- for (const prior of priorBranches) exclusiveBranch = and(exclusiveBranch, not(prior));
7109
- const simplified = simplifyCondition(exclusiveBranch);
7110
- if (simplified.kind === "false") {
7111
- priorBranches.push(branch);
7112
- continue;
7113
- }
7114
- result.push({
7115
- ...entry,
7116
- stateKey: `${entry.stateKey}[or:${i}]`,
7117
- exclusiveCondition: simplified
7118
- });
7119
- priorBranches.push(branch);
7120
- }
7121
- return result;
7122
- }
7123
- //#endregion
7124
- //#region src/pipeline/materialize-contradictions.ts
7125
- /**
7126
- * Generic deduplication by a key extraction function.
7127
- * Preserves insertion order, keeping the first occurrence of each key.
7128
- */
7129
- function dedupeByKey(items, getKey) {
7130
- const seen = /* @__PURE__ */ new Set();
7131
- const result = [];
7132
- for (const item of items) {
7133
- const key = getKey(item);
7134
- if (!seen.has(key)) {
7135
- seen.add(key);
7136
- result.push(item);
7137
- }
7138
- }
7139
- return result;
7140
- }
7141
- function dedupeMediaConditions(conditions) {
7142
- return dedupeByKey(conditions, (c) => `${c.subtype}|${c.condition}|${c.negated}`);
7143
- }
7144
- function dedupeContainerConditions(conditions) {
7145
- return dedupeByKey(conditions, (c) => `${c.name ?? ""}|${c.condition}|${c.negated}`);
7146
- }
7147
- function dedupeSupportsConditions(conditions) {
7148
- return dedupeByKey(conditions, (c) => `${c.subtype}|${c.condition}|${c.negated}`);
7149
- }
7150
- /**
7151
- * Check if supports conditions contain contradictions
7152
- * e.g., @supports(display: grid) AND NOT @supports(display: grid)
7153
- */
7154
- function hasSupportsContradiction(conditions) {
7155
- const conditionMap = /* @__PURE__ */ new Map();
7156
- for (const cond of conditions) {
7157
- const key = `${cond.subtype}|${cond.condition}`;
7158
- const existing = conditionMap.get(key);
7159
- if (existing !== void 0 && existing !== !cond.negated) return true;
7160
- conditionMap.set(key, !cond.negated);
7161
- }
7162
- return false;
7163
- }
7164
- /**
7165
- * Check if a set of media conditions contains contradictions
7166
- * e.g., (prefers-color-scheme: light) AND NOT (prefers-color-scheme: light)
7167
- * or (width >= 900px) AND (width < 600px)
7168
- *
7169
- * Uses parsed media conditions for efficient analysis without regex parsing.
6972
+ * Check if a set of media conditions contains contradictions
6973
+ * e.g., (prefers-color-scheme: light) AND NOT (prefers-color-scheme: light)
6974
+ * or (width >= 900px) AND (width < 600px)
6975
+ *
6976
+ * Uses parsed media conditions for efficient analysis without regex parsing.
7170
6977
  */
7171
6978
  function hasMediaContradiction(conditions) {
7172
6979
  const featureConditions = /* @__PURE__ */ new Map();
@@ -7777,7 +7584,9 @@ function dedupeSelectorConditions(conditions) {
7777
7584
  if (facts.negatedBooleanAttrs.size === 0 && facts.positiveExactValuesByAttr.size === 0) return result;
7778
7585
  return result.filter((c) => {
7779
7586
  if (!("attribute" in c)) return true;
7780
- return !isSubsumedNegatedModifier(c, facts);
7587
+ if (isSubsumedNegatedModifier(c, facts)) return false;
7588
+ if (!c.negated && c.value === void 0 && facts.positiveExactValuesByAttr.has(c.attribute)) return false;
7589
+ return true;
7781
7590
  });
7782
7591
  }
7783
7592
  /**
@@ -8100,6 +7909,12 @@ function makeOrBranchesExclusive(children) {
8100
7909
  * Check if OR branches produce different at-rule contexts when
8101
7910
  * materialized. If so, the Cartesian product in andToCSS will
8102
7911
  * create overlapping CSS variants that need exclusive expansion.
7912
+ *
7913
+ * Exported so Stage 2a (`expandOrConditions` in `exclusive.ts`) can
7914
+ * reuse the same heuristic and skip OR expansion when every branch
7915
+ * lives in the same at-rule/root/parent/own context — pure-selector
7916
+ * ORs are better collapsed into `:is(...)` at materialization time
7917
+ * than expanded into mutually-exclusive `A | (B & !A) | …` cascades.
8103
7918
  */
8104
7919
  function branchesProduceDifferentContexts(branches) {
8105
7920
  const contextKeys = /* @__PURE__ */ new Set();
@@ -8117,161 +7932,634 @@ function branchesProduceDifferentContexts(branches) {
8117
7932
  * After deduplication, variants that differ only in their base
8118
7933
  * modifier/pseudo conditions are merged into :is() groups.
8119
7934
  *
8120
- * Note: OR exclusivity is handled at the pipeline level (expandOrConditions),
8121
- * so here we just collect all variants. Any remaining ORs in the condition
8122
- * tree (e.g., from De Morgan expansion) are handled as simple alternatives.
7935
+ * Note: OR exclusivity is handled at the pipeline level (expandOrConditions),
7936
+ * so here we just collect all variants. Any remaining ORs in the condition
7937
+ * tree (e.g., from De Morgan expansion) are handled as simple alternatives.
7938
+ */
7939
+ function orToCSS(children) {
7940
+ const allVariants = [];
7941
+ for (const child of children) {
7942
+ const childCSS = conditionToCSSInner(child);
7943
+ if (childCSS.isImpossible) continue;
7944
+ allVariants.push(...childCSS.variants);
7945
+ }
7946
+ if (allVariants.length === 0) return {
7947
+ variants: [],
7948
+ isImpossible: true
7949
+ };
7950
+ return {
7951
+ variants: dedupeVariants(allVariants),
7952
+ isImpossible: false
7953
+ };
7954
+ }
7955
+ /**
7956
+ * Find keys present in ALL condition arrays.
7957
+ */
7958
+ function findCommonKeys(conditionSets, getKey) {
7959
+ if (conditionSets.length === 0) return /* @__PURE__ */ new Set();
7960
+ const common = new Set(conditionSets[0].map(getKey));
7961
+ for (let i = 1; i < conditionSets.length; i++) {
7962
+ const keys = new Set(conditionSets[i].map(getKey));
7963
+ for (const key of common) if (!keys.has(key)) common.delete(key);
7964
+ }
7965
+ return common;
7966
+ }
7967
+ /**
7968
+ * Merge OR variants that share the same "context" (at-rules, root, parent,
7969
+ * own, starting) into a single variant with a SelectorGroup.
7970
+ *
7971
+ * Variants with no modifier/pseudo conditions are kept separate (they match
7972
+ * unconditionally and can't be expressed inside :is()).
7973
+ */
7974
+ function mergeVariantsIntoSelectorGroups(variants) {
7975
+ if (variants.length <= 1) return variants;
7976
+ const groups = /* @__PURE__ */ new Map();
7977
+ for (const v of variants) {
7978
+ const key = getVariantContextKey(v);
7979
+ const group = groups.get(key);
7980
+ if (group) group.push(v);
7981
+ else groups.set(key, [v]);
7982
+ }
7983
+ const result = [];
7984
+ for (const group of groups.values()) {
7985
+ if (group.length === 1) {
7986
+ result.push(group[0]);
7987
+ continue;
7988
+ }
7989
+ const withSelectors = [];
7990
+ const withoutSelectors = [];
7991
+ for (const v of group) if (v.modifierConditions.length === 0 && v.pseudoConditions.length === 0) withoutSelectors.push(v);
7992
+ else withSelectors.push(v);
7993
+ result.push(...withoutSelectors);
7994
+ if (withSelectors.length <= 1) {
7995
+ result.push(...withSelectors);
7996
+ continue;
7997
+ }
7998
+ result.push(factorAndGroup(withSelectors));
7999
+ }
8000
+ return result;
8001
+ }
8002
+ /**
8003
+ * Factor common modifier/pseudo conditions out of variants and create
8004
+ * a single variant with a SelectorGroup for the remaining (differing)
8005
+ * conditions.
8006
+ *
8007
+ * Precondition: all variants must share the same context key (identical
8008
+ * at-rules, root/parent/own/selector groups, startingStyle).
8009
+ */
8010
+ function factorAndGroup(variants) {
8011
+ {
8012
+ const key0 = getVariantContextKey(variants[0]);
8013
+ for (let i = 1; i < variants.length; i++) {
8014
+ const keyI = getVariantContextKey(variants[i]);
8015
+ if (keyI !== key0) throw new Error(`factorAndGroup: context key mismatch at index ${i}.\n expected: ${key0}\n got: ${keyI}`);
8016
+ }
8017
+ }
8018
+ const commonModKeys = findCommonKeys(variants.map((v) => v.modifierConditions), getModifierKey);
8019
+ const commonPseudoKeys = findCommonKeys(variants.map((v) => v.pseudoConditions), getPseudoKey);
8020
+ const commonModifiers = variants[0].modifierConditions.filter((m) => commonModKeys.has(getModifierKey(m)));
8021
+ const commonPseudos = variants[0].pseudoConditions.filter((p) => commonPseudoKeys.has(getPseudoKey(p)));
8022
+ const branches = [];
8023
+ let hasEmptyBranch = false;
8024
+ for (const v of variants) {
8025
+ const branch = [];
8026
+ for (const mod of v.modifierConditions) if (!commonModKeys.has(getModifierKey(mod))) branch.push(mod);
8027
+ for (const pseudo of v.pseudoConditions) if (!commonPseudoKeys.has(getPseudoKey(pseudo))) branch.push(pseudo);
8028
+ if (branch.length > 0) branches.push(branch);
8029
+ else hasEmptyBranch = true;
8030
+ }
8031
+ if (hasEmptyBranch) return {
8032
+ ...variants[0],
8033
+ modifierConditions: commonModifiers,
8034
+ pseudoConditions: commonPseudos
8035
+ };
8036
+ const factoredGroups = tryFactorIntoDimensions(branches);
8037
+ if (factoredGroups) return {
8038
+ modifierConditions: commonModifiers,
8039
+ pseudoConditions: commonPseudos,
8040
+ selectorGroups: [...variants[0].selectorGroups, ...factoredGroups],
8041
+ ownGroups: [...variants[0].ownGroups],
8042
+ mediaConditions: [...variants[0].mediaConditions],
8043
+ containerConditions: [...variants[0].containerConditions],
8044
+ supportsConditions: [...variants[0].supportsConditions],
8045
+ rootGroups: [...variants[0].rootGroups],
8046
+ parentGroups: [...variants[0].parentGroups],
8047
+ startingStyle: variants[0].startingStyle
8048
+ };
8049
+ return {
8050
+ modifierConditions: commonModifiers,
8051
+ pseudoConditions: commonPseudos,
8052
+ selectorGroups: [...variants[0].selectorGroups, {
8053
+ branches,
8054
+ negated: false
8055
+ }],
8056
+ ownGroups: [...variants[0].ownGroups],
8057
+ mediaConditions: [...variants[0].mediaConditions],
8058
+ containerConditions: [...variants[0].containerConditions],
8059
+ supportsConditions: [...variants[0].supportsConditions],
8060
+ rootGroups: [...variants[0].rootGroups],
8061
+ parentGroups: [...variants[0].parentGroups],
8062
+ startingStyle: variants[0].startingStyle
8063
+ };
8064
+ }
8065
+ /**
8066
+ * Detect when branches form a complete Cartesian product of independent
8067
+ * modifier attribute dimensions and return one SelectorGroup per dimension.
8068
+ *
8069
+ * Example: 4 branches for 2 attributes × 2 values each →
8070
+ * :is(A1, A2):is(B1, B2) instead of :is(A1B1, A1B2, A2B1, A2B2)
8071
+ */
8072
+ function tryFactorIntoDimensions(branches) {
8073
+ if (branches.length < 4) return null;
8074
+ const dimensions = /* @__PURE__ */ new Map();
8075
+ for (const branch of branches) for (const cond of branch) {
8076
+ if (!("attribute" in cond)) return null;
8077
+ if (!dimensions.has(cond.attribute)) dimensions.set(cond.attribute, /* @__PURE__ */ new Map());
8078
+ dimensions.get(cond.attribute).set(getModifierKey(cond), cond);
8079
+ }
8080
+ if (dimensions.size < 2) return null;
8081
+ for (const branch of branches) {
8082
+ const seen = /* @__PURE__ */ new Set();
8083
+ for (const cond of branch) {
8084
+ const attr = cond.attribute;
8085
+ if (seen.has(attr)) return null;
8086
+ seen.add(attr);
8087
+ }
8088
+ if (seen.size !== dimensions.size) return null;
8089
+ }
8090
+ let expectedCount = 1;
8091
+ for (const vals of dimensions.values()) expectedCount *= vals.size;
8092
+ if (branches.length !== expectedCount) return null;
8093
+ return [...dimensions.values()].map((vals) => ({
8094
+ branches: [...vals.values()].map((cond) => [cond]),
8095
+ negated: false
8096
+ }));
8097
+ }
8098
+ /**
8099
+ * Build at-rules array from a variant
8100
+ */
8101
+ function buildAtRulesFromVariant(variant) {
8102
+ const atRules = [];
8103
+ if (variant.mediaConditions.length > 0) {
8104
+ const conditionParts = variant.mediaConditions.map((c) => {
8105
+ if (c.subtype === "type") return c.negated ? `not ${c.condition}` : c.condition;
8106
+ else return c.negated ? `(not ${c.condition})` : c.condition;
8107
+ });
8108
+ atRules.push(`@media ${conditionParts.sort().join(" and ")}`);
8109
+ }
8110
+ if (variant.containerConditions.length > 0) {
8111
+ const byName = /* @__PURE__ */ new Map();
8112
+ for (const cond of variant.containerConditions) {
8113
+ const group = byName.get(cond.name) || [];
8114
+ group.push(cond);
8115
+ byName.set(cond.name, group);
8116
+ }
8117
+ for (const [name, conditions] of byName) {
8118
+ const conditionParts = conditions.map((c) => c.negated ? `(not ${c.condition})` : c.condition);
8119
+ const namePrefix = name ? `${name} ` : "";
8120
+ atRules.push(`@container ${namePrefix}${conditionParts.join(" and ")}`);
8121
+ }
8122
+ }
8123
+ if (variant.supportsConditions.length > 0) {
8124
+ const conditionParts = variant.supportsConditions.map((c) => {
8125
+ if (c.subtype === "selector") {
8126
+ const selectorCond = `selector(${c.condition})`;
8127
+ return c.negated ? `(not ${selectorCond})` : selectorCond;
8128
+ } else {
8129
+ const featureCond = `(${c.condition})`;
8130
+ return c.negated ? `(not ${featureCond})` : featureCond;
8131
+ }
8132
+ });
8133
+ atRules.push(`@supports ${conditionParts.join(" and ")}`);
8134
+ }
8135
+ return atRules;
8136
+ }
8137
+ //#endregion
8138
+ //#region src/pipeline/exclusive.ts
8139
+ /**
8140
+ * Build exclusive conditions for a list of parsed style entries.
8141
+ *
8142
+ * The entries should be ordered by priority (highest priority first).
8143
+ *
8144
+ * For each entry, we compute:
8145
+ * exclusiveCondition = condition & !prior[0] & !prior[1] & ...
8146
+ *
8147
+ * This ensures exactly one condition matches at any time.
8148
+ *
8149
+ * Example:
8150
+ * Input (ordered highest to lowest priority):
8151
+ * A: value1 (priority 2)
8152
+ * B: value2 (priority 1)
8153
+ * C: value3 (priority 0)
8154
+ *
8155
+ * Output:
8156
+ * A: A
8157
+ * B: B & !A
8158
+ * C: C & !A & !B
8159
+ *
8160
+ * @param entries Parsed style entries ordered by priority (highest first)
8161
+ * @returns Entries with exclusive conditions, filtered to remove impossible ones
8162
+ */
8163
+ function buildExclusiveConditions(entries) {
8164
+ const result = [];
8165
+ const priorConditions = [];
8166
+ for (const entry of entries) {
8167
+ let exclusive = entry.condition;
8168
+ for (const prior of priorConditions) if (prior.kind !== "true") exclusive = and(exclusive, not(prior));
8169
+ const simplified = simplifyCondition(exclusive);
8170
+ if (simplified.kind === "false") continue;
8171
+ result.push({
8172
+ ...entry,
8173
+ exclusiveCondition: simplified
8174
+ });
8175
+ if (entry.condition.kind !== "true") priorConditions.push(entry.condition);
8176
+ }
8177
+ return result;
8178
+ }
8179
+ /**
8180
+ * Parse style entries from a value mapping object.
8181
+ *
8182
+ * @param styleKey The style key (e.g., 'padding')
8183
+ * @param valueMap The value mapping { '': '2x', 'compact': '1x', '@media(w < 768px)': '0.5x' }
8184
+ * @param parseCondition Function to parse state keys into conditions
8185
+ * @returns Parsed entries ordered by priority (highest first)
8186
+ */
8187
+ function parseStyleEntries(styleKey, valueMap, parseCondition) {
8188
+ const entries = [];
8189
+ Object.keys(valueMap).forEach((stateKey, index) => {
8190
+ const value = valueMap[stateKey];
8191
+ const condition = stateKey === "" ? trueCondition() : parseCondition(stateKey);
8192
+ entries.push({
8193
+ styleKey,
8194
+ stateKey,
8195
+ value,
8196
+ condition,
8197
+ priority: index
8198
+ });
8199
+ });
8200
+ entries.reverse();
8201
+ return entries;
8202
+ }
8203
+ /**
8204
+ * Merge parsed entries that share the same value.
8205
+ *
8206
+ * When multiple **non-default** state keys map to the same value, their
8207
+ * conditions can be combined with OR and treated as a single entry.
8208
+ * This must happen **before** exclusive expansion and OR branch splitting
8209
+ * to avoid combinatorial explosion and duplicate CSS output.
8210
+ *
8211
+ * **Merging must preserve the authored cascade.** Merging two same-value
8212
+ * entries with priorities `p_h > p_l` lifts the lower-priority entry up
8213
+ * to `p_h` and changes the "blocker" for intermediate-priority entries
8214
+ * from `!C_h` to `!(C_h | C_l) = !C_h & !C_l`. The added `!C_l`
8215
+ * constraint can incorrectly block an intermediate entry that should
8216
+ * have won.
8217
+ *
8218
+ * Two same-value entries with conditions `C_h` (higher priority) and
8219
+ * `C_l` (lower priority) are safe to merge iff for every entry
8220
+ * `e_m` strictly between them in priority with a different value,
8221
+ *
8222
+ * simplify(C_m & C_l & !C_h) = FALSE
8223
+ *
8224
+ * i.e. there is no scenario where the intermediate state could have
8225
+ * matched (`C_m`), the lower-priority same-value entry would also have
8226
+ * matched (`C_l`), and the higher-priority entry would not (`!C_h`).
8227
+ * In such scenarios the intermediate is supposed to win; the merge
8228
+ * would block it by introducing `!C_l`.
8229
+ *
8230
+ * Example (UNSAFE — must not merge):
8231
+ * `{ hovered: 'red', pressed: 'blue', disabled: 'red' }`.
8232
+ * C_h = disabled, C_l = hovered, C_m = pressed. `pressed & hovered &
8233
+ * !disabled` is satisfiable (three independent modifiers), so the
8234
+ * intermediate `pressed` would lose to a merged red rule when both
8235
+ * `pressed` and `hovered` are active — breaking the cascade
8236
+ * `disabled > pressed > hovered`.
8237
+ *
8238
+ * Example (SAFE — still merges):
8239
+ * `{ '': light, '@dark': dark, '@hc': hc, '@dark & @hc': dark }`.
8240
+ * C_h = `@dark & @hc`, C_l = `@dark`, C_m = `@hc`.
8241
+ * `@hc & @dark & !(@dark & @hc) = @hc & @dark & (!@dark | !@hc)`
8242
+ * simplifies to FALSE, so merging the two darks into one `@dark` rule
8243
+ * at the higher priority does not affect the `@hc` rule.
8244
+ *
8245
+ * Default (TRUE) entries are never merged with non-default entries.
8246
+ * Merging `TRUE | X` collapses to `TRUE`, destroying the non-default
8247
+ * condition's participation in exclusive building. Stage 6
8248
+ * `mergeByValue` handles combining rules with identical CSS output
8249
+ * after exclusive conditions are correctly built.
8250
+ *
8251
+ * The merged entry keeps the highest priority of the merged entries.
8252
+ */
8253
+ function mergeEntriesByValue(entries) {
8254
+ if (entries.length <= 1) return entries;
8255
+ const merged = [];
8256
+ for (const entry of entries) {
8257
+ if (entry.condition.kind === "true") {
8258
+ merged.push(entry);
8259
+ continue;
8260
+ }
8261
+ const valueKey = serializeValue(entry.value);
8262
+ let mergeIdx = -1;
8263
+ for (let j = merged.length - 1; j >= 0; j--) {
8264
+ const prev = merged[j];
8265
+ if (prev.condition.kind === "true") continue;
8266
+ if (serializeValue(prev.value) !== valueKey) continue;
8267
+ let safe = true;
8268
+ for (let k = j + 1; k < merged.length; k++) {
8269
+ const inter = merged[k];
8270
+ if (inter.condition.kind === "true") continue;
8271
+ if (serializeValue(inter.value) === valueKey) continue;
8272
+ if (simplifyCondition(and(inter.condition, entry.condition, not(prev.condition))).kind !== "false") {
8273
+ safe = false;
8274
+ break;
8275
+ }
8276
+ }
8277
+ if (safe) {
8278
+ mergeIdx = j;
8279
+ break;
8280
+ }
8281
+ }
8282
+ if (mergeIdx >= 0) {
8283
+ const prev = merged[mergeIdx];
8284
+ const newCondition = simplifyCondition(or(prev.condition, entry.condition));
8285
+ merged[mergeIdx] = {
8286
+ styleKey: prev.styleKey,
8287
+ stateKey: `${prev.stateKey} | ${entry.stateKey}`,
8288
+ value: prev.value,
8289
+ condition: newCondition,
8290
+ priority: Math.max(prev.priority, entry.priority)
8291
+ };
8292
+ } else merged.push(entry);
8293
+ }
8294
+ return merged;
8295
+ }
8296
+ function serializeValue(value) {
8297
+ if (value === null || value === void 0) return "null";
8298
+ if (typeof value === "string" || typeof value === "number") return String(value);
8299
+ return JSON.stringify(value);
8300
+ }
8301
+ /**
8302
+ * Eliminate redundant state dimensions from a value map.
8303
+ *
8304
+ * When a value map contains compound AND state keys (e.g. `@dark & @hc`),
8305
+ * checks whether any state atom is a "don't-care" variable — i.e. the
8306
+ * value is the same whether that atom is present or absent. Redundant
8307
+ * atoms are removed from all keys and duplicate entries are collapsed.
8308
+ *
8309
+ * This runs **before** condition parsing so that downstream stages
8310
+ * (`mergeEntriesByValue`, `buildExclusiveConditions`, materialization)
8311
+ * never see the irrelevant dimension, producing simpler, smaller CSS.
8312
+ *
8313
+ * Only pure top-level AND combinations are eligible. Keys that contain
8314
+ * `|`, `^`, or `,` at the top level are treated as opaque single atoms.
8315
+ *
8316
+ * @example
8317
+ * { '': A, '@dark': B, '@hc': A, '@dark & @hc': B }
8318
+ * // @hc is redundant → { '': A, '@dark': B }
8319
+ */
8320
+ function extractCompoundStates(valueMap) {
8321
+ const keys = Object.keys(valueMap);
8322
+ if (keys.length < 3 || !keys.some((k) => k.includes("&"))) return valueMap;
8323
+ const entries = keys.map((key) => {
8324
+ return {
8325
+ atoms: splitTopLevelAnd(key) ?? [key],
8326
+ value: valueMap[key]
8327
+ };
8328
+ });
8329
+ const allAtoms = /* @__PURE__ */ new Set();
8330
+ for (const e of entries) for (const a of e.atoms) allAtoms.add(a);
8331
+ const redundant = /* @__PURE__ */ new Set();
8332
+ for (const atom of allAtoms) if (isAtomRedundant(entries, atom)) redundant.add(atom);
8333
+ if (redundant.size === 0) return valueMap;
8334
+ const newMap = {};
8335
+ for (const e of entries) {
8336
+ const newKey = e.atoms.filter((a) => !redundant.has(a)).join(" & ");
8337
+ if (!(newKey in newMap)) newMap[newKey] = e.value;
8338
+ }
8339
+ return newMap;
8340
+ }
8341
+ /**
8342
+ * Split a state key by top-level `&` operators.
8343
+ *
8344
+ * Returns `null` if the key contains `|`, `^`, or `,` at the top level
8345
+ * (making it ineligible for atom-level extraction).
8346
+ * Returns `[]` for the empty string (default key).
8347
+ */
8348
+ function splitTopLevelAnd(key) {
8349
+ if (key === "") return [];
8350
+ const parts = [];
8351
+ let depth = 0;
8352
+ let current = "";
8353
+ for (const ch of key) {
8354
+ if (ch === "(" || ch === "[") depth++;
8355
+ else if (ch === ")" || ch === "]") depth--;
8356
+ if (depth === 0) {
8357
+ if (ch === "&") {
8358
+ const trimmed = current.trim();
8359
+ if (trimmed) parts.push(trimmed);
8360
+ current = "";
8361
+ continue;
8362
+ }
8363
+ if (ch === "|" || ch === "^" || ch === ",") return null;
8364
+ }
8365
+ current += ch;
8366
+ }
8367
+ const trimmed = current.trim();
8368
+ if (trimmed) parts.push(trimmed);
8369
+ return parts;
8370
+ }
8371
+ /**
8372
+ * An atom is redundant when every entry that contains it has a matching
8373
+ * partner (same remaining atoms, atom absent) with the same value.
8374
+ */
8375
+ function isAtomRedundant(entries, atom) {
8376
+ const withAtom = entries.filter((e) => e.atoms.includes(atom));
8377
+ if (withAtom.length === 0) return false;
8378
+ for (const wa of withAtom) {
8379
+ const remaining = wa.atoms.filter((a) => a !== atom);
8380
+ const pair = entries.find((e) => !e.atoms.includes(atom) && e.atoms.length === remaining.length && remaining.every((r) => e.atoms.includes(r)));
8381
+ if (!pair) return false;
8382
+ if (serializeValue(wa.value) !== serializeValue(pair.value)) return false;
8383
+ }
8384
+ return true;
8385
+ }
8386
+ /**
8387
+ * Check if a value is a style value mapping (object with state keys)
8388
+ */
8389
+ function isValueMapping(value) {
8390
+ return value !== null && typeof value === "object" && !Array.isArray(value) && !(value instanceof Date);
8391
+ }
8392
+ /**
8393
+ * Expand OR conditions in parsed entries into multiple exclusive entries.
8394
+ *
8395
+ * For an entry with condition `A | B | C`, this creates 3 entries:
8396
+ * - condition: A
8397
+ * - condition: B & !A
8398
+ * - condition: C & !A & !B
8399
+ *
8400
+ * This ensures OR branches are mutually exclusive BEFORE the main
8401
+ * exclusive condition building pass.
8402
+ *
8403
+ * @param entries Parsed entries (may contain OR conditions)
8404
+ * @returns Expanded entries with OR branches made exclusive
8405
+ */
8406
+ function expandOrConditions(entries) {
8407
+ const result = [];
8408
+ for (const entry of entries) {
8409
+ const expanded = expandSingleEntry(entry);
8410
+ result.push(...expanded);
8411
+ }
8412
+ return result;
8413
+ }
8414
+ /**
8415
+ * Expand a single entry's OR condition into multiple exclusive entries.
8416
+ *
8417
+ * Note: branches are NOT sorted by at-rule context here (unlike the
8418
+ * `expandExclusiveOrs` pass below). User-authored ORs in state keys aren't
8419
+ * the product of De Morgan negation, so each branch is expected to render
8420
+ * independently in its own scope and at-rule sort isn't load-bearing.
8421
+ * The post-build pass needs the sort because it has to preserve at-rule
8422
+ * wrapping across branches that came from negating a compound at-rule.
8423
+ *
8424
+ * Skip optimisation: when every branch renders into the same at-rule /
8425
+ * root / parent / own context (see "Key Design Decision #2" in
8426
+ * `docs/pipeline.md`), forcing mutual exclusivity here produces dead
8427
+ * `B & !A`-style branches that materialization later folds back into
8428
+ * `:is(A, B)`. Bail out and let `materialize.ts` collapse the OR via
8429
+ * `mergeVariantsIntoSelectorGroups`. Cross-entry exclusivity is still
8430
+ * enforced by `buildExclusiveConditions`; the post-build `expandExclusiveOrs`
8431
+ * pass still handles De Morgan ORs whose branches actually differ in
8432
+ * context.
8123
8433
  */
8124
- function orToCSS(children) {
8125
- const allVariants = [];
8126
- for (const child of children) {
8127
- const childCSS = conditionToCSSInner(child);
8128
- if (childCSS.isImpossible) continue;
8129
- allVariants.push(...childCSS.variants);
8434
+ function expandSingleEntry(entry) {
8435
+ const orBranches = collectOrBranches(entry.condition);
8436
+ if (orBranches.length <= 1) return [entry];
8437
+ if (!branchesProduceDifferentContexts(orBranches)) return [entry];
8438
+ const result = [];
8439
+ const priorBranches = [];
8440
+ for (let i = 0; i < orBranches.length; i++) {
8441
+ const branch = orBranches[i];
8442
+ let exclusiveBranch = branch;
8443
+ for (const prior of priorBranches) exclusiveBranch = and(exclusiveBranch, not(prior));
8444
+ const simplified = simplifyCondition(exclusiveBranch);
8445
+ if (simplified.kind === "false") {
8446
+ priorBranches.push(branch);
8447
+ continue;
8448
+ }
8449
+ result.push({
8450
+ ...entry,
8451
+ stateKey: `${entry.stateKey}[${i}]`,
8452
+ condition: simplified
8453
+ });
8454
+ priorBranches.push(branch);
8130
8455
  }
8131
- if (allVariants.length === 0) return {
8132
- variants: [],
8133
- isImpossible: true
8134
- };
8135
- return {
8136
- variants: dedupeVariants(allVariants),
8137
- isImpossible: false
8138
- };
8456
+ return result;
8139
8457
  }
8140
8458
  /**
8141
- * Find keys present in ALL condition arrays.
8459
+ * Collect top-level OR branches from a condition.
8460
+ *
8461
+ * For `A | B | C`, returns [A, B, C]
8462
+ * For `A & B`, returns [A & B] (single branch)
8463
+ * For `A | (B & C)`, returns [A, B & C]
8142
8464
  */
8143
- function findCommonKeys(conditionSets, getKey) {
8144
- if (conditionSets.length === 0) return /* @__PURE__ */ new Set();
8145
- const common = new Set(conditionSets[0].map(getKey));
8146
- for (let i = 1; i < conditionSets.length; i++) {
8147
- const keys = new Set(conditionSets[i].map(getKey));
8148
- for (const key of common) if (!keys.has(key)) common.delete(key);
8465
+ function collectOrBranches(condition) {
8466
+ if (condition.kind === "true" || condition.kind === "false") return [condition];
8467
+ if (isCompoundCondition(condition) && condition.operator === "OR") {
8468
+ const branches = [];
8469
+ for (const child of condition.children) branches.push(...collectOrBranches(child));
8470
+ return branches;
8149
8471
  }
8150
- return common;
8472
+ return [condition];
8151
8473
  }
8152
8474
  /**
8153
- * Merge OR variants that share the same "context" (at-rules, root, parent,
8154
- * own, starting) into a single variant with a SelectorGroup.
8475
+ * Expand OR conditions in exclusive entries AFTER buildExclusiveConditions.
8155
8476
  *
8156
- * Variants with no modifier/pseudo conditions are kept separate (they match
8157
- * unconditionally and can't be expressed inside :is()).
8477
+ * This handles ORs that arise from De Morgan expansion during negation:
8478
+ * !(A & B) = !A | !B
8479
+ *
8480
+ * These ORs need to be made exclusive to avoid overlapping CSS rules:
8481
+ * !A | !B → !A | (A & !B)
8482
+ *
8483
+ * This is logically equivalent but ensures each branch has proper context.
8484
+ *
8485
+ * Example:
8486
+ * Input: { "": V1, "@supports(...) & :has()": V2 }
8487
+ * V2's exclusive = @supports & :has
8488
+ * V1's exclusive = !(@supports & :has) = !@supports | !:has
8489
+ *
8490
+ * Without this fix: V1 gets two rules:
8491
+ * - @supports (not ...) → V1 ✓
8492
+ * - :not(:has()) → V1 ✗ (missing @supports context!)
8493
+ *
8494
+ * With this fix: V1 gets two exclusive rules:
8495
+ * - @supports (not ...) → V1 ✓
8496
+ * - @supports (...) { :not(:has()) } → V1 ✓ (proper context!)
8158
8497
  */
8159
- function mergeVariantsIntoSelectorGroups(variants) {
8160
- if (variants.length <= 1) return variants;
8161
- const groups = /* @__PURE__ */ new Map();
8162
- for (const v of variants) {
8163
- const key = getVariantContextKey(v);
8164
- const group = groups.get(key);
8165
- if (group) group.push(v);
8166
- else groups.set(key, [v]);
8167
- }
8498
+ function expandExclusiveOrs(entries) {
8168
8499
  const result = [];
8169
- for (const group of groups.values()) {
8170
- if (group.length === 1) {
8171
- result.push(group[0]);
8172
- continue;
8173
- }
8174
- const withSelectors = [];
8175
- const withoutSelectors = [];
8176
- for (const v of group) if (v.modifierConditions.length === 0 && v.pseudoConditions.length === 0) withoutSelectors.push(v);
8177
- else withSelectors.push(v);
8178
- result.push(...withoutSelectors);
8179
- if (withSelectors.length <= 1) {
8180
- result.push(...withSelectors);
8181
- continue;
8182
- }
8183
- result.push(factorAndGroup(withSelectors));
8500
+ for (const entry of entries) {
8501
+ const expanded = expandExclusiveConditionOrs(entry);
8502
+ result.push(...expanded);
8184
8503
  }
8185
8504
  return result;
8186
8505
  }
8187
8506
  /**
8188
- * Factor common modifier/pseudo conditions out of variants and create
8189
- * a single variant with a SelectorGroup for the remaining (differing)
8190
- * conditions.
8507
+ * Check if a condition involves at-rules (media, container, supports, starting)
8508
+ */
8509
+ function hasAtRuleContext(node) {
8510
+ if (node.kind === "true" || node.kind === "false") return false;
8511
+ if (node.kind === "state") return node.type === "media" || node.type === "container" || node.type === "supports" || node.type === "starting";
8512
+ if (node.kind === "compound") return node.children.some(hasAtRuleContext);
8513
+ return false;
8514
+ }
8515
+ /**
8516
+ * Sort OR branches to prioritize at-rule conditions first.
8191
8517
  *
8192
- * Precondition: all variants must share the same context key (identical
8193
- * at-rules, root/parent/own/selector groups, startingStyle).
8518
+ * This is critical for correct CSS generation. For `!A | !B` where A is at-rule
8519
+ * and B is modifier, we want:
8520
+ * - Branch 0: !A (at-rule negation - covers "no @supports/media" case)
8521
+ * - Branch 1: A & !B (modifier negation with at-rule context)
8522
+ *
8523
+ * If we process in wrong order (!B first), we'd get:
8524
+ * - Branch 0: !B (modifier negation WITHOUT at-rule context - WRONG!)
8525
+ * - Branch 1: B & !A (at-rule negation with modifier - incomplete coverage)
8194
8526
  */
8195
- function factorAndGroup(variants) {
8196
- {
8197
- const key0 = getVariantContextKey(variants[0]);
8198
- for (let i = 1; i < variants.length; i++) {
8199
- const keyI = getVariantContextKey(variants[i]);
8200
- if (keyI !== key0) throw new Error(`factorAndGroup: context key mismatch at index ${i}.\n expected: ${key0}\n got: ${keyI}`);
8201
- }
8202
- }
8203
- const commonModKeys = findCommonKeys(variants.map((v) => v.modifierConditions), getModifierKey);
8204
- const commonPseudoKeys = findCommonKeys(variants.map((v) => v.pseudoConditions), getPseudoKey);
8205
- const commonModifiers = variants[0].modifierConditions.filter((m) => commonModKeys.has(getModifierKey(m)));
8206
- const commonPseudos = variants[0].pseudoConditions.filter((p) => commonPseudoKeys.has(getPseudoKey(p)));
8207
- const branches = [];
8208
- let hasEmptyBranch = false;
8209
- for (const v of variants) {
8210
- const branch = [];
8211
- for (const mod of v.modifierConditions) if (!commonModKeys.has(getModifierKey(mod))) branch.push(mod);
8212
- for (const pseudo of v.pseudoConditions) if (!commonPseudoKeys.has(getPseudoKey(pseudo))) branch.push(pseudo);
8213
- if (branch.length > 0) branches.push(branch);
8214
- else hasEmptyBranch = true;
8215
- }
8216
- if (hasEmptyBranch) return {
8217
- ...variants[0],
8218
- modifierConditions: commonModifiers,
8219
- pseudoConditions: commonPseudos
8220
- };
8221
- return {
8222
- modifierConditions: commonModifiers,
8223
- pseudoConditions: commonPseudos,
8224
- selectorGroups: [...variants[0].selectorGroups, {
8225
- branches,
8226
- negated: false
8227
- }],
8228
- ownGroups: [...variants[0].ownGroups],
8229
- mediaConditions: [...variants[0].mediaConditions],
8230
- containerConditions: [...variants[0].containerConditions],
8231
- supportsConditions: [...variants[0].supportsConditions],
8232
- rootGroups: [...variants[0].rootGroups],
8233
- parentGroups: [...variants[0].parentGroups],
8234
- startingStyle: variants[0].startingStyle
8235
- };
8527
+ function sortOrBranchesForExpansion(branches) {
8528
+ return [...branches].sort((a, b) => {
8529
+ const aHasAtRule = hasAtRuleContext(a);
8530
+ const bHasAtRule = hasAtRuleContext(b);
8531
+ if (aHasAtRule && !bHasAtRule) return -1;
8532
+ if (!aHasAtRule && bHasAtRule) return 1;
8533
+ return 0;
8534
+ });
8236
8535
  }
8237
8536
  /**
8238
- * Build at-rules array from a variant
8537
+ * Expand ORs in a single entry's exclusive condition
8239
8538
  */
8240
- function buildAtRulesFromVariant(variant) {
8241
- const atRules = [];
8242
- if (variant.mediaConditions.length > 0) {
8243
- const conditionParts = variant.mediaConditions.map((c) => {
8244
- if (c.subtype === "type") return c.negated ? `not ${c.condition}` : c.condition;
8245
- else return c.negated ? `(not ${c.condition})` : c.condition;
8246
- });
8247
- atRules.push(`@media ${conditionParts.join(" and ")}`);
8248
- }
8249
- if (variant.containerConditions.length > 0) {
8250
- const byName = /* @__PURE__ */ new Map();
8251
- for (const cond of variant.containerConditions) {
8252
- const group = byName.get(cond.name) || [];
8253
- group.push(cond);
8254
- byName.set(cond.name, group);
8255
- }
8256
- for (const [name, conditions] of byName) {
8257
- const conditionParts = conditions.map((c) => c.negated ? `(not ${c.condition})` : c.condition);
8258
- const namePrefix = name ? `${name} ` : "";
8259
- atRules.push(`@container ${namePrefix}${conditionParts.join(" and ")}`);
8539
+ function expandExclusiveConditionOrs(entry) {
8540
+ let orBranches = collectOrBranches(entry.exclusiveCondition);
8541
+ if (orBranches.length <= 1) return [entry];
8542
+ if (!branchesProduceDifferentContexts(orBranches)) return [entry];
8543
+ orBranches = sortOrBranchesForExpansion(orBranches);
8544
+ const result = [];
8545
+ const priorBranches = [];
8546
+ for (let i = 0; i < orBranches.length; i++) {
8547
+ const branch = orBranches[i];
8548
+ let exclusiveBranch = branch;
8549
+ for (const prior of priorBranches) exclusiveBranch = and(exclusiveBranch, not(prior));
8550
+ const simplified = simplifyCondition(exclusiveBranch);
8551
+ if (simplified.kind === "false") {
8552
+ priorBranches.push(branch);
8553
+ continue;
8260
8554
  }
8261
- }
8262
- if (variant.supportsConditions.length > 0) {
8263
- const conditionParts = variant.supportsConditions.map((c) => {
8264
- if (c.subtype === "selector") {
8265
- const selectorCond = `selector(${c.condition})`;
8266
- return c.negated ? `(not ${selectorCond})` : selectorCond;
8267
- } else {
8268
- const featureCond = `(${c.condition})`;
8269
- return c.negated ? `(not ${featureCond})` : featureCond;
8270
- }
8555
+ result.push({
8556
+ ...entry,
8557
+ stateKey: `${entry.stateKey}[or:${i}]`,
8558
+ exclusiveCondition: simplified
8271
8559
  });
8272
- atRules.push(`@supports ${conditionParts.join(" and ")}`);
8560
+ priorBranches.push(branch);
8273
8561
  }
8274
- return atRules;
8562
+ return result;
8275
8563
  }
8276
8564
  //#endregion
8277
8565
  //#region src/utils/case-converter.ts
@@ -8338,6 +8626,15 @@ function emitWarning(code, message) {
8338
8626
  const MAX_XOR_CHAIN_LENGTH = 4;
8339
8627
  const parseCache = new Lru(5e3);
8340
8628
  /**
8629
+ * Chrome-internal pseudo-classes (e.g. `:-internal-autofill-selected`,
8630
+ * `:-internal-autofill-previewed`) cannot be targeted from user CSS and
8631
+ * may invalidate the surrounding rule in Safari even when wrapped in
8632
+ * forgiving `:is(...)`. The regex matches both bare uses and references
8633
+ * inside enhanced pseudo arguments like `:is(:-webkit-autofill,
8634
+ * :-internal-autofill-selected)`.
8635
+ */
8636
+ const INTERNAL_PSEUDO_PATTERN = /:-internal-[a-z0-9-]+/g;
8637
+ /**
8341
8638
  * Pattern for tokenizing state notation.
8342
8639
  * Matches: operators, parentheses, @-prefixed states, value mods, boolean mods,
8343
8640
  * pseudo-classes, class selectors, and attribute selectors.
@@ -8755,6 +9052,14 @@ function parseStateKey(stateKey, options = {}) {
8755
9052
  const cacheKey = trimmed + "\0" + (options.isSubElement ? "1" : "0") + "\0" + localStatesKey;
8756
9053
  const cached = parseCache.get(cacheKey);
8757
9054
  if (cached) return cached;
9055
+ if (isDevEnv()) {
9056
+ INTERNAL_PSEUDO_PATTERN.lastIndex = 0;
9057
+ const internalMatches = trimmed.match(INTERNAL_PSEUDO_PATTERN);
9058
+ if (internalMatches && internalMatches.length > 0) {
9059
+ const unique = Array.from(new Set(internalMatches));
9060
+ 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.`);
9061
+ }
9062
+ }
8758
9063
  const result = new Parser(tokenize(trimmed), options).parse();
8759
9064
  parseCache.set(cacheKey, result);
8760
9065
  return result;
@@ -8824,7 +9129,15 @@ function runPipeline(styles, parserContext) {
8824
9129
  function processStyles(styles, selectorSuffix, parserContext, allRules) {
8825
9130
  const keys = Object.keys(styles);
8826
9131
  const selectorKeys = keys.filter((key) => isSelector(key));
8827
- const styleKeys = keys.filter((key) => !isSelector(key) && !key.startsWith("@"));
9132
+ const styleKeys = [];
9133
+ for (const key of keys) {
9134
+ if (isSelector(key) || key.startsWith("@")) continue;
9135
+ if (key.startsWith(":")) {
9136
+ 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.`);
9137
+ continue;
9138
+ }
9139
+ styleKeys.push(key);
9140
+ }
8828
9141
  processNestedSelectors(styles, selectorKeys, selectorSuffix, parserContext, allRules);
8829
9142
  processHandlerQueue(buildHandlerQueue(styleKeys, styles), selectorSuffix, parserContext, allRules);
8830
9143
  }
@@ -9106,14 +9419,15 @@ function validatePattern(pattern) {
9106
9419
  * processSinglePattern('>Body>Row>', 'Cell')
9107
9420
  * // → '> [data-element="Body"] > [data-element="Row"] > [data-element="Cell"]'
9108
9421
  *
9109
- * processSinglePattern('::before', 'Before')
9110
- * // → '::before' (no key injection for pseudo on root)
9422
+ * processSinglePattern('&::before', 'Before')
9423
+ * // → '::before' (& attaches pseudo directly to root, no key injection)
9111
9424
  *
9112
9425
  * processSinglePattern('>@:hover', 'Item')
9113
9426
  * // → '> [data-element="Item"]:hover'
9114
9427
  */
9115
9428
  function processSinglePattern(pattern, key) {
9116
- const normalized = pattern.replace(/^&/, "").trim();
9429
+ const startsWithAmpersand = pattern.startsWith("&");
9430
+ const normalized = (startsWithAmpersand ? pattern.slice(1) : pattern).trim();
9117
9431
  if (!normalized) return ` [data-element="${key}"]`;
9118
9432
  const startsWithPseudo = /^::?[a-z]/.test(normalized);
9119
9433
  let result = transformPattern(normalized);
@@ -9123,8 +9437,8 @@ function processSinglePattern(pattern, key) {
9123
9437
  if (!startsWithPseudo && !result.startsWith(" ")) result = " " + result;
9124
9438
  return result;
9125
9439
  }
9126
- if (shouldInjectKey(normalized)) result = result + ` [data-element="${key}"]`;
9127
- if (!startsWithPseudo && !result.startsWith(" ")) result = " " + result;
9440
+ if (shouldInjectKey(normalized, key)) result = result + ` [data-element="${key}"]`;
9441
+ if (!startsWithAmpersand && !result.startsWith(" ")) result = " " + result;
9128
9442
  return result;
9129
9443
  }
9130
9444
  /**
@@ -9262,22 +9576,28 @@ function transformPattern(pattern) {
9262
9576
  * | Attribute ([type]) | No | `'[type="text"]'` | `[type="text"]` |
9263
9577
  *
9264
9578
  * @param pattern - The normalized pattern (after stripping &)
9579
+ * @param key - The sub-element key being styled. If the trailing element name
9580
+ * equals this key, it acts as an explicit placeholder for the key
9581
+ * (same role as `@`) and no auto-injection happens.
9265
9582
  * @returns true if key should be injected, false otherwise
9266
9583
  *
9267
9584
  * @example
9268
- * shouldInjectKey('>') // → true (trailing combinator)
9269
- * shouldInjectKey('>Body>Row') // → true (ends with element)
9270
- * shouldInjectKey('h1') // → false (ends with tag)
9271
- * shouldInjectKey('*') // → false (universal selector)
9272
- * shouldInjectKey('::before') // → false (ends with pseudo)
9273
- * shouldInjectKey('.active') // → false (ends with class)
9274
- * shouldInjectKey('a:hover') // → false (ends with pseudo)
9275
- * shouldInjectKey('button.primary') // → false (ends with class)
9276
- */
9277
- function shouldInjectKey(pattern) {
9585
+ * shouldInjectKey('>', 'Key') // → true (trailing combinator)
9586
+ * shouldInjectKey('>Body>Row', 'Key') // → true (ends with different element)
9587
+ * shouldInjectKey('>Body>Key', 'Key') // → false (trailing name === key)
9588
+ * shouldInjectKey('Key', 'Key') // → false (sole element === key)
9589
+ * shouldInjectKey('h1', 'Key') // → false (ends with tag)
9590
+ * shouldInjectKey('*', 'Key') // → false (universal selector)
9591
+ * shouldInjectKey('::before', 'Key') // → false (ends with pseudo)
9592
+ * shouldInjectKey('.active', 'Key') // → false (ends with class)
9593
+ * shouldInjectKey('a:hover', 'Key') // → false (ends with pseudo)
9594
+ * shouldInjectKey('button.primary', 'Key') // → false (ends with class)
9595
+ */
9596
+ function shouldInjectKey(pattern, key) {
9278
9597
  const trimmed = pattern.trim();
9279
9598
  if (/[>+~]$/.test(trimmed)) return true;
9280
- if (/(?:^|[\s>+~\]:])[A-Z][a-zA-Z0-9]*$/.test(trimmed)) return true;
9599
+ const trailingElement = trimmed.match(/(?:^|[\s>+~\]:])([A-Z][a-zA-Z0-9]*)$/);
9600
+ if (trailingElement) return trailingElement[1] !== key;
9281
9601
  return false;
9282
9602
  }
9283
9603
  /**
@@ -9683,7 +10003,10 @@ function isTestEnvironment() {
9683
10003
  const g = global;
9684
10004
  if (g.vi || g.jest || g.expect || g.describe || g.it) return true;
9685
10005
  }
9686
- if (typeof window !== "undefined" && window.navigator?.userAgent?.includes("jsdom")) return true;
10006
+ if (typeof window !== "undefined") {
10007
+ const ua = window.navigator?.userAgent;
10008
+ if (ua?.includes("jsdom") || ua?.includes("HappyDOM")) return true;
10009
+ }
9687
10010
  if (typeof globalThis !== "undefined") {
9688
10011
  const gt = globalThis;
9689
10012
  if (gt.vitest || gt.mocha) return true;
@@ -9697,7 +10020,8 @@ function createDefaultConfig(isTest) {
9697
10020
  return {
9698
10021
  maxRulesPerSheet: 8192,
9699
10022
  forceTextInjection: isTest ?? false,
9700
- devMode: isDevEnv()
10023
+ devMode: isDevEnv(),
10024
+ namePrefix: "t"
9701
10025
  };
9702
10026
  }
9703
10027
  /**
@@ -9939,6 +10263,7 @@ function configure(config = {}) {
9939
10263
  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.");
9940
10264
  return;
9941
10265
  }
10266
+ if (config.namePrefix !== void 0) validateNamePrefix(config.namePrefix);
9942
10267
  let mergedStates = {};
9943
10268
  let mergedUnits = {};
9944
10269
  let mergedFuncs = {};
@@ -10090,6 +10415,17 @@ function getConfig() {
10090
10415
  return currentConfig;
10091
10416
  }
10092
10417
  /**
10418
+ * Get the configured prefix used for every generated identifier
10419
+ * (class names, keyframe names, counter-style names).
10420
+ *
10421
+ * Falls back to the default prefix (`'t'`) when `configure()` has not
10422
+ * been called yet — this matches the auto-configuration behavior used
10423
+ * by the rest of the system.
10424
+ */
10425
+ function getNamePrefix() {
10426
+ return currentConfig?.namePrefix ?? "t";
10427
+ }
10428
+ /**
10093
10429
  * Get the global injector instance.
10094
10430
  * Auto-configures with defaults if not already configured.
10095
10431
  */
@@ -10123,6 +10459,6 @@ function resetConfig() {
10123
10459
  delete storage[GLOBAL_INJECTOR_KEY];
10124
10460
  }
10125
10461
  //#endregion
10126
- 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 };
10462
+ 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 };
10127
10463
 
10128
- //# sourceMappingURL=config-CtJeETqW.js.map
10464
+ //# sourceMappingURL=config-BaxtQMS3.js.map