domflax 0.1.4 → 0.2.0

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 (42) hide show
  1. package/README.md +47 -29
  2. package/dist/{chunk-EVENAJYI.js → chunk-EYQXQQQH.js} +3 -3
  3. package/dist/{chunk-3Z5ZWLXX.js → chunk-FPT4EJ6Q.js} +805 -1612
  4. package/dist/chunk-FPT4EJ6Q.js.map +1 -0
  5. package/dist/{chunk-5FWENSD2.js → chunk-JBM3MJRM.js} +149 -10
  6. package/dist/chunk-JBM3MJRM.js.map +1 -0
  7. package/dist/{chunk-H5KTGI3A.js → chunk-TTJEXWAC.js} +172 -5
  8. package/dist/chunk-TTJEXWAC.js.map +1 -0
  9. package/dist/cli.cjs +1032 -1640
  10. package/dist/cli.cjs.map +1 -1
  11. package/dist/cli.js +30 -10
  12. package/dist/cli.js.map +1 -1
  13. package/dist/index.cjs +1116 -1627
  14. package/dist/index.cjs.map +1 -1
  15. package/dist/index.d.cts +226 -485
  16. package/dist/index.d.ts +226 -485
  17. package/dist/index.js +16 -36
  18. package/dist/{pattern-CP9_HpVK.d.cts → pattern-DotR_dHs.d.cts} +1 -1
  19. package/dist/pattern-kit.cjs +60 -1
  20. package/dist/pattern-kit.cjs.map +1 -1
  21. package/dist/pattern-kit.d.cts +2 -2
  22. package/dist/pattern-kit.d.ts +2 -2
  23. package/dist/pattern-kit.js +1 -1
  24. package/dist/{pattern-CYgsv-jO.d.ts → pattern-urm5uuwj.d.ts} +1 -1
  25. package/dist/{resolve-ops-Ci7LgYHC.d.ts → resolve-ops-D8aQina5.d.cts} +11 -0
  26. package/dist/{resolve-ops-Ci7LgYHC.d.cts → resolve-ops-D8aQina5.d.ts} +11 -0
  27. package/dist/verify.d.cts +1 -1
  28. package/dist/verify.d.ts +1 -1
  29. package/dist/webpack-loader.cjs +1014 -1578
  30. package/dist/webpack-loader.cjs.map +1 -1
  31. package/dist/webpack-loader.d.cts +8 -2
  32. package/dist/webpack-loader.d.ts +8 -2
  33. package/dist/webpack-loader.js +7 -4
  34. package/dist/webpack-loader.js.map +1 -1
  35. package/dist/worker.cjs +983 -1601
  36. package/dist/worker.cjs.map +1 -1
  37. package/dist/worker.js +3 -3
  38. package/package.json +1 -1
  39. package/dist/chunk-3Z5ZWLXX.js.map +0 -1
  40. package/dist/chunk-5FWENSD2.js.map +0 -1
  41. package/dist/chunk-H5KTGI3A.js.map +0 -1
  42. /package/dist/{chunk-EVENAJYI.js.map → chunk-EYQXQQQH.js.map} +0 -0
package/dist/worker.cjs CHANGED
@@ -27,8 +27,8 @@ var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${_
27
27
  var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
28
28
 
29
29
  // ../cli/src/worker-main.ts
30
- var import_node_fs3 = require("fs");
31
- var path6 = __toESM(require("path"), 1);
30
+ var import_node_fs5 = require("fs");
31
+ var path8 = __toESM(require("path"), 1);
32
32
  var import_node_worker_threads = require("worker_threads");
33
33
 
34
34
  // ../cli/src/safety.ts
@@ -114,6 +114,7 @@ function defaultMeta(safetyFloor = 0) {
114
114
  hasDynamicChildren: false,
115
115
  isComponent: false,
116
116
  hasDangerousHtml: false,
117
+ hasUnresolvedClasses: false,
117
118
  targetedByCombinator: false,
118
119
  targetedByStructuralPseudo: false,
119
120
  selectorDependents: 0,
@@ -1151,6 +1152,9 @@ function classifyFlattenOps(before, after, ops, norm) {
1151
1152
  if (!wrapper || wrapper.kind !== "element") {
1152
1153
  return { kind: "provably-safe", wrapperId: null, childId: null };
1153
1154
  }
1155
+ if (wrapper.meta.hasUnresolvedClasses) {
1156
+ return { kind: "needs-verification", wrapperId, childId: survivingChildOf(ops, wrapper, before) };
1157
+ }
1154
1158
  const childId = survivingChildOf(ops, wrapper, before);
1155
1159
  const wrapperComputed = norm.normalizeStyleMap(wrapper.computed);
1156
1160
  const childAfter = childId != null ? getElement(after, childId)?.computed ?? null : null;
@@ -1452,15 +1456,26 @@ function residualStyle(computed2, covered, norm) {
1452
1456
  }
1453
1457
  return { blocks };
1454
1458
  }
1459
+ function joinedLength(tokens) {
1460
+ if (tokens.length === 0) return 0;
1461
+ let len = tokens.length - 1;
1462
+ for (const t of tokens) len += t.length;
1463
+ return len;
1464
+ }
1465
+ var COMPRESS_FLOOR = 1;
1455
1466
  function syncClassesFromComputed(doc, resolver, norm) {
1456
1467
  const sink = createSyntheticSink();
1468
+ const isDroppable = (t) => resolver.owns(t) && resolver.selectorUsage(t).droppable;
1457
1469
  for (const id of elementIds(doc)) {
1458
1470
  const el = getElement(doc, id);
1459
1471
  if (!el) continue;
1460
- if (!el.meta.styleDirty) continue;
1461
1472
  if (el.classes.opaque || el.classes.hasDynamic) continue;
1473
+ const compressOnly = !el.meta.styleDirty;
1474
+ if (compressOnly && el.meta.safetyFloor < COMPRESS_FLOOR) continue;
1462
1475
  const tokens = staticTokensOf(el.classes);
1463
- const retained = tokens.filter((t) => !resolver.selectorUsage(t).droppable);
1476
+ if (tokens.length === 0) continue;
1477
+ const retained = tokens.filter((t) => !isDroppable(t));
1478
+ if (compressOnly && retained.length === tokens.length) continue;
1464
1479
  const covered = retained.length > 0 ? resolver.resolve({ classes: retained }).styles : null;
1465
1480
  const target = covered ? residualStyle(el.computed, covered, norm) : el.computed;
1466
1481
  const ctx = { normalizer: norm, sink };
@@ -1471,7 +1486,7 @@ function syncClassesFromComputed(doc, resolver, norm) {
1471
1486
  const seen = /* @__PURE__ */ new Set();
1472
1487
  for (const t of tokens) {
1473
1488
  if (seen.has(t)) continue;
1474
- const keep = emittedSet.has(t) || !resolver.selectorUsage(t).droppable;
1489
+ const keep = emittedSet.has(t) || !isDroppable(t);
1475
1490
  if (keep) {
1476
1491
  next.push(t);
1477
1492
  seen.add(t);
@@ -1483,10 +1498,98 @@ function syncClassesFromComputed(doc, resolver, norm) {
1483
1498
  seen.add(c);
1484
1499
  }
1485
1500
  if (sameTokens(next, tokens)) continue;
1501
+ if (compressOnly) {
1502
+ if (!norm.equals(resolver.resolve({ classes: next }).styles, el.computed)) continue;
1503
+ if (joinedLength(next) > joinedLength(tokens)) continue;
1504
+ }
1486
1505
  el.classes = staticClassList(el.classes, next);
1487
1506
  }
1488
1507
  }
1489
1508
 
1509
+ // ../core/src/compress-engine.ts
1510
+ var SEP = "";
1511
+ function tupleKey(condition, property, value, important) {
1512
+ return `${condition}${SEP}${property}${SEP}${value}${SEP}${important ? "1" : "0"}`;
1513
+ }
1514
+ function styleMapTuples(map, norm) {
1515
+ const out = [];
1516
+ const normalized = norm.normalizeStyleMap(map);
1517
+ for (const [ck, block] of normalized.blocks) {
1518
+ for (const [prop, decl] of block.decls) {
1519
+ out.push(tupleKey(String(ck), String(prop), String(decl.value), decl.important));
1520
+ }
1521
+ }
1522
+ return out;
1523
+ }
1524
+ var DEFAULT_MAX_UNIVERSE = 20;
1525
+ function minStringCover(universe, vocabulary, options = {}) {
1526
+ const uniq = [...new Set(universe)];
1527
+ if (uniq.length === 0) return [];
1528
+ const n = uniq.length;
1529
+ const max = options.maxUniverse ?? DEFAULT_MAX_UNIVERSE;
1530
+ if (n > max) return null;
1531
+ const bitOf = /* @__PURE__ */ new Map();
1532
+ uniq.forEach((t, i) => bitOf.set(t, i));
1533
+ const byMask = /* @__PURE__ */ new Map();
1534
+ for (const entry of vocabulary) {
1535
+ if (entry.tuples.length === 0) continue;
1536
+ let mask = 0;
1537
+ let ok = true;
1538
+ for (const t of entry.tuples) {
1539
+ const b = bitOf.get(t);
1540
+ if (b === void 0) {
1541
+ ok = false;
1542
+ break;
1543
+ }
1544
+ mask |= 1 << b;
1545
+ }
1546
+ if (!ok || mask === 0) continue;
1547
+ const cost = entry.token.length + 1;
1548
+ const prev = byMask.get(mask);
1549
+ if (!prev || cost < prev.cost || cost === prev.cost && entry.token < prev.token) {
1550
+ byMask.set(mask, { token: entry.token, mask, cost });
1551
+ }
1552
+ }
1553
+ const cands = [...byMask.values()];
1554
+ if (cands.length === 0) return null;
1555
+ const full = (1 << n) - 1;
1556
+ const byBit = Array.from({ length: n }, () => []);
1557
+ cands.forEach((c, ci) => {
1558
+ for (let b = 0; b < n; b += 1) if (c.mask & 1 << b) byBit[b].push(ci);
1559
+ });
1560
+ const size = full + 1;
1561
+ const dp = new Float64Array(size).fill(Infinity);
1562
+ const fromCand = new Int32Array(size).fill(-1);
1563
+ const fromMask = new Int32Array(size).fill(-1);
1564
+ dp[0] = 0;
1565
+ for (let mask = 0; mask < full; mask += 1) {
1566
+ const cur = dp[mask];
1567
+ if (!Number.isFinite(cur)) continue;
1568
+ let b = 0;
1569
+ while (b < n && mask & 1 << b) b += 1;
1570
+ for (const ci of byBit[b]) {
1571
+ const c = cands[ci];
1572
+ const nm = mask | c.mask;
1573
+ const cost = cur + c.cost;
1574
+ if (cost < dp[nm]) {
1575
+ dp[nm] = cost;
1576
+ fromCand[nm] = ci;
1577
+ fromMask[nm] = mask;
1578
+ }
1579
+ }
1580
+ }
1581
+ if (!Number.isFinite(dp[full])) return null;
1582
+ const chosen = [];
1583
+ let m = full;
1584
+ while (m !== 0) {
1585
+ const ci = fromCand[m];
1586
+ if (ci < 0) return null;
1587
+ chosen.push(cands[ci].token);
1588
+ m = fromMask[m];
1589
+ }
1590
+ return [...new Set(chosen)].sort();
1591
+ }
1592
+
1490
1593
  // ../frontend-html/src/backend.ts
1491
1594
  var import_magic_string = __toESM(require("magic-string"), 1);
1492
1595
  function staticTokensOf2(classes) {
@@ -1672,9 +1775,10 @@ function doParse(code, ctx) {
1672
1775
  native: document2
1673
1776
  };
1674
1777
  doc.sources.set(FILE_ID, sourceFile);
1675
- const resolveComputed = (tokens, tag, nodeId) => {
1778
+ const resolveComputed = (tokens, tag, nodeId, meta) => {
1676
1779
  if (tokens.length === 0) return emptyStyleMap();
1677
1780
  const res = ctx.resolver.resolve({ classes: tokens, element: { tagName: tag, namespace: "html" } });
1781
+ if (res.unknown.length > 0) meta.hasUnresolvedClasses = true;
1678
1782
  for (const w of res.warnings) {
1679
1783
  diagnostics.push({
1680
1784
  code: "DF_STYLE_CONFLICT_UNRESOLVED",
@@ -1764,7 +1868,7 @@ function doParse(code, ctx) {
1764
1868
  order.push(a.name);
1765
1869
  }
1766
1870
  const attrs = { entries, spreads: [], order };
1767
- const computed2 = resolveComputed(classTokens, tag, id);
1871
+ const computed2 = resolveComputed(classTokens, tag, id, meta);
1768
1872
  const children = [];
1769
1873
  if (!opaqueSubtree) {
1770
1874
  for (const c of node.childNodes ?? []) appendChild(c, id, children);
@@ -2167,6 +2271,7 @@ function doParse2(code, ctx) {
2167
2271
  element: { tagName: tag, namespace: component ? void 0 : "html" }
2168
2272
  });
2169
2273
  computed2 = ctx.normalizer.normalizeStyleMap(res.styles);
2274
+ if (res.unknown.length > 0) meta.hasUnresolvedClasses = true;
2170
2275
  for (const w of res.warnings) {
2171
2276
  diagnostics.push({
2172
2277
  code: "DF_STYLE_CONFLICT_UNRESOLVED",
@@ -2209,13 +2314,13 @@ function doParse2(code, ctx) {
2209
2314
  };
2210
2315
  const roots = [];
2211
2316
  traverse(ast, {
2212
- JSXElement(path7) {
2213
- roots.push(path7.node);
2214
- path7.skip();
2317
+ JSXElement(path9) {
2318
+ roots.push(path9.node);
2319
+ path9.skip();
2215
2320
  },
2216
- JSXFragment(path7) {
2217
- roots.push(path7.node);
2218
- path7.skip();
2321
+ JSXFragment(path9) {
2322
+ roots.push(path9.node);
2323
+ path9.skip();
2219
2324
  }
2220
2325
  });
2221
2326
  const rootFrag = doc.nodes.get(doc.root);
@@ -2572,6 +2677,18 @@ var BOX_SIDES = {
2572
2677
  padding: ["padding-top", "padding-right", "padding-bottom", "padding-left"],
2573
2678
  margin: ["margin-top", "margin-right", "margin-bottom", "margin-left"],
2574
2679
  inset: ["top", "right", "bottom", "left"],
2680
+ "scroll-margin": [
2681
+ "scroll-margin-top",
2682
+ "scroll-margin-right",
2683
+ "scroll-margin-bottom",
2684
+ "scroll-margin-left"
2685
+ ],
2686
+ "scroll-padding": [
2687
+ "scroll-padding-top",
2688
+ "scroll-padding-right",
2689
+ "scroll-padding-bottom",
2690
+ "scroll-padding-left"
2691
+ ],
2575
2692
  "border-width": [
2576
2693
  "border-top-width",
2577
2694
  "border-right-width",
@@ -2589,8 +2706,35 @@ var BOX_SIDES = {
2589
2706
  "border-right-color",
2590
2707
  "border-bottom-color",
2591
2708
  "border-left-color"
2709
+ ],
2710
+ // `border-radius` 1–4 value form maps to the four CORNERS (TL, TR, BR, BL) — the same positional
2711
+ // pattern boxFourSides implements. Only the slash-free form is expanded (see expandShorthand).
2712
+ "border-radius": [
2713
+ "border-top-left-radius",
2714
+ "border-top-right-radius",
2715
+ "border-bottom-right-radius",
2716
+ "border-bottom-left-radius"
2592
2717
  ]
2593
2718
  };
2719
+ var AXIS_PAIRS = {
2720
+ overflow: ["overflow-x", "overflow-y"],
2721
+ "overscroll-behavior": ["overscroll-behavior-x", "overscroll-behavior-y"],
2722
+ "place-items": ["align-items", "justify-items"],
2723
+ "place-content": ["align-content", "justify-content"],
2724
+ "place-self": ["align-self", "justify-self"]
2725
+ };
2726
+ var LOGICAL_PAIRS = {
2727
+ "padding-inline": ["padding-left", "padding-right"],
2728
+ "padding-block": ["padding-top", "padding-bottom"],
2729
+ "margin-inline": ["margin-left", "margin-right"],
2730
+ "margin-block": ["margin-top", "margin-bottom"],
2731
+ "inset-inline": ["left", "right"],
2732
+ "inset-block": ["top", "bottom"],
2733
+ "scroll-padding-inline": ["scroll-padding-left", "scroll-padding-right"],
2734
+ "scroll-padding-block": ["scroll-padding-top", "scroll-padding-bottom"],
2735
+ "scroll-margin-inline": ["scroll-margin-left", "scroll-margin-right"],
2736
+ "scroll-margin-block": ["scroll-margin-top", "scroll-margin-bottom"]
2737
+ };
2594
2738
  function splitTopLevel(value) {
2595
2739
  const out = [];
2596
2740
  let depth = 0;
@@ -2624,6 +2768,7 @@ function boxFourSides(values) {
2624
2768
  }
2625
2769
  }
2626
2770
  function expandShorthand(prop, value) {
2771
+ if (prop === "border-radius" && value.includes("/")) return [[prop, value]];
2627
2772
  const box = BOX_SIDES[prop];
2628
2773
  if (box) {
2629
2774
  const parts = splitTopLevel(value);
@@ -2633,6 +2778,19 @@ function expandShorthand(prop, value) {
2633
2778
  }
2634
2779
  return [[prop, value]];
2635
2780
  }
2781
+ const axis = AXIS_PAIRS[prop];
2782
+ if (axis) {
2783
+ const parts = splitTopLevel(value);
2784
+ if (parts.length === 1) return [[axis[0], parts[0]], [axis[1], parts[0]]];
2785
+ if (parts.length === 2) return [[axis[0], parts[0]], [axis[1], parts[1]]];
2786
+ return [[prop, value]];
2787
+ }
2788
+ const logical = LOGICAL_PAIRS[prop];
2789
+ if (logical) {
2790
+ const parts = splitTopLevel(value);
2791
+ if (parts.length === 1) return [[logical[0], parts[0]], [logical[1], parts[0]]];
2792
+ return [[prop, value]];
2793
+ }
2636
2794
  if (prop === "gap" || prop === "grid-gap") {
2637
2795
  const parts = splitTopLevel(value);
2638
2796
  if (parts.length === 1) {
@@ -2790,7 +2948,12 @@ var VISUAL_PROPERTIES = /* @__PURE__ */ new Set([
2790
2948
  "border-right-color",
2791
2949
  "border-bottom-color",
2792
2950
  "border-left-color",
2793
- "border-radius",
2951
+ // `border-radius` is expanded to its four corner longhands by the shared normalizer, so the
2952
+ // paint-establishing check must match those (a rounded wrapper still clips its background).
2953
+ "border-top-left-radius",
2954
+ "border-top-right-radius",
2955
+ "border-bottom-right-radius",
2956
+ "border-bottom-left-radius",
2794
2957
  "box-shadow",
2795
2958
  "outline",
2796
2959
  "outline-width",
@@ -2816,6 +2979,7 @@ var hasOwnVisualStyle = (node, ctx) => {
2816
2979
  const el = asElement(node);
2817
2980
  if (!el) return false;
2818
2981
  if (el.meta.hasOwnVisualStyle) return true;
2982
+ if (el.meta.hasUnresolvedClasses) return true;
2819
2983
  const computedMap = ctx.computedOf(el) ?? el.computed;
2820
2984
  const norm = normalizer.normalizeStyleMap(computedMap);
2821
2985
  for (const block of norm.blocks.values()) {
@@ -3020,7 +3184,160 @@ function definePattern(config) {
3020
3184
  return validatePattern(spec);
3021
3185
  }
3022
3186
 
3023
- // ../patterns/src/library/flatten/display-contents-wrapper.pattern.ts
3187
+ // ../patterns/src/library/flex/flex-center-wrapper.pattern.ts
3188
+ var flexCenterWrapper = definePattern({
3189
+ name: "flex-center-wrapper",
3190
+ category: "flatten/flex/flex-center-wrapper",
3191
+ safety: 2,
3192
+ doc: {
3193
+ title: "Flatten flex-centering wrapper",
3194
+ summary: "A div that only centers a single child (display:flex; align-items:center; justify-content:center) is removed; the child gains place-self:center.",
3195
+ before: '<div style="display:flex;align-items:center;justify-content:center"><Child/></div>',
3196
+ after: '<Child style="place-self:center"/>',
3197
+ safetyRationale: "Wrapper paints nothing, carries no ref/handlers/dynamic children, and is not a combinator subject; inheritable styles are folded onto the child before removal."
3198
+ },
3199
+ match: {
3200
+ tag: "div",
3201
+ style: { display: "flex", alignItems: "center", justifyContent: "center" },
3202
+ onlyChild: "element",
3203
+ paintsNothing: true
3204
+ },
3205
+ rewrite: {
3206
+ flattenInto: "child",
3207
+ childGains: { placeSelf: "center" }
3208
+ },
3209
+ // Collapsing a flex-centering wrapper to `place-self:center` on the child is render-identical ONLY
3210
+ // when the child's NEW parent is a statically-known GRID that lets the wrapper fill its area (there
3211
+ // `place-self`'s align-self AND justify-self both take effect). Under that ONE context the flatten is
3212
+ // classified `provably-safe` and commits; under a flex/block/unknown parent — or when the wrapper
3213
+ // drops any own style — it stays `needs-verification` and the conservative production gate PRESERVES
3214
+ // it. Op-level correctness (purity, id-preserving unwrap, opacity-barrier safety) is additionally
3215
+ // asserted by the invariant suite over every pattern.
3216
+ test: {
3217
+ cases: [
3218
+ {
3219
+ name: "grid parent \u2192 flattened (child gains place-self-center)",
3220
+ before: '<div className="grid"><div className="flex items-center justify-center"><span className="bg-red-200">x</span></div></div>',
3221
+ after: '<div className="grid"><span className="bg-red-200 place-self-center">x</span></div>'
3222
+ }
3223
+ ],
3224
+ noMatch: [
3225
+ // Non-grid (flex) parent (document root): `justify-self` is ignored in flex → not provably safe.
3226
+ '<div className="flex justify-center items-center"><div className="bg-red-200">Hello</div></div>',
3227
+ // Grid parent, but the wrapper drops padding when removed → not layout-neutral (rule 3).
3228
+ '<div className="grid"><div className="p-4 flex items-center justify-center"><span className="bg-red-200">x</span></div></div>',
3229
+ // Grid parent forcing place-items-center: the wrapper would not fill its area → fill guard skips.
3230
+ '<div className="grid place-items-center"><div className="flex items-center justify-center"><span className="bg-red-200">x</span></div></div>',
3231
+ // onClick is a hard opacity barrier → the wrapper is load-bearing regardless of the gate.
3232
+ '<div className="flex justify-center items-center" onClick={handleClick}><div className="bg-red-200">Hello</div></div>'
3233
+ ]
3234
+ }
3235
+ });
3236
+
3237
+ // ../patterns/src/library/fragment/redundant-fragment.pattern.ts
3238
+ function parentIsRedundantFragment(node, ctx) {
3239
+ const el = node;
3240
+ if (el.kind !== "element") return false;
3241
+ const parentId = el.parent;
3242
+ if (parentId == null) return false;
3243
+ const parent = ctx.doc.nodes.get(parentId);
3244
+ if (!parent || parent.kind !== "fragment") return false;
3245
+ if (parent.parent == null) return false;
3246
+ if (parent.children.length !== 1) return false;
3247
+ const m = parent.meta;
3248
+ if (m.hasKey || m.hasRef || m.hasEventHandlers || m.hasDynamicChildren || m.hasDangerousHtml || m.hasSpreadAttrs || m.isComponent) {
3249
+ return false;
3250
+ }
3251
+ if (m.targetedByCombinator || m.targetedByStructuralPseudo) return false;
3252
+ const fid = parentId;
3253
+ if (ctx.selectors.targetedByCombinator(fid) || ctx.selectors.targetedByStructuralPseudo(fid)) {
3254
+ return false;
3255
+ }
3256
+ if (ctx.selectors.reparentImpact(fid).size > 0) return false;
3257
+ return true;
3258
+ }
3259
+ var redundantFragment = definePattern({
3260
+ name: "redundant-fragment",
3261
+ category: "flatten/fragment/redundant-fragment",
3262
+ safety: 1,
3263
+ doc: {
3264
+ title: "Flatten redundant single-child fragment",
3265
+ summary: "A fragment whose only child is a single node is removed; the child is spliced up into the fragment's slot, preserving its IRNodeId, siblings, attributes and the CSS cascade.",
3266
+ before: "<><Child/></>",
3267
+ after: "<Child/>",
3268
+ safetyRationale: "A fragment paints nothing and renders no box; with exactly one child its removal changes no sibling/structural-pseudo match-set. Keyed fragments and fragments carrying ref/handlers/dynamic-children/raw-html/spread are excluded as opacity barriers."
3269
+ },
3270
+ match: parentIsRedundantFragment,
3271
+ rewrite: (ctx, rw) => {
3272
+ const parentId = ctx.node.parent;
3273
+ if (parentId == null) return null;
3274
+ const fragment = ctx.doc.nodes.get(parentId);
3275
+ if (!fragment || fragment.kind !== "fragment") return null;
3276
+ return [rw.unwrap(fragment)];
3277
+ },
3278
+ test: {
3279
+ cases: [
3280
+ {
3281
+ // A fragment renders no box, so unwrapping a single-child fragment is always layout-identical
3282
+ // → a provably-safe flatten: the child is spliced up into the fragment's slot.
3283
+ before: '<><span className="bg-red-200">Hi</span></>',
3284
+ after: '<span className="bg-red-200">Hi</span>'
3285
+ }
3286
+ ],
3287
+ noMatch: [
3288
+ // Two children ⇒ not a single-child fragment, so the fragment is load-bearing and stays.
3289
+ '<><span className="bg-red-200">A</span><span className="bg-green-200">B</span></>'
3290
+ ]
3291
+ }
3292
+ });
3293
+
3294
+ // ../patterns/src/library/grid/grid-center-wrapper.pattern.ts
3295
+ var gridCenterWrapper = definePattern({
3296
+ name: "grid-center-wrapper",
3297
+ category: "flatten/grid/grid-center-wrapper",
3298
+ safety: 2,
3299
+ doc: {
3300
+ title: "Flatten grid-centering wrapper",
3301
+ summary: "A div that only centers a single child (display:grid; align-items:center; justify-content:center) is removed; the child gains place-self:center.",
3302
+ before: '<div style="display:grid;align-items:center;justify-content:center"><Child/></div>',
3303
+ after: '<Child style="place-self:center"/>',
3304
+ safetyRationale: "Wrapper paints nothing, carries no ref/handlers/dynamic children, and is not a combinator subject; inheritable styles are folded onto the child before removal. The place-self:center collapse is committed by the gate only under a statically-known filling grid parent."
3305
+ },
3306
+ match: {
3307
+ tag: "div",
3308
+ style: { display: "grid", alignItems: "center", justifyContent: "center" },
3309
+ onlyChild: "element",
3310
+ paintsNothing: true
3311
+ },
3312
+ rewrite: {
3313
+ flattenInto: "child",
3314
+ childGains: { placeSelf: "center" }
3315
+ },
3316
+ // Like `flex-center-wrapper`, collapsing to `place-self:center` is render-identical ONLY when the
3317
+ // child's NEW parent is a statically-known GRID that lets the wrapper fill its area (there both halves
3318
+ // of place-self take effect). Under that ONE context the flatten is `provably-safe` and commits; under
3319
+ // a flex/block/unknown parent — or when the wrapper drops any own style — it stays `needs-verification`
3320
+ // and the conservative production gate PRESERVES it. Op-level correctness is asserted by the invariant suite.
3321
+ test: {
3322
+ cases: [
3323
+ {
3324
+ name: "grid parent \u2192 flattened (child gains place-self-center)",
3325
+ before: '<div className="grid"><div className="grid items-center justify-center"><span className="bg-red-200">x</span></div></div>',
3326
+ after: '<div className="grid"><span className="bg-red-200 place-self-center">x</span></div>'
3327
+ }
3328
+ ],
3329
+ noMatch: [
3330
+ // Non-grid (document-root) parent: justify-self is ignored outside a grid → not provably safe.
3331
+ '<div className="grid justify-center items-center"><div className="bg-red-200">Hello</div></div>',
3332
+ // Grid parent, but the wrapper drops padding when removed → not layout-neutral, preserved.
3333
+ '<div className="grid"><div className="p-4 grid items-center justify-center"><span className="bg-red-200">x</span></div></div>',
3334
+ // onClick is a hard opacity barrier → the wrapper is load-bearing regardless of the gate.
3335
+ '<div className="grid justify-center items-center" onClick={handleClick}><div className="bg-red-200">Hello</div></div>'
3336
+ ]
3337
+ }
3338
+ });
3339
+
3340
+ // ../patterns/src/library/wrapper/display-contents-wrapper.pattern.ts
3024
3341
  function asEl(node) {
3025
3342
  const n = node;
3026
3343
  return n.kind === "element" ? n : null;
@@ -3044,7 +3361,7 @@ var targetedByStructuralPseudo = (node, ctx) => {
3044
3361
  };
3045
3362
  var displayContentsWrapper = definePattern({
3046
3363
  name: "display-contents-wrapper",
3047
- category: "flatten/display-contents-wrapper",
3364
+ category: "flatten/wrapper/display-contents-wrapper",
3048
3365
  safety: 2,
3049
3366
  doc: {
3050
3367
  title: "Flatten display:contents wrapper",
@@ -3084,7 +3401,7 @@ var displayContentsWrapper = definePattern({
3084
3401
  }
3085
3402
  });
3086
3403
 
3087
- // ../patterns/src/library/flatten/empty-style-div.pattern.ts
3404
+ // ../patterns/src/library/wrapper/empty-style-div.pattern.ts
3088
3405
  function asEl2(node) {
3089
3406
  const n = node;
3090
3407
  return n.kind === "element" ? n : null;
@@ -3116,7 +3433,7 @@ var hasNonBlockDisplay = (node, ctx) => {
3116
3433
  };
3117
3434
  var emptyStyleDiv = definePattern({
3118
3435
  name: "empty-style-div",
3119
- category: "flatten/empty-style-div",
3436
+ category: "flatten/wrapper/empty-style-div",
3120
3437
  safety: 1,
3121
3438
  doc: {
3122
3439
  title: "Flatten empty-style div wrapper",
@@ -3155,1604 +3472,229 @@ var emptyStyleDiv = definePattern({
3155
3472
  }
3156
3473
  });
3157
3474
 
3158
- // ../patterns/src/library/flatten/flex-center-wrapper.pattern.ts
3159
- var flexCenterWrapper = definePattern({
3160
- name: "flex-center-wrapper",
3161
- category: "flatten/flex-center-wrapper",
3475
+ // ../patterns/src/library/wrapper/inherited-only-wrapper.pattern.ts
3476
+ var INERT_HOST_TAGS = /* @__PURE__ */ new Set(["div", "span"]);
3477
+ var isInertHostTag = (node) => {
3478
+ const n = node;
3479
+ if (n.kind !== "element") return false;
3480
+ return INERT_HOST_TAGS.has(String(n.tag).toLowerCase());
3481
+ };
3482
+ var isComponentNode2 = (node) => {
3483
+ const n = node;
3484
+ return n.kind === "element" ? n.meta.isComponent : false;
3485
+ };
3486
+ var hasOnlyInheritedStyle = (node, ctx) => {
3487
+ const sm = normalizer.normalizeStyleMap(ctx.computed());
3488
+ let sawAny = false;
3489
+ for (const block of sm.blocks.values()) {
3490
+ for (const decl of block.decls.values()) {
3491
+ sawAny = true;
3492
+ const inherited = decl.inherited || normalizer.inherited.isInherited(decl.property);
3493
+ if (!inherited) return false;
3494
+ }
3495
+ }
3496
+ return sawAny;
3497
+ };
3498
+ var inheritedOnlyWrapper = definePattern({
3499
+ name: "inherited-only-wrapper",
3500
+ category: "flatten/wrapper/inherited-only-wrapper",
3162
3501
  safety: 2,
3163
3502
  doc: {
3164
- title: "Flatten flex-centering wrapper",
3165
- summary: "A div that only centers a single child (display:flex; align-items:center; justify-content:center) is removed; the child gains place-self:center.",
3166
- before: '<div style="display:flex;align-items:center;justify-content:center"><Child/></div>',
3167
- after: '<Child style="place-self:center"/>',
3168
- safetyRationale: "Wrapper paints nothing, carries no ref/handlers/dynamic children, and is not a combinator subject; inheritable styles are folded onto the child before removal."
3503
+ title: "Flatten inherited-only styling wrapper",
3504
+ summary: "A paint-free wrapper whose only own declarations are inherited properties (text-align, color, font-*, \u2026) is removed; its inherited style is folded onto the sole child, which keeps the same inherited values for the whole subtree.",
3505
+ before: '<div style="text-align:center"><Child/></div>',
3506
+ after: '<Child style="text-align:center"/>',
3507
+ safetyRationale: "Inherited properties reach descendants purely through inheritance, so folding them onto the child and removing the box is render-identical. The wrapper carries nothing non-inherited, establishes no box/formatting/stacking context, and is guarded by the auto-applied opacity-barrier + selector-safety set."
3169
3508
  },
3170
3509
  match: {
3171
- tag: "div",
3172
- style: { display: "flex", alignItems: "center", justifyContent: "center" },
3173
3510
  onlyChild: "element",
3174
- paintsNothing: true
3175
- },
3176
- rewrite: {
3177
- flattenInto: "child",
3178
- childGains: { placeSelf: "center" }
3511
+ paintsNothing: true,
3512
+ where: [isInertHostTag, not(isComponentNode2), hasOnlyInheritedStyle]
3179
3513
  },
3180
- // Collapsing a flex-centering wrapper to `place-self:center` on the child is render-identical ONLY
3181
- // when the child's NEW parent is a statically-known GRID that lets the wrapper fill its area (there
3182
- // `place-self`'s align-self AND justify-self both take effect). Under that ONE context the flatten is
3183
- // classified `provably-safe` and commits; under a flex/block/unknown parent — or when the wrapper
3184
- // drops any own style — it stays `needs-verification` and the conservative production gate PRESERVES
3185
- // it. Op-level correctness (purity, id-preserving unwrap, opacity-barrier safety) is additionally
3186
- // asserted by the invariant suite over every pattern.
3514
+ rewrite: { flattenInto: "child" },
3187
3515
  test: {
3188
3516
  cases: [
3189
3517
  {
3190
- name: "grid parent \u2192 flattened (child gains place-self-center)",
3191
- before: '<div className="grid"><div className="flex items-center justify-center"><span className="bg-red-200">x</span></div></div>',
3192
- after: '<div className="grid"><span className="bg-red-200 place-self-center">x</span></div>'
3518
+ // `text-align:center` is inherited folded onto the child; the paint-free wrapper is removed.
3519
+ before: '<div className="text-center"><p className="bg-red-200">x</p></div>',
3520
+ after: '<p className="bg-red-200 text-center">x</p>'
3193
3521
  }
3194
3522
  ],
3195
3523
  noMatch: [
3196
- // Non-grid (flex) parent (document root): `justify-self` is ignored in flex not provably safe.
3197
- '<div className="flex justify-center items-center"><div className="bg-red-200">Hello</div></div>',
3198
- // Grid parent, but the wrapper drops padding when removed → not layout-neutral (rule 3).
3199
- '<div className="grid"><div className="p-4 flex items-center justify-center"><span className="bg-red-200">x</span></div></div>',
3200
- // Grid parent forcing place-items-center: the wrapper would not fill its area fill guard skips.
3201
- '<div className="grid place-items-center"><div className="flex items-center justify-center"><span className="bg-red-200">x</span></div></div>',
3202
- // onClick is a hard opacity barrier → the wrapper is load-bearing regardless of the gate.
3203
- '<div className="flex justify-center items-center" onClick={handleClick}><div className="bg-red-200">Hello</div></div>'
3524
+ // `p-4` is a NON-inherited padding: removing the box would drop it, so the flatten-safety gate
3525
+ // reverts the unwrap and the wrapper is left unchanged.
3526
+ '<div className="p-4"><p className="bg-red-200">x</p></div>',
3527
+ // A `<p>` wrapper is NOT an inert host box: its UA default display/margins are not captured in the
3528
+ // class-derived computed style, so removing it is not provably layout-neutralleft unchanged.
3529
+ '<p className="text-center"><span className="bg-red-200">x</span></p>'
3204
3530
  ]
3205
3531
  }
3206
3532
  });
3207
3533
 
3208
- // ../patterns/src/library/flatten/inline-flex-center-wrapper.pattern.ts
3209
- var inlineFlexCenterWrapper = definePattern({
3210
- name: "inline-flex-center-wrapper",
3211
- category: "flatten/inline-flex-center-wrapper",
3534
+ // ../patterns/src/library/wrapper/passthrough-wrapper.pattern.ts
3535
+ function metaOf2(node) {
3536
+ const n = node;
3537
+ return n.kind === "element" ? n.meta : null;
3538
+ }
3539
+ function elementOf(node) {
3540
+ const n = node;
3541
+ return n.kind === "element" ? n : null;
3542
+ }
3543
+ var establishesContext = (node) => {
3544
+ const m = metaOf2(node);
3545
+ if (!m) return false;
3546
+ return m.establishesBox || m.establishesFormattingContext || m.establishesStackingContext || m.isContainingBlock || m.declaresCustomProperties;
3547
+ };
3548
+ var hasSpreadAttrs2 = (node) => metaOf2(node)?.hasSpreadAttrs ?? false;
3549
+ var isComponentNode3 = (node) => metaOf2(node)?.isComponent ?? false;
3550
+ var hasOwnAttrs2 = (node) => {
3551
+ const el = elementOf(node);
3552
+ if (!el) return false;
3553
+ return el.attrs.entries.size > 0 || el.attrs.spreads.length > 0;
3554
+ };
3555
+ var targetedByStructuralPseudo3 = (node, ctx) => {
3556
+ const el = elementOf(node);
3557
+ if (!el) return false;
3558
+ if (el.meta.targetedByStructuralPseudo) return true;
3559
+ return ctx.selectors.targetedByStructuralPseudo(el.id);
3560
+ };
3561
+ var passthroughWrapper = definePattern({
3562
+ name: "passthrough-wrapper",
3563
+ category: "flatten/wrapper/passthrough-wrapper",
3212
3564
  safety: 2,
3213
3565
  doc: {
3214
- title: "Flatten inline-flex-centering wrapper",
3215
- summary: "A div that only centers a single child (display:inline-flex; align-items:center; justify-content:center) is removed; the child gains place-self:center.",
3216
- before: '<div style="display:inline-flex;align-items:center;justify-content:center"><Child/></div>',
3217
- after: '<Child style="place-self:center"/>',
3218
- safetyRationale: "Wrapper paints nothing, carries no ref/handlers/dynamic children, and is not a combinator subject; inheritable styles are folded onto the child before removal."
3566
+ title: "Flatten passthrough wrapper",
3567
+ summary: "A div with no own visual/box style, no attributes beyond an inert class, exactly one element child, and no opacity barriers is removed; its sole child is hoisted in its place.",
3568
+ before: "<div><Child/></div>",
3569
+ after: "<Child/>",
3570
+ safetyRationale: "Wrapper paints nothing and establishes no layout/paint/var context, carries no ref/handlers/dynamic-children/html/spread/component identity, owns no targetable attrs, and is not a combinator/structural-pseudo subject (reparenting changes no match-set); inheritable styles are folded onto the child before removal."
3219
3571
  },
3220
3572
  match: {
3221
3573
  tag: "div",
3222
- style: { display: "inline-flex", alignItems: "center", justifyContent: "center" },
3223
3574
  onlyChild: "element",
3224
- paintsNothing: true
3225
- },
3226
- rewrite: {
3227
- flattenInto: "child",
3228
- childGains: { placeSelf: "center" }
3229
- },
3230
- // Like its block-level sibling, this centering flatten is `needs-verification` (the wrapper's own
3231
- // `display:inline-flex` establishes a formatting context, and place-self centering only holds under
3232
- // a flex/grid parent), so the conservative production gate (`'provably-safe'`) REVERTS it — every
3233
- // case here is a no-match. Op-level correctness is covered by the invariant suite.
3234
- test: {
3235
- noMatch: [
3236
- // Even under a static flex/grid parent the centering flatten is not provably layout-neutral.
3237
- '<div className="grid"><div className="inline-flex items-center justify-center"><span className="bg-red-200">x</span></div></div>',
3238
- // Non-flex/grid parent (document root) → left unchanged.
3239
- '<div className="inline-flex justify-center items-center"><div className="bg-red-200">Hello</div></div>',
3240
- // onClick is a hard opacity barrier → the wrapper is load-bearing regardless of the gate.
3241
- '<div className="inline-flex justify-center items-center" onClick={handleClick}><div className="bg-red-200">Hello</div></div>'
3242
- ]
3243
- }
3244
- });
3245
-
3246
- // ../patterns/src/library/flatten/nested-flex-merge.pattern.ts
3247
- function baseConditionStyleMap(decls) {
3248
- const map = /* @__PURE__ */ new Map();
3249
- for (const [prop, value] of decls) {
3250
- for (const decl of normalizer.normalizeDeclaration(prop, value, false)) {
3251
- map.set(decl.property, decl);
3252
- }
3253
- }
3254
- const block = { condition: BASE_CONDITION, decls: map };
3255
- const blocks = /* @__PURE__ */ new Map([[conditionKey(BASE_CONDITION), block]]);
3256
- return { blocks };
3257
- }
3258
- var DISPLAY_FLEX = baseConditionStyleMap([["display", "flex"]]);
3259
- var FLEX_CONTAINER_PROPERTIES = /* @__PURE__ */ new Set([
3260
- "display",
3261
- "flex-direction",
3262
- "flex-wrap",
3263
- "justify-content",
3264
- "align-items",
3265
- "align-content",
3266
- "place-content",
3267
- "place-items",
3268
- "row-gap",
3269
- "column-gap"
3270
- ]);
3271
- function outerMergeSafe(sm) {
3272
- const norm = normalizer.normalizeStyleMap(sm);
3273
- for (const block of norm.blocks.values()) {
3274
- for (const decl of block.decls.values()) {
3275
- if (FLEX_CONTAINER_PROPERTIES.has(String(decl.property))) continue;
3276
- if (decl.inherited) continue;
3277
- return false;
3278
- }
3279
- }
3280
- return true;
3281
- }
3282
- function flexConflict(outer, inner) {
3283
- const a = normalizer.normalizeStyleMap(outer);
3284
- const b = normalizer.normalizeStyleMap(inner);
3285
- for (const [key, blockA] of a.blocks) {
3286
- const blockB = b.blocks.get(key);
3287
- if (!blockB) continue;
3288
- for (const [prop, declA] of blockA.decls) {
3289
- if (!FLEX_CONTAINER_PROPERTIES.has(String(prop))) continue;
3290
- const declB = blockB.decls.get(prop);
3291
- if (declB && declB.value !== declA.value) return true;
3292
- }
3293
- }
3294
- return false;
3295
- }
3296
- function extractFlexStyle(sm) {
3297
- const blocks = /* @__PURE__ */ new Map();
3298
- for (const [key, block] of sm.blocks) {
3299
- const decls = /* @__PURE__ */ new Map();
3300
- for (const [prop, decl] of block.decls) {
3301
- if (FLEX_CONTAINER_PROPERTIES.has(String(prop))) decls.set(prop, decl);
3302
- }
3303
- if (decls.size > 0) blocks.set(key, { condition: block.condition, decls });
3304
- }
3305
- return { blocks };
3306
- }
3307
- var isInnerFlex = and(
3308
- isElement("div"),
3309
- computed(DISPLAY_FLEX),
3310
- not(targetedByCombinator)
3311
- );
3312
- var nestedFlexMerge = definePattern({
3313
- name: "nested-flex-merge",
3314
- category: "flatten/nested-flex-merge",
3315
- safety: 2,
3316
- doc: {
3317
- title: "Merge nested flex containers",
3318
- summary: "A flex container whose only child is itself a flex container with non-conflicting flex properties is collapsed into one; the wrapper is removed and its flex declarations merge onto the surviving child.",
3319
- before: '<div style="display:flex;align-items:center;gap:8px"><div style="display:flex;flex-direction:column"/></div>',
3320
- after: '<div style="display:flex;flex-direction:column;align-items:center;gap:8px"/>',
3321
- safetyRationale: "The wrapper paints nothing, declares only flex-container/inheritable properties, carries no ref/handlers/dynamic children, and is not a combinator subject; the two containers do not conflict on any flex property, so the union is unambiguous and lossless."
3322
- },
3323
- match: {
3324
- tag: "div",
3325
- style: { display: "flex" },
3326
- onlyChild: "element",
3327
- paintsNothing: true
3328
- },
3329
- rewrite: (ctx, rw) => {
3330
- const outer = ctx.node;
3331
- const inner = ctx.onlyElementChild();
3332
- if (!inner) return null;
3333
- if (!isInnerFlex(inner, ctx)) return null;
3334
- const outerStyle = ctx.computed();
3335
- const innerStyle = ctx.computedOf(inner);
3336
- if (!outerMergeSafe(outerStyle)) return null;
3337
- if (flexConflict(outerStyle, innerStyle)) return null;
3338
- return [
3339
- // 1. Preserve inheritable values (color/font/…) by folding them onto the child first.
3340
- rw.foldInheritedStyles(outer, inner, { conditions: "all" }),
3341
- // 2. Transfer the wrapper's flex-container declarations onto the child (target-wins keeps the
3342
- // child's value for any shared property — identical anyway, we proved non-conflict).
3343
- rw.mergeStyle(inner, null, extractFlexStyle(outerStyle), "target-wins"),
3344
- // 3. Remove the wrapper (structural-safe; hoists the child and preserves its IRNodeId).
3345
- rw.unwrap(outer)
3346
- ];
3347
- },
3348
- // Merging the outer flex container into the inner removes the outer's box, but a `display:flex`
3349
- // wrapper establishes a formatting context, so this is a `needs-verification` flatten that the
3350
- // conservative production gate (`'provably-safe'`) REVERTS — every case here is a no-match. The
3351
- // merge's op-level correctness (purity, id-preserving unwrap, opacity-barrier safety) is asserted
3352
- // by the invariant suite over every pattern.
3353
- test: {
3354
- noMatch: [
3355
- // The merge is real but not provably layout-neutral (the wrapper establishes a flex context),
3356
- // so under the conservative gate the nested containers are left in place.
3357
- '<div className="flex items-center gap-2" data-x="1"><div className="flex flex-col">X</div></div>',
3358
- // A non-flex wrapper does not match the flex-container signature → left unchanged anyway.
3359
- '<div className="block bg-blue-500"><div className="flex flex-col">X</div></div>'
3360
- ]
3361
- }
3362
- });
3363
-
3364
- // ../patterns/src/library/flatten/nested-grid-merge.pattern.ts
3365
- function baseConditionStyleMap2(decls) {
3366
- const map = /* @__PURE__ */ new Map();
3367
- for (const [prop, value] of decls) {
3368
- for (const decl of normalizer.normalizeDeclaration(prop, value, false)) {
3369
- map.set(decl.property, decl);
3370
- }
3371
- }
3372
- const block = { condition: BASE_CONDITION, decls: map };
3373
- const blocks = /* @__PURE__ */ new Map([[conditionKey(BASE_CONDITION), block]]);
3374
- return { blocks };
3375
- }
3376
- var DISPLAY_GRID = baseConditionStyleMap2([["display", "grid"]]);
3377
- var GRID_CONTAINER_PROPERTIES = /* @__PURE__ */ new Set([
3378
- "display",
3379
- "grid-template-columns",
3380
- "grid-template-rows",
3381
- "grid-template-areas",
3382
- "grid-auto-columns",
3383
- "grid-auto-rows",
3384
- "grid-auto-flow",
3385
- "justify-content",
3386
- "align-content",
3387
- "place-content",
3388
- "justify-items",
3389
- "align-items",
3390
- "place-items",
3391
- "row-gap",
3392
- "column-gap"
3393
- ]);
3394
- function outerMergeSafe2(sm) {
3395
- const norm = normalizer.normalizeStyleMap(sm);
3396
- for (const block of norm.blocks.values()) {
3397
- for (const decl of block.decls.values()) {
3398
- if (GRID_CONTAINER_PROPERTIES.has(String(decl.property))) continue;
3399
- if (decl.inherited) continue;
3400
- return false;
3401
- }
3402
- }
3403
- return true;
3404
- }
3405
- function gridConflict(outer, inner) {
3406
- const a = normalizer.normalizeStyleMap(outer);
3407
- const b = normalizer.normalizeStyleMap(inner);
3408
- for (const [key, blockA] of a.blocks) {
3409
- const blockB = b.blocks.get(key);
3410
- if (!blockB) continue;
3411
- for (const [prop, declA] of blockA.decls) {
3412
- if (!GRID_CONTAINER_PROPERTIES.has(String(prop))) continue;
3413
- const declB = blockB.decls.get(prop);
3414
- if (declB && declB.value !== declA.value) return true;
3415
- }
3416
- }
3417
- return false;
3418
- }
3419
- function extractGridStyle(sm) {
3420
- const blocks = /* @__PURE__ */ new Map();
3421
- for (const [key, block] of sm.blocks) {
3422
- const decls = /* @__PURE__ */ new Map();
3423
- for (const [prop, decl] of block.decls) {
3424
- if (GRID_CONTAINER_PROPERTIES.has(String(prop))) decls.set(prop, decl);
3425
- }
3426
- if (decls.size > 0) blocks.set(key, { condition: block.condition, decls });
3427
- }
3428
- return { blocks };
3429
- }
3430
- var isInnerGrid = and(
3431
- isElement("div"),
3432
- computed(DISPLAY_GRID),
3433
- not(targetedByCombinator)
3434
- );
3435
- var nestedGridMerge = definePattern({
3436
- name: "nested-grid-merge",
3437
- category: "flatten/nested-grid-merge",
3438
- safety: 2,
3439
- doc: {
3440
- title: "Merge nested grid containers",
3441
- summary: "A grid container whose only child is itself a grid container with non-conflicting grid properties is collapsed into one; the wrapper is removed and its grid declarations merge onto the surviving child.",
3442
- before: '<div style="display:grid;gap:8px"><div style="display:grid;grid-template-columns:1fr 1fr"/></div>',
3443
- after: '<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px"/>',
3444
- safetyRationale: "The wrapper paints nothing, declares only grid-container/inheritable properties, carries no ref/handlers/dynamic children, and is not a combinator subject; the two containers do not conflict on any grid property, so the union is unambiguous and lossless."
3445
- },
3446
- match: {
3447
- tag: "div",
3448
- style: { display: "grid" },
3449
- onlyChild: "element",
3450
- paintsNothing: true
3451
- },
3452
- rewrite: (ctx, rw) => {
3453
- const outer = ctx.node;
3454
- const inner = ctx.onlyElementChild();
3455
- if (!inner) return null;
3456
- if (!isInnerGrid(inner, ctx)) return null;
3457
- const outerStyle = ctx.computed();
3458
- const innerStyle = ctx.computedOf(inner);
3459
- if (!outerMergeSafe2(outerStyle)) return null;
3460
- if (gridConflict(outerStyle, innerStyle)) return null;
3461
- return [
3462
- // 1. Preserve inheritable values (color/font/…) by folding them onto the child first.
3463
- rw.foldInheritedStyles(outer, inner, { conditions: "all" }),
3464
- // 2. Transfer the wrapper's grid-container declarations onto the child (target-wins keeps the
3465
- // child's value for any shared property — identical anyway, we proved non-conflict).
3466
- rw.mergeStyle(inner, null, extractGridStyle(outerStyle), "target-wins"),
3467
- // 3. Remove the wrapper (structural-safe; hoists the child and preserves its IRNodeId).
3468
- rw.unwrap(outer)
3469
- ];
3470
- },
3471
- // Like its flex sibling, this merge removes the outer container's box, but a `display:grid` wrapper
3472
- // establishes a formatting context, so it is a `needs-verification` flatten that the conservative
3473
- // production gate (`'provably-safe'`) REVERTS — every case here is a no-match. Op-level correctness
3474
- // is asserted by the invariant suite over every pattern.
3475
- test: {
3476
- noMatch: [
3477
- // The merge is real but not provably layout-neutral (the wrapper establishes a grid context),
3478
- // so under the conservative gate the nested containers are left in place.
3479
- '<div className="grid gap-2" data-x="1"><div className="grid grid-cols-2">X</div></div>',
3480
- // A non-grid wrapper does not match the grid-container signature → left unchanged anyway.
3481
- '<div className="block bg-blue-500"><div className="grid grid-cols-2">X</div></div>'
3482
- ]
3483
- }
3484
- });
3485
-
3486
- // ../patterns/src/library/flatten/passthrough-wrapper.pattern.ts
3487
- function metaOf2(node) {
3488
- const n = node;
3489
- return n.kind === "element" ? n.meta : null;
3490
- }
3491
- function elementOf(node) {
3492
- const n = node;
3493
- return n.kind === "element" ? n : null;
3494
- }
3495
- var establishesContext = (node) => {
3496
- const m = metaOf2(node);
3497
- if (!m) return false;
3498
- return m.establishesBox || m.establishesFormattingContext || m.establishesStackingContext || m.isContainingBlock || m.declaresCustomProperties;
3499
- };
3500
- var hasSpreadAttrs2 = (node) => metaOf2(node)?.hasSpreadAttrs ?? false;
3501
- var isComponentNode2 = (node) => metaOf2(node)?.isComponent ?? false;
3502
- var hasOwnAttrs2 = (node) => {
3503
- const el = elementOf(node);
3504
- if (!el) return false;
3505
- return el.attrs.entries.size > 0 || el.attrs.spreads.length > 0;
3506
- };
3507
- var targetedByStructuralPseudo3 = (node, ctx) => {
3508
- const el = elementOf(node);
3509
- if (!el) return false;
3510
- if (el.meta.targetedByStructuralPseudo) return true;
3511
- return ctx.selectors.targetedByStructuralPseudo(el.id);
3512
- };
3513
- var passthroughWrapper = definePattern({
3514
- name: "passthrough-wrapper",
3515
- category: "flatten/passthrough-wrapper",
3516
- safety: 2,
3517
- doc: {
3518
- title: "Flatten passthrough wrapper",
3519
- summary: "A div with no own visual/box style, no attributes beyond an inert class, exactly one element child, and no opacity barriers is removed; its sole child is hoisted in its place.",
3520
- before: "<div><Child/></div>",
3521
- after: "<Child/>",
3522
- safetyRationale: "Wrapper paints nothing and establishes no layout/paint/var context, carries no ref/handlers/dynamic-children/html/spread/component identity, owns no targetable attrs, and is not a combinator/structural-pseudo subject (reparenting changes no match-set); inheritable styles are folded onto the child before removal."
3523
- },
3524
- match: {
3525
- tag: "div",
3526
- onlyChild: "element",
3527
- paintsNothing: true,
3528
- where: [
3529
- not(establishesContext),
3530
- not(hasOwnAttrs2),
3531
- not(hasDynamicClasses),
3532
- not(hasSpreadAttrs2),
3533
- not(isComponentNode2),
3534
- not(targetedByStructuralPseudo3)
3535
- ]
3536
- },
3537
- rewrite: { flattenInto: "child" },
3538
- test: {
3539
- cases: [
3540
- {
3541
- // A plain, style-free wrapper paints nothing and establishes no context → a provably-safe
3542
- // flatten under the conservative gate: the wrapper is removed and its sole child hoisted.
3543
- before: '<div><a className="bg-red-200">Link</a></div>',
3544
- after: '<a className="bg-red-200">Link</a>'
3545
- }
3546
- ],
3547
- noMatch: [
3548
- // A ref pins the wrapper's element identity (a hard opacity barrier) → not a passthrough.
3549
- '<div ref={rootRef}><a className="bg-red-200">Link</a></div>',
3550
- // A `display:flex` wrapper establishes a formatting context, so removing its box is NOT
3551
- // provably layout-neutral → the conservative gate leaves it in place.
3552
- '<div className="flex"><a className="bg-red-200">Link</a></div>'
3553
- ]
3554
- }
3555
- });
3556
-
3557
- // ../patterns/src/library/flatten/redundant-fragment.pattern.ts
3558
- function parentIsRedundantFragment(node, ctx) {
3559
- const el = node;
3560
- if (el.kind !== "element") return false;
3561
- const parentId = el.parent;
3562
- if (parentId == null) return false;
3563
- const parent = ctx.doc.nodes.get(parentId);
3564
- if (!parent || parent.kind !== "fragment") return false;
3565
- if (parent.parent == null) return false;
3566
- if (parent.children.length !== 1) return false;
3567
- const m = parent.meta;
3568
- if (m.hasKey || m.hasRef || m.hasEventHandlers || m.hasDynamicChildren || m.hasDangerousHtml || m.hasSpreadAttrs || m.isComponent) {
3569
- return false;
3570
- }
3571
- if (m.targetedByCombinator || m.targetedByStructuralPseudo) return false;
3572
- const fid = parentId;
3573
- if (ctx.selectors.targetedByCombinator(fid) || ctx.selectors.targetedByStructuralPseudo(fid)) {
3574
- return false;
3575
- }
3576
- if (ctx.selectors.reparentImpact(fid).size > 0) return false;
3577
- return true;
3578
- }
3579
- var redundantFragment = definePattern({
3580
- name: "redundant-fragment",
3581
- category: "flatten/redundant-fragment",
3582
- safety: 1,
3583
- doc: {
3584
- title: "Flatten redundant single-child fragment",
3585
- summary: "A fragment whose only child is a single node is removed; the child is spliced up into the fragment's slot, preserving its IRNodeId, siblings, attributes and the CSS cascade.",
3586
- before: "<><Child/></>",
3587
- after: "<Child/>",
3588
- safetyRationale: "A fragment paints nothing and renders no box; with exactly one child its removal changes no sibling/structural-pseudo match-set. Keyed fragments and fragments carrying ref/handlers/dynamic-children/raw-html/spread are excluded as opacity barriers."
3589
- },
3590
- match: parentIsRedundantFragment,
3591
- rewrite: (ctx, rw) => {
3592
- const parentId = ctx.node.parent;
3593
- if (parentId == null) return null;
3594
- const fragment = ctx.doc.nodes.get(parentId);
3595
- if (!fragment || fragment.kind !== "fragment") return null;
3596
- return [rw.unwrap(fragment)];
3597
- },
3598
- test: {
3599
- cases: [
3600
- {
3601
- // A fragment renders no box, so unwrapping a single-child fragment is always layout-identical
3602
- // → a provably-safe flatten: the child is spliced up into the fragment's slot.
3603
- before: '<><span className="bg-red-200">Hi</span></>',
3604
- after: '<span className="bg-red-200">Hi</span>'
3605
- }
3606
- ],
3607
- noMatch: [
3608
- // Two children ⇒ not a single-child fragment, so the fragment is load-bearing and stays.
3609
- '<><span className="bg-red-200">A</span><span className="bg-green-200">B</span></>'
3610
- ]
3611
- }
3612
- });
3613
-
3614
- // ../patterns/src/library/flatten/redundant-inline-wrapper.pattern.ts
3615
- function asEl3(node) {
3616
- const n = node;
3617
- return n.kind === "element" ? n : null;
3618
- }
3619
- function metaOf3(node) {
3620
- return asEl3(node)?.meta ?? null;
3621
- }
3622
- var establishesContext2 = (node) => {
3623
- const m = metaOf3(node);
3624
- if (!m) return false;
3625
- return m.establishesBox || m.establishesFormattingContext || m.establishesStackingContext || m.isContainingBlock || m.declaresCustomProperties;
3626
- };
3627
- var hasSpreadAttrs3 = (node) => metaOf3(node)?.hasSpreadAttrs ?? false;
3628
- var isComponentNode3 = (node) => metaOf3(node)?.isComponent ?? false;
3629
- var hasOwnAttrs3 = (node) => {
3630
- const el = asEl3(node);
3631
- if (!el) return false;
3632
- return el.attrs.entries.size > 0 || el.attrs.spreads.length > 0;
3633
- };
3634
- var targetedByStructuralPseudo4 = (node, ctx) => {
3635
- const el = asEl3(node);
3636
- if (!el) return false;
3637
- if (el.meta.targetedByStructuralPseudo) return true;
3638
- return ctx.selectors.targetedByStructuralPseudo(el.id);
3639
- };
3640
- var DISPLAY3 = "display";
3641
- var hasNonInlineDisplay = (node, ctx) => {
3642
- const el = asEl3(node);
3643
- if (!el) return false;
3644
- const sm = ctx.computedOf(el) ?? el.computed;
3645
- for (const block of sm.blocks.values()) {
3646
- const decl = block.decls.get(DISPLAY3);
3647
- if (decl && String(decl.value) !== "inline") return true;
3648
- }
3649
- return false;
3650
- };
3651
- var redundantInlineWrapper = definePattern({
3652
- name: "redundant-inline-wrapper",
3653
- category: "flatten/redundant-inline-wrapper",
3654
- safety: 2,
3655
- doc: {
3656
- title: "Flatten redundant inline wrapper",
3657
- summary: "An inline span with no own visual/box style, no attributes beyond an inert class, exactly one element child, and no opacity barriers is removed; its sole child is hoisted in its place.",
3658
- before: "<span><Child/></span>",
3659
- after: "<Child/>",
3660
- safetyRationale: "An empty inline box paints nothing and establishes no layout/paint/var context; with the inline default display and a single element child its removal changes no paint and no flow. The span carries no ref/handlers/dynamic-children/html/spread/component identity, owns no targetable attrs, and is not a combinator/structural-pseudo subject; inheritable styles are folded onto the child before removal."
3661
- },
3662
- match: {
3663
- tag: "span",
3664
- onlyChild: "element",
3665
- paintsNothing: true,
3666
- where: [
3667
- not(hasNonInlineDisplay),
3668
- not(establishesContext2),
3669
- not(hasOwnAttrs3),
3670
- not(hasDynamicClasses),
3671
- not(hasSpreadAttrs3),
3672
- not(isComponentNode3),
3673
- not(targetedByStructuralPseudo4)
3674
- ]
3675
- },
3676
- rewrite: { flattenInto: "child" },
3677
- test: {
3678
- cases: [
3679
- {
3680
- // An empty inline span paints nothing and establishes no context → a provably-safe flatten:
3681
- // the span is removed and its sole child hoisted in place.
3682
- before: '<span><a className="text-blue-500">Link</a></span>',
3683
- after: '<a className="text-blue-500">Link</a>'
3684
- }
3685
- ],
3686
- noMatch: [
3687
- // A ref pins the span's element identity (a hard opacity barrier) → not a passthrough.
3688
- '<span ref={spanRef}><a className="text-blue-500">Link</a></span>',
3689
- // The span paints its own background (own visual style) → kept.
3690
- '<span className="bg-green-200"><a className="text-blue-500">Link</a></span>',
3691
- // Non-inline display (inline-block) participates in layout differently → kept.
3692
- '<span className="inline-block"><a className="text-blue-500">Link</a></span>'
3693
- ]
3694
- }
3695
- });
3696
-
3697
- // ../patterns/src/library/compress/border-radius-shorthand.pattern.ts
3698
- var CORNERS = [
3699
- "border-top-left-radius",
3700
- "border-top-right-radius",
3701
- "border-bottom-right-radius",
3702
- "border-bottom-left-radius"
3703
- ];
3704
- var CORNER_SET = new Set(CORNERS);
3705
- var BASE_KEY = conditionKey(BASE_CONDITION);
3706
- var RADIUS = "border-radius";
3707
- var NON_COLLAPSIBLE_VALUES = /* @__PURE__ */ new Set([
3708
- "initial",
3709
- "inherit",
3710
- "unset",
3711
- "revert",
3712
- "revert-layer"
3713
- ]);
3714
- function analyzeRadius(sm) {
3715
- const block = sm.blocks.get(BASE_KEY);
3716
- if (!block) return null;
3717
- const corners = [];
3718
- for (const corner of CORNERS) {
3719
- const decl = block.decls.get(corner);
3720
- if (!decl) return null;
3721
- corners.push(decl);
3722
- }
3723
- const important = corners[0].important;
3724
- if (!corners.every((d) => d.important === important)) return null;
3725
- const value = String(corners[0].value);
3726
- if (NON_COLLAPSIBLE_VALUES.has(value)) return null;
3727
- if (!corners.every((d) => String(d.value) === value)) return null;
3728
- const relative2 = corners.some((d) => d.relativeToParent);
3729
- return { value, important, relative: relative2 };
3730
- }
3731
- function withFoldedRadius(sm, fold) {
3732
- const blocks = /* @__PURE__ */ new Map();
3733
- for (const [key, block] of sm.blocks) {
3734
- if (key !== BASE_KEY) {
3735
- blocks.set(key, block);
3736
- continue;
3737
- }
3738
- const decls = /* @__PURE__ */ new Map();
3739
- for (const [prop, decl] of block.decls) {
3740
- if (CORNER_SET.has(String(prop))) continue;
3741
- decls.set(prop, decl);
3742
- }
3743
- const shorthand = {
3744
- property: RADIUS,
3745
- value: fold.value,
3746
- important: fold.important,
3747
- relativeToParent: fold.relative,
3748
- inherited: false
3749
- // border-radius is never inherited
3750
- };
3751
- decls.set(shorthand.property, shorthand);
3752
- blocks.set(key, { condition: block.condition, decls });
3753
- }
3754
- return { blocks };
3755
- }
3756
- var borderRadiusShorthand = definePattern({
3757
- name: "border-radius-shorthand",
3758
- category: "compress/border-radius-shorthand",
3759
- safety: 1,
3760
- doc: {
3761
- title: "Collapse equal corner radii into border-radius",
3762
- summary: "An element whose four corner radii (border-*-radius longhands) are all equal is rewritten to the single Tailwind rounded-* utility (border-radius === the four equal corners).",
3763
- before: '<div class="rounded-tl-lg rounded-tr-lg rounded-br-lg rounded-bl-lg"/>',
3764
- after: '<div class="rounded-lg"/>',
3765
- safetyRationale: "`border-radius` is value-identical to four equal corner radii \u2014 a class-only change. It is safe even on an element with a ref, event handler, dynamic child, or dangerouslySetInnerHTML \u2014 a className rewrite touches none of them; only a dynamic/opaque class list or a combinator-subject class is excluded, so no behaviour or project selector is disturbed."
3766
- },
3767
- rewrite: {
3768
- rewriteClasses(computed2) {
3769
- const fold = analyzeRadius(computed2);
3770
- return fold ? withFoldedRadius(computed2, fold) : null;
3771
- }
3772
- },
3773
- test: {
3774
- cases: [
3775
- {
3776
- // The four equal corner longhands collapse to a `border-radius` decl at the IR level; the
3777
- // minimizing reverse-emit then picks the single shortest utility (`rounded-lg`) that reproduces
3778
- // it, replacing the four `rounded-{tl,tr,br,bl}-lg` tokens. `bg-red-200` is preserved.
3779
- before: '<div className="rounded-tl-lg rounded-tr-lg rounded-br-lg rounded-bl-lg bg-red-200">box</div>',
3780
- after: '<div className="bg-red-200 rounded-lg">box</div>'
3781
- }
3782
- ],
3783
- // Corners differ (top corners vs bottom corners) → no all-equal collapse.
3784
- noMatch: ['<div className="rounded-t-lg rounded-b-sm bg-red-200">box</div>']
3785
- }
3786
- });
3787
-
3788
- // ../patterns/src/library/compress/border-shorthand.pattern.ts
3789
- var WIDTH_SIDES = [
3790
- "border-top-width",
3791
- "border-right-width",
3792
- "border-bottom-width",
3793
- "border-left-width"
3794
- ];
3795
- var WIDTH_SIDE_SET = new Set(WIDTH_SIDES);
3796
- var BASE_KEY2 = conditionKey(BASE_CONDITION);
3797
- var BORDER_WIDTH = "border-width";
3798
- function analyzeWidth(sm) {
3799
- const block = sm.blocks.get(BASE_KEY2);
3800
- if (!block) return null;
3801
- const sides = [];
3802
- for (const side of WIDTH_SIDES) {
3803
- const decl = block.decls.get(side);
3804
- if (!decl) return null;
3805
- sides.push(decl);
3806
- }
3807
- const [top, right, bottom, left] = sides;
3808
- if (!(top.important === right.important && right.important === bottom.important && bottom.important === left.important)) {
3809
- return null;
3810
- }
3811
- const tv = String(top.value);
3812
- const rv = String(right.value);
3813
- const bv = String(bottom.value);
3814
- const lv = String(left.value);
3815
- if (tv !== bv || lv !== rv) return null;
3816
- const value = tv === lv ? tv : `${tv} ${lv}`;
3817
- const relative2 = sides.some((d) => d.relativeToParent);
3818
- return { value, important: top.important, relative: relative2 };
3819
- }
3820
- function withFoldedWidth(sm, fold) {
3821
- const blocks = /* @__PURE__ */ new Map();
3822
- for (const [key, block] of sm.blocks) {
3823
- if (key !== BASE_KEY2) {
3824
- blocks.set(key, block);
3825
- continue;
3826
- }
3827
- const decls = /* @__PURE__ */ new Map();
3828
- for (const [prop, decl] of block.decls) {
3829
- if (WIDTH_SIDE_SET.has(String(prop))) continue;
3830
- decls.set(prop, decl);
3831
- }
3832
- const shorthand = {
3833
- property: BORDER_WIDTH,
3834
- value: fold.value,
3835
- important: fold.important,
3836
- relativeToParent: fold.relative,
3837
- inherited: false
3838
- // border-width is never inherited
3839
- };
3840
- decls.set(shorthand.property, shorthand);
3841
- blocks.set(key, { condition: block.condition, decls });
3842
- }
3843
- return { blocks };
3844
- }
3845
- var borderShorthand = definePattern({
3846
- name: "border-shorthand",
3847
- category: "compress/border-shorthand",
3848
- safety: 1,
3849
- doc: {
3850
- title: "Collapse border-width longhands to shorthand",
3851
- summary: "Equal border width on all four sides (or matching x/y pairs) expressed as separate longhand declarations is collapsed to the shortest equivalent border-width shorthand (border-* / border-x-* border-y-*).",
3852
- before: '<div class="border-t-2 border-r-2 border-b-2 border-l-2"/>',
3853
- after: '<div class="border-2"/>',
3854
- safetyRationale: "A value-preserving re-serialization of the same computed border widths (style/color longhands untouched) \u2014 a class-only change. It is safe even on an element with a ref, event handler, dynamic child, or dangerouslySetInnerHTML \u2014 a className rewrite touches none of them; only a dynamic/opaque class list or a combinator-subject class is excluded, so no behaviour or project selector is disturbed."
3855
- },
3856
- rewrite: {
3857
- rewriteClasses(computed2) {
3858
- const fold = analyzeWidth(computed2);
3859
- return fold ? withFoldedWidth(computed2, fold) : null;
3860
- }
3861
- },
3862
- test: {
3863
- cases: [
3864
- {
3865
- // The four equal width longhands collapse to a `border-width` shorthand at the IR level, and the
3866
- // minimizing reverse-emit picks the single shortest utility (`border-2`) that reproduces it,
3867
- // replacing the four `border-{t,r,b,l}-2` tokens. `bg-red-200` is preserved.
3868
- before: '<div className="border-t-2 border-r-2 border-b-2 border-l-2 bg-red-200">box</div>',
3869
- after: '<div className="bg-red-200 border-2">box</div>'
3870
- }
3871
- ],
3872
- // Asymmetric widths (top != bottom) cannot fold into a shorthand.
3873
- noMatch: ['<div className="border-t-2 border-r-4 border-b-8 border-l-4 bg-red-200">box</div>']
3874
- }
3875
- });
3876
-
3877
- // ../patterns/src/library/compress/dedupe-classes.pattern.ts
3878
- function findRedundantClasses(computed2) {
3879
- const winners = /* @__PURE__ */ new Set();
3880
- const shadowed = /* @__PURE__ */ new Set();
3881
- for (const block of computed2.blocks.values()) {
3882
- for (const decl of block.decls.values()) {
3883
- if (decl.origin && decl.origin.kind === "class") winners.add(decl.origin.className);
3884
- for (const o of decl.shadowed ?? []) {
3885
- if (o.kind === "class") shadowed.add(o.className);
3886
- }
3887
- }
3888
- }
3889
- return { winners, shadowed };
3890
- }
3891
- var dedupeClasses = definePattern({
3892
- name: "dedupe-classes",
3893
- category: "compress/dedupe-classes",
3894
- safety: 1,
3895
- doc: {
3896
- title: "Dedupe fully-overridden class tokens",
3897
- summary: "Drops class tokens whose every declaration is overridden by a later token resolving to the same property; the surviving token set produces a byte-for-byte identical computed style.",
3898
- before: '<p class="text-sm text-lg" />',
3899
- after: '<p class="text-lg" />',
3900
- safetyRationale: "A fully-overridden token contributes nothing to the computed style in any condition, so removing it changes no pixels \u2014 a class-only change. It is safe even on an element with a ref, event handler, dynamic child, or dangerouslySetInnerHTML \u2014 a className rewrite touches none of them; only a dynamic/opaque class list or a combinator-subject class is excluded, so no behaviour or project selector is disturbed."
3901
- },
3902
- rewrite: {
3903
- dropClasses(computed2, ctx) {
3904
- const { winners, shadowed } = findRedundantClasses(computed2);
3905
- const drop = /* @__PURE__ */ new Set();
3906
- for (const cls of shadowed) {
3907
- if (winners.has(cls)) continue;
3908
- if (!ctx.resolver.selectorUsage(cls).droppable) continue;
3909
- drop.add(cls);
3910
- }
3911
- return drop;
3912
- }
3913
- },
3914
- test: {
3915
- cases: [
3916
- {
3917
- // `text-sm` is fully overridden by `text-lg` (both set font-size + line-height). The resolver
3918
- // records that shadowing in provenance and reports the Tailwind utility as droppable, so the
3919
- // pattern drops `text-sm`; the reverse-emit then re-derives the minimal set (`text-lg`).
3920
- before: '<p className="text-sm text-lg">Hi</p>',
3921
- after: '<p className="text-lg">Hi</p>'
3922
- }
3923
- ],
3924
- // Both tokens win a distinct property (no full override) → nothing to dedupe.
3925
- noMatch: ['<p className="text-lg font-bold">Hi</p>']
3926
- }
3927
- });
3928
-
3929
- // ../patterns/src/library/compress/gap-shorthand.pattern.ts
3930
- var ROW_GAP = "row-gap";
3931
- var COLUMN_GAP = "column-gap";
3932
- var GAP = "gap";
3933
- var BASE_KEY3 = conditionKey(BASE_CONDITION);
3934
- function withGapShorthand(sm, gapDecl) {
3935
- const blocks = /* @__PURE__ */ new Map();
3936
- for (const [key, block] of sm.blocks) {
3937
- if (key !== BASE_KEY3) {
3938
- blocks.set(key, block);
3939
- continue;
3940
- }
3941
- const decls = /* @__PURE__ */ new Map();
3942
- for (const [prop, decl] of block.decls) {
3943
- if (prop === ROW_GAP || prop === COLUMN_GAP) continue;
3944
- decls.set(prop, decl);
3945
- }
3946
- decls.set(gapDecl.property, gapDecl);
3947
- blocks.set(key, { condition: block.condition, decls });
3948
- }
3949
- return { blocks };
3950
- }
3951
- var gapShorthand = definePattern({
3952
- name: "gap-shorthand",
3953
- category: "compress/gap-shorthand",
3954
- safety: 1,
3955
- doc: {
3956
- title: "Collapse equal row/column gap into the `gap` shorthand",
3957
- summary: "An element whose computed row-gap and column-gap are equal has the two axis longhands collapsed into a single-value `gap` shorthand (Tailwind gap-x-* gap-y-* \u2192 gap-*).",
3958
- before: '<div style="row-gap:16px;column-gap:16px"/>',
3959
- after: '<div style="gap:16px"/>',
3960
- safetyRationale: "A single-value `gap` is value-identical to an equal row-gap+column-gap pair \u2014 a class-only change. It is safe even on an element with a ref, event handler, dynamic child, or dangerouslySetInnerHTML \u2014 a className rewrite touches none of them; only a dynamic/opaque class list or a combinator-subject class is excluded, so no behaviour or project selector is disturbed."
3961
- },
3962
- rewrite: {
3963
- rewriteClasses(computed2) {
3964
- const base = computed2.blocks.get(BASE_KEY3);
3965
- if (!base) return null;
3966
- const rowGap = base.decls.get(ROW_GAP);
3967
- const colGap = base.decls.get(COLUMN_GAP);
3968
- if (!rowGap || !colGap) return null;
3969
- if (rowGap.important !== colGap.important) return null;
3970
- if (rowGap.value !== colGap.value) return null;
3971
- const gapDecl = {
3972
- property: GAP,
3973
- value: rowGap.value,
3974
- important: rowGap.important,
3975
- relativeToParent: rowGap.relativeToParent || colGap.relativeToParent,
3976
- inherited: false
3977
- // gap is not an inherited property
3978
- };
3979
- return withGapShorthand(computed2, gapDecl);
3980
- }
3981
- },
3982
- test: {
3983
- cases: [
3984
- {
3985
- // Equal row/column gap collapse to a `gap` decl at the IR level; the minimizing reverse-emit
3986
- // re-expands `gap` to row-gap+column-gap and picks the single utility covering both (`gap-4`),
3987
- // replacing the `gap-x-4`+`gap-y-4` pair. `bg-red-200` is preserved.
3988
- before: '<div className="gap-x-4 gap-y-4 bg-red-200">box</div>',
3989
- after: '<div className="bg-red-200 gap-4">box</div>'
3990
- }
3991
- ],
3992
- // Unequal axes (row-gap != column-gap) have no single-value `gap` equivalent → not collapsed.
3993
- noMatch: ['<div className="gap-x-2 gap-y-4 bg-red-200">box</div>']
3994
- }
3995
- });
3996
-
3997
- // ../patterns/src/library/compress/inset-shorthand.pattern.ts
3998
- var TOP = "top";
3999
- var RIGHT = "right";
4000
- var BOTTOM = "bottom";
4001
- var LEFT = "left";
4002
- var INSET = "inset";
4003
- var INSET_BLOCK = "inset-block";
4004
- var INSET_INLINE = "inset-inline";
4005
- function sameSide(a, b) {
4006
- return a !== void 0 && b !== void 0 && a.value === b.value && a.important === b.important;
4007
- }
4008
- function asProperty(src, property) {
4009
- return { ...src, property, inherited: normalizer.inherited.isInherited(property) };
4010
- }
4011
- function withBaseDecls(src, baseDecls) {
4012
- const blocks = /* @__PURE__ */ new Map();
4013
- for (const [key, block] of src.blocks) {
4014
- const decls = key === BASE_CONDITION_KEY ? baseDecls : new Map(block.decls);
4015
- blocks.set(key, { condition: block.condition, decls });
4016
- }
4017
- return { blocks };
4018
- }
4019
- var insetShorthand = definePattern({
4020
- name: "inset-shorthand",
4021
- category: "compress/inset-shorthand",
4022
- safety: 2,
4023
- doc: {
4024
- title: "Compress inset longhands into a shorthand",
4025
- summary: "top/right/bottom/left set to one value collapse to `inset`; a matching top/bottom or left/right pair collapses to `inset-block` / `inset-inline` (Tailwind inset-y / inset-x).",
4026
- before: '<div style="top:10px;right:10px;bottom:10px;left:10px"/>',
4027
- after: '<div style="inset:10px"/>',
4028
- safetyRationale: "Meaning-preserving inset shorthand compaction \u2014 a class-only change. It is safe even on an element with a ref, event handler, dynamic child, or dangerouslySetInnerHTML \u2014 a className rewrite touches none of them; only a dynamic/opaque class list or a combinator-subject class is excluded, so no behaviour or project selector is disturbed."
4029
- },
4030
- rewrite: {
4031
- rewriteClasses(computed2) {
4032
- const base = computed2.blocks.get(BASE_CONDITION_KEY);
4033
- if (!base) return null;
4034
- const top = base.decls.get(TOP);
4035
- const right = base.decls.get(RIGHT);
4036
- const bottom = base.decls.get(BOTTOM);
4037
- const left = base.decls.get(LEFT);
4038
- const next = new Map(base.decls);
4039
- if (top && sameSide(top, right) && sameSide(top, bottom) && sameSide(top, left)) {
4040
- next.delete(TOP);
4041
- next.delete(RIGHT);
4042
- next.delete(BOTTOM);
4043
- next.delete(LEFT);
4044
- next.set(INSET, asProperty(top, INSET));
4045
- } else {
4046
- let collapsed = false;
4047
- if (sameSide(top, bottom)) {
4048
- next.delete(TOP);
4049
- next.delete(BOTTOM);
4050
- next.set(INSET_BLOCK, asProperty(top, INSET_BLOCK));
4051
- collapsed = true;
4052
- }
4053
- if (sameSide(left, right)) {
4054
- next.delete(LEFT);
4055
- next.delete(RIGHT);
4056
- next.set(INSET_INLINE, asProperty(left, INSET_INLINE));
4057
- collapsed = true;
4058
- }
4059
- if (!collapsed) return null;
4060
- }
4061
- return withBaseDecls(computed2, next);
4062
- }
4063
- },
4064
- test: {
4065
- cases: [
4066
- {
4067
- // The four equal inset longhands collapse to an `inset` shorthand at the IR level; the
4068
- // minimizing reverse-emit expands it back to top/right/bottom/left and picks the single utility
4069
- // covering all four (`inset-0`), replacing the four physical-side tokens. `bg-red-200` survives.
4070
- before: '<div className="top-0 right-0 bottom-0 left-0 bg-red-200">box</div>',
4071
- after: '<div className="bg-red-200 inset-0">box</div>'
4072
- }
4073
- ],
4074
- // No matching inset pair (all four distinct) → nothing collapses.
4075
- noMatch: ['<div className="top-0 right-1 bottom-2 left-3 bg-red-200">box</div>']
4076
- }
4077
- });
4078
-
4079
- // ../patterns/src/library/compress/margin-shorthand.pattern.ts
4080
- var MARGIN_SIDES = [
4081
- "margin-top",
4082
- "margin-right",
4083
- "margin-bottom",
4084
- "margin-left"
4085
- ];
4086
- var MARGIN_SIDE_SET = new Set(MARGIN_SIDES);
4087
- var BASE_KEY4 = conditionKey(BASE_CONDITION);
4088
- function collapseMarginValue(top, right, bottom, left) {
4089
- if (right === left) {
4090
- if (top === bottom) {
4091
- return top === right ? top : `${top} ${right}`;
4092
- }
4093
- return `${top} ${right} ${bottom}`;
4094
- }
4095
- return `${top} ${right} ${bottom} ${left}`;
4096
- }
4097
- function withFoldedMargin(sm, marginDecl) {
4098
- const blocks = /* @__PURE__ */ new Map();
4099
- for (const [key, block] of sm.blocks) {
4100
- if (key !== BASE_KEY4) {
4101
- blocks.set(key, block);
4102
- continue;
4103
- }
4104
- const decls = /* @__PURE__ */ new Map();
4105
- for (const [prop, decl] of block.decls) {
4106
- if (!MARGIN_SIDE_SET.has(String(prop))) decls.set(prop, decl);
4107
- }
4108
- decls.set(marginDecl.property, marginDecl);
4109
- blocks.set(key, { condition: block.condition, decls });
4110
- }
4111
- return { blocks };
4112
- }
4113
- var marginShorthand = definePattern({
4114
- name: "margin-shorthand",
4115
- category: "compress/margin-shorthand",
4116
- safety: 2,
4117
- doc: {
4118
- title: "Compress margin longhands into the `margin` shorthand",
4119
- summary: "An element with margin-top/right/bottom/left all set has them collapsed into the shortest legal `margin` shorthand (the m / mx / my forms); meaning is preserved, declaration count drops.",
4120
- before: '<div style="margin-top:8px;margin-right:8px;margin-bottom:8px;margin-left:8px"/>',
4121
- after: '<div style="margin:8px"/>',
4122
- safetyRationale: "A pure representation change of the same computed margins (no pixels move) \u2014 a class-only change. It is safe even on an element with a ref, event handler, dynamic child, or dangerouslySetInnerHTML \u2014 a className rewrite touches none of them; only a dynamic/opaque class list or a combinator-subject class is excluded, so no behaviour or project selector is disturbed."
4123
- },
4124
- rewrite: {
4125
- rewriteClasses(computed2) {
4126
- const base = computed2.blocks.get(BASE_KEY4);
4127
- if (!base) return null;
4128
- const sides = MARGIN_SIDES.map((p) => base.decls.get(p));
4129
- if (sides.some((d) => d === void 0)) return null;
4130
- const [mt, mr, mb, ml] = sides;
4131
- if (mt.important || mr.important || mb.important || ml.important) return null;
4132
- const value = collapseMarginValue(
4133
- String(mt.value),
4134
- String(mr.value),
4135
- String(mb.value),
4136
- String(ml.value)
4137
- );
4138
- const marginDecl = {
4139
- property: "margin",
4140
- value,
4141
- important: false,
4142
- relativeToParent: mt.relativeToParent || mr.relativeToParent || mb.relativeToParent || ml.relativeToParent,
4143
- inherited: false
4144
- // margin is not an inherited property
4145
- };
4146
- return withFoldedMargin(computed2, marginDecl);
4147
- }
4148
- },
4149
- test: {
4150
- cases: [
4151
- {
4152
- // The four equal margin longhands collapse to a `margin` shorthand at the IR level, and the
4153
- // minimizing reverse-emit picks the single shortest utility (`m-2`) reproducing it, replacing
4154
- // the four `m{t,r,b,l}-2` tokens. `bg-red-200` is preserved.
4155
- before: '<div className="mt-2 mr-2 mb-2 ml-2 bg-red-200">box</div>',
4156
- after: '<div className="bg-red-200 m-2">box</div>'
4157
- }
4158
- ],
4159
- // Only two margin sides set → the four-longhand `margin` collapse does not apply.
4160
- noMatch: ['<div className="mt-2 mb-2 bg-red-200">box</div>']
4161
- }
4162
- });
4163
-
4164
- // ../patterns/src/library/compress/overflow-shorthand.pattern.ts
4165
- var OVERFLOW_X = "overflow-x";
4166
- var OVERFLOW_Y = "overflow-y";
4167
- var OVERFLOW = "overflow";
4168
- var BASE_KEY5 = conditionKey(BASE_CONDITION);
4169
- function withOverflowShorthand(sm, overflowDecl) {
4170
- const blocks = /* @__PURE__ */ new Map();
4171
- for (const [key, block] of sm.blocks) {
4172
- if (key !== BASE_KEY5) {
4173
- blocks.set(key, block);
4174
- continue;
4175
- }
4176
- const decls = /* @__PURE__ */ new Map();
4177
- for (const [prop, decl] of block.decls) {
4178
- if (prop === OVERFLOW_X || prop === OVERFLOW_Y) continue;
4179
- decls.set(prop, decl);
4180
- }
4181
- decls.set(overflowDecl.property, overflowDecl);
4182
- blocks.set(key, { condition: block.condition, decls });
4183
- }
4184
- return { blocks };
4185
- }
4186
- var overflowShorthand = definePattern({
4187
- name: "overflow-shorthand",
4188
- category: "compress/overflow-shorthand",
4189
- safety: 1,
4190
- doc: {
4191
- title: "Collapse equal overflow axes into the `overflow` shorthand",
4192
- summary: "An element whose computed overflow-x and overflow-y are equal has the two axis longhands collapsed into a single `overflow` shorthand (Tailwind overflow-x-* overflow-y-* \u2192 overflow-*).",
4193
- before: '<div style="overflow-x:auto;overflow-y:auto"/>',
4194
- after: '<div style="overflow:auto"/>',
4195
- safetyRationale: "A single-keyword `overflow` is value-identical to equal overflow-x+overflow-y \u2014 a class-only change. It is safe even on an element with a ref, event handler, dynamic child, or dangerouslySetInnerHTML \u2014 a className rewrite touches none of them; only a dynamic/opaque class list or a combinator-subject class is excluded, so no behaviour or project selector is disturbed."
4196
- },
4197
- rewrite: {
4198
- rewriteClasses(computed2) {
4199
- const base = computed2.blocks.get(BASE_KEY5);
4200
- if (!base) return null;
4201
- const overflowX = base.decls.get(OVERFLOW_X);
4202
- const overflowY = base.decls.get(OVERFLOW_Y);
4203
- if (!overflowX || !overflowY) return null;
4204
- if (overflowX.important !== overflowY.important) return null;
4205
- if (overflowX.value !== overflowY.value) return null;
4206
- const overflowDecl = {
4207
- property: OVERFLOW,
4208
- value: overflowX.value,
4209
- important: overflowX.important,
4210
- relativeToParent: overflowX.relativeToParent || overflowY.relativeToParent,
4211
- inherited: false
4212
- // overflow is not an inherited property
4213
- };
4214
- return withOverflowShorthand(computed2, overflowDecl);
4215
- }
4216
- },
4217
- test: {
4218
- cases: [
4219
- {
4220
- // Equal overflow axes collapse to an `overflow` decl at the IR level; the minimizing
4221
- // reverse-emit picks the single utility covering both (`overflow-auto`), replacing the
4222
- // `overflow-x-auto`+`overflow-y-auto` pair. `bg-red-200` is preserved.
4223
- before: '<div className="overflow-x-auto overflow-y-auto bg-red-200">box</div>',
4224
- after: '<div className="bg-red-200 overflow-auto">box</div>'
4225
- }
4226
- ],
4227
- // Mismatched axes (overflow-x != overflow-y) have no single-keyword equivalent → not collapsed.
4228
- noMatch: ['<div className="overflow-x-auto overflow-y-hidden bg-red-200">box</div>']
4229
- }
4230
- });
4231
-
4232
- // ../patterns/src/library/compress/overscroll-behavior-shorthand.pattern.ts
4233
- var OVERSCROLL_X = "overscroll-behavior-x";
4234
- var OVERSCROLL_Y = "overscroll-behavior-y";
4235
- var OVERSCROLL = "overscroll-behavior";
4236
- var BASE_KEY6 = conditionKey(BASE_CONDITION);
4237
- var NON_COLLAPSIBLE_VALUES2 = /* @__PURE__ */ new Set([
4238
- "initial",
4239
- "inherit",
4240
- "unset",
4241
- "revert",
4242
- "revert-layer"
4243
- ]);
4244
- function withOverscrollShorthand(sm, shorthand) {
4245
- const blocks = /* @__PURE__ */ new Map();
4246
- for (const [key, block] of sm.blocks) {
4247
- if (key !== BASE_KEY6) {
4248
- blocks.set(key, block);
4249
- continue;
4250
- }
4251
- const decls = /* @__PURE__ */ new Map();
4252
- for (const [prop, decl] of block.decls) {
4253
- if (prop === OVERSCROLL_X || prop === OVERSCROLL_Y) continue;
4254
- decls.set(prop, decl);
4255
- }
4256
- decls.set(shorthand.property, shorthand);
4257
- blocks.set(key, { condition: block.condition, decls });
4258
- }
4259
- return { blocks };
4260
- }
4261
- var overscrollBehaviorShorthand = definePattern({
4262
- name: "overscroll-behavior-shorthand",
4263
- category: "compress/overscroll-behavior-shorthand",
4264
- safety: 1,
4265
- doc: {
4266
- title: "Collapse equal overscroll-behavior axes into overscroll-behavior",
4267
- summary: "An element whose computed overscroll-behavior-x and overscroll-behavior-y are equal has the two axis longhands collapsed into a single `overscroll-behavior` shorthand (Tailwind overscroll-x-* overscroll-y-* \u2192 overscroll-*).",
4268
- before: '<div style="overscroll-behavior-x:contain;overscroll-behavior-y:contain"/>',
4269
- after: '<div class="overscroll-contain"/>',
4270
- safetyRationale: "`overscroll-behavior` is value-identical to an equal x+y axis pair \u2014 a class-only change. It is safe even on an element with a ref, event handler, dynamic child, or dangerouslySetInnerHTML \u2014 a className rewrite touches none of them; only a dynamic/opaque class list or a combinator-subject class is excluded, so no behaviour or project selector is disturbed."
4271
- },
4272
- rewrite: {
4273
- rewriteClasses(computed2) {
4274
- const base = computed2.blocks.get(BASE_KEY6);
4275
- if (!base) return null;
4276
- const x = base.decls.get(OVERSCROLL_X);
4277
- const y = base.decls.get(OVERSCROLL_Y);
4278
- if (!x || !y) return null;
4279
- if (x.important !== y.important) return null;
4280
- const value = String(x.value);
4281
- if (NON_COLLAPSIBLE_VALUES2.has(value)) return null;
4282
- if (value !== String(y.value)) return null;
4283
- const shorthand = {
4284
- property: OVERSCROLL,
4285
- value: x.value,
4286
- important: x.important,
4287
- relativeToParent: x.relativeToParent || y.relativeToParent,
4288
- inherited: false
4289
- // overscroll-behavior is not an inherited property
4290
- };
4291
- return withOverscrollShorthand(computed2, shorthand);
4292
- }
4293
- },
4294
- test: {
4295
- cases: [
4296
- {
4297
- // Equal x/y axes collapse to an `overscroll-behavior` decl at the IR level; the minimizing
4298
- // reverse-emit picks the single utility covering both (`overscroll-contain`), replacing the
4299
- // `overscroll-x-contain`+`overscroll-y-contain` pair. `bg-red-200` is preserved.
4300
- before: '<div className="overscroll-x-contain overscroll-y-contain bg-red-200">box</div>',
4301
- after: '<div className="bg-red-200 overscroll-contain">box</div>'
4302
- }
4303
- ],
4304
- // Axes differ (x != y) → no equal-axis collapse.
4305
- noMatch: ['<div className="overscroll-x-contain overscroll-y-auto bg-red-200">box</div>']
4306
- }
4307
- });
4308
-
4309
- // ../patterns/src/library/compress/padding-shorthand.pattern.ts
4310
- var PADDING_SIDES = [
4311
- "padding-top",
4312
- "padding-right",
4313
- "padding-bottom",
4314
- "padding-left"
4315
- ];
4316
- var PADDING_SIDE_SET = new Set(PADDING_SIDES);
4317
- var BASE_KEY7 = conditionKey(BASE_CONDITION);
4318
- function analyzePadding(sm) {
4319
- const block = sm.blocks.get(BASE_KEY7);
4320
- if (!block) return null;
4321
- const sides = [];
4322
- for (const side of PADDING_SIDES) {
4323
- const decl = block.decls.get(side);
4324
- if (!decl) return null;
4325
- sides.push(decl);
4326
- }
4327
- const [top, right, bottom, left] = sides;
4328
- if (!(top.important === right.important && right.important === bottom.important && bottom.important === left.important)) {
4329
- return null;
4330
- }
4331
- const tv = String(top.value);
4332
- const rv = String(right.value);
4333
- const bv = String(bottom.value);
4334
- const lv = String(left.value);
4335
- if (tv !== bv || lv !== rv) return null;
4336
- const value = tv === lv ? tv : `${tv} ${lv}`;
4337
- const relative2 = sides.some((d) => d.relativeToParent);
4338
- return { value, important: top.important, relative: relative2 };
4339
- }
4340
- function withFoldedPadding(sm, fold) {
4341
- const blocks = /* @__PURE__ */ new Map();
4342
- for (const [key, block] of sm.blocks) {
4343
- if (key !== BASE_KEY7) {
4344
- blocks.set(key, block);
4345
- continue;
4346
- }
4347
- const decls = /* @__PURE__ */ new Map();
4348
- for (const [prop, decl] of block.decls) {
4349
- if (PADDING_SIDE_SET.has(String(prop))) continue;
4350
- decls.set(prop, decl);
4351
- }
4352
- const shorthand = {
4353
- property: "padding",
4354
- value: fold.value,
4355
- important: fold.important,
4356
- relativeToParent: fold.relative,
4357
- inherited: false
4358
- // padding is never inherited
4359
- };
4360
- decls.set(shorthand.property, shorthand);
4361
- blocks.set(key, { condition: block.condition, decls });
4362
- }
4363
- return { blocks };
4364
- }
4365
- var paddingShorthand = definePattern({
4366
- name: "padding-shorthand",
4367
- category: "compress/padding-shorthand",
4368
- safety: 1,
4369
- doc: {
4370
- title: "Collapse padding longhands to shorthand",
4371
- summary: "Equal padding on all four sides (or matching x/y pairs) expressed as separate longhand declarations is collapsed to the shortest equivalent padding shorthand (p-* / px-* py-*).",
4372
- before: '<div class="pt-4 pr-4 pb-4 pl-4"/>',
4373
- after: '<div class="p-4"/>',
4374
- safetyRationale: "A value-preserving re-serialization of the same computed padding on the same node \u2014 a class-only change. It is safe even on an element with a ref, event handler, dynamic child, or dangerouslySetInnerHTML \u2014 a className rewrite touches none of them; only a dynamic/opaque class list or a combinator-subject class is excluded, so no behaviour or project selector is disturbed."
4375
- },
4376
- rewrite: {
4377
- rewriteClasses(computed2) {
4378
- const fold = analyzePadding(computed2);
4379
- return fold ? withFoldedPadding(computed2, fold) : null;
4380
- }
4381
- },
4382
- test: {
4383
- cases: [
4384
- {
4385
- // The four equal padding longhands collapse to a `padding` shorthand at the IR level, and the
4386
- // minimizing reverse-emit picks the single shortest utility (`p-4`) that reproduces it,
4387
- // replacing the four `p{t,r,b,l}-4` tokens. `bg-red-200` is preserved (its order is stable).
4388
- before: '<div className="pt-4 pr-4 pb-4 pl-4 bg-red-200">box</div>',
4389
- after: '<div className="bg-red-200 p-4">box</div>'
4390
- },
4391
- {
4392
- // A dynamic `{x}` child no longer blocks compress: only the element's OWN class tokens are
4393
- // rewritten (px-4 py-4 → p-4); the dynamic child is untouched by a class-only change. This is
4394
- // the real-app common case (most elements have dynamic content).
4395
- before: '<div className="px-4 py-4">{x}</div>',
4396
- after: '<div className="p-4">{x}</div>'
4397
- }
4398
- ],
4399
- // Asymmetric padding (top != bottom) cannot fold into a shorthand → left unchanged.
4400
- noMatch: ['<div className="pt-2 pr-4 pb-8 pl-4 bg-red-200">box</div>']
4401
- }
4402
- });
4403
-
4404
- // ../patterns/src/library/compress/place-shorthand.pattern.ts
4405
- var ALIGN_ITEMS2 = "align-items";
4406
- var JUSTIFY_ITEMS2 = "justify-items";
4407
- var PLACE_ITEMS2 = "place-items";
4408
- var ALIGN_CONTENT = "align-content";
4409
- var JUSTIFY_CONTENT2 = "justify-content";
4410
- var PLACE_CONTENT = "place-content";
4411
- var BASE_KEY8 = conditionKey(BASE_CONDITION);
4412
- function samePair(a, b) {
4413
- return a !== void 0 && b !== void 0 && a.value === b.value && a.important === b.important;
4414
- }
4415
- function placeDecl(property, align) {
4416
- return {
4417
- property,
4418
- value: align.value,
4419
- important: align.important,
4420
- relativeToParent: false,
4421
- // alignment keywords (center/start/stretch/…) are not length-relative
4422
- inherited: false
4423
- // none of the place-* alignment properties are inherited
4424
- };
4425
- }
4426
- function withBaseDecls2(sm, baseDecls) {
4427
- const blocks = /* @__PURE__ */ new Map();
4428
- for (const [key, block] of sm.blocks) {
4429
- const decls = key === BASE_KEY8 ? new Map(baseDecls) : block.decls;
4430
- blocks.set(key, { condition: block.condition, decls });
4431
- }
4432
- return { blocks };
4433
- }
4434
- var placeShorthand = definePattern({
4435
- name: "place-shorthand",
4436
- category: "compress/place-shorthand",
4437
- safety: 1,
4438
- doc: {
4439
- title: "Collapse matching alignment pairs into `place-*` shorthands",
4440
- summary: "When align-items equals justify-items they collapse to `place-items`; when align-content equals justify-content they collapse to `place-content`. The two collapses are independent.",
4441
- before: '<div style="align-items:center;justify-items:center"/>',
4442
- after: '<div style="place-items:center"/>',
4443
- safetyRationale: "A `place-*` shorthand is value-identical to its equal align/justify pair \u2014 a class-only change. It is safe even on an element with a ref, event handler, dynamic child, or dangerouslySetInnerHTML \u2014 a className rewrite touches none of them; only a dynamic/opaque class list or a combinator-subject class is excluded, so no behaviour or project selector is disturbed."
4444
- },
4445
- rewrite: {
4446
- rewriteClasses(computed2) {
4447
- const base = computed2.blocks.get(BASE_KEY8);
4448
- if (!base) return null;
4449
- const alignItems = base.decls.get(ALIGN_ITEMS2);
4450
- const justifyItems = base.decls.get(JUSTIFY_ITEMS2);
4451
- const alignContent = base.decls.get(ALIGN_CONTENT);
4452
- const justifyContent = base.decls.get(JUSTIFY_CONTENT2);
4453
- const next = new Map(base.decls);
4454
- let collapsed = false;
4455
- if (samePair(alignItems, justifyItems)) {
4456
- next.delete(ALIGN_ITEMS2);
4457
- next.delete(JUSTIFY_ITEMS2);
4458
- next.set(PLACE_ITEMS2, placeDecl(PLACE_ITEMS2, alignItems));
4459
- collapsed = true;
4460
- }
4461
- if (samePair(alignContent, justifyContent)) {
4462
- next.delete(ALIGN_CONTENT);
4463
- next.delete(JUSTIFY_CONTENT2);
4464
- next.set(PLACE_CONTENT, placeDecl(PLACE_CONTENT, alignContent));
4465
- collapsed = true;
4466
- }
4467
- if (!collapsed) return null;
4468
- return withBaseDecls2(computed2, next);
4469
- }
4470
- },
4471
- test: {
4472
- cases: [
4473
- {
4474
- // The matching items pair collapses to a `place-items` decl at the IR level; the minimizing
4475
- // reverse-emit picks the single utility covering both (`place-items-center`), replacing the
4476
- // `items-center`+`justify-items-center` pair. `bg-red-200` is preserved.
4477
- before: '<div className="items-center justify-items-center bg-red-200">box</div>',
4478
- after: '<div className="bg-red-200 place-items-center">box</div>'
4479
- }
4480
- ],
4481
- // Mismatched alignment (align-items != justify-items, no content pair) → nothing collapses.
4482
- noMatch: ['<div className="items-center justify-items-start bg-red-200">box</div>']
4483
- }
4484
- });
4485
-
4486
- // ../patterns/src/library/compress/scroll-margin-shorthand.pattern.ts
4487
- var SCROLL_MARGIN_SIDES = [
4488
- "scroll-margin-top",
4489
- "scroll-margin-right",
4490
- "scroll-margin-bottom",
4491
- "scroll-margin-left"
4492
- ];
4493
- var SIDE_SET = new Set(SCROLL_MARGIN_SIDES);
4494
- var BASE_KEY9 = conditionKey(BASE_CONDITION);
4495
- var SCROLL_MARGIN = "scroll-margin";
4496
- var NON_COLLAPSIBLE_VALUES3 = /* @__PURE__ */ new Set([
4497
- "initial",
4498
- "inherit",
4499
- "unset",
4500
- "revert",
4501
- "revert-layer"
4502
- ]);
4503
- function analyzeScrollMargin(sm) {
4504
- const block = sm.blocks.get(BASE_KEY9);
4505
- if (!block) return null;
4506
- const sides = [];
4507
- for (const side of SCROLL_MARGIN_SIDES) {
4508
- const decl = block.decls.get(side);
4509
- if (!decl) return null;
4510
- sides.push(decl);
4511
- }
4512
- const important = sides[0].important;
4513
- if (!sides.every((d) => d.important === important)) return null;
4514
- const value = String(sides[0].value);
4515
- if (NON_COLLAPSIBLE_VALUES3.has(value)) return null;
4516
- if (!sides.every((d) => String(d.value) === value)) return null;
4517
- const relative2 = sides.some((d) => d.relativeToParent);
4518
- return { value, important, relative: relative2 };
4519
- }
4520
- function withFoldedScrollMargin(sm, fold) {
4521
- const blocks = /* @__PURE__ */ new Map();
4522
- for (const [key, block] of sm.blocks) {
4523
- if (key !== BASE_KEY9) {
4524
- blocks.set(key, block);
4525
- continue;
4526
- }
4527
- const decls = /* @__PURE__ */ new Map();
4528
- for (const [prop, decl] of block.decls) {
4529
- if (SIDE_SET.has(String(prop))) continue;
4530
- decls.set(prop, decl);
4531
- }
4532
- const shorthand = {
4533
- property: SCROLL_MARGIN,
4534
- value: fold.value,
4535
- important: fold.important,
4536
- relativeToParent: fold.relative,
4537
- inherited: false
4538
- // scroll-margin is never inherited
4539
- };
4540
- decls.set(shorthand.property, shorthand);
4541
- blocks.set(key, { condition: block.condition, decls });
4542
- }
4543
- return { blocks };
4544
- }
4545
- var scrollMarginShorthand = definePattern({
4546
- name: "scroll-margin-shorthand",
4547
- category: "compress/scroll-margin-shorthand",
4548
- safety: 1,
4549
- doc: {
4550
- title: "Collapse equal scroll-margin sides into scroll-margin",
4551
- summary: "An element whose four scroll-margin sides are all equal is rewritten to the single Tailwind scroll-m-* utility (scroll-margin === the four equal sides).",
4552
- before: '<div class="scroll-mt-4 scroll-mr-4 scroll-mb-4 scroll-ml-4"/>',
4553
- after: '<div class="scroll-m-4"/>',
4554
- safetyRationale: "`scroll-margin` is value-identical to four equal scroll-margin sides \u2014 a class-only change. It is safe even on an element with a ref, event handler, dynamic child, or dangerouslySetInnerHTML \u2014 a className rewrite touches none of them; only a dynamic/opaque class list or a combinator-subject class is excluded, so no behaviour or project selector is disturbed."
4555
- },
4556
- rewrite: {
4557
- rewriteClasses(computed2) {
4558
- const fold = analyzeScrollMargin(computed2);
4559
- return fold ? withFoldedScrollMargin(computed2, fold) : null;
4560
- }
4561
- },
4562
- test: {
4563
- cases: [
4564
- {
4565
- // The four equal scroll-margin longhands collapse to a `scroll-margin` decl at the IR level; the
4566
- // minimizing reverse-emit then picks the single shortest utility (`scroll-m-4`) that reproduces
4567
- // it, replacing the four `scroll-m{t,r,b,l}-4` tokens. `bg-red-200` is preserved.
4568
- before: '<div className="scroll-mt-4 scroll-mr-4 scroll-mb-4 scroll-ml-4 bg-red-200">box</div>',
4569
- after: '<div className="bg-red-200 scroll-m-4">box</div>'
4570
- }
4571
- ],
4572
- // Sides differ (top != bottom) → no all-equal collapse.
4573
- noMatch: ['<div className="scroll-mt-2 scroll-mr-4 scroll-mb-8 scroll-ml-4 bg-red-200">box</div>']
4574
- }
4575
- });
4576
-
4577
- // ../patterns/src/library/compress/scroll-padding-shorthand.pattern.ts
4578
- var SCROLL_PADDING_SIDES = [
4579
- "scroll-padding-top",
4580
- "scroll-padding-right",
4581
- "scroll-padding-bottom",
4582
- "scroll-padding-left"
4583
- ];
4584
- var SIDE_SET2 = new Set(SCROLL_PADDING_SIDES);
4585
- var BASE_KEY10 = conditionKey(BASE_CONDITION);
4586
- var SCROLL_PADDING = "scroll-padding";
4587
- var NON_COLLAPSIBLE_VALUES4 = /* @__PURE__ */ new Set([
4588
- "initial",
4589
- "inherit",
4590
- "unset",
4591
- "revert",
4592
- "revert-layer"
4593
- ]);
4594
- function analyzeScrollPadding(sm) {
4595
- const block = sm.blocks.get(BASE_KEY10);
4596
- if (!block) return null;
4597
- const sides = [];
4598
- for (const side of SCROLL_PADDING_SIDES) {
4599
- const decl = block.decls.get(side);
4600
- if (!decl) return null;
4601
- sides.push(decl);
4602
- }
4603
- const important = sides[0].important;
4604
- if (!sides.every((d) => d.important === important)) return null;
4605
- const value = String(sides[0].value);
4606
- if (NON_COLLAPSIBLE_VALUES4.has(value)) return null;
4607
- if (!sides.every((d) => String(d.value) === value)) return null;
4608
- const relative2 = sides.some((d) => d.relativeToParent);
4609
- return { value, important, relative: relative2 };
4610
- }
4611
- function withFoldedScrollPadding(sm, fold) {
4612
- const blocks = /* @__PURE__ */ new Map();
4613
- for (const [key, block] of sm.blocks) {
4614
- if (key !== BASE_KEY10) {
4615
- blocks.set(key, block);
4616
- continue;
4617
- }
4618
- const decls = /* @__PURE__ */ new Map();
4619
- for (const [prop, decl] of block.decls) {
4620
- if (SIDE_SET2.has(String(prop))) continue;
4621
- decls.set(prop, decl);
4622
- }
4623
- const shorthand = {
4624
- property: SCROLL_PADDING,
4625
- value: fold.value,
4626
- important: fold.important,
4627
- relativeToParent: fold.relative,
4628
- inherited: false
4629
- // scroll-padding is never inherited
4630
- };
4631
- decls.set(shorthand.property, shorthand);
4632
- blocks.set(key, { condition: block.condition, decls });
4633
- }
4634
- return { blocks };
4635
- }
4636
- var scrollPaddingShorthand = definePattern({
4637
- name: "scroll-padding-shorthand",
4638
- category: "compress/scroll-padding-shorthand",
4639
- safety: 1,
4640
- doc: {
4641
- title: "Collapse equal scroll-padding sides into scroll-padding",
4642
- summary: "An element whose four scroll-padding sides are all equal is rewritten to the single Tailwind scroll-p-* utility (scroll-padding === the four equal sides).",
4643
- before: '<div class="scroll-pt-4 scroll-pr-4 scroll-pb-4 scroll-pl-4"/>',
4644
- after: '<div class="scroll-p-4"/>',
4645
- safetyRationale: "`scroll-padding` is value-identical to four equal scroll-padding sides \u2014 a class-only change. It is safe even on an element with a ref, event handler, dynamic child, or dangerouslySetInnerHTML \u2014 a className rewrite touches none of them; only a dynamic/opaque class list or a combinator-subject class is excluded, so no behaviour or project selector is disturbed."
4646
- },
4647
- rewrite: {
4648
- rewriteClasses(computed2) {
4649
- const fold = analyzeScrollPadding(computed2);
4650
- return fold ? withFoldedScrollPadding(computed2, fold) : null;
4651
- }
3575
+ paintsNothing: true,
3576
+ where: [
3577
+ not(establishesContext),
3578
+ not(hasOwnAttrs2),
3579
+ not(hasDynamicClasses),
3580
+ not(hasSpreadAttrs2),
3581
+ not(isComponentNode3),
3582
+ not(targetedByStructuralPseudo3)
3583
+ ]
4652
3584
  },
3585
+ rewrite: { flattenInto: "child" },
4653
3586
  test: {
4654
3587
  cases: [
4655
3588
  {
4656
- // The four equal scroll-padding longhands collapse to a `scroll-padding` decl at the IR level;
4657
- // the minimizing reverse-emit then picks the single shortest utility (`scroll-p-4`) that
4658
- // reproduces it, replacing the four `scroll-p{t,r,b,l}-4` tokens. `bg-red-200` is preserved.
4659
- before: '<div className="scroll-pt-4 scroll-pr-4 scroll-pb-4 scroll-pl-4 bg-red-200">box</div>',
4660
- after: '<div className="bg-red-200 scroll-p-4">box</div>'
3589
+ // A plain, style-free wrapper paints nothing and establishes no context a provably-safe
3590
+ // flatten under the conservative gate: the wrapper is removed and its sole child hoisted.
3591
+ before: '<div><a className="bg-red-200">Link</a></div>',
3592
+ after: '<a className="bg-red-200">Link</a>'
4661
3593
  }
4662
3594
  ],
4663
- // Sides differ (top != bottom) → no all-equal collapse.
4664
- noMatch: ['<div className="scroll-pt-2 scroll-pr-4 scroll-pb-8 scroll-pl-4 bg-red-200">box</div>']
3595
+ noMatch: [
3596
+ // A ref pins the wrapper's element identity (a hard opacity barrier) → not a passthrough.
3597
+ '<div ref={rootRef}><a className="bg-red-200">Link</a></div>',
3598
+ // A `display:flex` wrapper establishes a formatting context, so removing its box is NOT
3599
+ // provably layout-neutral → the conservative gate leaves it in place.
3600
+ '<div className="flex"><a className="bg-red-200">Link</a></div>'
3601
+ ]
4665
3602
  }
4666
3603
  });
4667
3604
 
4668
- // ../patterns/src/library/compress/size-shorthand.pattern.ts
4669
- var WIDTH = "width";
4670
- var HEIGHT = "height";
4671
- var SIZE = "size";
4672
- var NON_COLLAPSIBLE_VALUES5 = /* @__PURE__ */ new Set(["auto", "initial", "unset"]);
4673
- function baseBlock(sm) {
4674
- return sm.blocks.get(conditionKey(BASE_CONDITION));
4675
- }
4676
- function withSizeShorthand(sm, value, important) {
4677
- const baseKey = conditionKey(BASE_CONDITION);
4678
- const blocks = /* @__PURE__ */ new Map();
4679
- for (const [key, block] of sm.blocks) {
4680
- if (key !== baseKey) {
4681
- blocks.set(key, block);
4682
- continue;
4683
- }
4684
- const decls = new Map(block.decls);
4685
- decls.delete(WIDTH);
4686
- decls.delete(HEIGHT);
4687
- for (const decl of normalizer.normalizeDeclaration(String(SIZE), value, important)) {
4688
- decls.set(decl.property, decl);
4689
- }
4690
- blocks.set(key, { condition: block.condition, decls });
4691
- }
4692
- return { blocks };
3605
+ // ../patterns/src/library/wrapper/redundant-inline-wrapper.pattern.ts
3606
+ function asEl3(node) {
3607
+ const n = node;
3608
+ return n.kind === "element" ? n : null;
3609
+ }
3610
+ function metaOf3(node) {
3611
+ return asEl3(node)?.meta ?? null;
4693
3612
  }
4694
- var sizeShorthand = definePattern({
4695
- name: "size-shorthand",
4696
- category: "compress/size-shorthand",
3613
+ var establishesContext2 = (node) => {
3614
+ const m = metaOf3(node);
3615
+ if (!m) return false;
3616
+ return m.establishesBox || m.establishesFormattingContext || m.establishesStackingContext || m.isContainingBlock || m.declaresCustomProperties;
3617
+ };
3618
+ var hasSpreadAttrs3 = (node) => metaOf3(node)?.hasSpreadAttrs ?? false;
3619
+ var isComponentNode4 = (node) => metaOf3(node)?.isComponent ?? false;
3620
+ var hasOwnAttrs3 = (node) => {
3621
+ const el = asEl3(node);
3622
+ if (!el) return false;
3623
+ return el.attrs.entries.size > 0 || el.attrs.spreads.length > 0;
3624
+ };
3625
+ var targetedByStructuralPseudo4 = (node, ctx) => {
3626
+ const el = asEl3(node);
3627
+ if (!el) return false;
3628
+ if (el.meta.targetedByStructuralPseudo) return true;
3629
+ return ctx.selectors.targetedByStructuralPseudo(el.id);
3630
+ };
3631
+ var DISPLAY3 = "display";
3632
+ var hasNonInlineDisplay = (node, ctx) => {
3633
+ const el = asEl3(node);
3634
+ if (!el) return false;
3635
+ const sm = ctx.computedOf(el) ?? el.computed;
3636
+ for (const block of sm.blocks.values()) {
3637
+ const decl = block.decls.get(DISPLAY3);
3638
+ if (decl && String(decl.value) !== "inline") return true;
3639
+ }
3640
+ return false;
3641
+ };
3642
+ var redundantInlineWrapper = definePattern({
3643
+ name: "redundant-inline-wrapper",
3644
+ category: "flatten/wrapper/redundant-inline-wrapper",
4697
3645
  safety: 2,
4698
3646
  doc: {
4699
- title: "Collapse equal width/height into size-*",
4700
- summary: "An element whose computed width and height are equal is rewritten to the single Tailwind size-* utility (size-* === width + height at the same value).",
4701
- before: '<div style="width:1rem;height:1rem"/>',
4702
- after: '<div class="size-4"/>',
4703
- safetyRationale: "`size-*` is value-identical to equal width+height \u2014 a class-only change. It is safe even on an element with a ref, event handler, dynamic child, or dangerouslySetInnerHTML \u2014 a className rewrite touches none of them; only a dynamic/opaque class list or a combinator-subject class is excluded, so no behaviour or project selector is disturbed."
3647
+ title: "Flatten redundant inline wrapper",
3648
+ summary: "An inline span with no own visual/box style, no attributes beyond an inert class, exactly one element child, and no opacity barriers is removed; its sole child is hoisted in its place.",
3649
+ before: "<span><Child/></span>",
3650
+ after: "<Child/>",
3651
+ safetyRationale: "An empty inline box paints nothing and establishes no layout/paint/var context; with the inline default display and a single element child its removal changes no paint and no flow. The span carries no ref/handlers/dynamic-children/html/spread/component identity, owns no targetable attrs, and is not a combinator/structural-pseudo subject; inheritable styles are folded onto the child before removal."
4704
3652
  },
4705
- rewrite: {
4706
- rewriteClasses(computed2) {
4707
- const base = baseBlock(computed2);
4708
- const w = base?.decls.get(WIDTH);
4709
- const h = base?.decls.get(HEIGHT);
4710
- if (!w || !h) return null;
4711
- if (w.important !== h.important) return null;
4712
- if (NON_COLLAPSIBLE_VALUES5.has(String(w.value))) return null;
4713
- if (w.value !== h.value) return null;
4714
- return withSizeShorthand(computed2, String(w.value), w.important);
4715
- }
3653
+ match: {
3654
+ tag: "span",
3655
+ onlyChild: "element",
3656
+ paintsNothing: true,
3657
+ where: [
3658
+ not(hasNonInlineDisplay),
3659
+ not(establishesContext2),
3660
+ not(hasOwnAttrs3),
3661
+ not(hasDynamicClasses),
3662
+ not(hasSpreadAttrs3),
3663
+ not(isComponentNode4),
3664
+ not(targetedByStructuralPseudo4)
3665
+ ]
4716
3666
  },
3667
+ rewrite: { flattenInto: "child" },
4717
3668
  test: {
4718
3669
  cases: [
4719
3670
  {
4720
- // Equal width/height collapse to a `size` decl at the IR level; the minimizing reverse-emit
4721
- // expands `size` back to width+height, finds the single utility covering both (`size-10`), and
4722
- // replaces the `h-10`+`w-10` pair with it. `bg-red-200` is preserved.
4723
- before: '<div className="h-10 w-10 bg-red-200">box</div>',
4724
- after: '<div className="bg-red-200 size-10">box</div>'
3671
+ // An empty inline span paints nothing and establishes no context a provably-safe flatten:
3672
+ // the span is removed and its sole child hoisted in place.
3673
+ before: '<span><a className="text-blue-500">Link</a></span>',
3674
+ after: '<a className="text-blue-500">Link</a>'
4725
3675
  }
4726
3676
  ],
4727
- // Width and height differ → no equal-axis collapse.
4728
- noMatch: ['<div className="h-10 w-20 bg-red-200">box</div>']
3677
+ noMatch: [
3678
+ // A ref pins the span's element identity (a hard opacity barrier) → not a passthrough.
3679
+ '<span ref={spanRef}><a className="text-blue-500">Link</a></span>',
3680
+ // The span paints its own background (own visual style) → kept.
3681
+ '<span className="bg-green-200"><a className="text-blue-500">Link</a></span>',
3682
+ // Non-inline display (inline-block) participates in layout differently → kept.
3683
+ '<span className="inline-block"><a className="text-blue-500">Link</a></span>'
3684
+ ]
4729
3685
  }
4730
3686
  });
4731
3687
 
4732
3688
  // ../patterns/src/_registry.generated.ts
4733
3689
  var builtinPatterns = [
3690
+ flexCenterWrapper,
3691
+ redundantFragment,
3692
+ gridCenterWrapper,
4734
3693
  displayContentsWrapper,
4735
3694
  emptyStyleDiv,
4736
- flexCenterWrapper,
4737
- inlineFlexCenterWrapper,
4738
- nestedFlexMerge,
4739
- nestedGridMerge,
3695
+ inheritedOnlyWrapper,
4740
3696
  passthroughWrapper,
4741
- redundantFragment,
4742
- redundantInlineWrapper,
4743
- borderRadiusShorthand,
4744
- borderShorthand,
4745
- dedupeClasses,
4746
- gapShorthand,
4747
- insetShorthand,
4748
- marginShorthand,
4749
- overflowShorthand,
4750
- overscrollBehaviorShorthand,
4751
- paddingShorthand,
4752
- placeShorthand,
4753
- scrollMarginShorthand,
4754
- scrollPaddingShorthand,
4755
- sizeShorthand
3697
+ redundantInlineWrapper
4756
3698
  ];
4757
3699
 
4758
3700
  // ../resolver-css/src/constants.ts
@@ -4851,11 +3793,11 @@ var import_node_fs = require("fs");
4851
3793
  function isPlainClassToken(token) {
4852
3794
  return token.length > 0 && !/[\s.#>+~:[\]()]/.test(token);
4853
3795
  }
4854
- function readCssPath(path7) {
3796
+ function readCssPath(path9) {
4855
3797
  try {
4856
- return { id: path7, css: (0, import_node_fs.readFileSync)(path7, "utf8") };
3798
+ return { id: path9, css: (0, import_node_fs.readFileSync)(path9, "utf8") };
4857
3799
  } catch (cause) {
4858
- throw new Error(`resolver-css: cannot read CSS file "${path7}"`, { cause });
3800
+ throw new Error(`resolver-css: cannot read CSS file "${path9}"`, { cause });
4859
3801
  }
4860
3802
  }
4861
3803
  function deriveFingerprint(provider, files) {
@@ -4910,6 +3852,8 @@ var CustomCSSResolver = class {
4910
3852
  /** Distinct COMPLEX selectors (combinator or structural pseudo), sorted. */
4911
3853
  #complex;
4912
3854
  #reverse = null;
3855
+ /** Lazily built cover vocabulary (full condition-keyed tuple sets) for the exact-cover engine. */
3856
+ #coverVocab = null;
4913
3857
  constructor(cssFiles = [], options = {}) {
4914
3858
  ensurePostcss(options.projectRoot);
4915
3859
  const fromDisk = (options.files ?? []).map(readCssPath);
@@ -4942,8 +3886,23 @@ var CustomCSSResolver = class {
4942
3886
  }
4943
3887
  emit(styles, ctx) {
4944
3888
  const norm = ctx.normalizer ?? normalizer;
3889
+ const normalized = norm.normalizeStyleMap(styles);
3890
+ const universe = styleMapTuples(normalized, norm);
3891
+ if (universe.length === 0) return { classes: [], exact: true, warnings: [] };
3892
+ const chosen = minStringCover(universe, this.#buildCoverVocab());
3893
+ if (chosen && chosen.length > 0) {
3894
+ const reTuples = new Set(styleMapTuples(this.resolve({ classes: chosen }).styles, norm));
3895
+ let ok = reTuples.size === universe.length;
3896
+ if (ok) {
3897
+ for (const t of universe) if (!reTuples.has(t)) {
3898
+ ok = false;
3899
+ break;
3900
+ }
3901
+ }
3902
+ if (ok) return { classes: chosen, exact: true, warnings: [] };
3903
+ }
4945
3904
  const remaining = /* @__PURE__ */ new Map();
4946
- for (const [ck, block] of norm.normalizeStyleMap(styles).blocks) {
3905
+ for (const [ck, block] of normalized.blocks) {
4947
3906
  for (const [prop, decl] of block.decls) {
4948
3907
  remaining.set(`${ck} ${prop}`, String(decl.value));
4949
3908
  }
@@ -5153,7 +4112,23 @@ var CustomCSSResolver = class {
5153
4112
  if (rawBlocks.size === 0) return emptyStyleMap();
5154
4113
  return normalizer.normalizeStyleMap({ blocks: rawBlocks });
5155
4114
  }
5156
- /** Build (once) the reverse index used by {@link emit}. */
4115
+ /**
4116
+ * Build (once) the cover vocabulary for the exact-cover engine: every forward-resolvable class
4117
+ * mapped to the {@link styleMapTuples} of its full (condition-keyed, `!important`-aware) declaration
4118
+ * set. Unlike {@link #reverseIndex} this carries ALL style conditions and the important flag, so the
4119
+ * engine can pick a custom class covering hover/media declarations too.
4120
+ */
4121
+ #buildCoverVocab() {
4122
+ if (this.#coverVocab) return this.#coverVocab;
4123
+ const out = [];
4124
+ for (const token of this.#classIndex.keys()) {
4125
+ const tuples = styleMapTuples(this.#resolveTokens([token], [token]), normalizer);
4126
+ if (tuples.length > 0) out.push({ token, tuples });
4127
+ }
4128
+ this.#coverVocab = out;
4129
+ return out;
4130
+ }
4131
+ /** Build (once) the reverse index used by the greedy {@link emit} fallback. */
5157
4132
  #reverseIndex() {
5158
4133
  if (this.#reverse) return this.#reverse;
5159
4134
  const out = [];
@@ -5222,14 +4197,312 @@ function synthesizeResidual(remaining, ctx) {
5222
4197
 
5223
4198
  // ../resolver-tailwind/src/tailwind/engine.ts
5224
4199
  var import_node_module3 = require("module");
4200
+ var path5 = __toESM(require("path"), 1);
4201
+
4202
+ // ../resolver-tailwind/src/tailwind/engine-v4.ts
4203
+ var import_node_fs3 = require("fs");
4204
+ var path4 = __toESM(require("path"), 1);
4205
+
4206
+ // ../resolver-tailwind/src/tailwind/v4-bridge.ts
4207
+ var import_node_child_process2 = require("child_process");
4208
+ var import_node_fs2 = require("fs");
4209
+ var import_node_os = require("os");
5225
4210
  var path3 = __toESM(require("path"), 1);
4211
+ var CHILD_SOURCE = String.raw`
4212
+ import { createRequire } from 'node:module';
4213
+ import { pathToFileURL } from 'node:url';
4214
+ import * as fs from 'node:fs';
4215
+ import * as path from 'node:path';
4216
+
4217
+ function out(obj) { process.stdout.write(JSON.stringify(obj)); process.exit(0); }
4218
+
4219
+ let payload;
4220
+ try { payload = JSON.parse(fs.readFileSync(process.argv[2], 'utf8')); }
4221
+ catch { out({ ok: false }); }
4222
+
4223
+ const projectRoot = payload.projectRoot;
4224
+ const entries = payload.entries || [];
4225
+ const req = createRequire(path.join(projectRoot, '__domflax_tw4__.js'));
4226
+
4227
+ async function importFrom(id) {
4228
+ const resolved = req.resolve(id);
4229
+ return import(pathToFileURL(resolved).href);
4230
+ }
4231
+
4232
+ // Primary loader: @tailwindcss/node (the companion every v4 build tool installs). It resolves
4233
+ // '@import "tailwindcss"' and @theme against the project on disk.
4234
+ async function loadViaNode() {
4235
+ let mod;
4236
+ try { mod = await importFrom('@tailwindcss/node'); } catch { return null; }
4237
+ if (!mod || typeof mod.__unstable__loadDesignSystem !== 'function') return null;
4238
+ for (const e of entries) {
4239
+ try { return await mod.__unstable__loadDesignSystem(e.css, { base: e.base }); } catch {}
4240
+ }
4241
+ return null;
4242
+ }
4243
+
4244
+ // Secondary loader: bare 'tailwindcss' with a filesystem stylesheet resolver (best-effort).
4245
+ async function loadViaCore() {
4246
+ let tw;
4247
+ try { tw = await importFrom('tailwindcss'); } catch { return null; }
4248
+ if (!tw || typeof tw.__unstable__loadDesignSystem !== 'function') return null;
4249
+ const loadStylesheet = async (id, base) => {
4250
+ const r = createRequire(path.join(base, '__domflax_tw4__.js'));
4251
+ let p;
4252
+ const tries = id === 'tailwindcss' ? ['tailwindcss/index.css', 'tailwindcss'] : [id, id + '/index.css'];
4253
+ for (const t of tries) { try { p = r.resolve(t); break; } catch {} }
4254
+ if (!p) p = path.resolve(base, id);
4255
+ return { path: p, base: path.dirname(p), content: fs.readFileSync(p, 'utf8') };
4256
+ };
4257
+ const loadModule = async (id, base) => {
4258
+ const r = createRequire(path.join(base, '__domflax_tw4__.js'));
4259
+ const p = r.resolve(id);
4260
+ return { path: p, base: path.dirname(p), module: (await import(pathToFileURL(p).href)).default };
4261
+ };
4262
+ for (const e of entries) {
4263
+ try { return await tw.__unstable__loadDesignSystem(e.css, { base: e.base, loadStylesheet, loadModule }); } catch {}
4264
+ }
4265
+ return null;
4266
+ }
4267
+
4268
+ const ds = (await loadViaNode()) || (await loadViaCore());
4269
+ if (!ds) out({ ok: false });
4270
+
4271
+ let names = [];
4272
+ try {
4273
+ names = ds.getClassList().map((e) => (Array.isArray(e) ? e[0] : e)).filter((n) => typeof n === 'string');
4274
+ } catch { out({ ok: false }); }
4275
+
4276
+ let css = [];
4277
+ try { css = ds.candidatesToCss(names); } catch { out({ ok: false }); }
4278
+
4279
+ const result = [];
4280
+ for (let i = 0; i < names.length; i += 1) {
4281
+ const c = css[i];
4282
+ if (typeof c === 'string' && c.length > 0) result.push([names[i], c]);
4283
+ }
4284
+ out({ ok: true, entries: result });
4285
+ `;
4286
+ function runV4Bridge(payload) {
4287
+ let dir = null;
4288
+ try {
4289
+ dir = (0, import_node_fs2.mkdtempSync)(path3.join((0, import_node_os.tmpdir)(), "domflax-tw4-"));
4290
+ const scriptPath = path3.join(dir, "bridge.mjs");
4291
+ const payloadPath = path3.join(dir, "payload.json");
4292
+ (0, import_node_fs2.writeFileSync)(scriptPath, CHILD_SOURCE, "utf8");
4293
+ (0, import_node_fs2.writeFileSync)(payloadPath, JSON.stringify(payload), "utf8");
4294
+ const stdout = (0, import_node_child_process2.execFileSync)(process.execPath, [scriptPath, payloadPath], {
4295
+ cwd: payload.projectRoot,
4296
+ encoding: "utf8",
4297
+ timeout: 9e4,
4298
+ maxBuffer: 256 * 1024 * 1024,
4299
+ stdio: ["ignore", "pipe", "ignore"]
4300
+ });
4301
+ const parsed = JSON.parse(stdout);
4302
+ if (!parsed.ok || !Array.isArray(parsed.entries) || parsed.entries.length === 0) return null;
4303
+ const entries = parsed.entries.filter(
4304
+ (e) => Array.isArray(e) && typeof e[0] === "string" && typeof e[1] === "string"
4305
+ );
4306
+ return entries.length > 0 ? { entries } : null;
4307
+ } catch {
4308
+ return null;
4309
+ } finally {
4310
+ if (dir) {
4311
+ try {
4312
+ (0, import_node_fs2.rmSync)(dir, { recursive: true, force: true });
4313
+ } catch {
4314
+ }
4315
+ }
4316
+ }
4317
+ }
4318
+
4319
+ // ../resolver-tailwind/src/tailwind/v4-css.ts
4320
+ function stripComments(src) {
4321
+ return src.replace(/\/\*[\s\S]*?\*\//g, "");
4322
+ }
4323
+ function toDecl(buffer) {
4324
+ const buf = buffer.trim();
4325
+ if (buf.length === 0 || buf[0] === "@") return null;
4326
+ const colon = buf.indexOf(":");
4327
+ if (colon <= 0) return null;
4328
+ const prop = buf.slice(0, colon).trim();
4329
+ let value = buf.slice(colon + 1).trim();
4330
+ if (prop.length === 0 || value.length === 0) return null;
4331
+ let important = false;
4332
+ const bang = /!\s*important\s*$/i.exec(value);
4333
+ if (bang) {
4334
+ important = true;
4335
+ value = value.slice(0, bang.index).trim();
4336
+ }
4337
+ return { type: "decl", prop, value, important };
4338
+ }
4339
+ function splitAtRule(prelude) {
4340
+ const m = /^@([A-Za-z-]+)\s*([\s\S]*)$/.exec(prelude);
4341
+ if (!m) return { name: prelude.slice(1).trim(), params: "" };
4342
+ return { name: m[1].toLowerCase(), params: m[2].trim() };
4343
+ }
4344
+ function parseBlock(src, start) {
4345
+ const nodes = [];
4346
+ let buf = "";
4347
+ let i = start;
4348
+ while (i < src.length) {
4349
+ const c = src[i];
4350
+ if (c === "{") {
4351
+ const prelude = buf.trim();
4352
+ buf = "";
4353
+ const inner = parseBlock(src, i + 1);
4354
+ i = inner.next;
4355
+ if (prelude.startsWith("@")) {
4356
+ const { name, params } = splitAtRule(prelude);
4357
+ nodes.push({ type: "atrule", name, params, nodes: inner.nodes });
4358
+ } else if (prelude.length > 0) {
4359
+ nodes.push({ type: "rule", selector: prelude, nodes: inner.nodes });
4360
+ }
4361
+ } else if (c === "}") {
4362
+ const d = toDecl(buf);
4363
+ if (d) nodes.push(d);
4364
+ return { nodes, next: i + 1 };
4365
+ } else if (c === ";") {
4366
+ const d = toDecl(buf);
4367
+ if (d) nodes.push(d);
4368
+ buf = "";
4369
+ i += 1;
4370
+ } else {
4371
+ buf += c;
4372
+ i += 1;
4373
+ }
4374
+ }
4375
+ const tail = toDecl(buf);
4376
+ if (tail) nodes.push(tail);
4377
+ return { nodes, next: i };
4378
+ }
4379
+ function resolveNesting(child, parent) {
4380
+ const c = child.trim();
4381
+ if (parent.length === 0) return c;
4382
+ if (c.includes("&")) return c.split("&").join(parent);
4383
+ return `${parent} ${c}`;
4384
+ }
4385
+ var DROP_ATRULES = /* @__PURE__ */ new Set(["property", "keyframes", "font-face", "charset", "import"]);
4386
+ function flattenNodes(nodes, selector, at, out) {
4387
+ const own = [];
4388
+ for (const n of nodes) if (n.type === "decl") own.push(n);
4389
+ if (own.length > 0 && selector.length > 0) out.push({ selector, at: [...at], decls: own });
4390
+ for (const n of nodes) {
4391
+ if (n.type === "rule") {
4392
+ flattenNodes(n.nodes, resolveNesting(n.selector, selector), at, out);
4393
+ } else if (n.type === "atrule") {
4394
+ if (n.name === "media") {
4395
+ flattenNodes(n.nodes, selector, [...at, { name: "media", params: n.params }], out);
4396
+ } else if (n.name === "layer") {
4397
+ flattenNodes(n.nodes, selector, at, out);
4398
+ } else if (!DROP_ATRULES.has(n.name)) {
4399
+ flattenNodes(n.nodes, selector, [...at, { name: n.name, params: n.params }], out);
4400
+ }
4401
+ }
4402
+ }
4403
+ }
4404
+ function leafToNode(leaf) {
4405
+ const declNodes = leaf.decls.map((d) => ({
4406
+ type: "decl",
4407
+ prop: d.prop,
4408
+ value: d.value,
4409
+ important: d.important
4410
+ }));
4411
+ let node = { type: "rule", selector: leaf.selector, nodes: declNodes };
4412
+ for (let i = leaf.at.length - 1; i >= 0; i -= 1) {
4413
+ node = { type: "atrule", name: leaf.at[i].name, params: leaf.at[i].params, nodes: [node] };
4414
+ }
4415
+ return node;
4416
+ }
4417
+ function parseUtilityCss(css) {
4418
+ try {
4419
+ const { nodes } = parseBlock(stripComments(css), 0);
4420
+ const leaves = [];
4421
+ flattenNodes(nodes, "", [], leaves);
4422
+ return leaves.map(leafToNode);
4423
+ } catch {
4424
+ return [];
4425
+ }
4426
+ }
4427
+
4428
+ // ../resolver-tailwind/src/tailwind/engine-v4.ts
4429
+ var SEARCH_DIRS = ["", "src", "app", "styles", "src/styles", "src/app", "app/styles", "assets/css", "css"];
4430
+ var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "out", ".next", "coverage"]);
4431
+ var ENTRY_RE = /@import\s+["']tailwindcss["']|@tailwind\b|@theme\b/;
4432
+ function scanDir(dir) {
4433
+ let names;
4434
+ try {
4435
+ names = (0, import_node_fs3.readdirSync)(dir);
4436
+ } catch {
4437
+ return null;
4438
+ }
4439
+ for (const name of names) {
4440
+ if (!name.toLowerCase().endsWith(".css")) continue;
4441
+ const file = path4.join(dir, name);
4442
+ try {
4443
+ if (!(0, import_node_fs3.statSync)(file).isFile()) continue;
4444
+ const css = (0, import_node_fs3.readFileSync)(file, "utf8");
4445
+ if (ENTRY_RE.test(css)) return { css, base: path4.dirname(file) };
4446
+ } catch {
4447
+ }
4448
+ }
4449
+ return null;
4450
+ }
4451
+ function findCssEntries(projectRoot) {
4452
+ const out = [];
4453
+ const seen = /* @__PURE__ */ new Set();
4454
+ for (const rel of SEARCH_DIRS) {
4455
+ const dir = path4.resolve(projectRoot, rel);
4456
+ if (seen.has(dir) || [...SKIP_DIRS].some((s) => dir.includes(`${path4.sep}${s}`))) continue;
4457
+ seen.add(dir);
4458
+ const hit = scanDir(dir);
4459
+ if (hit) {
4460
+ out.push(hit);
4461
+ break;
4462
+ }
4463
+ }
4464
+ out.push({ css: '@import "tailwindcss";', base: projectRoot });
4465
+ return out;
4466
+ }
4467
+ function makeV4Engine(entries, version) {
4468
+ const cssByClass = new Map(entries.map(([name, css]) => [name, css]));
4469
+ const nodeCache = /* @__PURE__ */ new Map();
4470
+ const nodesFor = (token) => {
4471
+ let cached = nodeCache.get(token);
4472
+ if (!cached) {
4473
+ const css = cssByClass.get(token);
4474
+ cached = css ? parseUtilityCss(css) : [];
4475
+ nodeCache.set(token, cached);
4476
+ }
4477
+ return cached;
4478
+ };
4479
+ return {
4480
+ version,
4481
+ context: {
4482
+ // The resolver keeps only string entries; we hand it the concrete class names directly.
4483
+ getClassList: () => [...cssByClass.keys()]
4484
+ },
4485
+ generate(candidates) {
4486
+ const out = [];
4487
+ for (const c of candidates) for (const n of nodesFor(c)) out.push(n);
4488
+ return out;
4489
+ }
4490
+ };
4491
+ }
4492
+ function loadV4Engine(projectRoot, version) {
4493
+ const snapshot = runV4Bridge({ projectRoot, entries: findCssEntries(projectRoot) });
4494
+ if (!snapshot) return null;
4495
+ return makeV4Engine(snapshot.entries, version);
4496
+ }
4497
+
4498
+ // ../resolver-tailwind/src/tailwind/engine.ts
5226
4499
  function moduleBase2() {
5227
4500
  return typeof __filename === "string" ? __filename : importMetaUrl;
5228
4501
  }
5229
4502
  function projectRequire(projectRoot) {
5230
4503
  const bases = [];
5231
- if (projectRoot) bases.push(path3.join(projectRoot, "__domflax__.js"));
5232
- bases.push(path3.join(process.cwd(), "__domflax__.js"));
4504
+ if (projectRoot) bases.push(path5.join(projectRoot, "__domflax__.js"));
4505
+ bases.push(path5.join(process.cwd(), "__domflax__.js"));
5233
4506
  bases.push(moduleBase2());
5234
4507
  for (const base of bases) {
5235
4508
  try {
@@ -5241,14 +4514,36 @@ function projectRequire(projectRoot) {
5241
4514
  }
5242
4515
  return null;
5243
4516
  }
4517
+ var FIRST_UNSUPPORTED_MAJOR = 4;
4518
+ function majorOf(version) {
4519
+ const m = /^\s*(\d+)/.exec(version);
4520
+ return m ? Number(m[1]) : null;
4521
+ }
5244
4522
  function loadEngine(options) {
5245
4523
  const req = projectRequire(options.projectRoot);
5246
- if (!req) return null;
4524
+ if (!req) return { engine: null, version: null, unsupportedMajor: null };
4525
+ let version = null;
4526
+ try {
4527
+ version = req("tailwindcss/package.json").version;
4528
+ } catch {
4529
+ return { engine: null, version: null, unsupportedMajor: null };
4530
+ }
4531
+ const major = majorOf(version);
4532
+ if (major !== null && major >= FIRST_UNSUPPORTED_MAJOR) {
4533
+ const projectRoot = options.projectRoot ?? process.cwd();
4534
+ let v4 = null;
4535
+ try {
4536
+ v4 = loadV4Engine(projectRoot, version);
4537
+ } catch {
4538
+ v4 = null;
4539
+ }
4540
+ if (v4) return { engine: v4, version, unsupportedMajor: null };
4541
+ return { engine: null, version, unsupportedMajor: major };
4542
+ }
5247
4543
  try {
5248
4544
  const resolveConfig = req("tailwindcss/resolveConfig.js");
5249
4545
  const { createContext } = req("tailwindcss/lib/lib/setupContextUtils.js");
5250
4546
  const { generateRules } = req("tailwindcss/lib/lib/generateRules.js");
5251
- const pkg = req("tailwindcss/package.json");
5252
4547
  let userConfig = options.config ?? { content: [{ raw: "" }] };
5253
4548
  if (options.configPath !== void 0) {
5254
4549
  const loadConfig = req("tailwindcss/loadConfig.js");
@@ -5257,15 +4552,19 @@ function loadEngine(options) {
5257
4552
  const resolved = resolveConfig(userConfig);
5258
4553
  const context = createContext(resolved);
5259
4554
  return {
5260
- version: pkg.version,
5261
- context,
5262
- generate(candidates) {
5263
- const rules = generateRules(new Set(candidates), context);
5264
- return rules.map(([, node]) => node);
5265
- }
4555
+ engine: {
4556
+ version,
4557
+ context,
4558
+ generate(candidates) {
4559
+ const rules = generateRules(new Set(candidates), context);
4560
+ return rules.map(([, node]) => node);
4561
+ }
4562
+ },
4563
+ version,
4564
+ unsupportedMajor: null
5266
4565
  };
5267
4566
  } catch {
5268
- return null;
4567
+ return { engine: null, version, unsupportedMajor: null };
5269
4568
  }
5270
4569
  }
5271
4570
 
@@ -5444,22 +4743,39 @@ var DROPPABLE_USAGE = {
5444
4743
  };
5445
4744
 
5446
4745
  // ../resolver-tailwind/src/tailwind/resolver.ts
4746
+ var warnedUnsupported = /* @__PURE__ */ new Set();
5447
4747
  var TailwindResolver = class {
5448
4748
  id = "tailwind";
5449
4749
  provider;
5450
4750
  fingerprint;
4751
+ /**
4752
+ * SAFETY (Layer 1): the detected Tailwind MAJOR when the project's version is one this resolver
4753
+ * cannot drive (v4+), else `null`. When set, {@link resolve} reports every token as unknown, so
4754
+ * downstream files are left unchanged (never mis-optimized). Exposed for diagnostics/tests.
4755
+ */
4756
+ unsupportedMajor;
5451
4757
  #engine;
5452
4758
  /** Per-token extraction cache (engine output is pure for a fixed config). */
5453
4759
  #tokenCache = /* @__PURE__ */ new Map();
5454
4760
  /** Per-class-set forward-resolution cache. */
5455
4761
  #resolveCache = /* @__PURE__ */ new Map();
5456
- /** Lazily built reverse index for {@link emit}. */
4762
+ /** Lazily built reverse index for the greedy {@link emit} fallback. */
5457
4763
  #reverseIndex = null;
4764
+ /** Lazily built cover vocabulary (base-condition tuple sets) for the exact-cover engine. */
4765
+ #coverVocab = null;
5458
4766
  constructor(config = {}) {
5459
- this.#engine = loadEngine(config);
5460
- this.provider = config.provider ?? (this.#engine ? `tailwindcss@${this.#engine.version}` : "tailwindcss");
4767
+ const loaded = loadEngine(config);
4768
+ this.#engine = loaded.engine;
4769
+ this.unsupportedMajor = loaded.unsupportedMajor;
4770
+ this.provider = config.provider ?? (loaded.version ? `tailwindcss@${loaded.version}` : "tailwindcss");
5461
4771
  const seed = JSON.stringify(config.config ?? {}) + (config.configPath ?? "");
5462
4772
  this.fingerprint = config.fingerprint ?? `${this.provider}/${fnv1a(seed)}`;
4773
+ if (this.unsupportedMajor !== null && !warnedUnsupported.has(this.provider)) {
4774
+ warnedUnsupported.add(this.provider);
4775
+ console.warn(
4776
+ `domflax: detected Tailwind v${this.unsupportedMajor} (${this.provider}) but could not load its design system (is @tailwindcss/node installed?); classes cannot be resolved, so files are left unchanged to avoid unsafe edits.`
4777
+ );
4778
+ }
5463
4779
  }
5464
4780
  /** Engine-backed, cached single-token extraction. */
5465
4781
  #extract(token) {
@@ -5568,9 +4884,75 @@ var TailwindResolver = class {
5568
4884
  this.#reverseIndex = index;
5569
4885
  return index;
5570
4886
  }
4887
+ /**
4888
+ * The cover vocabulary: every base-condition, plain-subject utility mapped to the {@link tupleKey}s
4889
+ * of its full normalized-longhand declaration set. Built once from a SINGLE engine `generate` over
4890
+ * the enumerable class list (grouped by selector), so it is the same cost as {@link #buildReverseIndex}.
4891
+ * This is what the provider-uniform exact-cover engine searches; the element's own droppable tokens
4892
+ * are members of it, guaranteeing feasibility. Variant / combinator / pseudo utilities are excluded
4893
+ * (their effect is not the element's own base box), so a target carrying such conditions simply finds
4894
+ * no cover and falls back to the greedy emit.
4895
+ */
4896
+ #buildCoverVocab() {
4897
+ if (this.#coverVocab) return this.#coverVocab;
4898
+ const baseCk = String(conditionKey(BASE_CONDITION));
4899
+ const out = [];
4900
+ if (this.#engine) {
4901
+ try {
4902
+ const classes = this.#engine.context.getClassList().filter((c) => typeof c === "string");
4903
+ const nodes = this.#engine.generate(classes);
4904
+ for (const node of nodes) {
4905
+ if (node.type !== "rule") continue;
4906
+ const rule = node;
4907
+ const parsed = parseSelector(rule.selector);
4908
+ if (parsed.kind !== "simple" || parsed.states.length > 0 || parsed.pseudoElement !== "") {
4909
+ continue;
4910
+ }
4911
+ const className = unescapeClass(rule.selector);
4912
+ if (className === null) continue;
4913
+ const tuples = [];
4914
+ const seen = /* @__PURE__ */ new Set();
4915
+ for (const child of rule.nodes ?? []) {
4916
+ if (child.type !== "decl") continue;
4917
+ const d = child;
4918
+ if (typeof d.value !== "string") continue;
4919
+ for (const decl of normalizer.normalizeDeclaration(d.prop, d.value, d.important === true)) {
4920
+ const k = tupleKey(baseCk, String(decl.property), String(decl.value), decl.important);
4921
+ if (!seen.has(k)) {
4922
+ seen.add(k);
4923
+ tuples.push(k);
4924
+ }
4925
+ }
4926
+ }
4927
+ if (tuples.length > 0) out.push({ token: className, tuples });
4928
+ }
4929
+ } catch {
4930
+ }
4931
+ }
4932
+ this.#coverVocab = out;
4933
+ return out;
4934
+ }
4935
+ /**
4936
+ * Try the minimal-string exact-cover engine over the WHOLE utility vocabulary. On success the chosen
4937
+ * set is verified by the mandatory CORRECTNESS BACKSTOP — re-resolve it and assert it reproduces the
4938
+ * target's tuples EXACTLY — before it is returned; any mismatch (or no cover / oversize universe)
4939
+ * yields `null` so {@link emit} uses its greedy fallback. Never returns a set that misrepresents `U`.
4940
+ */
4941
+ #tryCover(normalized, norm) {
4942
+ const universe = styleMapTuples(normalized, norm);
4943
+ if (universe.length === 0) return { classes: [], exact: true, warnings: [] };
4944
+ const chosen = minStringCover(universe, this.#buildCoverVocab());
4945
+ if (!chosen || chosen.length === 0) return null;
4946
+ const reTuples = new Set(styleMapTuples(this.resolve({ classes: chosen }).styles, norm));
4947
+ if (reTuples.size !== universe.length) return null;
4948
+ for (const t of universe) if (!reTuples.has(t)) return null;
4949
+ return { classes: chosen, exact: true, warnings: [] };
4950
+ }
5571
4951
  emit(styles, ctx) {
5572
4952
  const norm = ctx.normalizer ?? normalizer;
5573
4953
  const normalized = norm.normalizeStyleMap(styles);
4954
+ const cover = this.#tryCover(normalized, norm);
4955
+ if (cover) return cover;
5574
4956
  const base = normalized.blocks.get(conditionKey(BASE_CONDITION));
5575
4957
  if (!base || base.decls.size === 0) return { classes: [], exact: true, warnings: [] };
5576
4958
  const hasNonBase = normalized.blocks.size > 1;
@@ -5600,13 +4982,13 @@ var TailwindResolver = class {
5600
4982
  let bestCover = 0;
5601
4983
  for (const entry of candidates) {
5602
4984
  const [token, declMap] = entry;
5603
- let cover = 0;
5604
- for (const prop of declMap.keys()) if (remaining.has(prop)) cover += 1;
5605
- if (cover === 0) continue;
5606
- const better = best === null || cover > bestCover || cover === bestCover && declMap.size < best[1].size || cover === bestCover && declMap.size === best[1].size && token < best[0];
4985
+ let cover2 = 0;
4986
+ for (const prop of declMap.keys()) if (remaining.has(prop)) cover2 += 1;
4987
+ if (cover2 === 0) continue;
4988
+ const better = best === null || cover2 > bestCover || cover2 === bestCover && declMap.size < best[1].size || cover2 === bestCover && declMap.size === best[1].size && token < best[0];
5607
4989
  if (better) {
5608
4990
  best = entry;
5609
- bestCover = cover;
4991
+ bestCover = cover2;
5610
4992
  }
5611
4993
  }
5612
4994
  if (!best) break;
@@ -5646,12 +5028,12 @@ function createTailwindResolver(config) {
5646
5028
  }
5647
5029
 
5648
5030
  // ../cli/src/transform.ts
5649
- var path5 = __toESM(require("path"), 1);
5031
+ var path7 = __toESM(require("path"), 1);
5650
5032
 
5651
5033
  // ../cli/src/html-css.ts
5652
5034
  var import_node_crypto = require("crypto");
5653
- var import_node_fs2 = require("fs");
5654
- var path4 = __toESM(require("path"), 1);
5035
+ var import_node_fs4 = require("fs");
5036
+ var path6 = __toESM(require("path"), 1);
5655
5037
  function isRemoteHref(href) {
5656
5038
  const h = href.trim();
5657
5039
  return h.startsWith("//") || /^[a-z][a-z0-9+.-]*:/i.test(h);
@@ -5688,16 +5070,16 @@ function extractInlineStyles(html) {
5688
5070
  return out;
5689
5071
  }
5690
5072
  function extractHtmlStylesheets(htmlCode, htmlAbsPath) {
5691
- const dir = path4.dirname(path4.resolve(htmlAbsPath));
5073
+ const dir = path6.dirname(path6.resolve(htmlAbsPath));
5692
5074
  const files = [];
5693
5075
  const seen = /* @__PURE__ */ new Set();
5694
5076
  for (const rawHref of extractLinkHrefs(htmlCode)) {
5695
5077
  const href = (rawHref.split(/[?#]/, 1)[0] ?? "").trim();
5696
5078
  if (!href || isRemoteHref(href)) continue;
5697
- const abs = path4.resolve(dir, href);
5079
+ const abs = path6.resolve(dir, href);
5698
5080
  if (seen.has(abs)) continue;
5699
5081
  seen.add(abs);
5700
- if ((0, import_node_fs2.existsSync)(abs)) files.push(abs);
5082
+ if ((0, import_node_fs4.existsSync)(abs)) files.push(abs);
5701
5083
  }
5702
5084
  return { files, inline: extractInlineStyles(htmlCode) };
5703
5085
  }
@@ -5792,7 +5174,7 @@ function createTransform(options) {
5792
5174
  if (options.provider !== "custom" || htmlKindOf(id) === null) return globalResolver;
5793
5175
  const { files: localFiles, inline } = extractHtmlStylesheets(code, id);
5794
5176
  if (localFiles.length === 0 && inline.length === 0) return globalResolver;
5795
- const globalPaths = options.css.map((p) => path5.resolve(p));
5177
+ const globalPaths = options.css.map((p) => path7.resolve(p));
5796
5178
  const sortedPaths = [.../* @__PURE__ */ new Set([...globalPaths, ...localFiles])].sort();
5797
5179
  const key = cssSetKey(sortedPaths, inline);
5798
5180
  let resolver = resolverCache.get(key);
@@ -5910,14 +5292,14 @@ function runWorker() {
5910
5292
  const transform = createTransform(init.options);
5911
5293
  const processOne = (file) => {
5912
5294
  try {
5913
- const code = (0, import_node_fs3.readFileSync)(file, "utf8");
5295
+ const code = (0, import_node_fs5.readFileSync)(file, "utf8");
5914
5296
  const result = transform.transformFile(code, file);
5915
5297
  let wrote = null;
5916
5298
  if (result.changed) {
5917
5299
  const target = destinationFor(file, inputRoot, plan);
5918
5300
  if (!target.ok) throw new Error(target.error);
5919
- (0, import_node_fs3.mkdirSync)(path6.dirname(target.value), { recursive: true });
5920
- (0, import_node_fs3.writeFileSync)(target.value, result.code, "utf8");
5301
+ (0, import_node_fs5.mkdirSync)(path8.dirname(target.value), { recursive: true });
5302
+ (0, import_node_fs5.writeFileSync)(target.value, result.code, "utf8");
5921
5303
  wrote = target.value;
5922
5304
  }
5923
5305
  port.postMessage({