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/index.cjs CHANGED
@@ -33,11 +33,10 @@ __export(src_exports, {
33
33
  BASE_CONDITION: () => BASE_CONDITION,
34
34
  BASE_CONDITION_KEY: () => BASE_CONDITION_KEY,
35
35
  DEFAULT_FIXPOINT: () => DEFAULT_FIXPOINT,
36
+ DEFAULT_MAX_UNIVERSE: () => DEFAULT_MAX_UNIVERSE,
36
37
  PHASE_ORDER: () => PHASE_ORDER,
37
38
  applyGroups: () => applyGroups,
38
39
  applyOps: () => applyOps,
39
- borderRadiusShorthand: () => borderRadiusShorthand,
40
- borderShorthand: () => borderShorthand,
41
40
  buildMatchContext: () => buildMatchContext,
42
41
  buildSelectorIndex: () => buildSelectorIndex,
43
42
  builtinPatterns: () => builtinPatterns,
@@ -61,7 +60,6 @@ __export(src_exports, {
61
60
  createRewriteFactory: () => createRewriteFactory,
62
61
  createSyntheticSink: () => createSyntheticSink,
63
62
  createText: () => createText,
64
- dedupeClasses: () => dedupeClasses,
65
63
  default: () => src_default,
66
64
  defaultMeta: () => defaultMeta,
67
65
  displayContentsWrapper: () => displayContentsWrapper,
@@ -77,29 +75,21 @@ __export(src_exports, {
77
75
  flattenVerdict: () => flattenVerdict,
78
76
  flattenWouldDropStyle: () => flattenWouldDropStyle,
79
77
  flexCenterWrapper: () => flexCenterWrapper,
80
- gapShorthand: () => gapShorthand,
81
78
  getElement: () => getElement,
82
79
  getNode: () => getNode,
83
- inlineFlexCenterWrapper: () => inlineFlexCenterWrapper,
84
- insetShorthand: () => insetShorthand,
85
- marginShorthand: () => marginShorthand,
86
- nestedFlexMerge: () => nestedFlexMerge,
87
- nestedGridMerge: () => nestedGridMerge,
88
- overflowShorthand: () => overflowShorthand,
89
- overscrollBehaviorShorthand: () => overscrollBehaviorShorthand,
90
- paddingShorthand: () => paddingShorthand,
80
+ gridCenterWrapper: () => gridCenterWrapper,
81
+ inheritedOnlyWrapper: () => inheritedOnlyWrapper,
82
+ minStringCover: () => minStringCover,
91
83
  passthroughWrapper: () => passthroughWrapper,
92
84
  patternsForPhase: () => patternsForPhase,
93
- placeShorthand: () => placeShorthand,
94
85
  redundantFragment: () => redundantFragment,
95
86
  redundantInlineWrapper: () => redundantInlineWrapper,
96
87
  revertDiagnostic: () => revertDiagnostic,
97
88
  runPasses: () => runPasses,
98
- scrollMarginShorthand: () => scrollMarginShorthand,
99
- scrollPaddingShorthand: () => scrollPaddingShorthand,
100
- sizeShorthand: () => sizeShorthand,
101
89
  stampOrigin: () => stampOrigin,
90
+ styleMapTuples: () => styleMapTuples,
102
91
  syncClassesFromComputed: () => syncClassesFromComputed,
92
+ tupleKey: () => tupleKey,
103
93
  vite: () => vite,
104
94
  walk: () => walk,
105
95
  webpack: () => webpack
@@ -168,6 +158,7 @@ function defaultMeta(safetyFloor = 0) {
168
158
  hasDynamicChildren: false,
169
159
  isComponent: false,
170
160
  hasDangerousHtml: false,
161
+ hasUnresolvedClasses: false,
171
162
  targetedByCombinator: false,
172
163
  targetedByStructuralPseudo: false,
173
164
  selectorDependents: 0,
@@ -1311,6 +1302,9 @@ function classifyFlattenOps(before, after, ops, norm) {
1311
1302
  if (!wrapper || wrapper.kind !== "element") {
1312
1303
  return { kind: "provably-safe", wrapperId: null, childId: null };
1313
1304
  }
1305
+ if (wrapper.meta.hasUnresolvedClasses) {
1306
+ return { kind: "needs-verification", wrapperId, childId: survivingChildOf(ops, wrapper, before) };
1307
+ }
1314
1308
  const childId = survivingChildOf(ops, wrapper, before);
1315
1309
  const wrapperComputed = norm.normalizeStyleMap(wrapper.computed);
1316
1310
  const childAfter = childId != null ? getElement(after, childId)?.computed ?? null : null;
@@ -1768,15 +1762,26 @@ function residualStyle(computed2, covered, norm) {
1768
1762
  }
1769
1763
  return { blocks };
1770
1764
  }
1765
+ function joinedLength(tokens) {
1766
+ if (tokens.length === 0) return 0;
1767
+ let len = tokens.length - 1;
1768
+ for (const t of tokens) len += t.length;
1769
+ return len;
1770
+ }
1771
+ var COMPRESS_FLOOR = 1;
1771
1772
  function syncClassesFromComputed(doc, resolver, norm) {
1772
1773
  const sink = createSyntheticSink();
1774
+ const isDroppable = (t) => resolver.owns(t) && resolver.selectorUsage(t).droppable;
1773
1775
  for (const id of elementIds(doc)) {
1774
1776
  const el = getElement(doc, id);
1775
1777
  if (!el) continue;
1776
- if (!el.meta.styleDirty) continue;
1777
1778
  if (el.classes.opaque || el.classes.hasDynamic) continue;
1779
+ const compressOnly = !el.meta.styleDirty;
1780
+ if (compressOnly && el.meta.safetyFloor < COMPRESS_FLOOR) continue;
1778
1781
  const tokens = staticTokensOf(el.classes);
1779
- const retained = tokens.filter((t) => !resolver.selectorUsage(t).droppable);
1782
+ if (tokens.length === 0) continue;
1783
+ const retained = tokens.filter((t) => !isDroppable(t));
1784
+ if (compressOnly && retained.length === tokens.length) continue;
1780
1785
  const covered = retained.length > 0 ? resolver.resolve({ classes: retained }).styles : null;
1781
1786
  const target = covered ? residualStyle(el.computed, covered, norm) : el.computed;
1782
1787
  const ctx = { normalizer: norm, sink };
@@ -1787,7 +1792,7 @@ function syncClassesFromComputed(doc, resolver, norm) {
1787
1792
  const seen = /* @__PURE__ */ new Set();
1788
1793
  for (const t of tokens) {
1789
1794
  if (seen.has(t)) continue;
1790
- const keep = emittedSet.has(t) || !resolver.selectorUsage(t).droppable;
1795
+ const keep = emittedSet.has(t) || !isDroppable(t);
1791
1796
  if (keep) {
1792
1797
  next.push(t);
1793
1798
  seen.add(t);
@@ -1799,10 +1804,98 @@ function syncClassesFromComputed(doc, resolver, norm) {
1799
1804
  seen.add(c);
1800
1805
  }
1801
1806
  if (sameTokens(next, tokens)) continue;
1807
+ if (compressOnly) {
1808
+ if (!norm.equals(resolver.resolve({ classes: next }).styles, el.computed)) continue;
1809
+ if (joinedLength(next) > joinedLength(tokens)) continue;
1810
+ }
1802
1811
  el.classes = staticClassList(el.classes, next);
1803
1812
  }
1804
1813
  }
1805
1814
 
1815
+ // ../core/src/compress-engine.ts
1816
+ var SEP = "";
1817
+ function tupleKey(condition, property, value, important) {
1818
+ return `${condition}${SEP}${property}${SEP}${value}${SEP}${important ? "1" : "0"}`;
1819
+ }
1820
+ function styleMapTuples(map, norm) {
1821
+ const out = [];
1822
+ const normalized = norm.normalizeStyleMap(map);
1823
+ for (const [ck, block] of normalized.blocks) {
1824
+ for (const [prop, decl] of block.decls) {
1825
+ out.push(tupleKey(String(ck), String(prop), String(decl.value), decl.important));
1826
+ }
1827
+ }
1828
+ return out;
1829
+ }
1830
+ var DEFAULT_MAX_UNIVERSE = 20;
1831
+ function minStringCover(universe, vocabulary, options = {}) {
1832
+ const uniq = [...new Set(universe)];
1833
+ if (uniq.length === 0) return [];
1834
+ const n = uniq.length;
1835
+ const max = options.maxUniverse ?? DEFAULT_MAX_UNIVERSE;
1836
+ if (n > max) return null;
1837
+ const bitOf = /* @__PURE__ */ new Map();
1838
+ uniq.forEach((t, i) => bitOf.set(t, i));
1839
+ const byMask = /* @__PURE__ */ new Map();
1840
+ for (const entry of vocabulary) {
1841
+ if (entry.tuples.length === 0) continue;
1842
+ let mask = 0;
1843
+ let ok = true;
1844
+ for (const t of entry.tuples) {
1845
+ const b = bitOf.get(t);
1846
+ if (b === void 0) {
1847
+ ok = false;
1848
+ break;
1849
+ }
1850
+ mask |= 1 << b;
1851
+ }
1852
+ if (!ok || mask === 0) continue;
1853
+ const cost = entry.token.length + 1;
1854
+ const prev = byMask.get(mask);
1855
+ if (!prev || cost < prev.cost || cost === prev.cost && entry.token < prev.token) {
1856
+ byMask.set(mask, { token: entry.token, mask, cost });
1857
+ }
1858
+ }
1859
+ const cands = [...byMask.values()];
1860
+ if (cands.length === 0) return null;
1861
+ const full = (1 << n) - 1;
1862
+ const byBit = Array.from({ length: n }, () => []);
1863
+ cands.forEach((c, ci) => {
1864
+ for (let b = 0; b < n; b += 1) if (c.mask & 1 << b) byBit[b].push(ci);
1865
+ });
1866
+ const size = full + 1;
1867
+ const dp = new Float64Array(size).fill(Infinity);
1868
+ const fromCand = new Int32Array(size).fill(-1);
1869
+ const fromMask = new Int32Array(size).fill(-1);
1870
+ dp[0] = 0;
1871
+ for (let mask = 0; mask < full; mask += 1) {
1872
+ const cur = dp[mask];
1873
+ if (!Number.isFinite(cur)) continue;
1874
+ let b = 0;
1875
+ while (b < n && mask & 1 << b) b += 1;
1876
+ for (const ci of byBit[b]) {
1877
+ const c = cands[ci];
1878
+ const nm = mask | c.mask;
1879
+ const cost = cur + c.cost;
1880
+ if (cost < dp[nm]) {
1881
+ dp[nm] = cost;
1882
+ fromCand[nm] = ci;
1883
+ fromMask[nm] = mask;
1884
+ }
1885
+ }
1886
+ }
1887
+ if (!Number.isFinite(dp[full])) return null;
1888
+ const chosen = [];
1889
+ let m = full;
1890
+ while (m !== 0) {
1891
+ const ci = fromCand[m];
1892
+ if (ci < 0) return null;
1893
+ chosen.push(cands[ci].token);
1894
+ m = fromMask[m];
1895
+ }
1896
+ return [...new Set(chosen)].sort();
1897
+ }
1898
+
1806
1899
  // ../pattern-kit/src/normalize.ts
1807
1900
  var INHERITED_PROPERTIES = [
1808
1901
  "azimuth",
@@ -1887,6 +1980,18 @@ var BOX_SIDES = {
1887
1980
  padding: ["padding-top", "padding-right", "padding-bottom", "padding-left"],
1888
1981
  margin: ["margin-top", "margin-right", "margin-bottom", "margin-left"],
1889
1982
  inset: ["top", "right", "bottom", "left"],
1983
+ "scroll-margin": [
1984
+ "scroll-margin-top",
1985
+ "scroll-margin-right",
1986
+ "scroll-margin-bottom",
1987
+ "scroll-margin-left"
1988
+ ],
1989
+ "scroll-padding": [
1990
+ "scroll-padding-top",
1991
+ "scroll-padding-right",
1992
+ "scroll-padding-bottom",
1993
+ "scroll-padding-left"
1994
+ ],
1890
1995
  "border-width": [
1891
1996
  "border-top-width",
1892
1997
  "border-right-width",
@@ -1904,8 +2009,35 @@ var BOX_SIDES = {
1904
2009
  "border-right-color",
1905
2010
  "border-bottom-color",
1906
2011
  "border-left-color"
2012
+ ],
2013
+ // `border-radius` 1–4 value form maps to the four CORNERS (TL, TR, BR, BL) — the same positional
2014
+ // pattern boxFourSides implements. Only the slash-free form is expanded (see expandShorthand).
2015
+ "border-radius": [
2016
+ "border-top-left-radius",
2017
+ "border-top-right-radius",
2018
+ "border-bottom-right-radius",
2019
+ "border-bottom-left-radius"
1907
2020
  ]
1908
2021
  };
2022
+ var AXIS_PAIRS = {
2023
+ overflow: ["overflow-x", "overflow-y"],
2024
+ "overscroll-behavior": ["overscroll-behavior-x", "overscroll-behavior-y"],
2025
+ "place-items": ["align-items", "justify-items"],
2026
+ "place-content": ["align-content", "justify-content"],
2027
+ "place-self": ["align-self", "justify-self"]
2028
+ };
2029
+ var LOGICAL_PAIRS = {
2030
+ "padding-inline": ["padding-left", "padding-right"],
2031
+ "padding-block": ["padding-top", "padding-bottom"],
2032
+ "margin-inline": ["margin-left", "margin-right"],
2033
+ "margin-block": ["margin-top", "margin-bottom"],
2034
+ "inset-inline": ["left", "right"],
2035
+ "inset-block": ["top", "bottom"],
2036
+ "scroll-padding-inline": ["scroll-padding-left", "scroll-padding-right"],
2037
+ "scroll-padding-block": ["scroll-padding-top", "scroll-padding-bottom"],
2038
+ "scroll-margin-inline": ["scroll-margin-left", "scroll-margin-right"],
2039
+ "scroll-margin-block": ["scroll-margin-top", "scroll-margin-bottom"]
2040
+ };
1909
2041
  function splitTopLevel(value) {
1910
2042
  const out = [];
1911
2043
  let depth = 0;
@@ -1939,6 +2071,7 @@ function boxFourSides(values) {
1939
2071
  }
1940
2072
  }
1941
2073
  function expandShorthand(prop, value) {
2074
+ if (prop === "border-radius" && value.includes("/")) return [[prop, value]];
1942
2075
  const box = BOX_SIDES[prop];
1943
2076
  if (box) {
1944
2077
  const parts = splitTopLevel(value);
@@ -1948,6 +2081,19 @@ function expandShorthand(prop, value) {
1948
2081
  }
1949
2082
  return [[prop, value]];
1950
2083
  }
2084
+ const axis = AXIS_PAIRS[prop];
2085
+ if (axis) {
2086
+ const parts = splitTopLevel(value);
2087
+ if (parts.length === 1) return [[axis[0], parts[0]], [axis[1], parts[0]]];
2088
+ if (parts.length === 2) return [[axis[0], parts[0]], [axis[1], parts[1]]];
2089
+ return [[prop, value]];
2090
+ }
2091
+ const logical = LOGICAL_PAIRS[prop];
2092
+ if (logical) {
2093
+ const parts = splitTopLevel(value);
2094
+ if (parts.length === 1) return [[logical[0], parts[0]], [logical[1], parts[0]]];
2095
+ return [[prop, value]];
2096
+ }
1951
2097
  if (prop === "gap" || prop === "grid-gap") {
1952
2098
  const parts = splitTopLevel(value);
1953
2099
  if (parts.length === 1) {
@@ -2105,7 +2251,12 @@ var VISUAL_PROPERTIES = /* @__PURE__ */ new Set([
2105
2251
  "border-right-color",
2106
2252
  "border-bottom-color",
2107
2253
  "border-left-color",
2108
- "border-radius",
2254
+ // `border-radius` is expanded to its four corner longhands by the shared normalizer, so the
2255
+ // paint-establishing check must match those (a rounded wrapper still clips its background).
2256
+ "border-top-left-radius",
2257
+ "border-top-right-radius",
2258
+ "border-bottom-right-radius",
2259
+ "border-bottom-left-radius",
2109
2260
  "box-shadow",
2110
2261
  "outline",
2111
2262
  "outline-width",
@@ -2131,6 +2282,7 @@ var hasOwnVisualStyle = (node, ctx) => {
2131
2282
  const el = asElement(node);
2132
2283
  if (!el) return false;
2133
2284
  if (el.meta.hasOwnVisualStyle) return true;
2285
+ if (el.meta.hasUnresolvedClasses) return true;
2134
2286
  const computedMap = ctx.computedOf(el) ?? el.computed;
2135
2287
  const norm = normalizer.normalizeStyleMap(computedMap);
2136
2288
  for (const block of norm.blocks.values()) {
@@ -2335,7 +2487,160 @@ function definePattern(config) {
2335
2487
  return validatePattern(spec);
2336
2488
  }
2337
2489
 
2338
- // ../patterns/src/library/flatten/display-contents-wrapper.pattern.ts
2490
+ // ../patterns/src/library/flex/flex-center-wrapper.pattern.ts
2491
+ var flexCenterWrapper = definePattern({
2492
+ name: "flex-center-wrapper",
2493
+ category: "flatten/flex/flex-center-wrapper",
2494
+ safety: 2,
2495
+ doc: {
2496
+ title: "Flatten flex-centering wrapper",
2497
+ 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.",
2498
+ before: '<div style="display:flex;align-items:center;justify-content:center"><Child/></div>',
2499
+ after: '<Child style="place-self:center"/>',
2500
+ 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."
2501
+ },
2502
+ match: {
2503
+ tag: "div",
2504
+ style: { display: "flex", alignItems: "center", justifyContent: "center" },
2505
+ onlyChild: "element",
2506
+ paintsNothing: true
2507
+ },
2508
+ rewrite: {
2509
+ flattenInto: "child",
2510
+ childGains: { placeSelf: "center" }
2511
+ },
2512
+ // Collapsing a flex-centering wrapper to `place-self:center` on the child is render-identical ONLY
2513
+ // when the child's NEW parent is a statically-known GRID that lets the wrapper fill its area (there
2514
+ // `place-self`'s align-self AND justify-self both take effect). Under that ONE context the flatten is
2515
+ // classified `provably-safe` and commits; under a flex/block/unknown parent — or when the wrapper
2516
+ // drops any own style — it stays `needs-verification` and the conservative production gate PRESERVES
2517
+ // it. Op-level correctness (purity, id-preserving unwrap, opacity-barrier safety) is additionally
2518
+ // asserted by the invariant suite over every pattern.
2519
+ test: {
2520
+ cases: [
2521
+ {
2522
+ name: "grid parent \u2192 flattened (child gains place-self-center)",
2523
+ before: '<div className="grid"><div className="flex items-center justify-center"><span className="bg-red-200">x</span></div></div>',
2524
+ after: '<div className="grid"><span className="bg-red-200 place-self-center">x</span></div>'
2525
+ }
2526
+ ],
2527
+ noMatch: [
2528
+ // Non-grid (flex) parent (document root): `justify-self` is ignored in flex → not provably safe.
2529
+ '<div className="flex justify-center items-center"><div className="bg-red-200">Hello</div></div>',
2530
+ // Grid parent, but the wrapper drops padding when removed → not layout-neutral (rule 3).
2531
+ '<div className="grid"><div className="p-4 flex items-center justify-center"><span className="bg-red-200">x</span></div></div>',
2532
+ // Grid parent forcing place-items-center: the wrapper would not fill its area → fill guard skips.
2533
+ '<div className="grid place-items-center"><div className="flex items-center justify-center"><span className="bg-red-200">x</span></div></div>',
2534
+ // onClick is a hard opacity barrier → the wrapper is load-bearing regardless of the gate.
2535
+ '<div className="flex justify-center items-center" onClick={handleClick}><div className="bg-red-200">Hello</div></div>'
2536
+ ]
2537
+ }
2538
+ });
2539
+
2540
+ // ../patterns/src/library/fragment/redundant-fragment.pattern.ts
2541
+ function parentIsRedundantFragment(node, ctx) {
2542
+ const el = node;
2543
+ if (el.kind !== "element") return false;
2544
+ const parentId = el.parent;
2545
+ if (parentId == null) return false;
2546
+ const parent = ctx.doc.nodes.get(parentId);
2547
+ if (!parent || parent.kind !== "fragment") return false;
2548
+ if (parent.parent == null) return false;
2549
+ if (parent.children.length !== 1) return false;
2550
+ const m = parent.meta;
2551
+ if (m.hasKey || m.hasRef || m.hasEventHandlers || m.hasDynamicChildren || m.hasDangerousHtml || m.hasSpreadAttrs || m.isComponent) {
2552
+ return false;
2553
+ }
2554
+ if (m.targetedByCombinator || m.targetedByStructuralPseudo) return false;
2555
+ const fid = parentId;
2556
+ if (ctx.selectors.targetedByCombinator(fid) || ctx.selectors.targetedByStructuralPseudo(fid)) {
2557
+ return false;
2558
+ }
2559
+ if (ctx.selectors.reparentImpact(fid).size > 0) return false;
2560
+ return true;
2561
+ }
2562
+ var redundantFragment = definePattern({
2563
+ name: "redundant-fragment",
2564
+ category: "flatten/fragment/redundant-fragment",
2565
+ safety: 1,
2566
+ doc: {
2567
+ title: "Flatten redundant single-child fragment",
2568
+ 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.",
2569
+ before: "<><Child/></>",
2570
+ after: "<Child/>",
2571
+ 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."
2572
+ },
2573
+ match: parentIsRedundantFragment,
2574
+ rewrite: (ctx, rw) => {
2575
+ const parentId = ctx.node.parent;
2576
+ if (parentId == null) return null;
2577
+ const fragment = ctx.doc.nodes.get(parentId);
2578
+ if (!fragment || fragment.kind !== "fragment") return null;
2579
+ return [rw.unwrap(fragment)];
2580
+ },
2581
+ test: {
2582
+ cases: [
2583
+ {
2584
+ // A fragment renders no box, so unwrapping a single-child fragment is always layout-identical
2585
+ // → a provably-safe flatten: the child is spliced up into the fragment's slot.
2586
+ before: '<><span className="bg-red-200">Hi</span></>',
2587
+ after: '<span className="bg-red-200">Hi</span>'
2588
+ }
2589
+ ],
2590
+ noMatch: [
2591
+ // Two children ⇒ not a single-child fragment, so the fragment is load-bearing and stays.
2592
+ '<><span className="bg-red-200">A</span><span className="bg-green-200">B</span></>'
2593
+ ]
2594
+ }
2595
+ });
2596
+
2597
+ // ../patterns/src/library/grid/grid-center-wrapper.pattern.ts
2598
+ var gridCenterWrapper = definePattern({
2599
+ name: "grid-center-wrapper",
2600
+ category: "flatten/grid/grid-center-wrapper",
2601
+ safety: 2,
2602
+ doc: {
2603
+ title: "Flatten grid-centering wrapper",
2604
+ 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.",
2605
+ before: '<div style="display:grid;align-items:center;justify-content:center"><Child/></div>',
2606
+ after: '<Child style="place-self:center"/>',
2607
+ 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."
2608
+ },
2609
+ match: {
2610
+ tag: "div",
2611
+ style: { display: "grid", alignItems: "center", justifyContent: "center" },
2612
+ onlyChild: "element",
2613
+ paintsNothing: true
2614
+ },
2615
+ rewrite: {
2616
+ flattenInto: "child",
2617
+ childGains: { placeSelf: "center" }
2618
+ },
2619
+ // Like `flex-center-wrapper`, collapsing to `place-self:center` is render-identical ONLY when the
2620
+ // child's NEW parent is a statically-known GRID that lets the wrapper fill its area (there both halves
2621
+ // of place-self take effect). Under that ONE context the flatten is `provably-safe` and commits; under
2622
+ // a flex/block/unknown parent — or when the wrapper drops any own style — it stays `needs-verification`
2623
+ // and the conservative production gate PRESERVES it. Op-level correctness is asserted by the invariant suite.
2624
+ test: {
2625
+ cases: [
2626
+ {
2627
+ name: "grid parent \u2192 flattened (child gains place-self-center)",
2628
+ before: '<div className="grid"><div className="grid items-center justify-center"><span className="bg-red-200">x</span></div></div>',
2629
+ after: '<div className="grid"><span className="bg-red-200 place-self-center">x</span></div>'
2630
+ }
2631
+ ],
2632
+ noMatch: [
2633
+ // Non-grid (document-root) parent: justify-self is ignored outside a grid → not provably safe.
2634
+ '<div className="grid justify-center items-center"><div className="bg-red-200">Hello</div></div>',
2635
+ // Grid parent, but the wrapper drops padding when removed → not layout-neutral, preserved.
2636
+ '<div className="grid"><div className="p-4 grid items-center justify-center"><span className="bg-red-200">x</span></div></div>',
2637
+ // onClick is a hard opacity barrier → the wrapper is load-bearing regardless of the gate.
2638
+ '<div className="grid justify-center items-center" onClick={handleClick}><div className="bg-red-200">Hello</div></div>'
2639
+ ]
2640
+ }
2641
+ });
2642
+
2643
+ // ../patterns/src/library/wrapper/display-contents-wrapper.pattern.ts
2339
2644
  function asEl(node) {
2340
2645
  const n = node;
2341
2646
  return n.kind === "element" ? n : null;
@@ -2359,7 +2664,7 @@ var targetedByStructuralPseudo = (node, ctx) => {
2359
2664
  };
2360
2665
  var displayContentsWrapper = definePattern({
2361
2666
  name: "display-contents-wrapper",
2362
- category: "flatten/display-contents-wrapper",
2667
+ category: "flatten/wrapper/display-contents-wrapper",
2363
2668
  safety: 2,
2364
2669
  doc: {
2365
2670
  title: "Flatten display:contents wrapper",
@@ -2399,7 +2704,7 @@ var displayContentsWrapper = definePattern({
2399
2704
  }
2400
2705
  });
2401
2706
 
2402
- // ../patterns/src/library/flatten/empty-style-div.pattern.ts
2707
+ // ../patterns/src/library/wrapper/empty-style-div.pattern.ts
2403
2708
  function asEl2(node) {
2404
2709
  const n = node;
2405
2710
  return n.kind === "element" ? n : null;
@@ -2431,7 +2736,7 @@ var hasNonBlockDisplay = (node, ctx) => {
2431
2736
  };
2432
2737
  var emptyStyleDiv = definePattern({
2433
2738
  name: "empty-style-div",
2434
- category: "flatten/empty-style-div",
2739
+ category: "flatten/wrapper/empty-style-div",
2435
2740
  safety: 1,
2436
2741
  doc: {
2437
2742
  title: "Flatten empty-style div wrapper",
@@ -2470,371 +2775,102 @@ var emptyStyleDiv = definePattern({
2470
2775
  }
2471
2776
  });
2472
2777
 
2473
- // ../patterns/src/library/flatten/flex-center-wrapper.pattern.ts
2474
- var flexCenterWrapper = definePattern({
2475
- name: "flex-center-wrapper",
2476
- category: "flatten/flex-center-wrapper",
2778
+ // ../patterns/src/library/wrapper/inherited-only-wrapper.pattern.ts
2779
+ var INERT_HOST_TAGS = /* @__PURE__ */ new Set(["div", "span"]);
2780
+ var isInertHostTag = (node) => {
2781
+ const n = node;
2782
+ if (n.kind !== "element") return false;
2783
+ return INERT_HOST_TAGS.has(String(n.tag).toLowerCase());
2784
+ };
2785
+ var isComponentNode2 = (node) => {
2786
+ const n = node;
2787
+ return n.kind === "element" ? n.meta.isComponent : false;
2788
+ };
2789
+ var hasOnlyInheritedStyle = (node, ctx) => {
2790
+ const sm = normalizer.normalizeStyleMap(ctx.computed());
2791
+ let sawAny = false;
2792
+ for (const block of sm.blocks.values()) {
2793
+ for (const decl of block.decls.values()) {
2794
+ sawAny = true;
2795
+ const inherited = decl.inherited || normalizer.inherited.isInherited(decl.property);
2796
+ if (!inherited) return false;
2797
+ }
2798
+ }
2799
+ return sawAny;
2800
+ };
2801
+ var inheritedOnlyWrapper = definePattern({
2802
+ name: "inherited-only-wrapper",
2803
+ category: "flatten/wrapper/inherited-only-wrapper",
2477
2804
  safety: 2,
2478
2805
  doc: {
2479
- title: "Flatten flex-centering wrapper",
2480
- 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.",
2481
- before: '<div style="display:flex;align-items:center;justify-content:center"><Child/></div>',
2482
- after: '<Child style="place-self:center"/>',
2483
- 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."
2806
+ title: "Flatten inherited-only styling wrapper",
2807
+ 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.",
2808
+ before: '<div style="text-align:center"><Child/></div>',
2809
+ after: '<Child style="text-align:center"/>',
2810
+ 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."
2484
2811
  },
2485
2812
  match: {
2486
- tag: "div",
2487
- style: { display: "flex", alignItems: "center", justifyContent: "center" },
2488
2813
  onlyChild: "element",
2489
- paintsNothing: true
2490
- },
2491
- rewrite: {
2492
- flattenInto: "child",
2493
- childGains: { placeSelf: "center" }
2814
+ paintsNothing: true,
2815
+ where: [isInertHostTag, not(isComponentNode2), hasOnlyInheritedStyle]
2494
2816
  },
2495
- // Collapsing a flex-centering wrapper to `place-self:center` on the child is render-identical ONLY
2496
- // when the child's NEW parent is a statically-known GRID that lets the wrapper fill its area (there
2497
- // `place-self`'s align-self AND justify-self both take effect). Under that ONE context the flatten is
2498
- // classified `provably-safe` and commits; under a flex/block/unknown parent — or when the wrapper
2499
- // drops any own style — it stays `needs-verification` and the conservative production gate PRESERVES
2500
- // it. Op-level correctness (purity, id-preserving unwrap, opacity-barrier safety) is additionally
2501
- // asserted by the invariant suite over every pattern.
2817
+ rewrite: { flattenInto: "child" },
2502
2818
  test: {
2503
2819
  cases: [
2504
2820
  {
2505
- name: "grid parent \u2192 flattened (child gains place-self-center)",
2506
- before: '<div className="grid"><div className="flex items-center justify-center"><span className="bg-red-200">x</span></div></div>',
2507
- after: '<div className="grid"><span className="bg-red-200 place-self-center">x</span></div>'
2821
+ // `text-align:center` is inherited folded onto the child; the paint-free wrapper is removed.
2822
+ before: '<div className="text-center"><p className="bg-red-200">x</p></div>',
2823
+ after: '<p className="bg-red-200 text-center">x</p>'
2508
2824
  }
2509
2825
  ],
2510
2826
  noMatch: [
2511
- // Non-grid (flex) parent (document root): `justify-self` is ignored in flex not provably safe.
2512
- '<div className="flex justify-center items-center"><div className="bg-red-200">Hello</div></div>',
2513
- // Grid parent, but the wrapper drops padding when removed → not layout-neutral (rule 3).
2514
- '<div className="grid"><div className="p-4 flex items-center justify-center"><span className="bg-red-200">x</span></div></div>',
2515
- // Grid parent forcing place-items-center: the wrapper would not fill its area fill guard skips.
2516
- '<div className="grid place-items-center"><div className="flex items-center justify-center"><span className="bg-red-200">x</span></div></div>',
2517
- // onClick is a hard opacity barrier → the wrapper is load-bearing regardless of the gate.
2518
- '<div className="flex justify-center items-center" onClick={handleClick}><div className="bg-red-200">Hello</div></div>'
2827
+ // `p-4` is a NON-inherited padding: removing the box would drop it, so the flatten-safety gate
2828
+ // reverts the unwrap and the wrapper is left unchanged.
2829
+ '<div className="p-4"><p className="bg-red-200">x</p></div>',
2830
+ // A `<p>` wrapper is NOT an inert host box: its UA default display/margins are not captured in the
2831
+ // class-derived computed style, so removing it is not provably layout-neutralleft unchanged.
2832
+ '<p className="text-center"><span className="bg-red-200">x</span></p>'
2519
2833
  ]
2520
2834
  }
2521
2835
  });
2522
2836
 
2523
- // ../patterns/src/library/flatten/inline-flex-center-wrapper.pattern.ts
2524
- var inlineFlexCenterWrapper = definePattern({
2525
- name: "inline-flex-center-wrapper",
2526
- category: "flatten/inline-flex-center-wrapper",
2837
+ // ../patterns/src/library/wrapper/passthrough-wrapper.pattern.ts
2838
+ function metaOf2(node) {
2839
+ const n = node;
2840
+ return n.kind === "element" ? n.meta : null;
2841
+ }
2842
+ function elementOf(node) {
2843
+ const n = node;
2844
+ return n.kind === "element" ? n : null;
2845
+ }
2846
+ var establishesContext = (node) => {
2847
+ const m = metaOf2(node);
2848
+ if (!m) return false;
2849
+ return m.establishesBox || m.establishesFormattingContext || m.establishesStackingContext || m.isContainingBlock || m.declaresCustomProperties;
2850
+ };
2851
+ var hasSpreadAttrs2 = (node) => metaOf2(node)?.hasSpreadAttrs ?? false;
2852
+ var isComponentNode3 = (node) => metaOf2(node)?.isComponent ?? false;
2853
+ var hasOwnAttrs2 = (node) => {
2854
+ const el = elementOf(node);
2855
+ if (!el) return false;
2856
+ return el.attrs.entries.size > 0 || el.attrs.spreads.length > 0;
2857
+ };
2858
+ var targetedByStructuralPseudo3 = (node, ctx) => {
2859
+ const el = elementOf(node);
2860
+ if (!el) return false;
2861
+ if (el.meta.targetedByStructuralPseudo) return true;
2862
+ return ctx.selectors.targetedByStructuralPseudo(el.id);
2863
+ };
2864
+ var passthroughWrapper = definePattern({
2865
+ name: "passthrough-wrapper",
2866
+ category: "flatten/wrapper/passthrough-wrapper",
2527
2867
  safety: 2,
2528
2868
  doc: {
2529
- title: "Flatten inline-flex-centering wrapper",
2530
- 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.",
2531
- before: '<div style="display:inline-flex;align-items:center;justify-content:center"><Child/></div>',
2532
- after: '<Child style="place-self:center"/>',
2533
- 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."
2534
- },
2535
- match: {
2536
- tag: "div",
2537
- style: { display: "inline-flex", alignItems: "center", justifyContent: "center" },
2538
- onlyChild: "element",
2539
- paintsNothing: true
2540
- },
2541
- rewrite: {
2542
- flattenInto: "child",
2543
- childGains: { placeSelf: "center" }
2544
- },
2545
- // Like its block-level sibling, this centering flatten is `needs-verification` (the wrapper's own
2546
- // `display:inline-flex` establishes a formatting context, and place-self centering only holds under
2547
- // a flex/grid parent), so the conservative production gate (`'provably-safe'`) REVERTS it — every
2548
- // case here is a no-match. Op-level correctness is covered by the invariant suite.
2549
- test: {
2550
- noMatch: [
2551
- // Even under a static flex/grid parent the centering flatten is not provably layout-neutral.
2552
- '<div className="grid"><div className="inline-flex items-center justify-center"><span className="bg-red-200">x</span></div></div>',
2553
- // Non-flex/grid parent (document root) → left unchanged.
2554
- '<div className="inline-flex justify-center items-center"><div className="bg-red-200">Hello</div></div>',
2555
- // onClick is a hard opacity barrier → the wrapper is load-bearing regardless of the gate.
2556
- '<div className="inline-flex justify-center items-center" onClick={handleClick}><div className="bg-red-200">Hello</div></div>'
2557
- ]
2558
- }
2559
- });
2560
-
2561
- // ../patterns/src/library/flatten/nested-flex-merge.pattern.ts
2562
- function baseConditionStyleMap(decls) {
2563
- const map = /* @__PURE__ */ new Map();
2564
- for (const [prop, value] of decls) {
2565
- for (const decl of normalizer.normalizeDeclaration(prop, value, false)) {
2566
- map.set(decl.property, decl);
2567
- }
2568
- }
2569
- const block = { condition: BASE_CONDITION, decls: map };
2570
- const blocks = /* @__PURE__ */ new Map([[conditionKey(BASE_CONDITION), block]]);
2571
- return { blocks };
2572
- }
2573
- var DISPLAY_FLEX = baseConditionStyleMap([["display", "flex"]]);
2574
- var FLEX_CONTAINER_PROPERTIES = /* @__PURE__ */ new Set([
2575
- "display",
2576
- "flex-direction",
2577
- "flex-wrap",
2578
- "justify-content",
2579
- "align-items",
2580
- "align-content",
2581
- "place-content",
2582
- "place-items",
2583
- "row-gap",
2584
- "column-gap"
2585
- ]);
2586
- function outerMergeSafe(sm) {
2587
- const norm = normalizer.normalizeStyleMap(sm);
2588
- for (const block of norm.blocks.values()) {
2589
- for (const decl of block.decls.values()) {
2590
- if (FLEX_CONTAINER_PROPERTIES.has(String(decl.property))) continue;
2591
- if (decl.inherited) continue;
2592
- return false;
2593
- }
2594
- }
2595
- return true;
2596
- }
2597
- function flexConflict(outer, inner) {
2598
- const a = normalizer.normalizeStyleMap(outer);
2599
- const b = normalizer.normalizeStyleMap(inner);
2600
- for (const [key, blockA] of a.blocks) {
2601
- const blockB = b.blocks.get(key);
2602
- if (!blockB) continue;
2603
- for (const [prop, declA] of blockA.decls) {
2604
- if (!FLEX_CONTAINER_PROPERTIES.has(String(prop))) continue;
2605
- const declB = blockB.decls.get(prop);
2606
- if (declB && declB.value !== declA.value) return true;
2607
- }
2608
- }
2609
- return false;
2610
- }
2611
- function extractFlexStyle(sm) {
2612
- const blocks = /* @__PURE__ */ new Map();
2613
- for (const [key, block] of sm.blocks) {
2614
- const decls = /* @__PURE__ */ new Map();
2615
- for (const [prop, decl] of block.decls) {
2616
- if (FLEX_CONTAINER_PROPERTIES.has(String(prop))) decls.set(prop, decl);
2617
- }
2618
- if (decls.size > 0) blocks.set(key, { condition: block.condition, decls });
2619
- }
2620
- return { blocks };
2621
- }
2622
- var isInnerFlex = and(
2623
- isElement("div"),
2624
- computed(DISPLAY_FLEX),
2625
- not(targetedByCombinator)
2626
- );
2627
- var nestedFlexMerge = definePattern({
2628
- name: "nested-flex-merge",
2629
- category: "flatten/nested-flex-merge",
2630
- safety: 2,
2631
- doc: {
2632
- title: "Merge nested flex containers",
2633
- 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.",
2634
- before: '<div style="display:flex;align-items:center;gap:8px"><div style="display:flex;flex-direction:column"/></div>',
2635
- after: '<div style="display:flex;flex-direction:column;align-items:center;gap:8px"/>',
2636
- 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."
2637
- },
2638
- match: {
2639
- tag: "div",
2640
- style: { display: "flex" },
2641
- onlyChild: "element",
2642
- paintsNothing: true
2643
- },
2644
- rewrite: (ctx, rw) => {
2645
- const outer = ctx.node;
2646
- const inner = ctx.onlyElementChild();
2647
- if (!inner) return null;
2648
- if (!isInnerFlex(inner, ctx)) return null;
2649
- const outerStyle = ctx.computed();
2650
- const innerStyle = ctx.computedOf(inner);
2651
- if (!outerMergeSafe(outerStyle)) return null;
2652
- if (flexConflict(outerStyle, innerStyle)) return null;
2653
- return [
2654
- // 1. Preserve inheritable values (color/font/…) by folding them onto the child first.
2655
- rw.foldInheritedStyles(outer, inner, { conditions: "all" }),
2656
- // 2. Transfer the wrapper's flex-container declarations onto the child (target-wins keeps the
2657
- // child's value for any shared property — identical anyway, we proved non-conflict).
2658
- rw.mergeStyle(inner, null, extractFlexStyle(outerStyle), "target-wins"),
2659
- // 3. Remove the wrapper (structural-safe; hoists the child and preserves its IRNodeId).
2660
- rw.unwrap(outer)
2661
- ];
2662
- },
2663
- // Merging the outer flex container into the inner removes the outer's box, but a `display:flex`
2664
- // wrapper establishes a formatting context, so this is a `needs-verification` flatten that the
2665
- // conservative production gate (`'provably-safe'`) REVERTS — every case here is a no-match. The
2666
- // merge's op-level correctness (purity, id-preserving unwrap, opacity-barrier safety) is asserted
2667
- // by the invariant suite over every pattern.
2668
- test: {
2669
- noMatch: [
2670
- // The merge is real but not provably layout-neutral (the wrapper establishes a flex context),
2671
- // so under the conservative gate the nested containers are left in place.
2672
- '<div className="flex items-center gap-2" data-x="1"><div className="flex flex-col">X</div></div>',
2673
- // A non-flex wrapper does not match the flex-container signature → left unchanged anyway.
2674
- '<div className="block bg-blue-500"><div className="flex flex-col">X</div></div>'
2675
- ]
2676
- }
2677
- });
2678
-
2679
- // ../patterns/src/library/flatten/nested-grid-merge.pattern.ts
2680
- function baseConditionStyleMap2(decls) {
2681
- const map = /* @__PURE__ */ new Map();
2682
- for (const [prop, value] of decls) {
2683
- for (const decl of normalizer.normalizeDeclaration(prop, value, false)) {
2684
- map.set(decl.property, decl);
2685
- }
2686
- }
2687
- const block = { condition: BASE_CONDITION, decls: map };
2688
- const blocks = /* @__PURE__ */ new Map([[conditionKey(BASE_CONDITION), block]]);
2689
- return { blocks };
2690
- }
2691
- var DISPLAY_GRID = baseConditionStyleMap2([["display", "grid"]]);
2692
- var GRID_CONTAINER_PROPERTIES = /* @__PURE__ */ new Set([
2693
- "display",
2694
- "grid-template-columns",
2695
- "grid-template-rows",
2696
- "grid-template-areas",
2697
- "grid-auto-columns",
2698
- "grid-auto-rows",
2699
- "grid-auto-flow",
2700
- "justify-content",
2701
- "align-content",
2702
- "place-content",
2703
- "justify-items",
2704
- "align-items",
2705
- "place-items",
2706
- "row-gap",
2707
- "column-gap"
2708
- ]);
2709
- function outerMergeSafe2(sm) {
2710
- const norm = normalizer.normalizeStyleMap(sm);
2711
- for (const block of norm.blocks.values()) {
2712
- for (const decl of block.decls.values()) {
2713
- if (GRID_CONTAINER_PROPERTIES.has(String(decl.property))) continue;
2714
- if (decl.inherited) continue;
2715
- return false;
2716
- }
2717
- }
2718
- return true;
2719
- }
2720
- function gridConflict(outer, inner) {
2721
- const a = normalizer.normalizeStyleMap(outer);
2722
- const b = normalizer.normalizeStyleMap(inner);
2723
- for (const [key, blockA] of a.blocks) {
2724
- const blockB = b.blocks.get(key);
2725
- if (!blockB) continue;
2726
- for (const [prop, declA] of blockA.decls) {
2727
- if (!GRID_CONTAINER_PROPERTIES.has(String(prop))) continue;
2728
- const declB = blockB.decls.get(prop);
2729
- if (declB && declB.value !== declA.value) return true;
2730
- }
2731
- }
2732
- return false;
2733
- }
2734
- function extractGridStyle(sm) {
2735
- const blocks = /* @__PURE__ */ new Map();
2736
- for (const [key, block] of sm.blocks) {
2737
- const decls = /* @__PURE__ */ new Map();
2738
- for (const [prop, decl] of block.decls) {
2739
- if (GRID_CONTAINER_PROPERTIES.has(String(prop))) decls.set(prop, decl);
2740
- }
2741
- if (decls.size > 0) blocks.set(key, { condition: block.condition, decls });
2742
- }
2743
- return { blocks };
2744
- }
2745
- var isInnerGrid = and(
2746
- isElement("div"),
2747
- computed(DISPLAY_GRID),
2748
- not(targetedByCombinator)
2749
- );
2750
- var nestedGridMerge = definePattern({
2751
- name: "nested-grid-merge",
2752
- category: "flatten/nested-grid-merge",
2753
- safety: 2,
2754
- doc: {
2755
- title: "Merge nested grid containers",
2756
- 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.",
2757
- before: '<div style="display:grid;gap:8px"><div style="display:grid;grid-template-columns:1fr 1fr"/></div>',
2758
- after: '<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px"/>',
2759
- 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."
2760
- },
2761
- match: {
2762
- tag: "div",
2763
- style: { display: "grid" },
2764
- onlyChild: "element",
2765
- paintsNothing: true
2766
- },
2767
- rewrite: (ctx, rw) => {
2768
- const outer = ctx.node;
2769
- const inner = ctx.onlyElementChild();
2770
- if (!inner) return null;
2771
- if (!isInnerGrid(inner, ctx)) return null;
2772
- const outerStyle = ctx.computed();
2773
- const innerStyle = ctx.computedOf(inner);
2774
- if (!outerMergeSafe2(outerStyle)) return null;
2775
- if (gridConflict(outerStyle, innerStyle)) return null;
2776
- return [
2777
- // 1. Preserve inheritable values (color/font/…) by folding them onto the child first.
2778
- rw.foldInheritedStyles(outer, inner, { conditions: "all" }),
2779
- // 2. Transfer the wrapper's grid-container declarations onto the child (target-wins keeps the
2780
- // child's value for any shared property — identical anyway, we proved non-conflict).
2781
- rw.mergeStyle(inner, null, extractGridStyle(outerStyle), "target-wins"),
2782
- // 3. Remove the wrapper (structural-safe; hoists the child and preserves its IRNodeId).
2783
- rw.unwrap(outer)
2784
- ];
2785
- },
2786
- // Like its flex sibling, this merge removes the outer container's box, but a `display:grid` wrapper
2787
- // establishes a formatting context, so it is a `needs-verification` flatten that the conservative
2788
- // production gate (`'provably-safe'`) REVERTS — every case here is a no-match. Op-level correctness
2789
- // is asserted by the invariant suite over every pattern.
2790
- test: {
2791
- noMatch: [
2792
- // The merge is real but not provably layout-neutral (the wrapper establishes a grid context),
2793
- // so under the conservative gate the nested containers are left in place.
2794
- '<div className="grid gap-2" data-x="1"><div className="grid grid-cols-2">X</div></div>',
2795
- // A non-grid wrapper does not match the grid-container signature → left unchanged anyway.
2796
- '<div className="block bg-blue-500"><div className="grid grid-cols-2">X</div></div>'
2797
- ]
2798
- }
2799
- });
2800
-
2801
- // ../patterns/src/library/flatten/passthrough-wrapper.pattern.ts
2802
- function metaOf2(node) {
2803
- const n = node;
2804
- return n.kind === "element" ? n.meta : null;
2805
- }
2806
- function elementOf(node) {
2807
- const n = node;
2808
- return n.kind === "element" ? n : null;
2809
- }
2810
- var establishesContext = (node) => {
2811
- const m = metaOf2(node);
2812
- if (!m) return false;
2813
- return m.establishesBox || m.establishesFormattingContext || m.establishesStackingContext || m.isContainingBlock || m.declaresCustomProperties;
2814
- };
2815
- var hasSpreadAttrs2 = (node) => metaOf2(node)?.hasSpreadAttrs ?? false;
2816
- var isComponentNode2 = (node) => metaOf2(node)?.isComponent ?? false;
2817
- var hasOwnAttrs2 = (node) => {
2818
- const el = elementOf(node);
2819
- if (!el) return false;
2820
- return el.attrs.entries.size > 0 || el.attrs.spreads.length > 0;
2821
- };
2822
- var targetedByStructuralPseudo3 = (node, ctx) => {
2823
- const el = elementOf(node);
2824
- if (!el) return false;
2825
- if (el.meta.targetedByStructuralPseudo) return true;
2826
- return ctx.selectors.targetedByStructuralPseudo(el.id);
2827
- };
2828
- var passthroughWrapper = definePattern({
2829
- name: "passthrough-wrapper",
2830
- category: "flatten/passthrough-wrapper",
2831
- safety: 2,
2832
- doc: {
2833
- title: "Flatten passthrough wrapper",
2834
- 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.",
2835
- before: "<div><Child/></div>",
2836
- after: "<Child/>",
2837
- 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."
2869
+ title: "Flatten passthrough wrapper",
2870
+ 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.",
2871
+ before: "<div><Child/></div>",
2872
+ after: "<Child/>",
2873
+ 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."
2838
2874
  },
2839
2875
  match: {
2840
2876
  tag: "div",
@@ -2844,1230 +2880,124 @@ var passthroughWrapper = definePattern({
2844
2880
  not(establishesContext),
2845
2881
  not(hasOwnAttrs2),
2846
2882
  not(hasDynamicClasses),
2847
- not(hasSpreadAttrs2),
2848
- not(isComponentNode2),
2849
- not(targetedByStructuralPseudo3)
2850
- ]
2851
- },
2852
- rewrite: { flattenInto: "child" },
2853
- test: {
2854
- cases: [
2855
- {
2856
- // A plain, style-free wrapper paints nothing and establishes no context → a provably-safe
2857
- // flatten under the conservative gate: the wrapper is removed and its sole child hoisted.
2858
- before: '<div><a className="bg-red-200">Link</a></div>',
2859
- after: '<a className="bg-red-200">Link</a>'
2860
- }
2861
- ],
2862
- noMatch: [
2863
- // A ref pins the wrapper's element identity (a hard opacity barrier) → not a passthrough.
2864
- '<div ref={rootRef}><a className="bg-red-200">Link</a></div>',
2865
- // A `display:flex` wrapper establishes a formatting context, so removing its box is NOT
2866
- // provably layout-neutral → the conservative gate leaves it in place.
2867
- '<div className="flex"><a className="bg-red-200">Link</a></div>'
2868
- ]
2869
- }
2870
- });
2871
-
2872
- // ../patterns/src/library/flatten/redundant-fragment.pattern.ts
2873
- function parentIsRedundantFragment(node, ctx) {
2874
- const el = node;
2875
- if (el.kind !== "element") return false;
2876
- const parentId = el.parent;
2877
- if (parentId == null) return false;
2878
- const parent = ctx.doc.nodes.get(parentId);
2879
- if (!parent || parent.kind !== "fragment") return false;
2880
- if (parent.parent == null) return false;
2881
- if (parent.children.length !== 1) return false;
2882
- const m = parent.meta;
2883
- if (m.hasKey || m.hasRef || m.hasEventHandlers || m.hasDynamicChildren || m.hasDangerousHtml || m.hasSpreadAttrs || m.isComponent) {
2884
- return false;
2885
- }
2886
- if (m.targetedByCombinator || m.targetedByStructuralPseudo) return false;
2887
- const fid = parentId;
2888
- if (ctx.selectors.targetedByCombinator(fid) || ctx.selectors.targetedByStructuralPseudo(fid)) {
2889
- return false;
2890
- }
2891
- if (ctx.selectors.reparentImpact(fid).size > 0) return false;
2892
- return true;
2893
- }
2894
- var redundantFragment = definePattern({
2895
- name: "redundant-fragment",
2896
- category: "flatten/redundant-fragment",
2897
- safety: 1,
2898
- doc: {
2899
- title: "Flatten redundant single-child fragment",
2900
- 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.",
2901
- before: "<><Child/></>",
2902
- after: "<Child/>",
2903
- 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."
2904
- },
2905
- match: parentIsRedundantFragment,
2906
- rewrite: (ctx, rw) => {
2907
- const parentId = ctx.node.parent;
2908
- if (parentId == null) return null;
2909
- const fragment = ctx.doc.nodes.get(parentId);
2910
- if (!fragment || fragment.kind !== "fragment") return null;
2911
- return [rw.unwrap(fragment)];
2912
- },
2913
- test: {
2914
- cases: [
2915
- {
2916
- // A fragment renders no box, so unwrapping a single-child fragment is always layout-identical
2917
- // → a provably-safe flatten: the child is spliced up into the fragment's slot.
2918
- before: '<><span className="bg-red-200">Hi</span></>',
2919
- after: '<span className="bg-red-200">Hi</span>'
2920
- }
2921
- ],
2922
- noMatch: [
2923
- // Two children ⇒ not a single-child fragment, so the fragment is load-bearing and stays.
2924
- '<><span className="bg-red-200">A</span><span className="bg-green-200">B</span></>'
2925
- ]
2926
- }
2927
- });
2928
-
2929
- // ../patterns/src/library/flatten/redundant-inline-wrapper.pattern.ts
2930
- function asEl3(node) {
2931
- const n = node;
2932
- return n.kind === "element" ? n : null;
2933
- }
2934
- function metaOf3(node) {
2935
- return asEl3(node)?.meta ?? null;
2936
- }
2937
- var establishesContext2 = (node) => {
2938
- const m = metaOf3(node);
2939
- if (!m) return false;
2940
- return m.establishesBox || m.establishesFormattingContext || m.establishesStackingContext || m.isContainingBlock || m.declaresCustomProperties;
2941
- };
2942
- var hasSpreadAttrs3 = (node) => metaOf3(node)?.hasSpreadAttrs ?? false;
2943
- var isComponentNode3 = (node) => metaOf3(node)?.isComponent ?? false;
2944
- var hasOwnAttrs3 = (node) => {
2945
- const el = asEl3(node);
2946
- if (!el) return false;
2947
- return el.attrs.entries.size > 0 || el.attrs.spreads.length > 0;
2948
- };
2949
- var targetedByStructuralPseudo4 = (node, ctx) => {
2950
- const el = asEl3(node);
2951
- if (!el) return false;
2952
- if (el.meta.targetedByStructuralPseudo) return true;
2953
- return ctx.selectors.targetedByStructuralPseudo(el.id);
2954
- };
2955
- var DISPLAY3 = "display";
2956
- var hasNonInlineDisplay = (node, ctx) => {
2957
- const el = asEl3(node);
2958
- if (!el) return false;
2959
- const sm = ctx.computedOf(el) ?? el.computed;
2960
- for (const block of sm.blocks.values()) {
2961
- const decl = block.decls.get(DISPLAY3);
2962
- if (decl && String(decl.value) !== "inline") return true;
2963
- }
2964
- return false;
2965
- };
2966
- var redundantInlineWrapper = definePattern({
2967
- name: "redundant-inline-wrapper",
2968
- category: "flatten/redundant-inline-wrapper",
2969
- safety: 2,
2970
- doc: {
2971
- title: "Flatten redundant inline wrapper",
2972
- 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.",
2973
- before: "<span><Child/></span>",
2974
- after: "<Child/>",
2975
- 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."
2976
- },
2977
- match: {
2978
- tag: "span",
2979
- onlyChild: "element",
2980
- paintsNothing: true,
2981
- where: [
2982
- not(hasNonInlineDisplay),
2983
- not(establishesContext2),
2984
- not(hasOwnAttrs3),
2985
- not(hasDynamicClasses),
2986
- not(hasSpreadAttrs3),
2987
- not(isComponentNode3),
2988
- not(targetedByStructuralPseudo4)
2989
- ]
2990
- },
2991
- rewrite: { flattenInto: "child" },
2992
- test: {
2993
- cases: [
2994
- {
2995
- // An empty inline span paints nothing and establishes no context → a provably-safe flatten:
2996
- // the span is removed and its sole child hoisted in place.
2997
- before: '<span><a className="text-blue-500">Link</a></span>',
2998
- after: '<a className="text-blue-500">Link</a>'
2999
- }
3000
- ],
3001
- noMatch: [
3002
- // A ref pins the span's element identity (a hard opacity barrier) → not a passthrough.
3003
- '<span ref={spanRef}><a className="text-blue-500">Link</a></span>',
3004
- // The span paints its own background (own visual style) → kept.
3005
- '<span className="bg-green-200"><a className="text-blue-500">Link</a></span>',
3006
- // Non-inline display (inline-block) participates in layout differently → kept.
3007
- '<span className="inline-block"><a className="text-blue-500">Link</a></span>'
3008
- ]
3009
- }
3010
- });
3011
-
3012
- // ../patterns/src/library/compress/border-radius-shorthand.pattern.ts
3013
- var CORNERS = [
3014
- "border-top-left-radius",
3015
- "border-top-right-radius",
3016
- "border-bottom-right-radius",
3017
- "border-bottom-left-radius"
3018
- ];
3019
- var CORNER_SET = new Set(CORNERS);
3020
- var BASE_KEY = conditionKey(BASE_CONDITION);
3021
- var RADIUS = "border-radius";
3022
- var NON_COLLAPSIBLE_VALUES = /* @__PURE__ */ new Set([
3023
- "initial",
3024
- "inherit",
3025
- "unset",
3026
- "revert",
3027
- "revert-layer"
3028
- ]);
3029
- function analyzeRadius(sm) {
3030
- const block = sm.blocks.get(BASE_KEY);
3031
- if (!block) return null;
3032
- const corners = [];
3033
- for (const corner of CORNERS) {
3034
- const decl = block.decls.get(corner);
3035
- if (!decl) return null;
3036
- corners.push(decl);
3037
- }
3038
- const important = corners[0].important;
3039
- if (!corners.every((d) => d.important === important)) return null;
3040
- const value = String(corners[0].value);
3041
- if (NON_COLLAPSIBLE_VALUES.has(value)) return null;
3042
- if (!corners.every((d) => String(d.value) === value)) return null;
3043
- const relative = corners.some((d) => d.relativeToParent);
3044
- return { value, important, relative };
3045
- }
3046
- function withFoldedRadius(sm, fold) {
3047
- const blocks = /* @__PURE__ */ new Map();
3048
- for (const [key, block] of sm.blocks) {
3049
- if (key !== BASE_KEY) {
3050
- blocks.set(key, block);
3051
- continue;
3052
- }
3053
- const decls = /* @__PURE__ */ new Map();
3054
- for (const [prop, decl] of block.decls) {
3055
- if (CORNER_SET.has(String(prop))) continue;
3056
- decls.set(prop, decl);
3057
- }
3058
- const shorthand = {
3059
- property: RADIUS,
3060
- value: fold.value,
3061
- important: fold.important,
3062
- relativeToParent: fold.relative,
3063
- inherited: false
3064
- // border-radius is never inherited
3065
- };
3066
- decls.set(shorthand.property, shorthand);
3067
- blocks.set(key, { condition: block.condition, decls });
3068
- }
3069
- return { blocks };
3070
- }
3071
- var borderRadiusShorthand = definePattern({
3072
- name: "border-radius-shorthand",
3073
- category: "compress/border-radius-shorthand",
3074
- safety: 1,
3075
- doc: {
3076
- title: "Collapse equal corner radii into border-radius",
3077
- 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).",
3078
- before: '<div class="rounded-tl-lg rounded-tr-lg rounded-br-lg rounded-bl-lg"/>',
3079
- after: '<div class="rounded-lg"/>',
3080
- 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."
3081
- },
3082
- rewrite: {
3083
- rewriteClasses(computed2) {
3084
- const fold = analyzeRadius(computed2);
3085
- return fold ? withFoldedRadius(computed2, fold) : null;
3086
- }
3087
- },
3088
- test: {
3089
- cases: [
3090
- {
3091
- // The four equal corner longhands collapse to a `border-radius` decl at the IR level; the
3092
- // minimizing reverse-emit then picks the single shortest utility (`rounded-lg`) that reproduces
3093
- // it, replacing the four `rounded-{tl,tr,br,bl}-lg` tokens. `bg-red-200` is preserved.
3094
- before: '<div className="rounded-tl-lg rounded-tr-lg rounded-br-lg rounded-bl-lg bg-red-200">box</div>',
3095
- after: '<div className="bg-red-200 rounded-lg">box</div>'
3096
- }
3097
- ],
3098
- // Corners differ (top corners vs bottom corners) → no all-equal collapse.
3099
- noMatch: ['<div className="rounded-t-lg rounded-b-sm bg-red-200">box</div>']
3100
- }
3101
- });
3102
-
3103
- // ../patterns/src/library/compress/border-shorthand.pattern.ts
3104
- var WIDTH_SIDES = [
3105
- "border-top-width",
3106
- "border-right-width",
3107
- "border-bottom-width",
3108
- "border-left-width"
3109
- ];
3110
- var WIDTH_SIDE_SET = new Set(WIDTH_SIDES);
3111
- var BASE_KEY2 = conditionKey(BASE_CONDITION);
3112
- var BORDER_WIDTH = "border-width";
3113
- function analyzeWidth(sm) {
3114
- const block = sm.blocks.get(BASE_KEY2);
3115
- if (!block) return null;
3116
- const sides = [];
3117
- for (const side of WIDTH_SIDES) {
3118
- const decl = block.decls.get(side);
3119
- if (!decl) return null;
3120
- sides.push(decl);
3121
- }
3122
- const [top, right, bottom, left] = sides;
3123
- if (!(top.important === right.important && right.important === bottom.important && bottom.important === left.important)) {
3124
- return null;
3125
- }
3126
- const tv = String(top.value);
3127
- const rv = String(right.value);
3128
- const bv = String(bottom.value);
3129
- const lv = String(left.value);
3130
- if (tv !== bv || lv !== rv) return null;
3131
- const value = tv === lv ? tv : `${tv} ${lv}`;
3132
- const relative = sides.some((d) => d.relativeToParent);
3133
- return { value, important: top.important, relative };
3134
- }
3135
- function withFoldedWidth(sm, fold) {
3136
- const blocks = /* @__PURE__ */ new Map();
3137
- for (const [key, block] of sm.blocks) {
3138
- if (key !== BASE_KEY2) {
3139
- blocks.set(key, block);
3140
- continue;
3141
- }
3142
- const decls = /* @__PURE__ */ new Map();
3143
- for (const [prop, decl] of block.decls) {
3144
- if (WIDTH_SIDE_SET.has(String(prop))) continue;
3145
- decls.set(prop, decl);
3146
- }
3147
- const shorthand = {
3148
- property: BORDER_WIDTH,
3149
- value: fold.value,
3150
- important: fold.important,
3151
- relativeToParent: fold.relative,
3152
- inherited: false
3153
- // border-width is never inherited
3154
- };
3155
- decls.set(shorthand.property, shorthand);
3156
- blocks.set(key, { condition: block.condition, decls });
3157
- }
3158
- return { blocks };
3159
- }
3160
- var borderShorthand = definePattern({
3161
- name: "border-shorthand",
3162
- category: "compress/border-shorthand",
3163
- safety: 1,
3164
- doc: {
3165
- title: "Collapse border-width longhands to shorthand",
3166
- 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-*).",
3167
- before: '<div class="border-t-2 border-r-2 border-b-2 border-l-2"/>',
3168
- after: '<div class="border-2"/>',
3169
- 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."
3170
- },
3171
- rewrite: {
3172
- rewriteClasses(computed2) {
3173
- const fold = analyzeWidth(computed2);
3174
- return fold ? withFoldedWidth(computed2, fold) : null;
3175
- }
3176
- },
3177
- test: {
3178
- cases: [
3179
- {
3180
- // The four equal width longhands collapse to a `border-width` shorthand at the IR level, and the
3181
- // minimizing reverse-emit picks the single shortest utility (`border-2`) that reproduces it,
3182
- // replacing the four `border-{t,r,b,l}-2` tokens. `bg-red-200` is preserved.
3183
- before: '<div className="border-t-2 border-r-2 border-b-2 border-l-2 bg-red-200">box</div>',
3184
- after: '<div className="bg-red-200 border-2">box</div>'
3185
- }
3186
- ],
3187
- // Asymmetric widths (top != bottom) cannot fold into a shorthand.
3188
- noMatch: ['<div className="border-t-2 border-r-4 border-b-8 border-l-4 bg-red-200">box</div>']
3189
- }
3190
- });
3191
-
3192
- // ../patterns/src/library/compress/dedupe-classes.pattern.ts
3193
- function findRedundantClasses(computed2) {
3194
- const winners = /* @__PURE__ */ new Set();
3195
- const shadowed = /* @__PURE__ */ new Set();
3196
- for (const block of computed2.blocks.values()) {
3197
- for (const decl of block.decls.values()) {
3198
- if (decl.origin && decl.origin.kind === "class") winners.add(decl.origin.className);
3199
- for (const o of decl.shadowed ?? []) {
3200
- if (o.kind === "class") shadowed.add(o.className);
3201
- }
3202
- }
3203
- }
3204
- return { winners, shadowed };
3205
- }
3206
- var dedupeClasses = definePattern({
3207
- name: "dedupe-classes",
3208
- category: "compress/dedupe-classes",
3209
- safety: 1,
3210
- doc: {
3211
- title: "Dedupe fully-overridden class tokens",
3212
- 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.",
3213
- before: '<p class="text-sm text-lg" />',
3214
- after: '<p class="text-lg" />',
3215
- 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."
3216
- },
3217
- rewrite: {
3218
- dropClasses(computed2, ctx) {
3219
- const { winners, shadowed } = findRedundantClasses(computed2);
3220
- const drop = /* @__PURE__ */ new Set();
3221
- for (const cls of shadowed) {
3222
- if (winners.has(cls)) continue;
3223
- if (!ctx.resolver.selectorUsage(cls).droppable) continue;
3224
- drop.add(cls);
3225
- }
3226
- return drop;
3227
- }
3228
- },
3229
- test: {
3230
- cases: [
3231
- {
3232
- // `text-sm` is fully overridden by `text-lg` (both set font-size + line-height). The resolver
3233
- // records that shadowing in provenance and reports the Tailwind utility as droppable, so the
3234
- // pattern drops `text-sm`; the reverse-emit then re-derives the minimal set (`text-lg`).
3235
- before: '<p className="text-sm text-lg">Hi</p>',
3236
- after: '<p className="text-lg">Hi</p>'
3237
- }
3238
- ],
3239
- // Both tokens win a distinct property (no full override) → nothing to dedupe.
3240
- noMatch: ['<p className="text-lg font-bold">Hi</p>']
3241
- }
3242
- });
3243
-
3244
- // ../patterns/src/library/compress/gap-shorthand.pattern.ts
3245
- var ROW_GAP = "row-gap";
3246
- var COLUMN_GAP = "column-gap";
3247
- var GAP = "gap";
3248
- var BASE_KEY3 = conditionKey(BASE_CONDITION);
3249
- function withGapShorthand(sm, gapDecl) {
3250
- const blocks = /* @__PURE__ */ new Map();
3251
- for (const [key, block] of sm.blocks) {
3252
- if (key !== BASE_KEY3) {
3253
- blocks.set(key, block);
3254
- continue;
3255
- }
3256
- const decls = /* @__PURE__ */ new Map();
3257
- for (const [prop, decl] of block.decls) {
3258
- if (prop === ROW_GAP || prop === COLUMN_GAP) continue;
3259
- decls.set(prop, decl);
3260
- }
3261
- decls.set(gapDecl.property, gapDecl);
3262
- blocks.set(key, { condition: block.condition, decls });
3263
- }
3264
- return { blocks };
3265
- }
3266
- var gapShorthand = definePattern({
3267
- name: "gap-shorthand",
3268
- category: "compress/gap-shorthand",
3269
- safety: 1,
3270
- doc: {
3271
- title: "Collapse equal row/column gap into the `gap` shorthand",
3272
- 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-*).",
3273
- before: '<div style="row-gap:16px;column-gap:16px"/>',
3274
- after: '<div style="gap:16px"/>',
3275
- 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."
3276
- },
3277
- rewrite: {
3278
- rewriteClasses(computed2) {
3279
- const base = computed2.blocks.get(BASE_KEY3);
3280
- if (!base) return null;
3281
- const rowGap = base.decls.get(ROW_GAP);
3282
- const colGap = base.decls.get(COLUMN_GAP);
3283
- if (!rowGap || !colGap) return null;
3284
- if (rowGap.important !== colGap.important) return null;
3285
- if (rowGap.value !== colGap.value) return null;
3286
- const gapDecl = {
3287
- property: GAP,
3288
- value: rowGap.value,
3289
- important: rowGap.important,
3290
- relativeToParent: rowGap.relativeToParent || colGap.relativeToParent,
3291
- inherited: false
3292
- // gap is not an inherited property
3293
- };
3294
- return withGapShorthand(computed2, gapDecl);
3295
- }
3296
- },
3297
- test: {
3298
- cases: [
3299
- {
3300
- // Equal row/column gap collapse to a `gap` decl at the IR level; the minimizing reverse-emit
3301
- // re-expands `gap` to row-gap+column-gap and picks the single utility covering both (`gap-4`),
3302
- // replacing the `gap-x-4`+`gap-y-4` pair. `bg-red-200` is preserved.
3303
- before: '<div className="gap-x-4 gap-y-4 bg-red-200">box</div>',
3304
- after: '<div className="bg-red-200 gap-4">box</div>'
3305
- }
3306
- ],
3307
- // Unequal axes (row-gap != column-gap) have no single-value `gap` equivalent → not collapsed.
3308
- noMatch: ['<div className="gap-x-2 gap-y-4 bg-red-200">box</div>']
3309
- }
3310
- });
3311
-
3312
- // ../patterns/src/library/compress/inset-shorthand.pattern.ts
3313
- var TOP = "top";
3314
- var RIGHT = "right";
3315
- var BOTTOM = "bottom";
3316
- var LEFT = "left";
3317
- var INSET = "inset";
3318
- var INSET_BLOCK = "inset-block";
3319
- var INSET_INLINE = "inset-inline";
3320
- function sameSide(a, b) {
3321
- return a !== void 0 && b !== void 0 && a.value === b.value && a.important === b.important;
3322
- }
3323
- function asProperty(src, property) {
3324
- return { ...src, property, inherited: normalizer.inherited.isInherited(property) };
3325
- }
3326
- function withBaseDecls(src, baseDecls) {
3327
- const blocks = /* @__PURE__ */ new Map();
3328
- for (const [key, block] of src.blocks) {
3329
- const decls = key === BASE_CONDITION_KEY ? baseDecls : new Map(block.decls);
3330
- blocks.set(key, { condition: block.condition, decls });
3331
- }
3332
- return { blocks };
3333
- }
3334
- var insetShorthand = definePattern({
3335
- name: "inset-shorthand",
3336
- category: "compress/inset-shorthand",
3337
- safety: 2,
3338
- doc: {
3339
- title: "Compress inset longhands into a shorthand",
3340
- 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).",
3341
- before: '<div style="top:10px;right:10px;bottom:10px;left:10px"/>',
3342
- after: '<div style="inset:10px"/>',
3343
- 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."
3344
- },
3345
- rewrite: {
3346
- rewriteClasses(computed2) {
3347
- const base = computed2.blocks.get(BASE_CONDITION_KEY);
3348
- if (!base) return null;
3349
- const top = base.decls.get(TOP);
3350
- const right = base.decls.get(RIGHT);
3351
- const bottom = base.decls.get(BOTTOM);
3352
- const left = base.decls.get(LEFT);
3353
- const next = new Map(base.decls);
3354
- if (top && sameSide(top, right) && sameSide(top, bottom) && sameSide(top, left)) {
3355
- next.delete(TOP);
3356
- next.delete(RIGHT);
3357
- next.delete(BOTTOM);
3358
- next.delete(LEFT);
3359
- next.set(INSET, asProperty(top, INSET));
3360
- } else {
3361
- let collapsed = false;
3362
- if (sameSide(top, bottom)) {
3363
- next.delete(TOP);
3364
- next.delete(BOTTOM);
3365
- next.set(INSET_BLOCK, asProperty(top, INSET_BLOCK));
3366
- collapsed = true;
3367
- }
3368
- if (sameSide(left, right)) {
3369
- next.delete(LEFT);
3370
- next.delete(RIGHT);
3371
- next.set(INSET_INLINE, asProperty(left, INSET_INLINE));
3372
- collapsed = true;
3373
- }
3374
- if (!collapsed) return null;
3375
- }
3376
- return withBaseDecls(computed2, next);
3377
- }
3378
- },
3379
- test: {
3380
- cases: [
3381
- {
3382
- // The four equal inset longhands collapse to an `inset` shorthand at the IR level; the
3383
- // minimizing reverse-emit expands it back to top/right/bottom/left and picks the single utility
3384
- // covering all four (`inset-0`), replacing the four physical-side tokens. `bg-red-200` survives.
3385
- before: '<div className="top-0 right-0 bottom-0 left-0 bg-red-200">box</div>',
3386
- after: '<div className="bg-red-200 inset-0">box</div>'
3387
- }
3388
- ],
3389
- // No matching inset pair (all four distinct) → nothing collapses.
3390
- noMatch: ['<div className="top-0 right-1 bottom-2 left-3 bg-red-200">box</div>']
3391
- }
3392
- });
3393
-
3394
- // ../patterns/src/library/compress/margin-shorthand.pattern.ts
3395
- var MARGIN_SIDES = [
3396
- "margin-top",
3397
- "margin-right",
3398
- "margin-bottom",
3399
- "margin-left"
3400
- ];
3401
- var MARGIN_SIDE_SET = new Set(MARGIN_SIDES);
3402
- var BASE_KEY4 = conditionKey(BASE_CONDITION);
3403
- function collapseMarginValue(top, right, bottom, left) {
3404
- if (right === left) {
3405
- if (top === bottom) {
3406
- return top === right ? top : `${top} ${right}`;
3407
- }
3408
- return `${top} ${right} ${bottom}`;
3409
- }
3410
- return `${top} ${right} ${bottom} ${left}`;
3411
- }
3412
- function withFoldedMargin(sm, marginDecl) {
3413
- const blocks = /* @__PURE__ */ new Map();
3414
- for (const [key, block] of sm.blocks) {
3415
- if (key !== BASE_KEY4) {
3416
- blocks.set(key, block);
3417
- continue;
3418
- }
3419
- const decls = /* @__PURE__ */ new Map();
3420
- for (const [prop, decl] of block.decls) {
3421
- if (!MARGIN_SIDE_SET.has(String(prop))) decls.set(prop, decl);
3422
- }
3423
- decls.set(marginDecl.property, marginDecl);
3424
- blocks.set(key, { condition: block.condition, decls });
3425
- }
3426
- return { blocks };
3427
- }
3428
- var marginShorthand = definePattern({
3429
- name: "margin-shorthand",
3430
- category: "compress/margin-shorthand",
3431
- safety: 2,
3432
- doc: {
3433
- title: "Compress margin longhands into the `margin` shorthand",
3434
- 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.",
3435
- before: '<div style="margin-top:8px;margin-right:8px;margin-bottom:8px;margin-left:8px"/>',
3436
- after: '<div style="margin:8px"/>',
3437
- 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."
3438
- },
3439
- rewrite: {
3440
- rewriteClasses(computed2) {
3441
- const base = computed2.blocks.get(BASE_KEY4);
3442
- if (!base) return null;
3443
- const sides = MARGIN_SIDES.map((p) => base.decls.get(p));
3444
- if (sides.some((d) => d === void 0)) return null;
3445
- const [mt, mr, mb, ml] = sides;
3446
- if (mt.important || mr.important || mb.important || ml.important) return null;
3447
- const value = collapseMarginValue(
3448
- String(mt.value),
3449
- String(mr.value),
3450
- String(mb.value),
3451
- String(ml.value)
3452
- );
3453
- const marginDecl = {
3454
- property: "margin",
3455
- value,
3456
- important: false,
3457
- relativeToParent: mt.relativeToParent || mr.relativeToParent || mb.relativeToParent || ml.relativeToParent,
3458
- inherited: false
3459
- // margin is not an inherited property
3460
- };
3461
- return withFoldedMargin(computed2, marginDecl);
3462
- }
3463
- },
3464
- test: {
3465
- cases: [
3466
- {
3467
- // The four equal margin longhands collapse to a `margin` shorthand at the IR level, and the
3468
- // minimizing reverse-emit picks the single shortest utility (`m-2`) reproducing it, replacing
3469
- // the four `m{t,r,b,l}-2` tokens. `bg-red-200` is preserved.
3470
- before: '<div className="mt-2 mr-2 mb-2 ml-2 bg-red-200">box</div>',
3471
- after: '<div className="bg-red-200 m-2">box</div>'
3472
- }
3473
- ],
3474
- // Only two margin sides set → the four-longhand `margin` collapse does not apply.
3475
- noMatch: ['<div className="mt-2 mb-2 bg-red-200">box</div>']
3476
- }
3477
- });
3478
-
3479
- // ../patterns/src/library/compress/overflow-shorthand.pattern.ts
3480
- var OVERFLOW_X = "overflow-x";
3481
- var OVERFLOW_Y = "overflow-y";
3482
- var OVERFLOW = "overflow";
3483
- var BASE_KEY5 = conditionKey(BASE_CONDITION);
3484
- function withOverflowShorthand(sm, overflowDecl) {
3485
- const blocks = /* @__PURE__ */ new Map();
3486
- for (const [key, block] of sm.blocks) {
3487
- if (key !== BASE_KEY5) {
3488
- blocks.set(key, block);
3489
- continue;
3490
- }
3491
- const decls = /* @__PURE__ */ new Map();
3492
- for (const [prop, decl] of block.decls) {
3493
- if (prop === OVERFLOW_X || prop === OVERFLOW_Y) continue;
3494
- decls.set(prop, decl);
3495
- }
3496
- decls.set(overflowDecl.property, overflowDecl);
3497
- blocks.set(key, { condition: block.condition, decls });
3498
- }
3499
- return { blocks };
3500
- }
3501
- var overflowShorthand = definePattern({
3502
- name: "overflow-shorthand",
3503
- category: "compress/overflow-shorthand",
3504
- safety: 1,
3505
- doc: {
3506
- title: "Collapse equal overflow axes into the `overflow` shorthand",
3507
- 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-*).",
3508
- before: '<div style="overflow-x:auto;overflow-y:auto"/>',
3509
- after: '<div style="overflow:auto"/>',
3510
- 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."
3511
- },
3512
- rewrite: {
3513
- rewriteClasses(computed2) {
3514
- const base = computed2.blocks.get(BASE_KEY5);
3515
- if (!base) return null;
3516
- const overflowX = base.decls.get(OVERFLOW_X);
3517
- const overflowY = base.decls.get(OVERFLOW_Y);
3518
- if (!overflowX || !overflowY) return null;
3519
- if (overflowX.important !== overflowY.important) return null;
3520
- if (overflowX.value !== overflowY.value) return null;
3521
- const overflowDecl = {
3522
- property: OVERFLOW,
3523
- value: overflowX.value,
3524
- important: overflowX.important,
3525
- relativeToParent: overflowX.relativeToParent || overflowY.relativeToParent,
3526
- inherited: false
3527
- // overflow is not an inherited property
3528
- };
3529
- return withOverflowShorthand(computed2, overflowDecl);
3530
- }
3531
- },
3532
- test: {
3533
- cases: [
3534
- {
3535
- // Equal overflow axes collapse to an `overflow` decl at the IR level; the minimizing
3536
- // reverse-emit picks the single utility covering both (`overflow-auto`), replacing the
3537
- // `overflow-x-auto`+`overflow-y-auto` pair. `bg-red-200` is preserved.
3538
- before: '<div className="overflow-x-auto overflow-y-auto bg-red-200">box</div>',
3539
- after: '<div className="bg-red-200 overflow-auto">box</div>'
3540
- }
3541
- ],
3542
- // Mismatched axes (overflow-x != overflow-y) have no single-keyword equivalent → not collapsed.
3543
- noMatch: ['<div className="overflow-x-auto overflow-y-hidden bg-red-200">box</div>']
3544
- }
3545
- });
3546
-
3547
- // ../patterns/src/library/compress/overscroll-behavior-shorthand.pattern.ts
3548
- var OVERSCROLL_X = "overscroll-behavior-x";
3549
- var OVERSCROLL_Y = "overscroll-behavior-y";
3550
- var OVERSCROLL = "overscroll-behavior";
3551
- var BASE_KEY6 = conditionKey(BASE_CONDITION);
3552
- var NON_COLLAPSIBLE_VALUES2 = /* @__PURE__ */ new Set([
3553
- "initial",
3554
- "inherit",
3555
- "unset",
3556
- "revert",
3557
- "revert-layer"
3558
- ]);
3559
- function withOverscrollShorthand(sm, shorthand) {
3560
- const blocks = /* @__PURE__ */ new Map();
3561
- for (const [key, block] of sm.blocks) {
3562
- if (key !== BASE_KEY6) {
3563
- blocks.set(key, block);
3564
- continue;
3565
- }
3566
- const decls = /* @__PURE__ */ new Map();
3567
- for (const [prop, decl] of block.decls) {
3568
- if (prop === OVERSCROLL_X || prop === OVERSCROLL_Y) continue;
3569
- decls.set(prop, decl);
3570
- }
3571
- decls.set(shorthand.property, shorthand);
3572
- blocks.set(key, { condition: block.condition, decls });
3573
- }
3574
- return { blocks };
3575
- }
3576
- var overscrollBehaviorShorthand = definePattern({
3577
- name: "overscroll-behavior-shorthand",
3578
- category: "compress/overscroll-behavior-shorthand",
3579
- safety: 1,
3580
- doc: {
3581
- title: "Collapse equal overscroll-behavior axes into overscroll-behavior",
3582
- 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-*).",
3583
- before: '<div style="overscroll-behavior-x:contain;overscroll-behavior-y:contain"/>',
3584
- after: '<div class="overscroll-contain"/>',
3585
- 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."
3586
- },
3587
- rewrite: {
3588
- rewriteClasses(computed2) {
3589
- const base = computed2.blocks.get(BASE_KEY6);
3590
- if (!base) return null;
3591
- const x = base.decls.get(OVERSCROLL_X);
3592
- const y = base.decls.get(OVERSCROLL_Y);
3593
- if (!x || !y) return null;
3594
- if (x.important !== y.important) return null;
3595
- const value = String(x.value);
3596
- if (NON_COLLAPSIBLE_VALUES2.has(value)) return null;
3597
- if (value !== String(y.value)) return null;
3598
- const shorthand = {
3599
- property: OVERSCROLL,
3600
- value: x.value,
3601
- important: x.important,
3602
- relativeToParent: x.relativeToParent || y.relativeToParent,
3603
- inherited: false
3604
- // overscroll-behavior is not an inherited property
3605
- };
3606
- return withOverscrollShorthand(computed2, shorthand);
3607
- }
3608
- },
3609
- test: {
3610
- cases: [
3611
- {
3612
- // Equal x/y axes collapse to an `overscroll-behavior` decl at the IR level; the minimizing
3613
- // reverse-emit picks the single utility covering both (`overscroll-contain`), replacing the
3614
- // `overscroll-x-contain`+`overscroll-y-contain` pair. `bg-red-200` is preserved.
3615
- before: '<div className="overscroll-x-contain overscroll-y-contain bg-red-200">box</div>',
3616
- after: '<div className="bg-red-200 overscroll-contain">box</div>'
3617
- }
3618
- ],
3619
- // Axes differ (x != y) → no equal-axis collapse.
3620
- noMatch: ['<div className="overscroll-x-contain overscroll-y-auto bg-red-200">box</div>']
3621
- }
3622
- });
3623
-
3624
- // ../patterns/src/library/compress/padding-shorthand.pattern.ts
3625
- var PADDING_SIDES = [
3626
- "padding-top",
3627
- "padding-right",
3628
- "padding-bottom",
3629
- "padding-left"
3630
- ];
3631
- var PADDING_SIDE_SET = new Set(PADDING_SIDES);
3632
- var BASE_KEY7 = conditionKey(BASE_CONDITION);
3633
- function analyzePadding(sm) {
3634
- const block = sm.blocks.get(BASE_KEY7);
3635
- if (!block) return null;
3636
- const sides = [];
3637
- for (const side of PADDING_SIDES) {
3638
- const decl = block.decls.get(side);
3639
- if (!decl) return null;
3640
- sides.push(decl);
3641
- }
3642
- const [top, right, bottom, left] = sides;
3643
- if (!(top.important === right.important && right.important === bottom.important && bottom.important === left.important)) {
3644
- return null;
3645
- }
3646
- const tv = String(top.value);
3647
- const rv = String(right.value);
3648
- const bv = String(bottom.value);
3649
- const lv = String(left.value);
3650
- if (tv !== bv || lv !== rv) return null;
3651
- const value = tv === lv ? tv : `${tv} ${lv}`;
3652
- const relative = sides.some((d) => d.relativeToParent);
3653
- return { value, important: top.important, relative };
3654
- }
3655
- function withFoldedPadding(sm, fold) {
3656
- const blocks = /* @__PURE__ */ new Map();
3657
- for (const [key, block] of sm.blocks) {
3658
- if (key !== BASE_KEY7) {
3659
- blocks.set(key, block);
3660
- continue;
3661
- }
3662
- const decls = /* @__PURE__ */ new Map();
3663
- for (const [prop, decl] of block.decls) {
3664
- if (PADDING_SIDE_SET.has(String(prop))) continue;
3665
- decls.set(prop, decl);
3666
- }
3667
- const shorthand = {
3668
- property: "padding",
3669
- value: fold.value,
3670
- important: fold.important,
3671
- relativeToParent: fold.relative,
3672
- inherited: false
3673
- // padding is never inherited
3674
- };
3675
- decls.set(shorthand.property, shorthand);
3676
- blocks.set(key, { condition: block.condition, decls });
3677
- }
3678
- return { blocks };
3679
- }
3680
- var paddingShorthand = definePattern({
3681
- name: "padding-shorthand",
3682
- category: "compress/padding-shorthand",
3683
- safety: 1,
3684
- doc: {
3685
- title: "Collapse padding longhands to shorthand",
3686
- 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-*).",
3687
- before: '<div class="pt-4 pr-4 pb-4 pl-4"/>',
3688
- after: '<div class="p-4"/>',
3689
- 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."
3690
- },
3691
- rewrite: {
3692
- rewriteClasses(computed2) {
3693
- const fold = analyzePadding(computed2);
3694
- return fold ? withFoldedPadding(computed2, fold) : null;
3695
- }
3696
- },
3697
- test: {
3698
- cases: [
3699
- {
3700
- // The four equal padding longhands collapse to a `padding` shorthand at the IR level, and the
3701
- // minimizing reverse-emit picks the single shortest utility (`p-4`) that reproduces it,
3702
- // replacing the four `p{t,r,b,l}-4` tokens. `bg-red-200` is preserved (its order is stable).
3703
- before: '<div className="pt-4 pr-4 pb-4 pl-4 bg-red-200">box</div>',
3704
- after: '<div className="bg-red-200 p-4">box</div>'
3705
- },
3706
- {
3707
- // A dynamic `{x}` child no longer blocks compress: only the element's OWN class tokens are
3708
- // rewritten (px-4 py-4 → p-4); the dynamic child is untouched by a class-only change. This is
3709
- // the real-app common case (most elements have dynamic content).
3710
- before: '<div className="px-4 py-4">{x}</div>',
3711
- after: '<div className="p-4">{x}</div>'
3712
- }
3713
- ],
3714
- // Asymmetric padding (top != bottom) cannot fold into a shorthand → left unchanged.
3715
- noMatch: ['<div className="pt-2 pr-4 pb-8 pl-4 bg-red-200">box</div>']
3716
- }
3717
- });
3718
-
3719
- // ../patterns/src/library/compress/place-shorthand.pattern.ts
3720
- var ALIGN_ITEMS2 = "align-items";
3721
- var JUSTIFY_ITEMS2 = "justify-items";
3722
- var PLACE_ITEMS2 = "place-items";
3723
- var ALIGN_CONTENT = "align-content";
3724
- var JUSTIFY_CONTENT2 = "justify-content";
3725
- var PLACE_CONTENT = "place-content";
3726
- var BASE_KEY8 = conditionKey(BASE_CONDITION);
3727
- function samePair(a, b) {
3728
- return a !== void 0 && b !== void 0 && a.value === b.value && a.important === b.important;
3729
- }
3730
- function placeDecl(property, align) {
3731
- return {
3732
- property,
3733
- value: align.value,
3734
- important: align.important,
3735
- relativeToParent: false,
3736
- // alignment keywords (center/start/stretch/…) are not length-relative
3737
- inherited: false
3738
- // none of the place-* alignment properties are inherited
3739
- };
3740
- }
3741
- function withBaseDecls2(sm, baseDecls) {
3742
- const blocks = /* @__PURE__ */ new Map();
3743
- for (const [key, block] of sm.blocks) {
3744
- const decls = key === BASE_KEY8 ? new Map(baseDecls) : block.decls;
3745
- blocks.set(key, { condition: block.condition, decls });
3746
- }
3747
- return { blocks };
3748
- }
3749
- var placeShorthand = definePattern({
3750
- name: "place-shorthand",
3751
- category: "compress/place-shorthand",
3752
- safety: 1,
3753
- doc: {
3754
- title: "Collapse matching alignment pairs into `place-*` shorthands",
3755
- 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.",
3756
- before: '<div style="align-items:center;justify-items:center"/>',
3757
- after: '<div style="place-items:center"/>',
3758
- 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."
3759
- },
3760
- rewrite: {
3761
- rewriteClasses(computed2) {
3762
- const base = computed2.blocks.get(BASE_KEY8);
3763
- if (!base) return null;
3764
- const alignItems = base.decls.get(ALIGN_ITEMS2);
3765
- const justifyItems = base.decls.get(JUSTIFY_ITEMS2);
3766
- const alignContent = base.decls.get(ALIGN_CONTENT);
3767
- const justifyContent = base.decls.get(JUSTIFY_CONTENT2);
3768
- const next = new Map(base.decls);
3769
- let collapsed = false;
3770
- if (samePair(alignItems, justifyItems)) {
3771
- next.delete(ALIGN_ITEMS2);
3772
- next.delete(JUSTIFY_ITEMS2);
3773
- next.set(PLACE_ITEMS2, placeDecl(PLACE_ITEMS2, alignItems));
3774
- collapsed = true;
3775
- }
3776
- if (samePair(alignContent, justifyContent)) {
3777
- next.delete(ALIGN_CONTENT);
3778
- next.delete(JUSTIFY_CONTENT2);
3779
- next.set(PLACE_CONTENT, placeDecl(PLACE_CONTENT, alignContent));
3780
- collapsed = true;
3781
- }
3782
- if (!collapsed) return null;
3783
- return withBaseDecls2(computed2, next);
3784
- }
3785
- },
3786
- test: {
3787
- cases: [
3788
- {
3789
- // The matching items pair collapses to a `place-items` decl at the IR level; the minimizing
3790
- // reverse-emit picks the single utility covering both (`place-items-center`), replacing the
3791
- // `items-center`+`justify-items-center` pair. `bg-red-200` is preserved.
3792
- before: '<div className="items-center justify-items-center bg-red-200">box</div>',
3793
- after: '<div className="bg-red-200 place-items-center">box</div>'
3794
- }
3795
- ],
3796
- // Mismatched alignment (align-items != justify-items, no content pair) → nothing collapses.
3797
- noMatch: ['<div className="items-center justify-items-start bg-red-200">box</div>']
3798
- }
3799
- });
3800
-
3801
- // ../patterns/src/library/compress/scroll-margin-shorthand.pattern.ts
3802
- var SCROLL_MARGIN_SIDES = [
3803
- "scroll-margin-top",
3804
- "scroll-margin-right",
3805
- "scroll-margin-bottom",
3806
- "scroll-margin-left"
3807
- ];
3808
- var SIDE_SET = new Set(SCROLL_MARGIN_SIDES);
3809
- var BASE_KEY9 = conditionKey(BASE_CONDITION);
3810
- var SCROLL_MARGIN = "scroll-margin";
3811
- var NON_COLLAPSIBLE_VALUES3 = /* @__PURE__ */ new Set([
3812
- "initial",
3813
- "inherit",
3814
- "unset",
3815
- "revert",
3816
- "revert-layer"
3817
- ]);
3818
- function analyzeScrollMargin(sm) {
3819
- const block = sm.blocks.get(BASE_KEY9);
3820
- if (!block) return null;
3821
- const sides = [];
3822
- for (const side of SCROLL_MARGIN_SIDES) {
3823
- const decl = block.decls.get(side);
3824
- if (!decl) return null;
3825
- sides.push(decl);
3826
- }
3827
- const important = sides[0].important;
3828
- if (!sides.every((d) => d.important === important)) return null;
3829
- const value = String(sides[0].value);
3830
- if (NON_COLLAPSIBLE_VALUES3.has(value)) return null;
3831
- if (!sides.every((d) => String(d.value) === value)) return null;
3832
- const relative = sides.some((d) => d.relativeToParent);
3833
- return { value, important, relative };
3834
- }
3835
- function withFoldedScrollMargin(sm, fold) {
3836
- const blocks = /* @__PURE__ */ new Map();
3837
- for (const [key, block] of sm.blocks) {
3838
- if (key !== BASE_KEY9) {
3839
- blocks.set(key, block);
3840
- continue;
3841
- }
3842
- const decls = /* @__PURE__ */ new Map();
3843
- for (const [prop, decl] of block.decls) {
3844
- if (SIDE_SET.has(String(prop))) continue;
3845
- decls.set(prop, decl);
3846
- }
3847
- const shorthand = {
3848
- property: SCROLL_MARGIN,
3849
- value: fold.value,
3850
- important: fold.important,
3851
- relativeToParent: fold.relative,
3852
- inherited: false
3853
- // scroll-margin is never inherited
3854
- };
3855
- decls.set(shorthand.property, shorthand);
3856
- blocks.set(key, { condition: block.condition, decls });
3857
- }
3858
- return { blocks };
3859
- }
3860
- var scrollMarginShorthand = definePattern({
3861
- name: "scroll-margin-shorthand",
3862
- category: "compress/scroll-margin-shorthand",
3863
- safety: 1,
3864
- doc: {
3865
- title: "Collapse equal scroll-margin sides into scroll-margin",
3866
- 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).",
3867
- before: '<div class="scroll-mt-4 scroll-mr-4 scroll-mb-4 scroll-ml-4"/>',
3868
- after: '<div class="scroll-m-4"/>',
3869
- 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."
3870
- },
3871
- rewrite: {
3872
- rewriteClasses(computed2) {
3873
- const fold = analyzeScrollMargin(computed2);
3874
- return fold ? withFoldedScrollMargin(computed2, fold) : null;
3875
- }
3876
- },
3877
- test: {
3878
- cases: [
3879
- {
3880
- // The four equal scroll-margin longhands collapse to a `scroll-margin` decl at the IR level; the
3881
- // minimizing reverse-emit then picks the single shortest utility (`scroll-m-4`) that reproduces
3882
- // it, replacing the four `scroll-m{t,r,b,l}-4` tokens. `bg-red-200` is preserved.
3883
- before: '<div className="scroll-mt-4 scroll-mr-4 scroll-mb-4 scroll-ml-4 bg-red-200">box</div>',
3884
- after: '<div className="bg-red-200 scroll-m-4">box</div>'
3885
- }
3886
- ],
3887
- // Sides differ (top != bottom) → no all-equal collapse.
3888
- noMatch: ['<div className="scroll-mt-2 scroll-mr-4 scroll-mb-8 scroll-ml-4 bg-red-200">box</div>']
3889
- }
3890
- });
3891
-
3892
- // ../patterns/src/library/compress/scroll-padding-shorthand.pattern.ts
3893
- var SCROLL_PADDING_SIDES = [
3894
- "scroll-padding-top",
3895
- "scroll-padding-right",
3896
- "scroll-padding-bottom",
3897
- "scroll-padding-left"
3898
- ];
3899
- var SIDE_SET2 = new Set(SCROLL_PADDING_SIDES);
3900
- var BASE_KEY10 = conditionKey(BASE_CONDITION);
3901
- var SCROLL_PADDING = "scroll-padding";
3902
- var NON_COLLAPSIBLE_VALUES4 = /* @__PURE__ */ new Set([
3903
- "initial",
3904
- "inherit",
3905
- "unset",
3906
- "revert",
3907
- "revert-layer"
3908
- ]);
3909
- function analyzeScrollPadding(sm) {
3910
- const block = sm.blocks.get(BASE_KEY10);
3911
- if (!block) return null;
3912
- const sides = [];
3913
- for (const side of SCROLL_PADDING_SIDES) {
3914
- const decl = block.decls.get(side);
3915
- if (!decl) return null;
3916
- sides.push(decl);
3917
- }
3918
- const important = sides[0].important;
3919
- if (!sides.every((d) => d.important === important)) return null;
3920
- const value = String(sides[0].value);
3921
- if (NON_COLLAPSIBLE_VALUES4.has(value)) return null;
3922
- if (!sides.every((d) => String(d.value) === value)) return null;
3923
- const relative = sides.some((d) => d.relativeToParent);
3924
- return { value, important, relative };
3925
- }
3926
- function withFoldedScrollPadding(sm, fold) {
3927
- const blocks = /* @__PURE__ */ new Map();
3928
- for (const [key, block] of sm.blocks) {
3929
- if (key !== BASE_KEY10) {
3930
- blocks.set(key, block);
3931
- continue;
3932
- }
3933
- const decls = /* @__PURE__ */ new Map();
3934
- for (const [prop, decl] of block.decls) {
3935
- if (SIDE_SET2.has(String(prop))) continue;
3936
- decls.set(prop, decl);
3937
- }
3938
- const shorthand = {
3939
- property: SCROLL_PADDING,
3940
- value: fold.value,
3941
- important: fold.important,
3942
- relativeToParent: fold.relative,
3943
- inherited: false
3944
- // scroll-padding is never inherited
3945
- };
3946
- decls.set(shorthand.property, shorthand);
3947
- blocks.set(key, { condition: block.condition, decls });
3948
- }
3949
- return { blocks };
3950
- }
3951
- var scrollPaddingShorthand = definePattern({
3952
- name: "scroll-padding-shorthand",
3953
- category: "compress/scroll-padding-shorthand",
3954
- safety: 1,
3955
- doc: {
3956
- title: "Collapse equal scroll-padding sides into scroll-padding",
3957
- 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).",
3958
- before: '<div class="scroll-pt-4 scroll-pr-4 scroll-pb-4 scroll-pl-4"/>',
3959
- after: '<div class="scroll-p-4"/>',
3960
- 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."
3961
- },
3962
- rewrite: {
3963
- rewriteClasses(computed2) {
3964
- const fold = analyzeScrollPadding(computed2);
3965
- return fold ? withFoldedScrollPadding(computed2, fold) : null;
3966
- }
2883
+ not(hasSpreadAttrs2),
2884
+ not(isComponentNode3),
2885
+ not(targetedByStructuralPseudo3)
2886
+ ]
3967
2887
  },
2888
+ rewrite: { flattenInto: "child" },
3968
2889
  test: {
3969
2890
  cases: [
3970
2891
  {
3971
- // The four equal scroll-padding longhands collapse to a `scroll-padding` decl at the IR level;
3972
- // the minimizing reverse-emit then picks the single shortest utility (`scroll-p-4`) that
3973
- // reproduces it, replacing the four `scroll-p{t,r,b,l}-4` tokens. `bg-red-200` is preserved.
3974
- before: '<div className="scroll-pt-4 scroll-pr-4 scroll-pb-4 scroll-pl-4 bg-red-200">box</div>',
3975
- after: '<div className="bg-red-200 scroll-p-4">box</div>'
2892
+ // A plain, style-free wrapper paints nothing and establishes no context a provably-safe
2893
+ // flatten under the conservative gate: the wrapper is removed and its sole child hoisted.
2894
+ before: '<div><a className="bg-red-200">Link</a></div>',
2895
+ after: '<a className="bg-red-200">Link</a>'
3976
2896
  }
3977
2897
  ],
3978
- // Sides differ (top != bottom) → no all-equal collapse.
3979
- noMatch: ['<div className="scroll-pt-2 scroll-pr-4 scroll-pb-8 scroll-pl-4 bg-red-200">box</div>']
2898
+ noMatch: [
2899
+ // A ref pins the wrapper's element identity (a hard opacity barrier) → not a passthrough.
2900
+ '<div ref={rootRef}><a className="bg-red-200">Link</a></div>',
2901
+ // A `display:flex` wrapper establishes a formatting context, so removing its box is NOT
2902
+ // provably layout-neutral → the conservative gate leaves it in place.
2903
+ '<div className="flex"><a className="bg-red-200">Link</a></div>'
2904
+ ]
3980
2905
  }
3981
2906
  });
3982
2907
 
3983
- // ../patterns/src/library/compress/size-shorthand.pattern.ts
3984
- var WIDTH = "width";
3985
- var HEIGHT = "height";
3986
- var SIZE = "size";
3987
- var NON_COLLAPSIBLE_VALUES5 = /* @__PURE__ */ new Set(["auto", "initial", "unset"]);
3988
- function baseBlock(sm) {
3989
- return sm.blocks.get(conditionKey(BASE_CONDITION));
3990
- }
3991
- function withSizeShorthand(sm, value, important) {
3992
- const baseKey = conditionKey(BASE_CONDITION);
3993
- const blocks = /* @__PURE__ */ new Map();
3994
- for (const [key, block] of sm.blocks) {
3995
- if (key !== baseKey) {
3996
- blocks.set(key, block);
3997
- continue;
3998
- }
3999
- const decls = new Map(block.decls);
4000
- decls.delete(WIDTH);
4001
- decls.delete(HEIGHT);
4002
- for (const decl of normalizer.normalizeDeclaration(String(SIZE), value, important)) {
4003
- decls.set(decl.property, decl);
4004
- }
4005
- blocks.set(key, { condition: block.condition, decls });
4006
- }
4007
- return { blocks };
2908
+ // ../patterns/src/library/wrapper/redundant-inline-wrapper.pattern.ts
2909
+ function asEl3(node) {
2910
+ const n = node;
2911
+ return n.kind === "element" ? n : null;
4008
2912
  }
4009
- var sizeShorthand = definePattern({
4010
- name: "size-shorthand",
4011
- category: "compress/size-shorthand",
2913
+ function metaOf3(node) {
2914
+ return asEl3(node)?.meta ?? null;
2915
+ }
2916
+ var establishesContext2 = (node) => {
2917
+ const m = metaOf3(node);
2918
+ if (!m) return false;
2919
+ return m.establishesBox || m.establishesFormattingContext || m.establishesStackingContext || m.isContainingBlock || m.declaresCustomProperties;
2920
+ };
2921
+ var hasSpreadAttrs3 = (node) => metaOf3(node)?.hasSpreadAttrs ?? false;
2922
+ var isComponentNode4 = (node) => metaOf3(node)?.isComponent ?? false;
2923
+ var hasOwnAttrs3 = (node) => {
2924
+ const el = asEl3(node);
2925
+ if (!el) return false;
2926
+ return el.attrs.entries.size > 0 || el.attrs.spreads.length > 0;
2927
+ };
2928
+ var targetedByStructuralPseudo4 = (node, ctx) => {
2929
+ const el = asEl3(node);
2930
+ if (!el) return false;
2931
+ if (el.meta.targetedByStructuralPseudo) return true;
2932
+ return ctx.selectors.targetedByStructuralPseudo(el.id);
2933
+ };
2934
+ var DISPLAY3 = "display";
2935
+ var hasNonInlineDisplay = (node, ctx) => {
2936
+ const el = asEl3(node);
2937
+ if (!el) return false;
2938
+ const sm = ctx.computedOf(el) ?? el.computed;
2939
+ for (const block of sm.blocks.values()) {
2940
+ const decl = block.decls.get(DISPLAY3);
2941
+ if (decl && String(decl.value) !== "inline") return true;
2942
+ }
2943
+ return false;
2944
+ };
2945
+ var redundantInlineWrapper = definePattern({
2946
+ name: "redundant-inline-wrapper",
2947
+ category: "flatten/wrapper/redundant-inline-wrapper",
4012
2948
  safety: 2,
4013
2949
  doc: {
4014
- title: "Collapse equal width/height into size-*",
4015
- 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).",
4016
- before: '<div style="width:1rem;height:1rem"/>',
4017
- after: '<div class="size-4"/>',
4018
- 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."
2950
+ title: "Flatten redundant inline wrapper",
2951
+ 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.",
2952
+ before: "<span><Child/></span>",
2953
+ after: "<Child/>",
2954
+ 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."
4019
2955
  },
4020
- rewrite: {
4021
- rewriteClasses(computed2) {
4022
- const base = baseBlock(computed2);
4023
- const w = base?.decls.get(WIDTH);
4024
- const h = base?.decls.get(HEIGHT);
4025
- if (!w || !h) return null;
4026
- if (w.important !== h.important) return null;
4027
- if (NON_COLLAPSIBLE_VALUES5.has(String(w.value))) return null;
4028
- if (w.value !== h.value) return null;
4029
- return withSizeShorthand(computed2, String(w.value), w.important);
4030
- }
2956
+ match: {
2957
+ tag: "span",
2958
+ onlyChild: "element",
2959
+ paintsNothing: true,
2960
+ where: [
2961
+ not(hasNonInlineDisplay),
2962
+ not(establishesContext2),
2963
+ not(hasOwnAttrs3),
2964
+ not(hasDynamicClasses),
2965
+ not(hasSpreadAttrs3),
2966
+ not(isComponentNode4),
2967
+ not(targetedByStructuralPseudo4)
2968
+ ]
4031
2969
  },
2970
+ rewrite: { flattenInto: "child" },
4032
2971
  test: {
4033
2972
  cases: [
4034
2973
  {
4035
- // Equal width/height collapse to a `size` decl at the IR level; the minimizing reverse-emit
4036
- // expands `size` back to width+height, finds the single utility covering both (`size-10`), and
4037
- // replaces the `h-10`+`w-10` pair with it. `bg-red-200` is preserved.
4038
- before: '<div className="h-10 w-10 bg-red-200">box</div>',
4039
- after: '<div className="bg-red-200 size-10">box</div>'
2974
+ // An empty inline span paints nothing and establishes no context a provably-safe flatten:
2975
+ // the span is removed and its sole child hoisted in place.
2976
+ before: '<span><a className="text-blue-500">Link</a></span>',
2977
+ after: '<a className="text-blue-500">Link</a>'
4040
2978
  }
4041
2979
  ],
4042
- // Width and height differ → no equal-axis collapse.
4043
- noMatch: ['<div className="h-10 w-20 bg-red-200">box</div>']
2980
+ noMatch: [
2981
+ // A ref pins the span's element identity (a hard opacity barrier) → not a passthrough.
2982
+ '<span ref={spanRef}><a className="text-blue-500">Link</a></span>',
2983
+ // The span paints its own background (own visual style) → kept.
2984
+ '<span className="bg-green-200"><a className="text-blue-500">Link</a></span>',
2985
+ // Non-inline display (inline-block) participates in layout differently → kept.
2986
+ '<span className="inline-block"><a className="text-blue-500">Link</a></span>'
2987
+ ]
4044
2988
  }
4045
2989
  });
4046
2990
 
4047
2991
  // ../patterns/src/_registry.generated.ts
4048
2992
  var builtinPatterns = [
2993
+ flexCenterWrapper,
2994
+ redundantFragment,
2995
+ gridCenterWrapper,
4049
2996
  displayContentsWrapper,
4050
2997
  emptyStyleDiv,
4051
- flexCenterWrapper,
4052
- inlineFlexCenterWrapper,
4053
- nestedFlexMerge,
4054
- nestedGridMerge,
2998
+ inheritedOnlyWrapper,
4055
2999
  passthroughWrapper,
4056
- redundantFragment,
4057
- redundantInlineWrapper,
4058
- borderRadiusShorthand,
4059
- borderShorthand,
4060
- dedupeClasses,
4061
- gapShorthand,
4062
- insetShorthand,
4063
- marginShorthand,
4064
- overflowShorthand,
4065
- overscrollBehaviorShorthand,
4066
- paddingShorthand,
4067
- placeShorthand,
4068
- scrollMarginShorthand,
4069
- scrollPaddingShorthand,
4070
- sizeShorthand
3000
+ redundantInlineWrapper
4071
3001
  ];
4072
3002
 
4073
3003
  // ../resolver-tailwind/src/tailwind/fingerprint.ts
@@ -4118,14 +3048,312 @@ function synthesizeResidual(remaining, ctx) {
4118
3048
 
4119
3049
  // ../resolver-tailwind/src/tailwind/engine.ts
4120
3050
  var import_node_module = require("module");
3051
+ var path3 = __toESM(require("path"), 1);
3052
+
3053
+ // ../resolver-tailwind/src/tailwind/engine-v4.ts
3054
+ var import_node_fs2 = require("fs");
3055
+ var path2 = __toESM(require("path"), 1);
3056
+
3057
+ // ../resolver-tailwind/src/tailwind/v4-bridge.ts
3058
+ var import_node_child_process = require("child_process");
3059
+ var import_node_fs = require("fs");
3060
+ var import_node_os = require("os");
4121
3061
  var path = __toESM(require("path"), 1);
3062
+ var CHILD_SOURCE = String.raw`
3063
+ import { createRequire } from 'node:module';
3064
+ import { pathToFileURL } from 'node:url';
3065
+ import * as fs from 'node:fs';
3066
+ import * as path from 'node:path';
3067
+
3068
+ function out(obj) { process.stdout.write(JSON.stringify(obj)); process.exit(0); }
3069
+
3070
+ let payload;
3071
+ try { payload = JSON.parse(fs.readFileSync(process.argv[2], 'utf8')); }
3072
+ catch { out({ ok: false }); }
3073
+
3074
+ const projectRoot = payload.projectRoot;
3075
+ const entries = payload.entries || [];
3076
+ const req = createRequire(path.join(projectRoot, '__domflax_tw4__.js'));
3077
+
3078
+ async function importFrom(id) {
3079
+ const resolved = req.resolve(id);
3080
+ return import(pathToFileURL(resolved).href);
3081
+ }
3082
+
3083
+ // Primary loader: @tailwindcss/node (the companion every v4 build tool installs). It resolves
3084
+ // '@import "tailwindcss"' and @theme against the project on disk.
3085
+ async function loadViaNode() {
3086
+ let mod;
3087
+ try { mod = await importFrom('@tailwindcss/node'); } catch { return null; }
3088
+ if (!mod || typeof mod.__unstable__loadDesignSystem !== 'function') return null;
3089
+ for (const e of entries) {
3090
+ try { return await mod.__unstable__loadDesignSystem(e.css, { base: e.base }); } catch {}
3091
+ }
3092
+ return null;
3093
+ }
3094
+
3095
+ // Secondary loader: bare 'tailwindcss' with a filesystem stylesheet resolver (best-effort).
3096
+ async function loadViaCore() {
3097
+ let tw;
3098
+ try { tw = await importFrom('tailwindcss'); } catch { return null; }
3099
+ if (!tw || typeof tw.__unstable__loadDesignSystem !== 'function') return null;
3100
+ const loadStylesheet = async (id, base) => {
3101
+ const r = createRequire(path.join(base, '__domflax_tw4__.js'));
3102
+ let p;
3103
+ const tries = id === 'tailwindcss' ? ['tailwindcss/index.css', 'tailwindcss'] : [id, id + '/index.css'];
3104
+ for (const t of tries) { try { p = r.resolve(t); break; } catch {} }
3105
+ if (!p) p = path.resolve(base, id);
3106
+ return { path: p, base: path.dirname(p), content: fs.readFileSync(p, 'utf8') };
3107
+ };
3108
+ const loadModule = async (id, base) => {
3109
+ const r = createRequire(path.join(base, '__domflax_tw4__.js'));
3110
+ const p = r.resolve(id);
3111
+ return { path: p, base: path.dirname(p), module: (await import(pathToFileURL(p).href)).default };
3112
+ };
3113
+ for (const e of entries) {
3114
+ try { return await tw.__unstable__loadDesignSystem(e.css, { base: e.base, loadStylesheet, loadModule }); } catch {}
3115
+ }
3116
+ return null;
3117
+ }
3118
+
3119
+ const ds = (await loadViaNode()) || (await loadViaCore());
3120
+ if (!ds) out({ ok: false });
3121
+
3122
+ let names = [];
3123
+ try {
3124
+ names = ds.getClassList().map((e) => (Array.isArray(e) ? e[0] : e)).filter((n) => typeof n === 'string');
3125
+ } catch { out({ ok: false }); }
3126
+
3127
+ let css = [];
3128
+ try { css = ds.candidatesToCss(names); } catch { out({ ok: false }); }
3129
+
3130
+ const result = [];
3131
+ for (let i = 0; i < names.length; i += 1) {
3132
+ const c = css[i];
3133
+ if (typeof c === 'string' && c.length > 0) result.push([names[i], c]);
3134
+ }
3135
+ out({ ok: true, entries: result });
3136
+ `;
3137
+ function runV4Bridge(payload) {
3138
+ let dir = null;
3139
+ try {
3140
+ dir = (0, import_node_fs.mkdtempSync)(path.join((0, import_node_os.tmpdir)(), "domflax-tw4-"));
3141
+ const scriptPath = path.join(dir, "bridge.mjs");
3142
+ const payloadPath = path.join(dir, "payload.json");
3143
+ (0, import_node_fs.writeFileSync)(scriptPath, CHILD_SOURCE, "utf8");
3144
+ (0, import_node_fs.writeFileSync)(payloadPath, JSON.stringify(payload), "utf8");
3145
+ const stdout = (0, import_node_child_process.execFileSync)(process.execPath, [scriptPath, payloadPath], {
3146
+ cwd: payload.projectRoot,
3147
+ encoding: "utf8",
3148
+ timeout: 9e4,
3149
+ maxBuffer: 256 * 1024 * 1024,
3150
+ stdio: ["ignore", "pipe", "ignore"]
3151
+ });
3152
+ const parsed = JSON.parse(stdout);
3153
+ if (!parsed.ok || !Array.isArray(parsed.entries) || parsed.entries.length === 0) return null;
3154
+ const entries = parsed.entries.filter(
3155
+ (e) => Array.isArray(e) && typeof e[0] === "string" && typeof e[1] === "string"
3156
+ );
3157
+ return entries.length > 0 ? { entries } : null;
3158
+ } catch {
3159
+ return null;
3160
+ } finally {
3161
+ if (dir) {
3162
+ try {
3163
+ (0, import_node_fs.rmSync)(dir, { recursive: true, force: true });
3164
+ } catch {
3165
+ }
3166
+ }
3167
+ }
3168
+ }
3169
+
3170
+ // ../resolver-tailwind/src/tailwind/v4-css.ts
3171
+ function stripComments(src) {
3172
+ return src.replace(/\/\*[\s\S]*?\*\//g, "");
3173
+ }
3174
+ function toDecl(buffer) {
3175
+ const buf = buffer.trim();
3176
+ if (buf.length === 0 || buf[0] === "@") return null;
3177
+ const colon = buf.indexOf(":");
3178
+ if (colon <= 0) return null;
3179
+ const prop = buf.slice(0, colon).trim();
3180
+ let value = buf.slice(colon + 1).trim();
3181
+ if (prop.length === 0 || value.length === 0) return null;
3182
+ let important = false;
3183
+ const bang = /!\s*important\s*$/i.exec(value);
3184
+ if (bang) {
3185
+ important = true;
3186
+ value = value.slice(0, bang.index).trim();
3187
+ }
3188
+ return { type: "decl", prop, value, important };
3189
+ }
3190
+ function splitAtRule(prelude) {
3191
+ const m = /^@([A-Za-z-]+)\s*([\s\S]*)$/.exec(prelude);
3192
+ if (!m) return { name: prelude.slice(1).trim(), params: "" };
3193
+ return { name: m[1].toLowerCase(), params: m[2].trim() };
3194
+ }
3195
+ function parseBlock(src, start) {
3196
+ const nodes = [];
3197
+ let buf = "";
3198
+ let i = start;
3199
+ while (i < src.length) {
3200
+ const c = src[i];
3201
+ if (c === "{") {
3202
+ const prelude = buf.trim();
3203
+ buf = "";
3204
+ const inner = parseBlock(src, i + 1);
3205
+ i = inner.next;
3206
+ if (prelude.startsWith("@")) {
3207
+ const { name, params } = splitAtRule(prelude);
3208
+ nodes.push({ type: "atrule", name, params, nodes: inner.nodes });
3209
+ } else if (prelude.length > 0) {
3210
+ nodes.push({ type: "rule", selector: prelude, nodes: inner.nodes });
3211
+ }
3212
+ } else if (c === "}") {
3213
+ const d = toDecl(buf);
3214
+ if (d) nodes.push(d);
3215
+ return { nodes, next: i + 1 };
3216
+ } else if (c === ";") {
3217
+ const d = toDecl(buf);
3218
+ if (d) nodes.push(d);
3219
+ buf = "";
3220
+ i += 1;
3221
+ } else {
3222
+ buf += c;
3223
+ i += 1;
3224
+ }
3225
+ }
3226
+ const tail = toDecl(buf);
3227
+ if (tail) nodes.push(tail);
3228
+ return { nodes, next: i };
3229
+ }
3230
+ function resolveNesting(child, parent) {
3231
+ const c = child.trim();
3232
+ if (parent.length === 0) return c;
3233
+ if (c.includes("&")) return c.split("&").join(parent);
3234
+ return `${parent} ${c}`;
3235
+ }
3236
+ var DROP_ATRULES = /* @__PURE__ */ new Set(["property", "keyframes", "font-face", "charset", "import"]);
3237
+ function flattenNodes(nodes, selector, at, out) {
3238
+ const own = [];
3239
+ for (const n of nodes) if (n.type === "decl") own.push(n);
3240
+ if (own.length > 0 && selector.length > 0) out.push({ selector, at: [...at], decls: own });
3241
+ for (const n of nodes) {
3242
+ if (n.type === "rule") {
3243
+ flattenNodes(n.nodes, resolveNesting(n.selector, selector), at, out);
3244
+ } else if (n.type === "atrule") {
3245
+ if (n.name === "media") {
3246
+ flattenNodes(n.nodes, selector, [...at, { name: "media", params: n.params }], out);
3247
+ } else if (n.name === "layer") {
3248
+ flattenNodes(n.nodes, selector, at, out);
3249
+ } else if (!DROP_ATRULES.has(n.name)) {
3250
+ flattenNodes(n.nodes, selector, [...at, { name: n.name, params: n.params }], out);
3251
+ }
3252
+ }
3253
+ }
3254
+ }
3255
+ function leafToNode(leaf) {
3256
+ const declNodes = leaf.decls.map((d) => ({
3257
+ type: "decl",
3258
+ prop: d.prop,
3259
+ value: d.value,
3260
+ important: d.important
3261
+ }));
3262
+ let node = { type: "rule", selector: leaf.selector, nodes: declNodes };
3263
+ for (let i = leaf.at.length - 1; i >= 0; i -= 1) {
3264
+ node = { type: "atrule", name: leaf.at[i].name, params: leaf.at[i].params, nodes: [node] };
3265
+ }
3266
+ return node;
3267
+ }
3268
+ function parseUtilityCss(css) {
3269
+ try {
3270
+ const { nodes } = parseBlock(stripComments(css), 0);
3271
+ const leaves = [];
3272
+ flattenNodes(nodes, "", [], leaves);
3273
+ return leaves.map(leafToNode);
3274
+ } catch {
3275
+ return [];
3276
+ }
3277
+ }
3278
+
3279
+ // ../resolver-tailwind/src/tailwind/engine-v4.ts
3280
+ var SEARCH_DIRS = ["", "src", "app", "styles", "src/styles", "src/app", "app/styles", "assets/css", "css"];
3281
+ var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "out", ".next", "coverage"]);
3282
+ var ENTRY_RE = /@import\s+["']tailwindcss["']|@tailwind\b|@theme\b/;
3283
+ function scanDir(dir) {
3284
+ let names;
3285
+ try {
3286
+ names = (0, import_node_fs2.readdirSync)(dir);
3287
+ } catch {
3288
+ return null;
3289
+ }
3290
+ for (const name of names) {
3291
+ if (!name.toLowerCase().endsWith(".css")) continue;
3292
+ const file = path2.join(dir, name);
3293
+ try {
3294
+ if (!(0, import_node_fs2.statSync)(file).isFile()) continue;
3295
+ const css = (0, import_node_fs2.readFileSync)(file, "utf8");
3296
+ if (ENTRY_RE.test(css)) return { css, base: path2.dirname(file) };
3297
+ } catch {
3298
+ }
3299
+ }
3300
+ return null;
3301
+ }
3302
+ function findCssEntries(projectRoot) {
3303
+ const out = [];
3304
+ const seen = /* @__PURE__ */ new Set();
3305
+ for (const rel of SEARCH_DIRS) {
3306
+ const dir = path2.resolve(projectRoot, rel);
3307
+ if (seen.has(dir) || [...SKIP_DIRS].some((s) => dir.includes(`${path2.sep}${s}`))) continue;
3308
+ seen.add(dir);
3309
+ const hit = scanDir(dir);
3310
+ if (hit) {
3311
+ out.push(hit);
3312
+ break;
3313
+ }
3314
+ }
3315
+ out.push({ css: '@import "tailwindcss";', base: projectRoot });
3316
+ return out;
3317
+ }
3318
+ function makeV4Engine(entries, version) {
3319
+ const cssByClass = new Map(entries.map(([name, css]) => [name, css]));
3320
+ const nodeCache = /* @__PURE__ */ new Map();
3321
+ const nodesFor = (token) => {
3322
+ let cached = nodeCache.get(token);
3323
+ if (!cached) {
3324
+ const css = cssByClass.get(token);
3325
+ cached = css ? parseUtilityCss(css) : [];
3326
+ nodeCache.set(token, cached);
3327
+ }
3328
+ return cached;
3329
+ };
3330
+ return {
3331
+ version,
3332
+ context: {
3333
+ // The resolver keeps only string entries; we hand it the concrete class names directly.
3334
+ getClassList: () => [...cssByClass.keys()]
3335
+ },
3336
+ generate(candidates) {
3337
+ const out = [];
3338
+ for (const c of candidates) for (const n of nodesFor(c)) out.push(n);
3339
+ return out;
3340
+ }
3341
+ };
3342
+ }
3343
+ function loadV4Engine(projectRoot, version) {
3344
+ const snapshot = runV4Bridge({ projectRoot, entries: findCssEntries(projectRoot) });
3345
+ if (!snapshot) return null;
3346
+ return makeV4Engine(snapshot.entries, version);
3347
+ }
3348
+
3349
+ // ../resolver-tailwind/src/tailwind/engine.ts
4122
3350
  function moduleBase() {
4123
3351
  return typeof __filename === "string" ? __filename : importMetaUrl;
4124
3352
  }
4125
3353
  function projectRequire(projectRoot) {
4126
3354
  const bases = [];
4127
- if (projectRoot) bases.push(path.join(projectRoot, "__domflax__.js"));
4128
- bases.push(path.join(process.cwd(), "__domflax__.js"));
3355
+ if (projectRoot) bases.push(path3.join(projectRoot, "__domflax__.js"));
3356
+ bases.push(path3.join(process.cwd(), "__domflax__.js"));
4129
3357
  bases.push(moduleBase());
4130
3358
  for (const base of bases) {
4131
3359
  try {
@@ -4137,14 +3365,36 @@ function projectRequire(projectRoot) {
4137
3365
  }
4138
3366
  return null;
4139
3367
  }
3368
+ var FIRST_UNSUPPORTED_MAJOR = 4;
3369
+ function majorOf(version) {
3370
+ const m = /^\s*(\d+)/.exec(version);
3371
+ return m ? Number(m[1]) : null;
3372
+ }
4140
3373
  function loadEngine(options) {
4141
3374
  const req = projectRequire(options.projectRoot);
4142
- if (!req) return null;
3375
+ if (!req) return { engine: null, version: null, unsupportedMajor: null };
3376
+ let version = null;
3377
+ try {
3378
+ version = req("tailwindcss/package.json").version;
3379
+ } catch {
3380
+ return { engine: null, version: null, unsupportedMajor: null };
3381
+ }
3382
+ const major = majorOf(version);
3383
+ if (major !== null && major >= FIRST_UNSUPPORTED_MAJOR) {
3384
+ const projectRoot = options.projectRoot ?? process.cwd();
3385
+ let v4 = null;
3386
+ try {
3387
+ v4 = loadV4Engine(projectRoot, version);
3388
+ } catch {
3389
+ v4 = null;
3390
+ }
3391
+ if (v4) return { engine: v4, version, unsupportedMajor: null };
3392
+ return { engine: null, version, unsupportedMajor: major };
3393
+ }
4143
3394
  try {
4144
3395
  const resolveConfig = req("tailwindcss/resolveConfig.js");
4145
3396
  const { createContext } = req("tailwindcss/lib/lib/setupContextUtils.js");
4146
3397
  const { generateRules } = req("tailwindcss/lib/lib/generateRules.js");
4147
- const pkg = req("tailwindcss/package.json");
4148
3398
  let userConfig = options.config ?? { content: [{ raw: "" }] };
4149
3399
  if (options.configPath !== void 0) {
4150
3400
  const loadConfig = req("tailwindcss/loadConfig.js");
@@ -4153,15 +3403,19 @@ function loadEngine(options) {
4153
3403
  const resolved = resolveConfig(userConfig);
4154
3404
  const context = createContext(resolved);
4155
3405
  return {
4156
- version: pkg.version,
4157
- context,
4158
- generate(candidates) {
4159
- const rules = generateRules(new Set(candidates), context);
4160
- return rules.map(([, node]) => node);
4161
- }
3406
+ engine: {
3407
+ version,
3408
+ context,
3409
+ generate(candidates) {
3410
+ const rules = generateRules(new Set(candidates), context);
3411
+ return rules.map(([, node]) => node);
3412
+ }
3413
+ },
3414
+ version,
3415
+ unsupportedMajor: null
4162
3416
  };
4163
3417
  } catch {
4164
- return null;
3418
+ return { engine: null, version, unsupportedMajor: null };
4165
3419
  }
4166
3420
  }
4167
3421
 
@@ -4340,22 +3594,39 @@ var DROPPABLE_USAGE = {
4340
3594
  };
4341
3595
 
4342
3596
  // ../resolver-tailwind/src/tailwind/resolver.ts
3597
+ var warnedUnsupported = /* @__PURE__ */ new Set();
4343
3598
  var TailwindResolver = class {
4344
3599
  id = "tailwind";
4345
3600
  provider;
4346
3601
  fingerprint;
3602
+ /**
3603
+ * SAFETY (Layer 1): the detected Tailwind MAJOR when the project's version is one this resolver
3604
+ * cannot drive (v4+), else `null`. When set, {@link resolve} reports every token as unknown, so
3605
+ * downstream files are left unchanged (never mis-optimized). Exposed for diagnostics/tests.
3606
+ */
3607
+ unsupportedMajor;
4347
3608
  #engine;
4348
3609
  /** Per-token extraction cache (engine output is pure for a fixed config). */
4349
3610
  #tokenCache = /* @__PURE__ */ new Map();
4350
3611
  /** Per-class-set forward-resolution cache. */
4351
3612
  #resolveCache = /* @__PURE__ */ new Map();
4352
- /** Lazily built reverse index for {@link emit}. */
3613
+ /** Lazily built reverse index for the greedy {@link emit} fallback. */
4353
3614
  #reverseIndex = null;
3615
+ /** Lazily built cover vocabulary (base-condition tuple sets) for the exact-cover engine. */
3616
+ #coverVocab = null;
4354
3617
  constructor(config = {}) {
4355
- this.#engine = loadEngine(config);
4356
- this.provider = config.provider ?? (this.#engine ? `tailwindcss@${this.#engine.version}` : "tailwindcss");
3618
+ const loaded = loadEngine(config);
3619
+ this.#engine = loaded.engine;
3620
+ this.unsupportedMajor = loaded.unsupportedMajor;
3621
+ this.provider = config.provider ?? (loaded.version ? `tailwindcss@${loaded.version}` : "tailwindcss");
4357
3622
  const seed = JSON.stringify(config.config ?? {}) + (config.configPath ?? "");
4358
3623
  this.fingerprint = config.fingerprint ?? `${this.provider}/${fnv1a(seed)}`;
3624
+ if (this.unsupportedMajor !== null && !warnedUnsupported.has(this.provider)) {
3625
+ warnedUnsupported.add(this.provider);
3626
+ console.warn(
3627
+ `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.`
3628
+ );
3629
+ }
4359
3630
  }
4360
3631
  /** Engine-backed, cached single-token extraction. */
4361
3632
  #extract(token) {
@@ -4464,9 +3735,75 @@ var TailwindResolver = class {
4464
3735
  this.#reverseIndex = index;
4465
3736
  return index;
4466
3737
  }
3738
+ /**
3739
+ * The cover vocabulary: every base-condition, plain-subject utility mapped to the {@link tupleKey}s
3740
+ * of its full normalized-longhand declaration set. Built once from a SINGLE engine `generate` over
3741
+ * the enumerable class list (grouped by selector), so it is the same cost as {@link #buildReverseIndex}.
3742
+ * This is what the provider-uniform exact-cover engine searches; the element's own droppable tokens
3743
+ * are members of it, guaranteeing feasibility. Variant / combinator / pseudo utilities are excluded
3744
+ * (their effect is not the element's own base box), so a target carrying such conditions simply finds
3745
+ * no cover and falls back to the greedy emit.
3746
+ */
3747
+ #buildCoverVocab() {
3748
+ if (this.#coverVocab) return this.#coverVocab;
3749
+ const baseCk = String(conditionKey(BASE_CONDITION));
3750
+ const out = [];
3751
+ if (this.#engine) {
3752
+ try {
3753
+ const classes = this.#engine.context.getClassList().filter((c) => typeof c === "string");
3754
+ const nodes = this.#engine.generate(classes);
3755
+ for (const node of nodes) {
3756
+ if (node.type !== "rule") continue;
3757
+ const rule = node;
3758
+ const parsed = parseSelector(rule.selector);
3759
+ if (parsed.kind !== "simple" || parsed.states.length > 0 || parsed.pseudoElement !== "") {
3760
+ continue;
3761
+ }
3762
+ const className = unescapeClass(rule.selector);
3763
+ if (className === null) continue;
3764
+ const tuples = [];
3765
+ const seen = /* @__PURE__ */ new Set();
3766
+ for (const child of rule.nodes ?? []) {
3767
+ if (child.type !== "decl") continue;
3768
+ const d = child;
3769
+ if (typeof d.value !== "string") continue;
3770
+ for (const decl of normalizer.normalizeDeclaration(d.prop, d.value, d.important === true)) {
3771
+ const k = tupleKey(baseCk, String(decl.property), String(decl.value), decl.important);
3772
+ if (!seen.has(k)) {
3773
+ seen.add(k);
3774
+ tuples.push(k);
3775
+ }
3776
+ }
3777
+ }
3778
+ if (tuples.length > 0) out.push({ token: className, tuples });
3779
+ }
3780
+ } catch {
3781
+ }
3782
+ }
3783
+ this.#coverVocab = out;
3784
+ return out;
3785
+ }
3786
+ /**
3787
+ * Try the minimal-string exact-cover engine over the WHOLE utility vocabulary. On success the chosen
3788
+ * set is verified by the mandatory CORRECTNESS BACKSTOP — re-resolve it and assert it reproduces the
3789
+ * target's tuples EXACTLY — before it is returned; any mismatch (or no cover / oversize universe)
3790
+ * yields `null` so {@link emit} uses its greedy fallback. Never returns a set that misrepresents `U`.
3791
+ */
3792
+ #tryCover(normalized, norm) {
3793
+ const universe = styleMapTuples(normalized, norm);
3794
+ if (universe.length === 0) return { classes: [], exact: true, warnings: [] };
3795
+ const chosen = minStringCover(universe, this.#buildCoverVocab());
3796
+ if (!chosen || chosen.length === 0) return null;
3797
+ const reTuples = new Set(styleMapTuples(this.resolve({ classes: chosen }).styles, norm));
3798
+ if (reTuples.size !== universe.length) return null;
3799
+ for (const t of universe) if (!reTuples.has(t)) return null;
3800
+ return { classes: chosen, exact: true, warnings: [] };
3801
+ }
4467
3802
  emit(styles, ctx) {
4468
3803
  const norm = ctx.normalizer ?? normalizer;
4469
3804
  const normalized = norm.normalizeStyleMap(styles);
3805
+ const cover = this.#tryCover(normalized, norm);
3806
+ if (cover) return cover;
4470
3807
  const base = normalized.blocks.get(conditionKey(BASE_CONDITION));
4471
3808
  if (!base || base.decls.size === 0) return { classes: [], exact: true, warnings: [] };
4472
3809
  const hasNonBase = normalized.blocks.size > 1;
@@ -4496,13 +3833,13 @@ var TailwindResolver = class {
4496
3833
  let bestCover = 0;
4497
3834
  for (const entry of candidates) {
4498
3835
  const [token, declMap] = entry;
4499
- let cover = 0;
4500
- for (const prop of declMap.keys()) if (remaining.has(prop)) cover += 1;
4501
- if (cover === 0) continue;
4502
- const better = best === null || cover > bestCover || cover === bestCover && declMap.size < best[1].size || cover === bestCover && declMap.size === best[1].size && token < best[0];
3836
+ let cover2 = 0;
3837
+ for (const prop of declMap.keys()) if (remaining.has(prop)) cover2 += 1;
3838
+ if (cover2 === 0) continue;
3839
+ const better = best === null || cover2 > bestCover || cover2 === bestCover && declMap.size < best[1].size || cover2 === bestCover && declMap.size === best[1].size && token < best[0];
4503
3840
  if (better) {
4504
3841
  best = entry;
4505
- bestCover = cover;
3842
+ bestCover = cover2;
4506
3843
  }
4507
3844
  }
4508
3845
  if (!best) break;
@@ -4573,14 +3910,14 @@ var LEGACY_PSEUDO_ELEMENTS2 = /* @__PURE__ */ new Set([
4573
3910
 
4574
3911
  // ../resolver-css/src/engine.ts
4575
3912
  var import_node_module2 = require("module");
4576
- var path2 = __toESM(require("path"), 1);
3913
+ var path4 = __toESM(require("path"), 1);
4577
3914
  function moduleBase2() {
4578
3915
  return typeof __filename === "string" ? __filename : importMetaUrl;
4579
3916
  }
4580
3917
  function loadPostcssEngine(projectRoot) {
4581
3918
  const bases = [];
4582
- if (projectRoot) bases.push(path2.join(projectRoot, "__domflax__.js"));
4583
- bases.push(path2.join(process.cwd(), "__domflax__.js"));
3919
+ if (projectRoot) bases.push(path4.join(projectRoot, "__domflax__.js"));
3920
+ bases.push(path4.join(process.cwd(), "__domflax__.js"));
4584
3921
  bases.push(moduleBase2());
4585
3922
  for (const base of bases) {
4586
3923
  try {
@@ -4633,15 +3970,15 @@ function collectDecls(rule) {
4633
3970
  }
4634
3971
 
4635
3972
  // ../resolver-css/src/misc-helpers.ts
4636
- var import_node_fs = require("fs");
3973
+ var import_node_fs3 = require("fs");
4637
3974
  function isPlainClassToken(token) {
4638
3975
  return token.length > 0 && !/[\s.#>+~:[\]()]/.test(token);
4639
3976
  }
4640
- function readCssPath(path3) {
3977
+ function readCssPath(path5) {
4641
3978
  try {
4642
- return { id: path3, css: (0, import_node_fs.readFileSync)(path3, "utf8") };
3979
+ return { id: path5, css: (0, import_node_fs3.readFileSync)(path5, "utf8") };
4643
3980
  } catch (cause) {
4644
- throw new Error(`resolver-css: cannot read CSS file "${path3}"`, { cause });
3981
+ throw new Error(`resolver-css: cannot read CSS file "${path5}"`, { cause });
4645
3982
  }
4646
3983
  }
4647
3984
  function deriveFingerprint(provider, files) {
@@ -4696,6 +4033,8 @@ var CustomCSSResolver = class {
4696
4033
  /** Distinct COMPLEX selectors (combinator or structural pseudo), sorted. */
4697
4034
  #complex;
4698
4035
  #reverse = null;
4036
+ /** Lazily built cover vocabulary (full condition-keyed tuple sets) for the exact-cover engine. */
4037
+ #coverVocab = null;
4699
4038
  constructor(cssFiles = [], options = {}) {
4700
4039
  ensurePostcss(options.projectRoot);
4701
4040
  const fromDisk = (options.files ?? []).map(readCssPath);
@@ -4728,8 +4067,23 @@ var CustomCSSResolver = class {
4728
4067
  }
4729
4068
  emit(styles, ctx) {
4730
4069
  const norm = ctx.normalizer ?? normalizer;
4070
+ const normalized = norm.normalizeStyleMap(styles);
4071
+ const universe = styleMapTuples(normalized, norm);
4072
+ if (universe.length === 0) return { classes: [], exact: true, warnings: [] };
4073
+ const chosen = minStringCover(universe, this.#buildCoverVocab());
4074
+ if (chosen && chosen.length > 0) {
4075
+ const reTuples = new Set(styleMapTuples(this.resolve({ classes: chosen }).styles, norm));
4076
+ let ok = reTuples.size === universe.length;
4077
+ if (ok) {
4078
+ for (const t of universe) if (!reTuples.has(t)) {
4079
+ ok = false;
4080
+ break;
4081
+ }
4082
+ }
4083
+ if (ok) return { classes: chosen, exact: true, warnings: [] };
4084
+ }
4731
4085
  const remaining = /* @__PURE__ */ new Map();
4732
- for (const [ck, block] of norm.normalizeStyleMap(styles).blocks) {
4086
+ for (const [ck, block] of normalized.blocks) {
4733
4087
  for (const [prop, decl] of block.decls) {
4734
4088
  remaining.set(`${ck} ${prop}`, String(decl.value));
4735
4089
  }
@@ -4939,7 +4293,23 @@ var CustomCSSResolver = class {
4939
4293
  if (rawBlocks.size === 0) return emptyStyleMap();
4940
4294
  return normalizer.normalizeStyleMap({ blocks: rawBlocks });
4941
4295
  }
4942
- /** Build (once) the reverse index used by {@link emit}. */
4296
+ /**
4297
+ * Build (once) the cover vocabulary for the exact-cover engine: every forward-resolvable class
4298
+ * mapped to the {@link styleMapTuples} of its full (condition-keyed, `!important`-aware) declaration
4299
+ * set. Unlike {@link #reverseIndex} this carries ALL style conditions and the important flag, so the
4300
+ * engine can pick a custom class covering hover/media declarations too.
4301
+ */
4302
+ #buildCoverVocab() {
4303
+ if (this.#coverVocab) return this.#coverVocab;
4304
+ const out = [];
4305
+ for (const token of this.#classIndex.keys()) {
4306
+ const tuples = styleMapTuples(this.#resolveTokens([token], [token]), normalizer);
4307
+ if (tuples.length > 0) out.push({ token, tuples });
4308
+ }
4309
+ this.#coverVocab = out;
4310
+ return out;
4311
+ }
4312
+ /** Build (once) the reverse index used by the greedy {@link emit} fallback. */
4943
4313
  #reverseIndex() {
4944
4314
  if (this.#reverse) return this.#reverse;
4945
4315
  const out = [];
@@ -5149,9 +4519,10 @@ function doParse(code, ctx) {
5149
4519
  native: document2
5150
4520
  };
5151
4521
  doc.sources.set(FILE_ID, sourceFile);
5152
- const resolveComputed = (tokens, tag, nodeId) => {
4522
+ const resolveComputed = (tokens, tag, nodeId, meta) => {
5153
4523
  if (tokens.length === 0) return emptyStyleMap();
5154
4524
  const res = ctx.resolver.resolve({ classes: tokens, element: { tagName: tag, namespace: "html" } });
4525
+ if (res.unknown.length > 0) meta.hasUnresolvedClasses = true;
5155
4526
  for (const w of res.warnings) {
5156
4527
  diagnostics.push({
5157
4528
  code: "DF_STYLE_CONFLICT_UNRESOLVED",
@@ -5241,7 +4612,7 @@ function doParse(code, ctx) {
5241
4612
  order.push(a.name);
5242
4613
  }
5243
4614
  const attrs = { entries, spreads: [], order };
5244
- const computed2 = resolveComputed(classTokens, tag, id);
4615
+ const computed2 = resolveComputed(classTokens, tag, id, meta);
5245
4616
  const children = [];
5246
4617
  if (!opaqueSubtree) {
5247
4618
  for (const c of node.childNodes ?? []) appendChild(c, id, children);
@@ -5644,6 +5015,7 @@ function doParse2(code, ctx) {
5644
5015
  element: { tagName: tag, namespace: component ? void 0 : "html" }
5645
5016
  });
5646
5017
  computed2 = ctx.normalizer.normalizeStyleMap(res.styles);
5018
+ if (res.unknown.length > 0) meta.hasUnresolvedClasses = true;
5647
5019
  for (const w of res.warnings) {
5648
5020
  diagnostics.push({
5649
5021
  code: "DF_STYLE_CONFLICT_UNRESOLVED",
@@ -5686,13 +5058,13 @@ function doParse2(code, ctx) {
5686
5058
  };
5687
5059
  const roots = [];
5688
5060
  traverse(ast, {
5689
- JSXElement(path3) {
5690
- roots.push(path3.node);
5691
- path3.skip();
5061
+ JSXElement(path5) {
5062
+ roots.push(path5.node);
5063
+ path5.skip();
5692
5064
  },
5693
- JSXFragment(path3) {
5694
- roots.push(path3.node);
5695
- path3.skip();
5065
+ JSXFragment(path5) {
5066
+ roots.push(path5.node);
5067
+ path5.skip();
5696
5068
  }
5697
5069
  });
5698
5070
  const rootFrag = doc.nodes.get(doc.root);
@@ -5966,6 +5338,27 @@ function createJsxBackend() {
5966
5338
  }
5967
5339
 
5968
5340
  // src/pipeline-run.ts
5341
+ function bytes(s) {
5342
+ return Buffer.byteLength(s, "utf8");
5343
+ }
5344
+ function countClassTokens(code) {
5345
+ let total = 0;
5346
+ const re = /\b(?:className|class)\s*=\s*"([^"]*)"/g;
5347
+ let m;
5348
+ while ((m = re.exec(code)) !== null) {
5349
+ total += m[1].split(/\s+/).filter((t) => t.length > 0).length;
5350
+ }
5351
+ return total;
5352
+ }
5353
+ function computeStats(code, out, nodesIn, nodesOut) {
5354
+ const classesBefore = countClassTokens(code);
5355
+ const classesAfter = countClassTokens(out);
5356
+ return {
5357
+ nodesRemoved: Math.max(0, nodesIn - nodesOut),
5358
+ classesSaved: Math.max(0, classesBefore - classesAfter),
5359
+ bytesSaved: bytes(code) - bytes(out)
5360
+ };
5361
+ }
5969
5362
  function jsxKindOf(id) {
5970
5363
  const clean = id.split("?", 1)[0] ?? id;
5971
5364
  if (clean.endsWith(".tsx")) return "tsx";
@@ -6041,8 +5434,10 @@ function finishPipeline(optimized, id, resolver) {
6041
5434
  }
6042
5435
  function runJsxPipeline(code, id, kind, resolver, patterns, safety) {
6043
5436
  const { doc, ctx, passes } = preparePipeline(code, id, kind, resolver, patterns, safety, "provably-safe");
5437
+ const nodesIn = doc.nodes.size;
6044
5438
  const { doc: optimized } = runPasses(doc, passes, ctx);
6045
- return finishPipeline(optimized, id, resolver);
5439
+ const out = finishPipeline(optimized, id, resolver);
5440
+ return { code: out, stats: computeStats(code, out, nodesIn, optimized.nodes.size) };
6046
5441
  }
6047
5442
  function prepareHtml(code, id, resolver, patterns, safety, gate) {
6048
5443
  const parsed = createHtmlFrontend().parse(code, {
@@ -6083,8 +5478,74 @@ function finishHtmlPipeline(optimized, id, resolver) {
6083
5478
  }
6084
5479
  function runHtmlPipeline(code, id, resolver, patterns, safety) {
6085
5480
  const { doc, ctx, passes } = prepareHtml(code, id, resolver, patterns, safety, "provably-safe");
5481
+ const nodesIn = doc.nodes.size;
6086
5482
  const { doc: optimized } = runPasses(doc, passes, ctx);
6087
- return finishHtmlPipeline(optimized, id, resolver);
5483
+ const out = finishHtmlPipeline(optimized, id, resolver);
5484
+ return { code: out, stats: computeStats(code, out, nodesIn, optimized.nodes.size) };
5485
+ }
5486
+
5487
+ // src/summary.ts
5488
+ function zeroStats() {
5489
+ return { nodesRemoved: 0, classesSaved: 0, bytesSaved: 0 };
5490
+ }
5491
+ function emptyTotals() {
5492
+ return { files: 0, nodesRemoved: 0, classesCompressed: 0, bytesSaved: 0 };
5493
+ }
5494
+ function resetTotals(t) {
5495
+ t.files = 0;
5496
+ t.nodesRemoved = 0;
5497
+ t.classesCompressed = 0;
5498
+ t.bytesSaved = 0;
5499
+ }
5500
+ function addStats(t, s, changed) {
5501
+ if (!changed) return;
5502
+ t.files += 1;
5503
+ t.nodesRemoved += s.nodesRemoved;
5504
+ t.classesCompressed += s.classesSaved;
5505
+ t.bytesSaved += s.bytesSaved;
5506
+ }
5507
+ var BYTE_UNITS = ["KB", "MB", "GB", "TB"];
5508
+ function formatBytes(n) {
5509
+ const abs = Math.abs(n);
5510
+ if (abs < 1024) return `${n} B`;
5511
+ let value = n / 1024;
5512
+ let unit = 0;
5513
+ while (Math.abs(value) >= 1024 && unit < BYTE_UNITS.length - 1) {
5514
+ value /= 1024;
5515
+ unit += 1;
5516
+ }
5517
+ return `${value.toFixed(1)} ${BYTE_UNITS[unit]}`;
5518
+ }
5519
+ function formatCount(n) {
5520
+ return Math.trunc(n).toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
5521
+ }
5522
+ var LABEL_WIDTH = 20;
5523
+ var RULE = ` ${"\u2500".repeat(32)}`;
5524
+ function row(label, value) {
5525
+ return ` ${label.padEnd(LABEL_WIDTH)}${value}`;
5526
+ }
5527
+ function renderSummary(totals) {
5528
+ return [
5529
+ "",
5530
+ " \u25B2 domflax",
5531
+ RULE,
5532
+ row("files optimized", formatCount(totals.files)),
5533
+ row("DOM nodes removed", formatCount(totals.nodesRemoved)),
5534
+ row("classes compressed", formatCount(totals.classesCompressed)),
5535
+ row("size saved", formatBytes(totals.bytesSaved)),
5536
+ RULE,
5537
+ ""
5538
+ ].join("\n");
5539
+ }
5540
+ var TOTALS_KEY = /* @__PURE__ */ Symbol.for("domflax.buildTotals");
5541
+ var PRINTED_KEY = /* @__PURE__ */ Symbol.for("domflax.summaryPrinted");
5542
+ function printCompilationSummary(compilation) {
5543
+ if (compilation === null || typeof compilation !== "object") return;
5544
+ const bag = compilation;
5545
+ if (bag[PRINTED_KEY]) return;
5546
+ bag[PRINTED_KEY] = true;
5547
+ const totals = bag[TOTALS_KEY];
5548
+ if (totals && totals.files > 0) process.stdout.write(renderSummary(totals));
6088
5549
  }
6089
5550
 
6090
5551
  // src/index.ts
@@ -6122,29 +5583,48 @@ function createDomflax(options = {}) {
6122
5583
  },
6123
5584
  patterns,
6124
5585
  transform(code, id) {
6125
- if (!isSupported(id, resolved.include)) return { code, map: null };
5586
+ if (!isSupported(id, resolved.include)) return { code, map: null, stats: zeroStats() };
6126
5587
  const kind = jsxKindOf(id);
6127
5588
  if (kind !== null) {
6128
5589
  const out = runJsxPipeline(code, id, kind, getResolver(), patterns, resolved.safety);
6129
- return { code: out, map: null };
5590
+ return { code: out.code, map: null, stats: out.stats };
6130
5591
  }
6131
5592
  if (htmlKindOf(id) !== null) {
6132
5593
  const out = runHtmlPipeline(code, id, getResolver(), patterns, resolved.safety);
6133
- return { code: out, map: null };
5594
+ return { code: out.code, map: null, stats: out.stats };
6134
5595
  }
6135
- return { code, map: null };
5596
+ return { code, map: null, stats: zeroStats() };
6136
5597
  }
6137
5598
  };
6138
5599
  }
6139
5600
  function vite(options = {}) {
6140
5601
  const engine = createDomflax(options);
5602
+ const totals = emptyTotals();
5603
+ let printed = false;
5604
+ const printSummary = () => {
5605
+ if (printed) return;
5606
+ printed = true;
5607
+ if (totals.files > 0) process.stdout.write(renderSummary(totals));
5608
+ };
6141
5609
  return {
6142
5610
  name: "domflax",
6143
5611
  enforce: "pre",
5612
+ buildStart() {
5613
+ resetTotals(totals);
5614
+ printed = false;
5615
+ },
6144
5616
  transform(code, id) {
6145
5617
  if (!isSupported(id, engine.options.include)) return null;
6146
5618
  const out = engine.transform(code, id);
6147
- return out.code === code ? null : out;
5619
+ const changed = out.code !== code;
5620
+ addStats(totals, out.stats, changed);
5621
+ return changed ? out : null;
5622
+ },
5623
+ buildEnd() {
5624
+ printSummary();
5625
+ },
5626
+ closeBundle() {
5627
+ printSummary();
6148
5628
  }
6149
5629
  };
6150
5630
  }
@@ -6153,6 +5633,24 @@ function webpackLoaderPath() {
6153
5633
  const here = (0, import_node_path.dirname)((0, import_node_url.fileURLToPath)(importMetaUrl));
6154
5634
  return (0, import_node_path.join)(here, "webpack-loader.cjs");
6155
5635
  }
5636
+ function tapWebpackSummary(compiler) {
5637
+ const done = compiler.hooks?.done;
5638
+ if (typeof done?.tap !== "function") return;
5639
+ done.tap("domflax", (stats) => {
5640
+ const compilation = stats?.compilation ?? stats;
5641
+ printCompilationSummary(compilation);
5642
+ });
5643
+ }
5644
+ function installWebpackSummary(compiler, host) {
5645
+ if (typeof compiler.hooks?.done?.tap === "function") {
5646
+ tapWebpackSummary(compiler);
5647
+ return;
5648
+ }
5649
+ const plugins = host.plugins ??= [];
5650
+ if (Array.isArray(plugins)) {
5651
+ plugins.push({ apply: (real) => tapWebpackSummary(real) });
5652
+ }
5653
+ }
6156
5654
  function webpack(options = {}) {
6157
5655
  createDomflax(options);
6158
5656
  return {
@@ -6168,6 +5666,7 @@ function webpack(options = {}) {
6168
5666
  use: [{ loader: webpackLoaderPath(), options }]
6169
5667
  };
6170
5668
  rules.push(rule);
5669
+ installWebpackSummary(compiler, host);
6171
5670
  }
6172
5671
  };
6173
5672
  }
@@ -6178,11 +5677,10 @@ var src_default = domflax;
6178
5677
  BASE_CONDITION,
6179
5678
  BASE_CONDITION_KEY,
6180
5679
  DEFAULT_FIXPOINT,
5680
+ DEFAULT_MAX_UNIVERSE,
6181
5681
  PHASE_ORDER,
6182
5682
  applyGroups,
6183
5683
  applyOps,
6184
- borderRadiusShorthand,
6185
- borderShorthand,
6186
5684
  buildMatchContext,
6187
5685
  buildSelectorIndex,
6188
5686
  builtinPatterns,
@@ -6206,7 +5704,6 @@ var src_default = domflax;
6206
5704
  createRewriteFactory,
6207
5705
  createSyntheticSink,
6208
5706
  createText,
6209
- dedupeClasses,
6210
5707
  defaultMeta,
6211
5708
  displayContentsWrapper,
6212
5709
  docFingerprint,
@@ -6221,29 +5718,21 @@ var src_default = domflax;
6221
5718
  flattenVerdict,
6222
5719
  flattenWouldDropStyle,
6223
5720
  flexCenterWrapper,
6224
- gapShorthand,
6225
5721
  getElement,
6226
5722
  getNode,
6227
- inlineFlexCenterWrapper,
6228
- insetShorthand,
6229
- marginShorthand,
6230
- nestedFlexMerge,
6231
- nestedGridMerge,
6232
- overflowShorthand,
6233
- overscrollBehaviorShorthand,
6234
- paddingShorthand,
5723
+ gridCenterWrapper,
5724
+ inheritedOnlyWrapper,
5725
+ minStringCover,
6235
5726
  passthroughWrapper,
6236
5727
  patternsForPhase,
6237
- placeShorthand,
6238
5728
  redundantFragment,
6239
5729
  redundantInlineWrapper,
6240
5730
  revertDiagnostic,
6241
5731
  runPasses,
6242
- scrollMarginShorthand,
6243
- scrollPaddingShorthand,
6244
- sizeShorthand,
6245
5732
  stampOrigin,
5733
+ styleMapTuples,
6246
5734
  syncClassesFromComputed,
5735
+ tupleKey,
6247
5736
  vite,
6248
5737
  walk,
6249
5738
  webpack