@tenphi/tasty 0.13.0 → 0.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/dist/chunks/cacheKey.js +16 -8
  2. package/dist/chunks/cacheKey.js.map +1 -1
  3. package/dist/chunks/renderChunk.js +31 -32
  4. package/dist/chunks/renderChunk.js.map +1 -1
  5. package/dist/config.d.ts +1 -1
  6. package/dist/config.js +6 -3
  7. package/dist/config.js.map +1 -1
  8. package/dist/core/index.d.ts +3 -3
  9. package/dist/core/index.js +3 -3
  10. package/dist/hooks/useStyles.js +4 -3
  11. package/dist/hooks/useStyles.js.map +1 -1
  12. package/dist/index.d.ts +3 -3
  13. package/dist/index.js +3 -3
  14. package/dist/pipeline/index.d.ts +1 -1
  15. package/dist/pipeline/index.js +24 -14
  16. package/dist/pipeline/index.js.map +1 -1
  17. package/dist/pipeline/materialize.js +328 -79
  18. package/dist/pipeline/materialize.js.map +1 -1
  19. package/dist/pipeline/parseStateKey.d.ts +1 -1
  20. package/dist/pipeline/parseStateKey.js +2 -6
  21. package/dist/pipeline/parseStateKey.js.map +1 -1
  22. package/dist/states/index.js +10 -257
  23. package/dist/states/index.js.map +1 -1
  24. package/dist/tasty.js +23 -10
  25. package/dist/tasty.js.map +1 -1
  26. package/dist/utils/cache-wrapper.js +4 -8
  27. package/dist/utils/cache-wrapper.js.map +1 -1
  28. package/dist/utils/has-keys.js +13 -0
  29. package/dist/utils/has-keys.js.map +1 -0
  30. package/dist/utils/mod-attrs.js +1 -1
  31. package/dist/utils/styles.d.ts +1 -64
  32. package/dist/utils/styles.js +7 -319
  33. package/dist/utils/styles.js.map +1 -1
  34. package/dist/zero/babel.d.ts +8 -0
  35. package/dist/zero/babel.js +18 -3
  36. package/dist/zero/babel.js.map +1 -1
  37. package/dist/zero/next.js +9 -1
  38. package/dist/zero/next.js.map +1 -1
  39. package/docs/tasty-static.md +6 -7
  40. package/package.json +1 -1
@@ -1,5 +1,5 @@
1
1
  import { Lru } from "../parser/lru.js";
2
- import { getConditionUniqueId, not } from "./conditions.js";
2
+ import { getConditionUniqueId } from "./conditions.js";
3
3
 
4
4
  //#region src/pipeline/materialize.ts
5
5
  /**
@@ -24,11 +24,12 @@ function emptyVariant() {
24
24
  return {
25
25
  modifierConditions: [],
26
26
  pseudoConditions: [],
27
- ownConditions: [],
27
+ selectorGroups: [],
28
+ ownGroups: [],
28
29
  mediaConditions: [],
29
30
  containerConditions: [],
30
31
  supportsConditions: [],
31
- rootConditions: [],
32
+ rootGroups: [],
32
33
  parentGroups: [],
33
34
  startingStyle: false
34
35
  };
@@ -63,9 +64,9 @@ function stateToCSS(state) {
63
64
  }),
64
65
  isImpossible: false
65
66
  };
66
- case "root": return innerConditionToVariants(state.innerCondition, state.negated ?? false, "rootConditions");
67
+ case "root": return innerConditionToVariants(state.innerCondition, state.negated ?? false, "rootGroups");
67
68
  case "parent": return parentConditionToVariants(state.innerCondition, state.negated ?? false, state.direct);
68
- case "own": return innerConditionToVariants(state.innerCondition, state.negated ?? false, "ownConditions");
69
+ case "own": return innerConditionToVariants(state.innerCondition, state.negated ?? false, "ownGroups");
69
70
  case "modifier": {
70
71
  const v = emptyVariant();
71
72
  v.modifierConditions.push(modifierToParsed(state));
@@ -264,31 +265,40 @@ function collectSelectorConditions(variant) {
264
265
  return [...variant.modifierConditions, ...variant.pseudoConditions];
265
266
  }
266
267
  /**
267
- * Convert an inner condition tree into SelectorVariants.
268
- * Each inner OR branch becomes a separate variant, preserving disjunction.
268
+ * Convert an inner condition tree into a single SelectorVariant with
269
+ * one SelectorGroup whose branches represent the inner OR alternatives.
269
270
  * Shared by @root() and @own().
271
+ *
272
+ * Both positive and negated cases produce one variant with one group.
273
+ * Negation simply sets the `negated` flag, which swaps :is() for :not()
274
+ * in the final CSS output — no De Morgan transformation is needed.
275
+ *
276
+ * This mirrors parentConditionToVariants: OR branches are kept inside
277
+ * a single group and rendered as comma-separated arguments in
278
+ * :is()/:not(), e.g. :root:is([a], [b]) or [el]:not([a], [b]).
270
279
  */
271
280
  function innerConditionToVariants(innerCondition, negated, target) {
272
- const innerCSS = conditionToCSS(negated ? not(innerCondition) : innerCondition);
281
+ const innerCSS = conditionToCSS(innerCondition);
273
282
  if (innerCSS.isImpossible || innerCSS.variants.length === 0) return {
274
283
  variants: [],
275
284
  isImpossible: true
276
285
  };
277
- const variants = [];
286
+ const branches = [];
278
287
  for (const innerVariant of innerCSS.variants) {
279
288
  const conditions = collectSelectorConditions(innerVariant);
280
- if (conditions.length > 0) {
281
- const v = emptyVariant();
282
- v[target].push(...conditions);
283
- variants.push(v);
284
- }
289
+ if (conditions.length > 0) branches.push(conditions);
285
290
  }
286
- if (variants.length === 0) return {
291
+ if (branches.length === 0) return {
287
292
  variants: [emptyVariant()],
288
293
  isImpossible: false
289
294
  };
295
+ const v = emptyVariant();
296
+ v[target].push({
297
+ branches,
298
+ negated
299
+ });
290
300
  return {
291
- variants,
301
+ variants: [v],
292
302
  isImpossible: false
293
303
  };
294
304
  }
@@ -327,29 +337,157 @@ function parentConditionToVariants(innerCondition, negated, direct) {
327
337
  };
328
338
  }
329
339
  /**
330
- * Convert parsed root conditions to CSS selector prefix (for final output)
340
+ * Sort key for canonical condition output within selectors.
341
+ *
342
+ * Priority order:
343
+ * 0: Boolean attribute selectors ([data-hovered])
344
+ * 1: Value attribute selectors ([data-size="small"])
345
+ * 2: Negated boolean attributes (:not([data-disabled]))
346
+ * 3: Negated value attributes (:not([data-size="small"]))
347
+ * 4: Pseudo-classes (:hover, :focus)
348
+ * 5: Negated pseudo-classes (:not(:disabled))
349
+ *
350
+ * Secondary sort: alphabetical by attribute name / pseudo string.
351
+ */
352
+ function conditionSortKey(cond) {
353
+ if ("attribute" in cond) {
354
+ const hasValue = cond.value !== void 0 ? 1 : 0;
355
+ return `${(cond.negated ? 2 : 0) + hasValue}|${cond.attribute}|${cond.value ?? ""}`;
356
+ }
357
+ return `${cond.negated ? 5 : 4}|${cond.pseudo}`;
358
+ }
359
+ function sortConditions(conditions) {
360
+ return conditions.toSorted((a, b) => conditionSortKey(a).localeCompare(conditionSortKey(b)));
361
+ }
362
+ function branchToCSS(branch) {
363
+ let parts = "";
364
+ for (const cond of sortConditions(branch)) parts += selectorConditionToCSS(cond);
365
+ return parts;
366
+ }
367
+ /**
368
+ * Wrap serialized selector arguments in :is() or :not().
369
+ * Arguments are sorted for canonical output.
370
+ */
371
+ function wrapInIsOrNot(args, negated) {
372
+ return `${negated ? ":not" : ":is"}(${args.sort().join(", ")})`;
373
+ }
374
+ /**
375
+ * Convert a selector group to a CSS selector fragment.
376
+ *
377
+ * Single-branch groups are unwrapped (no :is() wrapper).
378
+ * Multi-branch groups use :is() or :not().
379
+ * Negation swaps :is() for :not().
380
+ */
381
+ function selectorGroupToCSS(group) {
382
+ if (group.branches.length === 0) return "";
383
+ if (group.branches.length === 1) {
384
+ const parts = branchToCSS(group.branches[0]);
385
+ if (group.negated) return `:not(${parts})`;
386
+ return parts;
387
+ }
388
+ return wrapInIsOrNot(group.branches.map(branchToCSS), group.negated);
389
+ }
390
+ /**
391
+ * Collect facts about modifier conditions for subsumption analysis.
392
+ * Tracks negated boolean attrs (:not([attr])) and positive exact values ([attr="X"]).
331
393
  */
332
- function rootConditionsToCSS(roots) {
333
- if (roots.length === 0) return void 0;
394
+ function collectSubsumptionFacts(modifiers) {
395
+ const negatedBooleanAttrs = /* @__PURE__ */ new Set();
396
+ const positiveExactValuesByAttr = /* @__PURE__ */ new Map();
397
+ for (const mod of modifiers) {
398
+ if (mod.negated && mod.value === void 0) negatedBooleanAttrs.add(mod.attribute);
399
+ if (!mod.negated && mod.value !== void 0 && (mod.operator ?? "=") === "=") {
400
+ let vals = positiveExactValuesByAttr.get(mod.attribute);
401
+ if (!vals) {
402
+ vals = /* @__PURE__ */ new Set();
403
+ positiveExactValuesByAttr.set(mod.attribute, vals);
404
+ }
405
+ vals.add(mod.value);
406
+ }
407
+ }
408
+ return {
409
+ negatedBooleanAttrs,
410
+ positiveExactValuesByAttr
411
+ };
412
+ }
413
+ /**
414
+ * Check if a negated-value modifier is subsumed by stronger facts:
415
+ * - :not([attr]) subsumes :not([attr="val"])
416
+ * - [attr="X"] implies :not([attr="Y"]) is redundant (single exact value)
417
+ *
418
+ * Only applies to exact-match (=) operators; substring operators don't
419
+ * imply exclusivity between values.
420
+ */
421
+ function isSubsumedNegatedModifier(mod, facts) {
422
+ if (!mod.negated || mod.value === void 0) return false;
423
+ if (facts.negatedBooleanAttrs.has(mod.attribute)) return true;
424
+ if ((mod.operator ?? "=") === "=") {
425
+ const posVals = facts.positiveExactValuesByAttr.get(mod.attribute);
426
+ if (posVals && posVals.size === 1 && !posVals.has(mod.value)) return true;
427
+ }
428
+ return false;
429
+ }
430
+ /**
431
+ * Remove redundant single-condition groups that are subsumed by stronger
432
+ * groups on the same attribute. O(n) — only inspects single-branch,
433
+ * single-condition groups.
434
+ */
435
+ function optimizeGroups(groups) {
436
+ if (groups.length <= 1) return groups;
437
+ const seen = /* @__PURE__ */ new Set();
438
+ const result = [];
439
+ for (const g of groups) {
440
+ const key = getSelectorGroupKey(g);
441
+ if (!seen.has(key)) {
442
+ seen.add(key);
443
+ result.push(g);
444
+ }
445
+ }
446
+ if (result.length <= 1) return result;
447
+ const effectiveModifiers = [];
448
+ for (const g of result) {
449
+ if (g.branches.length !== 1 || g.branches[0].length !== 1) continue;
450
+ const cond = g.branches[0][0];
451
+ if (!("attribute" in cond)) continue;
452
+ effectiveModifiers.push({
453
+ ...cond,
454
+ negated: g.negated !== cond.negated
455
+ });
456
+ }
457
+ const facts = collectSubsumptionFacts(effectiveModifiers);
458
+ if (facts.negatedBooleanAttrs.size === 0 && facts.positiveExactValuesByAttr.size === 0) return result;
459
+ return result.filter((g) => {
460
+ if (g.branches.length !== 1 || g.branches[0].length !== 1) return true;
461
+ const cond = g.branches[0][0];
462
+ if (!("attribute" in cond) || !g.negated || cond.negated || cond.value === void 0) return true;
463
+ return !isSubsumedNegatedModifier({
464
+ ...cond,
465
+ negated: true
466
+ }, facts);
467
+ });
468
+ }
469
+ /**
470
+ * Convert root groups to CSS selector prefix (for final output)
471
+ */
472
+ function rootGroupsToCSS(groups) {
473
+ if (groups.length === 0) return void 0;
474
+ const optimized = optimizeGroups(groups);
475
+ if (optimized.length === 0) return void 0;
334
476
  let prefix = ":root";
335
- for (const cond of roots) prefix += selectorConditionToCSS(cond);
477
+ for (const group of optimized) prefix += selectorGroupToCSS(group);
336
478
  return prefix;
337
479
  }
338
480
  /**
339
481
  * Convert parent groups to CSS selector fragments (for final output).
340
- * Each group produces its own :is() wrapper.
482
+ * Each group produces its own :is()/:not() wrapper with a combinator
483
+ * suffix (` *` or ` > *`) appended to each branch.
341
484
  */
342
485
  function parentGroupsToCSS(groups) {
343
486
  let result = "";
344
487
  for (const group of groups) {
345
488
  const combinator = group.direct ? " > *" : " *";
346
- const selectorArgs = group.branches.map((branch) => {
347
- let parts = "";
348
- for (const cond of branch) parts += selectorConditionToCSS(cond);
349
- return parts + combinator;
350
- });
351
- const wrapper = group.negated ? ":not" : ":is";
352
- result += `${wrapper}(${selectorArgs.join(", ")})`;
489
+ const args = group.branches.map((branch) => branchToCSS(branch) + combinator);
490
+ result += wrapInIsOrNot(args, group.negated);
353
491
  }
354
492
  return result;
355
493
  }
@@ -385,7 +523,7 @@ function getSelectorConditionKey(cond) {
385
523
  */
386
524
  function dedupeSelectorConditions(conditions) {
387
525
  const seen = /* @__PURE__ */ new Set();
388
- let result = [];
526
+ const result = [];
389
527
  for (const c of conditions) {
390
528
  const key = getSelectorConditionKey(c);
391
529
  if (!seen.has(key)) {
@@ -393,30 +531,12 @@ function dedupeSelectorConditions(conditions) {
393
531
  result.push(c);
394
532
  }
395
533
  }
396
- const negatedBooleanAttrs = /* @__PURE__ */ new Set();
397
- const positiveExactValuesByAttr = /* @__PURE__ */ new Map();
398
- for (const c of result) {
399
- if (!("attribute" in c)) continue;
400
- if (c.negated && c.value === void 0) negatedBooleanAttrs.add(c.attribute);
401
- const op = c.operator ?? "=";
402
- if (!c.negated && c.value !== void 0 && op === "=") {
403
- let values = positiveExactValuesByAttr.get(c.attribute);
404
- if (!values) {
405
- values = /* @__PURE__ */ new Set();
406
- positiveExactValuesByAttr.set(c.attribute, values);
407
- }
408
- values.add(c.value);
409
- }
410
- }
411
- result = result.filter((c) => {
412
- if (!("attribute" in c) || !c.negated || c.value === void 0) return true;
413
- if (negatedBooleanAttrs.has(c.attribute)) return false;
414
- if ((c.operator ?? "=") !== "=") return true;
415
- const positiveValues = positiveExactValuesByAttr.get(c.attribute);
416
- if (positiveValues !== void 0 && positiveValues.size === 1 && !positiveValues.has(c.value)) return false;
417
- return true;
534
+ const facts = collectSubsumptionFacts(result.filter((c) => "attribute" in c));
535
+ if (facts.negatedBooleanAttrs.size === 0 && facts.positiveExactValuesByAttr.size === 0) return result;
536
+ return result.filter((c) => {
537
+ if (!("attribute" in c)) return true;
538
+ return !isSubsumedNegatedModifier(c, facts);
418
539
  });
419
- return result;
420
540
  }
421
541
  /**
422
542
  * Check for modifier contradiction: same attribute with opposite negation
@@ -478,21 +598,37 @@ function hasParentGroupContradiction(groups) {
478
598
  return false;
479
599
  }
480
600
  /**
601
+ * Check for selector group contradiction: same branches with opposite negation.
602
+ * E.g. :is([data-a]) and :not([data-a]) in the same variant is impossible.
603
+ */
604
+ function hasSelectorGroupContradiction(groups) {
605
+ const byBaseKey = /* @__PURE__ */ new Map();
606
+ for (const g of groups) {
607
+ const baseKey = getBranchesKey(g.branches);
608
+ const existing = byBaseKey.get(baseKey);
609
+ if (existing !== void 0 && existing !== !g.negated) return true;
610
+ byBaseKey.set(baseKey, !g.negated);
611
+ }
612
+ return false;
613
+ }
614
+ /**
481
615
  * Merge two selector variants (AND operation)
482
616
  * Deduplicates conditions and checks for contradictions
483
617
  */
484
618
  function mergeVariants(a, b) {
485
619
  const mergedMedia = dedupeMediaConditions([...a.mediaConditions, ...b.mediaConditions]);
486
620
  if (hasMediaContradiction(mergedMedia)) return null;
487
- const mergedRoots = dedupeSelectorConditions([...a.rootConditions, ...b.rootConditions]);
488
- if (hasSelectorConditionContradiction(mergedRoots)) return null;
621
+ const mergedRootGroups = optimizeGroups([...a.rootGroups, ...b.rootGroups]);
622
+ if (hasSelectorGroupContradiction(mergedRootGroups)) return null;
489
623
  const mergedModifiers = dedupeSelectorConditions([...a.modifierConditions, ...b.modifierConditions]);
490
624
  const mergedPseudos = dedupeSelectorConditions([...a.pseudoConditions, ...b.pseudoConditions]);
491
625
  if (hasSelectorConditionContradiction([...mergedModifiers, ...mergedPseudos])) return null;
626
+ const mergedSelectorGroups = optimizeGroups([...a.selectorGroups, ...b.selectorGroups]);
627
+ if (hasSelectorGroupContradiction(mergedSelectorGroups)) return null;
492
628
  const mergedParentGroups = [...a.parentGroups, ...b.parentGroups];
493
629
  if (hasParentGroupContradiction(mergedParentGroups)) return null;
494
- const mergedOwn = dedupeSelectorConditions([...a.ownConditions, ...b.ownConditions]);
495
- if (hasSelectorConditionContradiction(mergedOwn)) return null;
630
+ const mergedOwnGroups = optimizeGroups([...a.ownGroups, ...b.ownGroups]);
631
+ if (hasSelectorGroupContradiction(mergedOwnGroups)) return null;
496
632
  const mergedContainers = dedupeContainerConditions([...a.containerConditions, ...b.containerConditions]);
497
633
  if (hasContainerStyleContradiction(mergedContainers)) return null;
498
634
  const mergedSupports = dedupeSupportsConditions([...a.supportsConditions, ...b.supportsConditions]);
@@ -500,11 +636,12 @@ function mergeVariants(a, b) {
500
636
  return {
501
637
  modifierConditions: mergedModifiers,
502
638
  pseudoConditions: mergedPseudos,
503
- ownConditions: mergedOwn,
639
+ selectorGroups: mergedSelectorGroups,
640
+ ownGroups: mergedOwnGroups,
504
641
  mediaConditions: mergedMedia,
505
642
  containerConditions: mergedContainers,
506
643
  supportsConditions: mergedSupports,
507
- rootConditions: mergedRoots,
644
+ rootGroups: mergedRootGroups,
508
645
  parentGroups: mergedParentGroups,
509
646
  startingStyle: a.startingStyle || b.startingStyle
510
647
  };
@@ -635,32 +772,43 @@ const variantKeyCache = /* @__PURE__ */ new WeakMap();
635
772
  * Cached via WeakMap since variants are compared multiple times during
636
773
  * deduplication and sorting.
637
774
  */
638
- function getVariantKey(v) {
639
- const cached = variantKeyCache.get(v);
640
- if (cached !== void 0) return cached;
641
- const modifierKey = v.modifierConditions.map(getModifierKey).sort().join("|");
642
- const pseudoKey = v.pseudoConditions.map(getPseudoKey).sort().join("|");
643
- const ownKey = v.ownConditions.map(getSelectorConditionKey).sort().join("|");
644
- const containerKey = v.containerConditions.map((c) => `${c.name ?? ""}:${c.negated ? "!" : ""}${c.condition}`).sort().join("|");
645
- const key = [
646
- modifierKey,
647
- pseudoKey,
648
- ownKey,
775
+ function getSelectorGroupKey(g) {
776
+ return `${g.negated ? "!" : ""}(${getBranchesKey(g.branches)})`;
777
+ }
778
+ /**
779
+ * Get a context key for a variant — everything except flat modifier/pseudo
780
+ * conditions. Variants with the same context key can be merged into an
781
+ * :is() group. Also used by getVariantKey as the shared non-selector portion.
782
+ */
783
+ function getVariantContextKey(v) {
784
+ return [
649
785
  v.mediaConditions.map((c) => `${c.subtype}:${c.negated ? "!" : ""}${c.condition}`).sort().join("|"),
650
- containerKey,
786
+ v.containerConditions.map((c) => `${c.name ?? ""}:${c.negated ? "!" : ""}${c.condition}`).sort().join("|"),
651
787
  v.supportsConditions.map((c) => `${c.subtype}:${c.negated ? "!" : ""}${c.condition}`).sort().join("|"),
652
- v.rootConditions.map(getSelectorConditionKey).sort().join("|"),
788
+ v.rootGroups.map(getSelectorGroupKey).sort().join("|"),
653
789
  v.parentGroups.map(getParentGroupKey).sort().join("|"),
790
+ v.ownGroups.map(getSelectorGroupKey).sort().join("|"),
791
+ v.selectorGroups.map(getSelectorGroupKey).sort().join("|"),
654
792
  v.startingStyle ? "1" : "0"
655
793
  ].join("###");
794
+ }
795
+ function getVariantKey(v) {
796
+ const cached = variantKeyCache.get(v);
797
+ if (cached !== void 0) return cached;
798
+ const modifierKey = v.modifierConditions.map(getModifierKey).sort().join("|");
799
+ const pseudoKey = v.pseudoConditions.map(getPseudoKey).sort().join("|");
800
+ const key = modifierKey + "###" + pseudoKey + "###" + getVariantContextKey(v);
656
801
  variantKeyCache.set(v, key);
657
802
  return key;
658
803
  }
659
804
  /**
660
805
  * Total number of leaf conditions in a variant (for superset / dedup comparisons).
661
806
  */
807
+ function groupConditionCount(groups) {
808
+ return groups.reduce((sum, g) => sum + g.branches.reduce((s, b) => s + b.length, 0), 0);
809
+ }
662
810
  function variantConditionCount(v) {
663
- return v.modifierConditions.length + v.pseudoConditions.length + v.ownConditions.length + v.mediaConditions.length + v.containerConditions.length + v.supportsConditions.length + v.rootConditions.length + v.parentGroups.reduce((sum, g) => sum + g.branches.reduce((s, b) => s + b.length, 0), 0);
811
+ return v.modifierConditions.length + v.pseudoConditions.length + groupConditionCount(v.selectorGroups) + groupConditionCount(v.ownGroups) + v.mediaConditions.length + v.containerConditions.length + v.supportsConditions.length + groupConditionCount(v.rootGroups) + groupConditionCount(v.parentGroups);
664
812
  }
665
813
  /**
666
814
  * Check if variant A is a superset of variant B (A is more restrictive)
@@ -675,13 +823,14 @@ function variantConditionCount(v) {
675
823
  */
676
824
  function isVariantSuperset(a, b) {
677
825
  if (a.startingStyle !== b.startingStyle) return false;
678
- if (!isSelectorConditionsSuperset(a.rootConditions, b.rootConditions)) return false;
826
+ if (!isSelectorGroupsSuperset(a.rootGroups, b.rootGroups)) return false;
679
827
  if (!isMediaConditionsSuperset(a.mediaConditions, b.mediaConditions)) return false;
680
828
  if (!isContainerConditionsSuperset(a.containerConditions, b.containerConditions)) return false;
681
829
  if (!isSupportsConditionsSuperset(a.supportsConditions, b.supportsConditions)) return false;
682
830
  if (!isModifierConditionsSuperset(a.modifierConditions, b.modifierConditions)) return false;
683
831
  if (!isPseudoConditionsSuperset(a.pseudoConditions, b.pseudoConditions)) return false;
684
- if (!isSelectorConditionsSuperset(a.ownConditions, b.ownConditions)) return false;
832
+ if (!isSelectorGroupsSuperset(a.selectorGroups, b.selectorGroups)) return false;
833
+ if (!isSelectorGroupsSuperset(a.ownGroups, b.ownGroups)) return false;
685
834
  if (!isParentGroupsSuperset(a.parentGroups, b.parentGroups)) return false;
686
835
  return variantConditionCount(a) > variantConditionCount(b);
687
836
  }
@@ -707,8 +856,9 @@ function isModifierConditionsSuperset(a, b) {
707
856
  function isPseudoConditionsSuperset(a, b) {
708
857
  return isConditionsSuperset(a, b, getPseudoKey);
709
858
  }
710
- function isSelectorConditionsSuperset(a, b) {
711
- return isConditionsSuperset(a, b, getSelectorConditionKey);
859
+ function isSelectorGroupsSuperset(a, b) {
860
+ if (a.length < b.length) return false;
861
+ return isConditionsSuperset(a, b, getSelectorGroupKey);
712
862
  }
713
863
  /**
714
864
  * Check if parent groups A is a superset of B.
@@ -729,6 +879,7 @@ function getParentGroupKey(g) {
729
879
  * 2. Superset variants (more restrictive selectors that are redundant)
730
880
  */
731
881
  function dedupeVariants(variants) {
882
+ if (variants.length <= 1) return variants;
732
883
  const seen = /* @__PURE__ */ new Set();
733
884
  const result = [];
734
885
  for (const v of variants) {
@@ -738,6 +889,7 @@ function dedupeVariants(variants) {
738
889
  result.push(v);
739
890
  }
740
891
  }
892
+ if (result.length <= 1) return result;
741
893
  result.sort((a, b) => variantConditionCount(a) - variantConditionCount(b));
742
894
  const filtered = [];
743
895
  for (const candidate of result) {
@@ -787,8 +939,8 @@ function andToCSS(children) {
787
939
  * Combine OR conditions into CSS
788
940
  *
789
941
  * OR in CSS means multiple selector variants (DNF).
790
- * Each variant becomes a separate selector in the comma-separated list,
791
- * or multiple CSS rules if they have different at-rules.
942
+ * After deduplication, variants that differ only in their base
943
+ * modifier/pseudo conditions are merged into :is() groups.
792
944
  *
793
945
  * Note: OR exclusivity is handled at the pipeline level (expandOrConditions),
794
946
  * so here we just collect all variants. Any remaining ORs in the condition
@@ -811,6 +963,103 @@ function orToCSS(children) {
811
963
  };
812
964
  }
813
965
  /**
966
+ * Find keys present in ALL condition arrays.
967
+ */
968
+ function findCommonKeys(conditionSets, getKey) {
969
+ if (conditionSets.length === 0) return /* @__PURE__ */ new Set();
970
+ const common = new Set(conditionSets[0].map(getKey));
971
+ for (let i = 1; i < conditionSets.length; i++) {
972
+ const keys = new Set(conditionSets[i].map(getKey));
973
+ for (const key of common) if (!keys.has(key)) common.delete(key);
974
+ }
975
+ return common;
976
+ }
977
+ /**
978
+ * Merge OR variants that share the same "context" (at-rules, root, parent,
979
+ * own, starting) into a single variant with a SelectorGroup.
980
+ *
981
+ * Variants with no modifier/pseudo conditions are kept separate (they match
982
+ * unconditionally and can't be expressed inside :is()).
983
+ */
984
+ function mergeVariantsIntoSelectorGroups(variants) {
985
+ if (variants.length <= 1) return variants;
986
+ const groups = /* @__PURE__ */ new Map();
987
+ for (const v of variants) {
988
+ const key = getVariantContextKey(v);
989
+ const group = groups.get(key);
990
+ if (group) group.push(v);
991
+ else groups.set(key, [v]);
992
+ }
993
+ const result = [];
994
+ for (const group of groups.values()) {
995
+ if (group.length === 1) {
996
+ result.push(group[0]);
997
+ continue;
998
+ }
999
+ const withSelectors = [];
1000
+ const withoutSelectors = [];
1001
+ for (const v of group) if (v.modifierConditions.length === 0 && v.pseudoConditions.length === 0) withoutSelectors.push(v);
1002
+ else withSelectors.push(v);
1003
+ result.push(...withoutSelectors);
1004
+ if (withSelectors.length <= 1) {
1005
+ result.push(...withSelectors);
1006
+ continue;
1007
+ }
1008
+ result.push(factorAndGroup(withSelectors));
1009
+ }
1010
+ return result;
1011
+ }
1012
+ /**
1013
+ * Factor common modifier/pseudo conditions out of variants and create
1014
+ * a single variant with a SelectorGroup for the remaining (differing)
1015
+ * conditions.
1016
+ *
1017
+ * Precondition: all variants must share the same context key (identical
1018
+ * at-rules, root/parent/own/selector groups, startingStyle).
1019
+ */
1020
+ function factorAndGroup(variants) {
1021
+ {
1022
+ const key0 = getVariantContextKey(variants[0]);
1023
+ for (let i = 1; i < variants.length; i++) {
1024
+ const keyI = getVariantContextKey(variants[i]);
1025
+ if (keyI !== key0) throw new Error(`factorAndGroup: context key mismatch at index ${i}.\n expected: ${key0}\n got: ${keyI}`);
1026
+ }
1027
+ }
1028
+ const commonModKeys = findCommonKeys(variants.map((v) => v.modifierConditions), getModifierKey);
1029
+ const commonPseudoKeys = findCommonKeys(variants.map((v) => v.pseudoConditions), getPseudoKey);
1030
+ const commonModifiers = variants[0].modifierConditions.filter((m) => commonModKeys.has(getModifierKey(m)));
1031
+ const commonPseudos = variants[0].pseudoConditions.filter((p) => commonPseudoKeys.has(getPseudoKey(p)));
1032
+ const branches = [];
1033
+ let hasEmptyBranch = false;
1034
+ for (const v of variants) {
1035
+ const branch = [];
1036
+ for (const mod of v.modifierConditions) if (!commonModKeys.has(getModifierKey(mod))) branch.push(mod);
1037
+ for (const pseudo of v.pseudoConditions) if (!commonPseudoKeys.has(getPseudoKey(pseudo))) branch.push(pseudo);
1038
+ if (branch.length > 0) branches.push(branch);
1039
+ else hasEmptyBranch = true;
1040
+ }
1041
+ if (hasEmptyBranch) return {
1042
+ ...variants[0],
1043
+ modifierConditions: commonModifiers,
1044
+ pseudoConditions: commonPseudos
1045
+ };
1046
+ return {
1047
+ modifierConditions: commonModifiers,
1048
+ pseudoConditions: commonPseudos,
1049
+ selectorGroups: [...variants[0].selectorGroups, {
1050
+ branches,
1051
+ negated: false
1052
+ }],
1053
+ ownGroups: [...variants[0].ownGroups],
1054
+ mediaConditions: [...variants[0].mediaConditions],
1055
+ containerConditions: [...variants[0].containerConditions],
1056
+ supportsConditions: [...variants[0].supportsConditions],
1057
+ rootGroups: [...variants[0].rootGroups],
1058
+ parentGroups: [...variants[0].parentGroups],
1059
+ startingStyle: variants[0].startingStyle
1060
+ };
1061
+ }
1062
+ /**
814
1063
  * Build at-rules array from a variant
815
1064
  */
816
1065
  function buildAtRulesFromVariant(variant) {
@@ -852,5 +1101,5 @@ function buildAtRulesFromVariant(variant) {
852
1101
  }
853
1102
 
854
1103
  //#endregion
855
- export { buildAtRulesFromVariant, conditionToCSS, modifierToCSS, parentGroupsToCSS, pseudoToCSS, rootConditionsToCSS, selectorConditionToCSS };
1104
+ export { branchToCSS, buildAtRulesFromVariant, conditionToCSS, mergeVariantsIntoSelectorGroups, optimizeGroups, parentGroupsToCSS, rootGroupsToCSS, selectorGroupToCSS };
856
1105
  //# sourceMappingURL=materialize.js.map