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
@@ -1,8 +1,5 @@
1
1
  import {
2
2
  BASE_CONDITION,
3
- BASE_CONDITION_KEY,
4
- and,
5
- computed,
6
3
  conditionKey,
7
4
  createComment,
8
5
  createDocument,
@@ -15,17 +12,174 @@ import {
15
12
  emptyClassList,
16
13
  emptyStyleMap,
17
14
  hasDynamicClasses,
18
- isElement,
15
+ minStringCover,
19
16
  normalizer,
20
17
  not,
21
- targetedByCombinator
22
- } from "./chunk-H5KTGI3A.js";
18
+ styleMapTuples,
19
+ tupleKey
20
+ } from "./chunk-TTJEXWAC.js";
23
21
  import {
24
22
  __filename,
25
23
  init_esm_shims
26
24
  } from "./chunk-U5GOONKV.js";
27
25
 
28
- // ../patterns/src/library/flatten/display-contents-wrapper.pattern.ts
26
+ // ../patterns/src/library/flex/flex-center-wrapper.pattern.ts
27
+ init_esm_shims();
28
+ var flexCenterWrapper = definePattern({
29
+ name: "flex-center-wrapper",
30
+ category: "flatten/flex/flex-center-wrapper",
31
+ safety: 2,
32
+ doc: {
33
+ title: "Flatten flex-centering wrapper",
34
+ 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.",
35
+ before: '<div style="display:flex;align-items:center;justify-content:center"><Child/></div>',
36
+ after: '<Child style="place-self:center"/>',
37
+ 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."
38
+ },
39
+ match: {
40
+ tag: "div",
41
+ style: { display: "flex", alignItems: "center", justifyContent: "center" },
42
+ onlyChild: "element",
43
+ paintsNothing: true
44
+ },
45
+ rewrite: {
46
+ flattenInto: "child",
47
+ childGains: { placeSelf: "center" }
48
+ },
49
+ // Collapsing a flex-centering wrapper to `place-self:center` on the child is render-identical ONLY
50
+ // when the child's NEW parent is a statically-known GRID that lets the wrapper fill its area (there
51
+ // `place-self`'s align-self AND justify-self both take effect). Under that ONE context the flatten is
52
+ // classified `provably-safe` and commits; under a flex/block/unknown parent — or when the wrapper
53
+ // drops any own style — it stays `needs-verification` and the conservative production gate PRESERVES
54
+ // it. Op-level correctness (purity, id-preserving unwrap, opacity-barrier safety) is additionally
55
+ // asserted by the invariant suite over every pattern.
56
+ test: {
57
+ cases: [
58
+ {
59
+ name: "grid parent \u2192 flattened (child gains place-self-center)",
60
+ before: '<div className="grid"><div className="flex items-center justify-center"><span className="bg-red-200">x</span></div></div>',
61
+ after: '<div className="grid"><span className="bg-red-200 place-self-center">x</span></div>'
62
+ }
63
+ ],
64
+ noMatch: [
65
+ // Non-grid (flex) parent (document root): `justify-self` is ignored in flex → not provably safe.
66
+ '<div className="flex justify-center items-center"><div className="bg-red-200">Hello</div></div>',
67
+ // Grid parent, but the wrapper drops padding when removed → not layout-neutral (rule 3).
68
+ '<div className="grid"><div className="p-4 flex items-center justify-center"><span className="bg-red-200">x</span></div></div>',
69
+ // Grid parent forcing place-items-center: the wrapper would not fill its area → fill guard skips.
70
+ '<div className="grid place-items-center"><div className="flex items-center justify-center"><span className="bg-red-200">x</span></div></div>',
71
+ // onClick is a hard opacity barrier → the wrapper is load-bearing regardless of the gate.
72
+ '<div className="flex justify-center items-center" onClick={handleClick}><div className="bg-red-200">Hello</div></div>'
73
+ ]
74
+ }
75
+ });
76
+
77
+ // ../patterns/src/library/fragment/redundant-fragment.pattern.ts
78
+ init_esm_shims();
79
+ function parentIsRedundantFragment(node, ctx) {
80
+ const el = node;
81
+ if (el.kind !== "element") return false;
82
+ const parentId = el.parent;
83
+ if (parentId == null) return false;
84
+ const parent = ctx.doc.nodes.get(parentId);
85
+ if (!parent || parent.kind !== "fragment") return false;
86
+ if (parent.parent == null) return false;
87
+ if (parent.children.length !== 1) return false;
88
+ const m = parent.meta;
89
+ if (m.hasKey || m.hasRef || m.hasEventHandlers || m.hasDynamicChildren || m.hasDangerousHtml || m.hasSpreadAttrs || m.isComponent) {
90
+ return false;
91
+ }
92
+ if (m.targetedByCombinator || m.targetedByStructuralPseudo) return false;
93
+ const fid = parentId;
94
+ if (ctx.selectors.targetedByCombinator(fid) || ctx.selectors.targetedByStructuralPseudo(fid)) {
95
+ return false;
96
+ }
97
+ if (ctx.selectors.reparentImpact(fid).size > 0) return false;
98
+ return true;
99
+ }
100
+ var redundantFragment = definePattern({
101
+ name: "redundant-fragment",
102
+ category: "flatten/fragment/redundant-fragment",
103
+ safety: 1,
104
+ doc: {
105
+ title: "Flatten redundant single-child fragment",
106
+ 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.",
107
+ before: "<><Child/></>",
108
+ after: "<Child/>",
109
+ 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."
110
+ },
111
+ match: parentIsRedundantFragment,
112
+ rewrite: (ctx, rw) => {
113
+ const parentId = ctx.node.parent;
114
+ if (parentId == null) return null;
115
+ const fragment = ctx.doc.nodes.get(parentId);
116
+ if (!fragment || fragment.kind !== "fragment") return null;
117
+ return [rw.unwrap(fragment)];
118
+ },
119
+ test: {
120
+ cases: [
121
+ {
122
+ // A fragment renders no box, so unwrapping a single-child fragment is always layout-identical
123
+ // → a provably-safe flatten: the child is spliced up into the fragment's slot.
124
+ before: '<><span className="bg-red-200">Hi</span></>',
125
+ after: '<span className="bg-red-200">Hi</span>'
126
+ }
127
+ ],
128
+ noMatch: [
129
+ // Two children ⇒ not a single-child fragment, so the fragment is load-bearing and stays.
130
+ '<><span className="bg-red-200">A</span><span className="bg-green-200">B</span></>'
131
+ ]
132
+ }
133
+ });
134
+
135
+ // ../patterns/src/library/grid/grid-center-wrapper.pattern.ts
136
+ init_esm_shims();
137
+ var gridCenterWrapper = definePattern({
138
+ name: "grid-center-wrapper",
139
+ category: "flatten/grid/grid-center-wrapper",
140
+ safety: 2,
141
+ doc: {
142
+ title: "Flatten grid-centering wrapper",
143
+ 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.",
144
+ before: '<div style="display:grid;align-items:center;justify-content:center"><Child/></div>',
145
+ after: '<Child style="place-self:center"/>',
146
+ 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."
147
+ },
148
+ match: {
149
+ tag: "div",
150
+ style: { display: "grid", alignItems: "center", justifyContent: "center" },
151
+ onlyChild: "element",
152
+ paintsNothing: true
153
+ },
154
+ rewrite: {
155
+ flattenInto: "child",
156
+ childGains: { placeSelf: "center" }
157
+ },
158
+ // Like `flex-center-wrapper`, collapsing to `place-self:center` is render-identical ONLY when the
159
+ // child's NEW parent is a statically-known GRID that lets the wrapper fill its area (there both halves
160
+ // of place-self take effect). Under that ONE context the flatten is `provably-safe` and commits; under
161
+ // a flex/block/unknown parent — or when the wrapper drops any own style — it stays `needs-verification`
162
+ // and the conservative production gate PRESERVES it. Op-level correctness is asserted by the invariant suite.
163
+ test: {
164
+ cases: [
165
+ {
166
+ name: "grid parent \u2192 flattened (child gains place-self-center)",
167
+ before: '<div className="grid"><div className="grid items-center justify-center"><span className="bg-red-200">x</span></div></div>',
168
+ after: '<div className="grid"><span className="bg-red-200 place-self-center">x</span></div>'
169
+ }
170
+ ],
171
+ noMatch: [
172
+ // Non-grid (document-root) parent: justify-self is ignored outside a grid → not provably safe.
173
+ '<div className="grid justify-center items-center"><div className="bg-red-200">Hello</div></div>',
174
+ // Grid parent, but the wrapper drops padding when removed → not layout-neutral, preserved.
175
+ '<div className="grid"><div className="p-4 grid items-center justify-center"><span className="bg-red-200">x</span></div></div>',
176
+ // onClick is a hard opacity barrier → the wrapper is load-bearing regardless of the gate.
177
+ '<div className="grid justify-center items-center" onClick={handleClick}><div className="bg-red-200">Hello</div></div>'
178
+ ]
179
+ }
180
+ });
181
+
182
+ // ../patterns/src/library/wrapper/display-contents-wrapper.pattern.ts
29
183
  init_esm_shims();
30
184
  function asEl(node) {
31
185
  const n = node;
@@ -50,7 +204,7 @@ var targetedByStructuralPseudo = (node, ctx) => {
50
204
  };
51
205
  var displayContentsWrapper = definePattern({
52
206
  name: "display-contents-wrapper",
53
- category: "flatten/display-contents-wrapper",
207
+ category: "flatten/wrapper/display-contents-wrapper",
54
208
  safety: 2,
55
209
  doc: {
56
210
  title: "Flatten display:contents wrapper",
@@ -90,7 +244,7 @@ var displayContentsWrapper = definePattern({
90
244
  }
91
245
  });
92
246
 
93
- // ../patterns/src/library/flatten/empty-style-div.pattern.ts
247
+ // ../patterns/src/library/wrapper/empty-style-div.pattern.ts
94
248
  init_esm_shims();
95
249
  function asEl2(node) {
96
250
  const n = node;
@@ -123,7 +277,7 @@ var hasNonBlockDisplay = (node, ctx) => {
123
277
  };
124
278
  var emptyStyleDiv = definePattern({
125
279
  name: "empty-style-div",
126
- category: "flatten/empty-style-div",
280
+ category: "flatten/wrapper/empty-style-div",
127
281
  safety: 1,
128
282
  doc: {
129
283
  title: "Flatten empty-style div wrapper",
@@ -162,339 +316,67 @@ var emptyStyleDiv = definePattern({
162
316
  }
163
317
  });
164
318
 
165
- // ../patterns/src/library/flatten/flex-center-wrapper.pattern.ts
319
+ // ../patterns/src/library/wrapper/inherited-only-wrapper.pattern.ts
166
320
  init_esm_shims();
167
- var flexCenterWrapper = definePattern({
168
- name: "flex-center-wrapper",
169
- category: "flatten/flex-center-wrapper",
321
+ var INERT_HOST_TAGS = /* @__PURE__ */ new Set(["div", "span"]);
322
+ var isInertHostTag = (node) => {
323
+ const n = node;
324
+ if (n.kind !== "element") return false;
325
+ return INERT_HOST_TAGS.has(String(n.tag).toLowerCase());
326
+ };
327
+ var isComponentNode2 = (node) => {
328
+ const n = node;
329
+ return n.kind === "element" ? n.meta.isComponent : false;
330
+ };
331
+ var hasOnlyInheritedStyle = (node, ctx) => {
332
+ const sm = normalizer.normalizeStyleMap(ctx.computed());
333
+ let sawAny = false;
334
+ for (const block of sm.blocks.values()) {
335
+ for (const decl of block.decls.values()) {
336
+ sawAny = true;
337
+ const inherited = decl.inherited || normalizer.inherited.isInherited(decl.property);
338
+ if (!inherited) return false;
339
+ }
340
+ }
341
+ return sawAny;
342
+ };
343
+ var inheritedOnlyWrapper = definePattern({
344
+ name: "inherited-only-wrapper",
345
+ category: "flatten/wrapper/inherited-only-wrapper",
170
346
  safety: 2,
171
347
  doc: {
172
- title: "Flatten flex-centering wrapper",
173
- 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.",
174
- before: '<div style="display:flex;align-items:center;justify-content:center"><Child/></div>',
175
- after: '<Child style="place-self:center"/>',
176
- 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."
348
+ title: "Flatten inherited-only styling wrapper",
349
+ 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.",
350
+ before: '<div style="text-align:center"><Child/></div>',
351
+ after: '<Child style="text-align:center"/>',
352
+ 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."
177
353
  },
178
354
  match: {
179
- tag: "div",
180
- style: { display: "flex", alignItems: "center", justifyContent: "center" },
181
355
  onlyChild: "element",
182
- paintsNothing: true
183
- },
184
- rewrite: {
185
- flattenInto: "child",
186
- childGains: { placeSelf: "center" }
356
+ paintsNothing: true,
357
+ where: [isInertHostTag, not(isComponentNode2), hasOnlyInheritedStyle]
187
358
  },
188
- // Collapsing a flex-centering wrapper to `place-self:center` on the child is render-identical ONLY
189
- // when the child's NEW parent is a statically-known GRID that lets the wrapper fill its area (there
190
- // `place-self`'s align-self AND justify-self both take effect). Under that ONE context the flatten is
191
- // classified `provably-safe` and commits; under a flex/block/unknown parent — or when the wrapper
192
- // drops any own style — it stays `needs-verification` and the conservative production gate PRESERVES
193
- // it. Op-level correctness (purity, id-preserving unwrap, opacity-barrier safety) is additionally
194
- // asserted by the invariant suite over every pattern.
359
+ rewrite: { flattenInto: "child" },
195
360
  test: {
196
361
  cases: [
197
362
  {
198
- name: "grid parent \u2192 flattened (child gains place-self-center)",
199
- before: '<div className="grid"><div className="flex items-center justify-center"><span className="bg-red-200">x</span></div></div>',
200
- after: '<div className="grid"><span className="bg-red-200 place-self-center">x</span></div>'
363
+ // `text-align:center` is inherited folded onto the child; the paint-free wrapper is removed.
364
+ before: '<div className="text-center"><p className="bg-red-200">x</p></div>',
365
+ after: '<p className="bg-red-200 text-center">x</p>'
201
366
  }
202
367
  ],
203
368
  noMatch: [
204
- // Non-grid (flex) parent (document root): `justify-self` is ignored in flex not provably safe.
205
- '<div className="flex justify-center items-center"><div className="bg-red-200">Hello</div></div>',
206
- // Grid parent, but the wrapper drops padding when removed → not layout-neutral (rule 3).
207
- '<div className="grid"><div className="p-4 flex items-center justify-center"><span className="bg-red-200">x</span></div></div>',
208
- // Grid parent forcing place-items-center: the wrapper would not fill its area fill guard skips.
209
- '<div className="grid place-items-center"><div className="flex items-center justify-center"><span className="bg-red-200">x</span></div></div>',
210
- // onClick is a hard opacity barrier → the wrapper is load-bearing regardless of the gate.
211
- '<div className="flex justify-center items-center" onClick={handleClick}><div className="bg-red-200">Hello</div></div>'
212
- ]
213
- }
214
- });
215
-
216
- // ../patterns/src/library/flatten/inline-flex-center-wrapper.pattern.ts
217
- init_esm_shims();
218
- var inlineFlexCenterWrapper = definePattern({
219
- name: "inline-flex-center-wrapper",
220
- category: "flatten/inline-flex-center-wrapper",
221
- safety: 2,
222
- doc: {
223
- title: "Flatten inline-flex-centering wrapper",
224
- 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.",
225
- before: '<div style="display:inline-flex;align-items:center;justify-content:center"><Child/></div>',
226
- after: '<Child style="place-self:center"/>',
227
- 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."
228
- },
229
- match: {
230
- tag: "div",
231
- style: { display: "inline-flex", alignItems: "center", justifyContent: "center" },
232
- onlyChild: "element",
233
- paintsNothing: true
234
- },
235
- rewrite: {
236
- flattenInto: "child",
237
- childGains: { placeSelf: "center" }
238
- },
239
- // Like its block-level sibling, this centering flatten is `needs-verification` (the wrapper's own
240
- // `display:inline-flex` establishes a formatting context, and place-self centering only holds under
241
- // a flex/grid parent), so the conservative production gate (`'provably-safe'`) REVERTS it — every
242
- // case here is a no-match. Op-level correctness is covered by the invariant suite.
243
- test: {
244
- noMatch: [
245
- // Even under a static flex/grid parent the centering flatten is not provably layout-neutral.
246
- '<div className="grid"><div className="inline-flex items-center justify-center"><span className="bg-red-200">x</span></div></div>',
247
- // Non-flex/grid parent (document root) → left unchanged.
248
- '<div className="inline-flex justify-center items-center"><div className="bg-red-200">Hello</div></div>',
249
- // onClick is a hard opacity barrier → the wrapper is load-bearing regardless of the gate.
250
- '<div className="inline-flex justify-center items-center" onClick={handleClick}><div className="bg-red-200">Hello</div></div>'
251
- ]
252
- }
253
- });
254
-
255
- // ../patterns/src/library/flatten/nested-flex-merge.pattern.ts
256
- init_esm_shims();
257
- function baseConditionStyleMap(decls) {
258
- const map = /* @__PURE__ */ new Map();
259
- for (const [prop, value] of decls) {
260
- for (const decl of normalizer.normalizeDeclaration(prop, value, false)) {
261
- map.set(decl.property, decl);
262
- }
263
- }
264
- const block = { condition: BASE_CONDITION, decls: map };
265
- const blocks = /* @__PURE__ */ new Map([[conditionKey(BASE_CONDITION), block]]);
266
- return { blocks };
267
- }
268
- var DISPLAY_FLEX = baseConditionStyleMap([["display", "flex"]]);
269
- var FLEX_CONTAINER_PROPERTIES = /* @__PURE__ */ new Set([
270
- "display",
271
- "flex-direction",
272
- "flex-wrap",
273
- "justify-content",
274
- "align-items",
275
- "align-content",
276
- "place-content",
277
- "place-items",
278
- "row-gap",
279
- "column-gap"
280
- ]);
281
- function outerMergeSafe(sm) {
282
- const norm = normalizer.normalizeStyleMap(sm);
283
- for (const block of norm.blocks.values()) {
284
- for (const decl of block.decls.values()) {
285
- if (FLEX_CONTAINER_PROPERTIES.has(String(decl.property))) continue;
286
- if (decl.inherited) continue;
287
- return false;
288
- }
289
- }
290
- return true;
291
- }
292
- function flexConflict(outer, inner) {
293
- const a = normalizer.normalizeStyleMap(outer);
294
- const b = normalizer.normalizeStyleMap(inner);
295
- for (const [key, blockA] of a.blocks) {
296
- const blockB = b.blocks.get(key);
297
- if (!blockB) continue;
298
- for (const [prop, declA] of blockA.decls) {
299
- if (!FLEX_CONTAINER_PROPERTIES.has(String(prop))) continue;
300
- const declB = blockB.decls.get(prop);
301
- if (declB && declB.value !== declA.value) return true;
302
- }
303
- }
304
- return false;
305
- }
306
- function extractFlexStyle(sm) {
307
- const blocks = /* @__PURE__ */ new Map();
308
- for (const [key, block] of sm.blocks) {
309
- const decls = /* @__PURE__ */ new Map();
310
- for (const [prop, decl] of block.decls) {
311
- if (FLEX_CONTAINER_PROPERTIES.has(String(prop))) decls.set(prop, decl);
312
- }
313
- if (decls.size > 0) blocks.set(key, { condition: block.condition, decls });
314
- }
315
- return { blocks };
316
- }
317
- var isInnerFlex = and(
318
- isElement("div"),
319
- computed(DISPLAY_FLEX),
320
- not(targetedByCombinator)
321
- );
322
- var nestedFlexMerge = definePattern({
323
- name: "nested-flex-merge",
324
- category: "flatten/nested-flex-merge",
325
- safety: 2,
326
- doc: {
327
- title: "Merge nested flex containers",
328
- 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.",
329
- before: '<div style="display:flex;align-items:center;gap:8px"><div style="display:flex;flex-direction:column"/></div>',
330
- after: '<div style="display:flex;flex-direction:column;align-items:center;gap:8px"/>',
331
- 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."
332
- },
333
- match: {
334
- tag: "div",
335
- style: { display: "flex" },
336
- onlyChild: "element",
337
- paintsNothing: true
338
- },
339
- rewrite: (ctx, rw) => {
340
- const outer = ctx.node;
341
- const inner = ctx.onlyElementChild();
342
- if (!inner) return null;
343
- if (!isInnerFlex(inner, ctx)) return null;
344
- const outerStyle = ctx.computed();
345
- const innerStyle = ctx.computedOf(inner);
346
- if (!outerMergeSafe(outerStyle)) return null;
347
- if (flexConflict(outerStyle, innerStyle)) return null;
348
- return [
349
- // 1. Preserve inheritable values (color/font/…) by folding them onto the child first.
350
- rw.foldInheritedStyles(outer, inner, { conditions: "all" }),
351
- // 2. Transfer the wrapper's flex-container declarations onto the child (target-wins keeps the
352
- // child's value for any shared property — identical anyway, we proved non-conflict).
353
- rw.mergeStyle(inner, null, extractFlexStyle(outerStyle), "target-wins"),
354
- // 3. Remove the wrapper (structural-safe; hoists the child and preserves its IRNodeId).
355
- rw.unwrap(outer)
356
- ];
357
- },
358
- // Merging the outer flex container into the inner removes the outer's box, but a `display:flex`
359
- // wrapper establishes a formatting context, so this is a `needs-verification` flatten that the
360
- // conservative production gate (`'provably-safe'`) REVERTS — every case here is a no-match. The
361
- // merge's op-level correctness (purity, id-preserving unwrap, opacity-barrier safety) is asserted
362
- // by the invariant suite over every pattern.
363
- test: {
364
- noMatch: [
365
- // The merge is real but not provably layout-neutral (the wrapper establishes a flex context),
366
- // so under the conservative gate the nested containers are left in place.
367
- '<div className="flex items-center gap-2" data-x="1"><div className="flex flex-col">X</div></div>',
368
- // A non-flex wrapper does not match the flex-container signature → left unchanged anyway.
369
- '<div className="block bg-blue-500"><div className="flex flex-col">X</div></div>'
370
- ]
371
- }
372
- });
373
-
374
- // ../patterns/src/library/flatten/nested-grid-merge.pattern.ts
375
- init_esm_shims();
376
- function baseConditionStyleMap2(decls) {
377
- const map = /* @__PURE__ */ new Map();
378
- for (const [prop, value] of decls) {
379
- for (const decl of normalizer.normalizeDeclaration(prop, value, false)) {
380
- map.set(decl.property, decl);
381
- }
382
- }
383
- const block = { condition: BASE_CONDITION, decls: map };
384
- const blocks = /* @__PURE__ */ new Map([[conditionKey(BASE_CONDITION), block]]);
385
- return { blocks };
386
- }
387
- var DISPLAY_GRID = baseConditionStyleMap2([["display", "grid"]]);
388
- var GRID_CONTAINER_PROPERTIES = /* @__PURE__ */ new Set([
389
- "display",
390
- "grid-template-columns",
391
- "grid-template-rows",
392
- "grid-template-areas",
393
- "grid-auto-columns",
394
- "grid-auto-rows",
395
- "grid-auto-flow",
396
- "justify-content",
397
- "align-content",
398
- "place-content",
399
- "justify-items",
400
- "align-items",
401
- "place-items",
402
- "row-gap",
403
- "column-gap"
404
- ]);
405
- function outerMergeSafe2(sm) {
406
- const norm = normalizer.normalizeStyleMap(sm);
407
- for (const block of norm.blocks.values()) {
408
- for (const decl of block.decls.values()) {
409
- if (GRID_CONTAINER_PROPERTIES.has(String(decl.property))) continue;
410
- if (decl.inherited) continue;
411
- return false;
412
- }
413
- }
414
- return true;
415
- }
416
- function gridConflict(outer, inner) {
417
- const a = normalizer.normalizeStyleMap(outer);
418
- const b = normalizer.normalizeStyleMap(inner);
419
- for (const [key, blockA] of a.blocks) {
420
- const blockB = b.blocks.get(key);
421
- if (!blockB) continue;
422
- for (const [prop, declA] of blockA.decls) {
423
- if (!GRID_CONTAINER_PROPERTIES.has(String(prop))) continue;
424
- const declB = blockB.decls.get(prop);
425
- if (declB && declB.value !== declA.value) return true;
426
- }
427
- }
428
- return false;
429
- }
430
- function extractGridStyle(sm) {
431
- const blocks = /* @__PURE__ */ new Map();
432
- for (const [key, block] of sm.blocks) {
433
- const decls = /* @__PURE__ */ new Map();
434
- for (const [prop, decl] of block.decls) {
435
- if (GRID_CONTAINER_PROPERTIES.has(String(prop))) decls.set(prop, decl);
436
- }
437
- if (decls.size > 0) blocks.set(key, { condition: block.condition, decls });
438
- }
439
- return { blocks };
440
- }
441
- var isInnerGrid = and(
442
- isElement("div"),
443
- computed(DISPLAY_GRID),
444
- not(targetedByCombinator)
445
- );
446
- var nestedGridMerge = definePattern({
447
- name: "nested-grid-merge",
448
- category: "flatten/nested-grid-merge",
449
- safety: 2,
450
- doc: {
451
- title: "Merge nested grid containers",
452
- 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.",
453
- before: '<div style="display:grid;gap:8px"><div style="display:grid;grid-template-columns:1fr 1fr"/></div>',
454
- after: '<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px"/>',
455
- 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."
456
- },
457
- match: {
458
- tag: "div",
459
- style: { display: "grid" },
460
- onlyChild: "element",
461
- paintsNothing: true
462
- },
463
- rewrite: (ctx, rw) => {
464
- const outer = ctx.node;
465
- const inner = ctx.onlyElementChild();
466
- if (!inner) return null;
467
- if (!isInnerGrid(inner, ctx)) return null;
468
- const outerStyle = ctx.computed();
469
- const innerStyle = ctx.computedOf(inner);
470
- if (!outerMergeSafe2(outerStyle)) return null;
471
- if (gridConflict(outerStyle, innerStyle)) return null;
472
- return [
473
- // 1. Preserve inheritable values (color/font/…) by folding them onto the child first.
474
- rw.foldInheritedStyles(outer, inner, { conditions: "all" }),
475
- // 2. Transfer the wrapper's grid-container declarations onto the child (target-wins keeps the
476
- // child's value for any shared property — identical anyway, we proved non-conflict).
477
- rw.mergeStyle(inner, null, extractGridStyle(outerStyle), "target-wins"),
478
- // 3. Remove the wrapper (structural-safe; hoists the child and preserves its IRNodeId).
479
- rw.unwrap(outer)
480
- ];
481
- },
482
- // Like its flex sibling, this merge removes the outer container's box, but a `display:grid` wrapper
483
- // establishes a formatting context, so it is a `needs-verification` flatten that the conservative
484
- // production gate (`'provably-safe'`) REVERTS — every case here is a no-match. Op-level correctness
485
- // is asserted by the invariant suite over every pattern.
486
- test: {
487
- noMatch: [
488
- // The merge is real but not provably layout-neutral (the wrapper establishes a grid context),
489
- // so under the conservative gate the nested containers are left in place.
490
- '<div className="grid gap-2" data-x="1"><div className="grid grid-cols-2">X</div></div>',
491
- // A non-grid wrapper does not match the grid-container signature → left unchanged anyway.
492
- '<div className="block bg-blue-500"><div className="grid grid-cols-2">X</div></div>'
369
+ // `p-4` is a NON-inherited padding: removing the box would drop it, so the flatten-safety gate
370
+ // reverts the unwrap and the wrapper is left unchanged.
371
+ '<div className="p-4"><p className="bg-red-200">x</p></div>',
372
+ // A `<p>` wrapper is NOT an inert host box: its UA default display/margins are not captured in the
373
+ // class-derived computed style, so removing it is not provably layout-neutralleft unchanged.
374
+ '<p className="text-center"><span className="bg-red-200">x</span></p>'
493
375
  ]
494
376
  }
495
377
  });
496
378
 
497
- // ../patterns/src/library/flatten/passthrough-wrapper.pattern.ts
379
+ // ../patterns/src/library/wrapper/passthrough-wrapper.pattern.ts
498
380
  init_esm_shims();
499
381
  function metaOf2(node) {
500
382
  const n = node;
@@ -510,7 +392,7 @@ var establishesContext = (node) => {
510
392
  return m.establishesBox || m.establishesFormattingContext || m.establishesStackingContext || m.isContainingBlock || m.declaresCustomProperties;
511
393
  };
512
394
  var hasSpreadAttrs2 = (node) => metaOf2(node)?.hasSpreadAttrs ?? false;
513
- var isComponentNode2 = (node) => metaOf2(node)?.isComponent ?? false;
395
+ var isComponentNode3 = (node) => metaOf2(node)?.isComponent ?? false;
514
396
  var hasOwnAttrs2 = (node) => {
515
397
  const el = elementOf(node);
516
398
  if (!el) return false;
@@ -524,1263 +406,143 @@ var targetedByStructuralPseudo3 = (node, ctx) => {
524
406
  };
525
407
  var passthroughWrapper = definePattern({
526
408
  name: "passthrough-wrapper",
527
- category: "flatten/passthrough-wrapper",
528
- safety: 2,
529
- doc: {
530
- title: "Flatten passthrough wrapper",
531
- 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.",
532
- before: "<div><Child/></div>",
533
- after: "<Child/>",
534
- 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."
535
- },
536
- match: {
537
- tag: "div",
538
- onlyChild: "element",
539
- paintsNothing: true,
540
- where: [
541
- not(establishesContext),
542
- not(hasOwnAttrs2),
543
- not(hasDynamicClasses),
544
- not(hasSpreadAttrs2),
545
- not(isComponentNode2),
546
- not(targetedByStructuralPseudo3)
547
- ]
548
- },
549
- rewrite: { flattenInto: "child" },
550
- test: {
551
- cases: [
552
- {
553
- // A plain, style-free wrapper paints nothing and establishes no context → a provably-safe
554
- // flatten under the conservative gate: the wrapper is removed and its sole child hoisted.
555
- before: '<div><a className="bg-red-200">Link</a></div>',
556
- after: '<a className="bg-red-200">Link</a>'
557
- }
558
- ],
559
- noMatch: [
560
- // A ref pins the wrapper's element identity (a hard opacity barrier) → not a passthrough.
561
- '<div ref={rootRef}><a className="bg-red-200">Link</a></div>',
562
- // A `display:flex` wrapper establishes a formatting context, so removing its box is NOT
563
- // provably layout-neutral → the conservative gate leaves it in place.
564
- '<div className="flex"><a className="bg-red-200">Link</a></div>'
565
- ]
566
- }
567
- });
568
-
569
- // ../patterns/src/library/flatten/redundant-fragment.pattern.ts
570
- init_esm_shims();
571
- function parentIsRedundantFragment(node, ctx) {
572
- const el = node;
573
- if (el.kind !== "element") return false;
574
- const parentId = el.parent;
575
- if (parentId == null) return false;
576
- const parent = ctx.doc.nodes.get(parentId);
577
- if (!parent || parent.kind !== "fragment") return false;
578
- if (parent.parent == null) return false;
579
- if (parent.children.length !== 1) return false;
580
- const m = parent.meta;
581
- if (m.hasKey || m.hasRef || m.hasEventHandlers || m.hasDynamicChildren || m.hasDangerousHtml || m.hasSpreadAttrs || m.isComponent) {
582
- return false;
583
- }
584
- if (m.targetedByCombinator || m.targetedByStructuralPseudo) return false;
585
- const fid = parentId;
586
- if (ctx.selectors.targetedByCombinator(fid) || ctx.selectors.targetedByStructuralPseudo(fid)) {
587
- return false;
588
- }
589
- if (ctx.selectors.reparentImpact(fid).size > 0) return false;
590
- return true;
591
- }
592
- var redundantFragment = definePattern({
593
- name: "redundant-fragment",
594
- category: "flatten/redundant-fragment",
595
- safety: 1,
596
- doc: {
597
- title: "Flatten redundant single-child fragment",
598
- 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.",
599
- before: "<><Child/></>",
600
- after: "<Child/>",
601
- 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."
602
- },
603
- match: parentIsRedundantFragment,
604
- rewrite: (ctx, rw) => {
605
- const parentId = ctx.node.parent;
606
- if (parentId == null) return null;
607
- const fragment = ctx.doc.nodes.get(parentId);
608
- if (!fragment || fragment.kind !== "fragment") return null;
609
- return [rw.unwrap(fragment)];
610
- },
611
- test: {
612
- cases: [
613
- {
614
- // A fragment renders no box, so unwrapping a single-child fragment is always layout-identical
615
- // → a provably-safe flatten: the child is spliced up into the fragment's slot.
616
- before: '<><span className="bg-red-200">Hi</span></>',
617
- after: '<span className="bg-red-200">Hi</span>'
618
- }
619
- ],
620
- noMatch: [
621
- // Two children ⇒ not a single-child fragment, so the fragment is load-bearing and stays.
622
- '<><span className="bg-red-200">A</span><span className="bg-green-200">B</span></>'
623
- ]
624
- }
625
- });
626
-
627
- // ../patterns/src/library/flatten/redundant-inline-wrapper.pattern.ts
628
- init_esm_shims();
629
- function asEl3(node) {
630
- const n = node;
631
- return n.kind === "element" ? n : null;
632
- }
633
- function metaOf3(node) {
634
- return asEl3(node)?.meta ?? null;
635
- }
636
- var establishesContext2 = (node) => {
637
- const m = metaOf3(node);
638
- if (!m) return false;
639
- return m.establishesBox || m.establishesFormattingContext || m.establishesStackingContext || m.isContainingBlock || m.declaresCustomProperties;
640
- };
641
- var hasSpreadAttrs3 = (node) => metaOf3(node)?.hasSpreadAttrs ?? false;
642
- var isComponentNode3 = (node) => metaOf3(node)?.isComponent ?? false;
643
- var hasOwnAttrs3 = (node) => {
644
- const el = asEl3(node);
645
- if (!el) return false;
646
- return el.attrs.entries.size > 0 || el.attrs.spreads.length > 0;
647
- };
648
- var targetedByStructuralPseudo4 = (node, ctx) => {
649
- const el = asEl3(node);
650
- if (!el) return false;
651
- if (el.meta.targetedByStructuralPseudo) return true;
652
- return ctx.selectors.targetedByStructuralPseudo(el.id);
653
- };
654
- var DISPLAY2 = "display";
655
- var hasNonInlineDisplay = (node, ctx) => {
656
- const el = asEl3(node);
657
- if (!el) return false;
658
- const sm = ctx.computedOf(el) ?? el.computed;
659
- for (const block of sm.blocks.values()) {
660
- const decl = block.decls.get(DISPLAY2);
661
- if (decl && String(decl.value) !== "inline") return true;
662
- }
663
- return false;
664
- };
665
- var redundantInlineWrapper = definePattern({
666
- name: "redundant-inline-wrapper",
667
- category: "flatten/redundant-inline-wrapper",
668
- safety: 2,
669
- doc: {
670
- title: "Flatten redundant inline wrapper",
671
- 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.",
672
- before: "<span><Child/></span>",
673
- after: "<Child/>",
674
- 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."
675
- },
676
- match: {
677
- tag: "span",
678
- onlyChild: "element",
679
- paintsNothing: true,
680
- where: [
681
- not(hasNonInlineDisplay),
682
- not(establishesContext2),
683
- not(hasOwnAttrs3),
684
- not(hasDynamicClasses),
685
- not(hasSpreadAttrs3),
686
- not(isComponentNode3),
687
- not(targetedByStructuralPseudo4)
688
- ]
689
- },
690
- rewrite: { flattenInto: "child" },
691
- test: {
692
- cases: [
693
- {
694
- // An empty inline span paints nothing and establishes no context → a provably-safe flatten:
695
- // the span is removed and its sole child hoisted in place.
696
- before: '<span><a className="text-blue-500">Link</a></span>',
697
- after: '<a className="text-blue-500">Link</a>'
698
- }
699
- ],
700
- noMatch: [
701
- // A ref pins the span's element identity (a hard opacity barrier) → not a passthrough.
702
- '<span ref={spanRef}><a className="text-blue-500">Link</a></span>',
703
- // The span paints its own background (own visual style) → kept.
704
- '<span className="bg-green-200"><a className="text-blue-500">Link</a></span>',
705
- // Non-inline display (inline-block) participates in layout differently → kept.
706
- '<span className="inline-block"><a className="text-blue-500">Link</a></span>'
707
- ]
708
- }
709
- });
710
-
711
- // ../patterns/src/library/compress/border-radius-shorthand.pattern.ts
712
- init_esm_shims();
713
- var CORNERS = [
714
- "border-top-left-radius",
715
- "border-top-right-radius",
716
- "border-bottom-right-radius",
717
- "border-bottom-left-radius"
718
- ];
719
- var CORNER_SET = new Set(CORNERS);
720
- var BASE_KEY = conditionKey(BASE_CONDITION);
721
- var RADIUS = "border-radius";
722
- var NON_COLLAPSIBLE_VALUES = /* @__PURE__ */ new Set([
723
- "initial",
724
- "inherit",
725
- "unset",
726
- "revert",
727
- "revert-layer"
728
- ]);
729
- function analyzeRadius(sm) {
730
- const block = sm.blocks.get(BASE_KEY);
731
- if (!block) return null;
732
- const corners = [];
733
- for (const corner of CORNERS) {
734
- const decl = block.decls.get(corner);
735
- if (!decl) return null;
736
- corners.push(decl);
737
- }
738
- const important = corners[0].important;
739
- if (!corners.every((d) => d.important === important)) return null;
740
- const value = String(corners[0].value);
741
- if (NON_COLLAPSIBLE_VALUES.has(value)) return null;
742
- if (!corners.every((d) => String(d.value) === value)) return null;
743
- const relative = corners.some((d) => d.relativeToParent);
744
- return { value, important, relative };
745
- }
746
- function withFoldedRadius(sm, fold) {
747
- const blocks = /* @__PURE__ */ new Map();
748
- for (const [key, block] of sm.blocks) {
749
- if (key !== BASE_KEY) {
750
- blocks.set(key, block);
751
- continue;
752
- }
753
- const decls = /* @__PURE__ */ new Map();
754
- for (const [prop, decl] of block.decls) {
755
- if (CORNER_SET.has(String(prop))) continue;
756
- decls.set(prop, decl);
757
- }
758
- const shorthand = {
759
- property: RADIUS,
760
- value: fold.value,
761
- important: fold.important,
762
- relativeToParent: fold.relative,
763
- inherited: false
764
- // border-radius is never inherited
765
- };
766
- decls.set(shorthand.property, shorthand);
767
- blocks.set(key, { condition: block.condition, decls });
768
- }
769
- return { blocks };
770
- }
771
- var borderRadiusShorthand = definePattern({
772
- name: "border-radius-shorthand",
773
- category: "compress/border-radius-shorthand",
774
- safety: 1,
775
- doc: {
776
- title: "Collapse equal corner radii into border-radius",
777
- 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).",
778
- before: '<div class="rounded-tl-lg rounded-tr-lg rounded-br-lg rounded-bl-lg"/>',
779
- after: '<div class="rounded-lg"/>',
780
- 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."
781
- },
782
- rewrite: {
783
- rewriteClasses(computed2) {
784
- const fold = analyzeRadius(computed2);
785
- return fold ? withFoldedRadius(computed2, fold) : null;
786
- }
787
- },
788
- test: {
789
- cases: [
790
- {
791
- // The four equal corner longhands collapse to a `border-radius` decl at the IR level; the
792
- // minimizing reverse-emit then picks the single shortest utility (`rounded-lg`) that reproduces
793
- // it, replacing the four `rounded-{tl,tr,br,bl}-lg` tokens. `bg-red-200` is preserved.
794
- before: '<div className="rounded-tl-lg rounded-tr-lg rounded-br-lg rounded-bl-lg bg-red-200">box</div>',
795
- after: '<div className="bg-red-200 rounded-lg">box</div>'
796
- }
797
- ],
798
- // Corners differ (top corners vs bottom corners) → no all-equal collapse.
799
- noMatch: ['<div className="rounded-t-lg rounded-b-sm bg-red-200">box</div>']
800
- }
801
- });
802
-
803
- // ../patterns/src/library/compress/border-shorthand.pattern.ts
804
- init_esm_shims();
805
- var WIDTH_SIDES = [
806
- "border-top-width",
807
- "border-right-width",
808
- "border-bottom-width",
809
- "border-left-width"
810
- ];
811
- var WIDTH_SIDE_SET = new Set(WIDTH_SIDES);
812
- var BASE_KEY2 = conditionKey(BASE_CONDITION);
813
- var BORDER_WIDTH = "border-width";
814
- function analyzeWidth(sm) {
815
- const block = sm.blocks.get(BASE_KEY2);
816
- if (!block) return null;
817
- const sides = [];
818
- for (const side of WIDTH_SIDES) {
819
- const decl = block.decls.get(side);
820
- if (!decl) return null;
821
- sides.push(decl);
822
- }
823
- const [top, right, bottom, left] = sides;
824
- if (!(top.important === right.important && right.important === bottom.important && bottom.important === left.important)) {
825
- return null;
826
- }
827
- const tv = String(top.value);
828
- const rv = String(right.value);
829
- const bv = String(bottom.value);
830
- const lv = String(left.value);
831
- if (tv !== bv || lv !== rv) return null;
832
- const value = tv === lv ? tv : `${tv} ${lv}`;
833
- const relative = sides.some((d) => d.relativeToParent);
834
- return { value, important: top.important, relative };
835
- }
836
- function withFoldedWidth(sm, fold) {
837
- const blocks = /* @__PURE__ */ new Map();
838
- for (const [key, block] of sm.blocks) {
839
- if (key !== BASE_KEY2) {
840
- blocks.set(key, block);
841
- continue;
842
- }
843
- const decls = /* @__PURE__ */ new Map();
844
- for (const [prop, decl] of block.decls) {
845
- if (WIDTH_SIDE_SET.has(String(prop))) continue;
846
- decls.set(prop, decl);
847
- }
848
- const shorthand = {
849
- property: BORDER_WIDTH,
850
- value: fold.value,
851
- important: fold.important,
852
- relativeToParent: fold.relative,
853
- inherited: false
854
- // border-width is never inherited
855
- };
856
- decls.set(shorthand.property, shorthand);
857
- blocks.set(key, { condition: block.condition, decls });
858
- }
859
- return { blocks };
860
- }
861
- var borderShorthand = definePattern({
862
- name: "border-shorthand",
863
- category: "compress/border-shorthand",
864
- safety: 1,
865
- doc: {
866
- title: "Collapse border-width longhands to shorthand",
867
- 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-*).",
868
- before: '<div class="border-t-2 border-r-2 border-b-2 border-l-2"/>',
869
- after: '<div class="border-2"/>',
870
- 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."
871
- },
872
- rewrite: {
873
- rewriteClasses(computed2) {
874
- const fold = analyzeWidth(computed2);
875
- return fold ? withFoldedWidth(computed2, fold) : null;
876
- }
877
- },
878
- test: {
879
- cases: [
880
- {
881
- // The four equal width longhands collapse to a `border-width` shorthand at the IR level, and the
882
- // minimizing reverse-emit picks the single shortest utility (`border-2`) that reproduces it,
883
- // replacing the four `border-{t,r,b,l}-2` tokens. `bg-red-200` is preserved.
884
- before: '<div className="border-t-2 border-r-2 border-b-2 border-l-2 bg-red-200">box</div>',
885
- after: '<div className="bg-red-200 border-2">box</div>'
886
- }
887
- ],
888
- // Asymmetric widths (top != bottom) cannot fold into a shorthand.
889
- noMatch: ['<div className="border-t-2 border-r-4 border-b-8 border-l-4 bg-red-200">box</div>']
890
- }
891
- });
892
-
893
- // ../patterns/src/library/compress/dedupe-classes.pattern.ts
894
- init_esm_shims();
895
- function findRedundantClasses(computed2) {
896
- const winners = /* @__PURE__ */ new Set();
897
- const shadowed = /* @__PURE__ */ new Set();
898
- for (const block of computed2.blocks.values()) {
899
- for (const decl of block.decls.values()) {
900
- if (decl.origin && decl.origin.kind === "class") winners.add(decl.origin.className);
901
- for (const o of decl.shadowed ?? []) {
902
- if (o.kind === "class") shadowed.add(o.className);
903
- }
904
- }
905
- }
906
- return { winners, shadowed };
907
- }
908
- var dedupeClasses = definePattern({
909
- name: "dedupe-classes",
910
- category: "compress/dedupe-classes",
911
- safety: 1,
912
- doc: {
913
- title: "Dedupe fully-overridden class tokens",
914
- 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.",
915
- before: '<p class="text-sm text-lg" />',
916
- after: '<p class="text-lg" />',
917
- 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."
918
- },
919
- rewrite: {
920
- dropClasses(computed2, ctx) {
921
- const { winners, shadowed } = findRedundantClasses(computed2);
922
- const drop = /* @__PURE__ */ new Set();
923
- for (const cls of shadowed) {
924
- if (winners.has(cls)) continue;
925
- if (!ctx.resolver.selectorUsage(cls).droppable) continue;
926
- drop.add(cls);
927
- }
928
- return drop;
929
- }
930
- },
931
- test: {
932
- cases: [
933
- {
934
- // `text-sm` is fully overridden by `text-lg` (both set font-size + line-height). The resolver
935
- // records that shadowing in provenance and reports the Tailwind utility as droppable, so the
936
- // pattern drops `text-sm`; the reverse-emit then re-derives the minimal set (`text-lg`).
937
- before: '<p className="text-sm text-lg">Hi</p>',
938
- after: '<p className="text-lg">Hi</p>'
939
- }
940
- ],
941
- // Both tokens win a distinct property (no full override) → nothing to dedupe.
942
- noMatch: ['<p className="text-lg font-bold">Hi</p>']
943
- }
944
- });
945
-
946
- // ../patterns/src/library/compress/gap-shorthand.pattern.ts
947
- init_esm_shims();
948
- var ROW_GAP = "row-gap";
949
- var COLUMN_GAP = "column-gap";
950
- var GAP = "gap";
951
- var BASE_KEY3 = conditionKey(BASE_CONDITION);
952
- function withGapShorthand(sm, gapDecl) {
953
- const blocks = /* @__PURE__ */ new Map();
954
- for (const [key, block] of sm.blocks) {
955
- if (key !== BASE_KEY3) {
956
- blocks.set(key, block);
957
- continue;
958
- }
959
- const decls = /* @__PURE__ */ new Map();
960
- for (const [prop, decl] of block.decls) {
961
- if (prop === ROW_GAP || prop === COLUMN_GAP) continue;
962
- decls.set(prop, decl);
963
- }
964
- decls.set(gapDecl.property, gapDecl);
965
- blocks.set(key, { condition: block.condition, decls });
966
- }
967
- return { blocks };
968
- }
969
- var gapShorthand = definePattern({
970
- name: "gap-shorthand",
971
- category: "compress/gap-shorthand",
972
- safety: 1,
973
- doc: {
974
- title: "Collapse equal row/column gap into the `gap` shorthand",
975
- 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-*).",
976
- before: '<div style="row-gap:16px;column-gap:16px"/>',
977
- after: '<div style="gap:16px"/>',
978
- 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."
979
- },
980
- rewrite: {
981
- rewriteClasses(computed2) {
982
- const base = computed2.blocks.get(BASE_KEY3);
983
- if (!base) return null;
984
- const rowGap = base.decls.get(ROW_GAP);
985
- const colGap = base.decls.get(COLUMN_GAP);
986
- if (!rowGap || !colGap) return null;
987
- if (rowGap.important !== colGap.important) return null;
988
- if (rowGap.value !== colGap.value) return null;
989
- const gapDecl = {
990
- property: GAP,
991
- value: rowGap.value,
992
- important: rowGap.important,
993
- relativeToParent: rowGap.relativeToParent || colGap.relativeToParent,
994
- inherited: false
995
- // gap is not an inherited property
996
- };
997
- return withGapShorthand(computed2, gapDecl);
998
- }
999
- },
1000
- test: {
1001
- cases: [
1002
- {
1003
- // Equal row/column gap collapse to a `gap` decl at the IR level; the minimizing reverse-emit
1004
- // re-expands `gap` to row-gap+column-gap and picks the single utility covering both (`gap-4`),
1005
- // replacing the `gap-x-4`+`gap-y-4` pair. `bg-red-200` is preserved.
1006
- before: '<div className="gap-x-4 gap-y-4 bg-red-200">box</div>',
1007
- after: '<div className="bg-red-200 gap-4">box</div>'
1008
- }
1009
- ],
1010
- // Unequal axes (row-gap != column-gap) have no single-value `gap` equivalent → not collapsed.
1011
- noMatch: ['<div className="gap-x-2 gap-y-4 bg-red-200">box</div>']
1012
- }
1013
- });
1014
-
1015
- // ../patterns/src/library/compress/inset-shorthand.pattern.ts
1016
- init_esm_shims();
1017
- var TOP = "top";
1018
- var RIGHT = "right";
1019
- var BOTTOM = "bottom";
1020
- var LEFT = "left";
1021
- var INSET = "inset";
1022
- var INSET_BLOCK = "inset-block";
1023
- var INSET_INLINE = "inset-inline";
1024
- function sameSide(a, b) {
1025
- return a !== void 0 && b !== void 0 && a.value === b.value && a.important === b.important;
1026
- }
1027
- function asProperty(src, property) {
1028
- return { ...src, property, inherited: normalizer.inherited.isInherited(property) };
1029
- }
1030
- function withBaseDecls(src, baseDecls) {
1031
- const blocks = /* @__PURE__ */ new Map();
1032
- for (const [key, block] of src.blocks) {
1033
- const decls = key === BASE_CONDITION_KEY ? baseDecls : new Map(block.decls);
1034
- blocks.set(key, { condition: block.condition, decls });
1035
- }
1036
- return { blocks };
1037
- }
1038
- var insetShorthand = definePattern({
1039
- name: "inset-shorthand",
1040
- category: "compress/inset-shorthand",
1041
- safety: 2,
1042
- doc: {
1043
- title: "Compress inset longhands into a shorthand",
1044
- 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).",
1045
- before: '<div style="top:10px;right:10px;bottom:10px;left:10px"/>',
1046
- after: '<div style="inset:10px"/>',
1047
- 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."
1048
- },
1049
- rewrite: {
1050
- rewriteClasses(computed2) {
1051
- const base = computed2.blocks.get(BASE_CONDITION_KEY);
1052
- if (!base) return null;
1053
- const top = base.decls.get(TOP);
1054
- const right = base.decls.get(RIGHT);
1055
- const bottom = base.decls.get(BOTTOM);
1056
- const left = base.decls.get(LEFT);
1057
- const next = new Map(base.decls);
1058
- if (top && sameSide(top, right) && sameSide(top, bottom) && sameSide(top, left)) {
1059
- next.delete(TOP);
1060
- next.delete(RIGHT);
1061
- next.delete(BOTTOM);
1062
- next.delete(LEFT);
1063
- next.set(INSET, asProperty(top, INSET));
1064
- } else {
1065
- let collapsed = false;
1066
- if (sameSide(top, bottom)) {
1067
- next.delete(TOP);
1068
- next.delete(BOTTOM);
1069
- next.set(INSET_BLOCK, asProperty(top, INSET_BLOCK));
1070
- collapsed = true;
1071
- }
1072
- if (sameSide(left, right)) {
1073
- next.delete(LEFT);
1074
- next.delete(RIGHT);
1075
- next.set(INSET_INLINE, asProperty(left, INSET_INLINE));
1076
- collapsed = true;
1077
- }
1078
- if (!collapsed) return null;
1079
- }
1080
- return withBaseDecls(computed2, next);
1081
- }
1082
- },
1083
- test: {
1084
- cases: [
1085
- {
1086
- // The four equal inset longhands collapse to an `inset` shorthand at the IR level; the
1087
- // minimizing reverse-emit expands it back to top/right/bottom/left and picks the single utility
1088
- // covering all four (`inset-0`), replacing the four physical-side tokens. `bg-red-200` survives.
1089
- before: '<div className="top-0 right-0 bottom-0 left-0 bg-red-200">box</div>',
1090
- after: '<div className="bg-red-200 inset-0">box</div>'
1091
- }
1092
- ],
1093
- // No matching inset pair (all four distinct) → nothing collapses.
1094
- noMatch: ['<div className="top-0 right-1 bottom-2 left-3 bg-red-200">box</div>']
1095
- }
1096
- });
1097
-
1098
- // ../patterns/src/library/compress/margin-shorthand.pattern.ts
1099
- init_esm_shims();
1100
- var MARGIN_SIDES = [
1101
- "margin-top",
1102
- "margin-right",
1103
- "margin-bottom",
1104
- "margin-left"
1105
- ];
1106
- var MARGIN_SIDE_SET = new Set(MARGIN_SIDES);
1107
- var BASE_KEY4 = conditionKey(BASE_CONDITION);
1108
- function collapseMarginValue(top, right, bottom, left) {
1109
- if (right === left) {
1110
- if (top === bottom) {
1111
- return top === right ? top : `${top} ${right}`;
1112
- }
1113
- return `${top} ${right} ${bottom}`;
1114
- }
1115
- return `${top} ${right} ${bottom} ${left}`;
1116
- }
1117
- function withFoldedMargin(sm, marginDecl) {
1118
- const blocks = /* @__PURE__ */ new Map();
1119
- for (const [key, block] of sm.blocks) {
1120
- if (key !== BASE_KEY4) {
1121
- blocks.set(key, block);
1122
- continue;
1123
- }
1124
- const decls = /* @__PURE__ */ new Map();
1125
- for (const [prop, decl] of block.decls) {
1126
- if (!MARGIN_SIDE_SET.has(String(prop))) decls.set(prop, decl);
1127
- }
1128
- decls.set(marginDecl.property, marginDecl);
1129
- blocks.set(key, { condition: block.condition, decls });
1130
- }
1131
- return { blocks };
1132
- }
1133
- var marginShorthand = definePattern({
1134
- name: "margin-shorthand",
1135
- category: "compress/margin-shorthand",
1136
- safety: 2,
1137
- doc: {
1138
- title: "Compress margin longhands into the `margin` shorthand",
1139
- 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.",
1140
- before: '<div style="margin-top:8px;margin-right:8px;margin-bottom:8px;margin-left:8px"/>',
1141
- after: '<div style="margin:8px"/>',
1142
- 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."
1143
- },
1144
- rewrite: {
1145
- rewriteClasses(computed2) {
1146
- const base = computed2.blocks.get(BASE_KEY4);
1147
- if (!base) return null;
1148
- const sides = MARGIN_SIDES.map((p) => base.decls.get(p));
1149
- if (sides.some((d) => d === void 0)) return null;
1150
- const [mt, mr, mb, ml] = sides;
1151
- if (mt.important || mr.important || mb.important || ml.important) return null;
1152
- const value = collapseMarginValue(
1153
- String(mt.value),
1154
- String(mr.value),
1155
- String(mb.value),
1156
- String(ml.value)
1157
- );
1158
- const marginDecl = {
1159
- property: "margin",
1160
- value,
1161
- important: false,
1162
- relativeToParent: mt.relativeToParent || mr.relativeToParent || mb.relativeToParent || ml.relativeToParent,
1163
- inherited: false
1164
- // margin is not an inherited property
1165
- };
1166
- return withFoldedMargin(computed2, marginDecl);
1167
- }
1168
- },
1169
- test: {
1170
- cases: [
1171
- {
1172
- // The four equal margin longhands collapse to a `margin` shorthand at the IR level, and the
1173
- // minimizing reverse-emit picks the single shortest utility (`m-2`) reproducing it, replacing
1174
- // the four `m{t,r,b,l}-2` tokens. `bg-red-200` is preserved.
1175
- before: '<div className="mt-2 mr-2 mb-2 ml-2 bg-red-200">box</div>',
1176
- after: '<div className="bg-red-200 m-2">box</div>'
1177
- }
1178
- ],
1179
- // Only two margin sides set → the four-longhand `margin` collapse does not apply.
1180
- noMatch: ['<div className="mt-2 mb-2 bg-red-200">box</div>']
1181
- }
1182
- });
1183
-
1184
- // ../patterns/src/library/compress/overflow-shorthand.pattern.ts
1185
- init_esm_shims();
1186
- var OVERFLOW_X = "overflow-x";
1187
- var OVERFLOW_Y = "overflow-y";
1188
- var OVERFLOW = "overflow";
1189
- var BASE_KEY5 = conditionKey(BASE_CONDITION);
1190
- function withOverflowShorthand(sm, overflowDecl) {
1191
- const blocks = /* @__PURE__ */ new Map();
1192
- for (const [key, block] of sm.blocks) {
1193
- if (key !== BASE_KEY5) {
1194
- blocks.set(key, block);
1195
- continue;
1196
- }
1197
- const decls = /* @__PURE__ */ new Map();
1198
- for (const [prop, decl] of block.decls) {
1199
- if (prop === OVERFLOW_X || prop === OVERFLOW_Y) continue;
1200
- decls.set(prop, decl);
1201
- }
1202
- decls.set(overflowDecl.property, overflowDecl);
1203
- blocks.set(key, { condition: block.condition, decls });
1204
- }
1205
- return { blocks };
1206
- }
1207
- var overflowShorthand = definePattern({
1208
- name: "overflow-shorthand",
1209
- category: "compress/overflow-shorthand",
1210
- safety: 1,
1211
- doc: {
1212
- title: "Collapse equal overflow axes into the `overflow` shorthand",
1213
- 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-*).",
1214
- before: '<div style="overflow-x:auto;overflow-y:auto"/>',
1215
- after: '<div style="overflow:auto"/>',
1216
- 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."
1217
- },
1218
- rewrite: {
1219
- rewriteClasses(computed2) {
1220
- const base = computed2.blocks.get(BASE_KEY5);
1221
- if (!base) return null;
1222
- const overflowX = base.decls.get(OVERFLOW_X);
1223
- const overflowY = base.decls.get(OVERFLOW_Y);
1224
- if (!overflowX || !overflowY) return null;
1225
- if (overflowX.important !== overflowY.important) return null;
1226
- if (overflowX.value !== overflowY.value) return null;
1227
- const overflowDecl = {
1228
- property: OVERFLOW,
1229
- value: overflowX.value,
1230
- important: overflowX.important,
1231
- relativeToParent: overflowX.relativeToParent || overflowY.relativeToParent,
1232
- inherited: false
1233
- // overflow is not an inherited property
1234
- };
1235
- return withOverflowShorthand(computed2, overflowDecl);
1236
- }
1237
- },
1238
- test: {
1239
- cases: [
1240
- {
1241
- // Equal overflow axes collapse to an `overflow` decl at the IR level; the minimizing
1242
- // reverse-emit picks the single utility covering both (`overflow-auto`), replacing the
1243
- // `overflow-x-auto`+`overflow-y-auto` pair. `bg-red-200` is preserved.
1244
- before: '<div className="overflow-x-auto overflow-y-auto bg-red-200">box</div>',
1245
- after: '<div className="bg-red-200 overflow-auto">box</div>'
1246
- }
1247
- ],
1248
- // Mismatched axes (overflow-x != overflow-y) have no single-keyword equivalent → not collapsed.
1249
- noMatch: ['<div className="overflow-x-auto overflow-y-hidden bg-red-200">box</div>']
1250
- }
1251
- });
1252
-
1253
- // ../patterns/src/library/compress/overscroll-behavior-shorthand.pattern.ts
1254
- init_esm_shims();
1255
- var OVERSCROLL_X = "overscroll-behavior-x";
1256
- var OVERSCROLL_Y = "overscroll-behavior-y";
1257
- var OVERSCROLL = "overscroll-behavior";
1258
- var BASE_KEY6 = conditionKey(BASE_CONDITION);
1259
- var NON_COLLAPSIBLE_VALUES2 = /* @__PURE__ */ new Set([
1260
- "initial",
1261
- "inherit",
1262
- "unset",
1263
- "revert",
1264
- "revert-layer"
1265
- ]);
1266
- function withOverscrollShorthand(sm, shorthand) {
1267
- const blocks = /* @__PURE__ */ new Map();
1268
- for (const [key, block] of sm.blocks) {
1269
- if (key !== BASE_KEY6) {
1270
- blocks.set(key, block);
1271
- continue;
1272
- }
1273
- const decls = /* @__PURE__ */ new Map();
1274
- for (const [prop, decl] of block.decls) {
1275
- if (prop === OVERSCROLL_X || prop === OVERSCROLL_Y) continue;
1276
- decls.set(prop, decl);
1277
- }
1278
- decls.set(shorthand.property, shorthand);
1279
- blocks.set(key, { condition: block.condition, decls });
1280
- }
1281
- return { blocks };
1282
- }
1283
- var overscrollBehaviorShorthand = definePattern({
1284
- name: "overscroll-behavior-shorthand",
1285
- category: "compress/overscroll-behavior-shorthand",
1286
- safety: 1,
1287
- doc: {
1288
- title: "Collapse equal overscroll-behavior axes into overscroll-behavior",
1289
- 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-*).",
1290
- before: '<div style="overscroll-behavior-x:contain;overscroll-behavior-y:contain"/>',
1291
- after: '<div class="overscroll-contain"/>',
1292
- 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."
1293
- },
1294
- rewrite: {
1295
- rewriteClasses(computed2) {
1296
- const base = computed2.blocks.get(BASE_KEY6);
1297
- if (!base) return null;
1298
- const x = base.decls.get(OVERSCROLL_X);
1299
- const y = base.decls.get(OVERSCROLL_Y);
1300
- if (!x || !y) return null;
1301
- if (x.important !== y.important) return null;
1302
- const value = String(x.value);
1303
- if (NON_COLLAPSIBLE_VALUES2.has(value)) return null;
1304
- if (value !== String(y.value)) return null;
1305
- const shorthand = {
1306
- property: OVERSCROLL,
1307
- value: x.value,
1308
- important: x.important,
1309
- relativeToParent: x.relativeToParent || y.relativeToParent,
1310
- inherited: false
1311
- // overscroll-behavior is not an inherited property
1312
- };
1313
- return withOverscrollShorthand(computed2, shorthand);
1314
- }
1315
- },
1316
- test: {
1317
- cases: [
1318
- {
1319
- // Equal x/y axes collapse to an `overscroll-behavior` decl at the IR level; the minimizing
1320
- // reverse-emit picks the single utility covering both (`overscroll-contain`), replacing the
1321
- // `overscroll-x-contain`+`overscroll-y-contain` pair. `bg-red-200` is preserved.
1322
- before: '<div className="overscroll-x-contain overscroll-y-contain bg-red-200">box</div>',
1323
- after: '<div className="bg-red-200 overscroll-contain">box</div>'
1324
- }
1325
- ],
1326
- // Axes differ (x != y) → no equal-axis collapse.
1327
- noMatch: ['<div className="overscroll-x-contain overscroll-y-auto bg-red-200">box</div>']
1328
- }
1329
- });
1330
-
1331
- // ../patterns/src/library/compress/padding-shorthand.pattern.ts
1332
- init_esm_shims();
1333
- var PADDING_SIDES = [
1334
- "padding-top",
1335
- "padding-right",
1336
- "padding-bottom",
1337
- "padding-left"
1338
- ];
1339
- var PADDING_SIDE_SET = new Set(PADDING_SIDES);
1340
- var BASE_KEY7 = conditionKey(BASE_CONDITION);
1341
- function analyzePadding(sm) {
1342
- const block = sm.blocks.get(BASE_KEY7);
1343
- if (!block) return null;
1344
- const sides = [];
1345
- for (const side of PADDING_SIDES) {
1346
- const decl = block.decls.get(side);
1347
- if (!decl) return null;
1348
- sides.push(decl);
1349
- }
1350
- const [top, right, bottom, left] = sides;
1351
- if (!(top.important === right.important && right.important === bottom.important && bottom.important === left.important)) {
1352
- return null;
1353
- }
1354
- const tv = String(top.value);
1355
- const rv = String(right.value);
1356
- const bv = String(bottom.value);
1357
- const lv = String(left.value);
1358
- if (tv !== bv || lv !== rv) return null;
1359
- const value = tv === lv ? tv : `${tv} ${lv}`;
1360
- const relative = sides.some((d) => d.relativeToParent);
1361
- return { value, important: top.important, relative };
1362
- }
1363
- function withFoldedPadding(sm, fold) {
1364
- const blocks = /* @__PURE__ */ new Map();
1365
- for (const [key, block] of sm.blocks) {
1366
- if (key !== BASE_KEY7) {
1367
- blocks.set(key, block);
1368
- continue;
1369
- }
1370
- const decls = /* @__PURE__ */ new Map();
1371
- for (const [prop, decl] of block.decls) {
1372
- if (PADDING_SIDE_SET.has(String(prop))) continue;
1373
- decls.set(prop, decl);
1374
- }
1375
- const shorthand = {
1376
- property: "padding",
1377
- value: fold.value,
1378
- important: fold.important,
1379
- relativeToParent: fold.relative,
1380
- inherited: false
1381
- // padding is never inherited
1382
- };
1383
- decls.set(shorthand.property, shorthand);
1384
- blocks.set(key, { condition: block.condition, decls });
1385
- }
1386
- return { blocks };
1387
- }
1388
- var paddingShorthand = definePattern({
1389
- name: "padding-shorthand",
1390
- category: "compress/padding-shorthand",
1391
- safety: 1,
1392
- doc: {
1393
- title: "Collapse padding longhands to shorthand",
1394
- 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-*).",
1395
- before: '<div class="pt-4 pr-4 pb-4 pl-4"/>',
1396
- after: '<div class="p-4"/>',
1397
- 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."
1398
- },
1399
- rewrite: {
1400
- rewriteClasses(computed2) {
1401
- const fold = analyzePadding(computed2);
1402
- return fold ? withFoldedPadding(computed2, fold) : null;
1403
- }
1404
- },
1405
- test: {
1406
- cases: [
1407
- {
1408
- // The four equal padding longhands collapse to a `padding` shorthand at the IR level, and the
1409
- // minimizing reverse-emit picks the single shortest utility (`p-4`) that reproduces it,
1410
- // replacing the four `p{t,r,b,l}-4` tokens. `bg-red-200` is preserved (its order is stable).
1411
- before: '<div className="pt-4 pr-4 pb-4 pl-4 bg-red-200">box</div>',
1412
- after: '<div className="bg-red-200 p-4">box</div>'
1413
- },
1414
- {
1415
- // A dynamic `{x}` child no longer blocks compress: only the element's OWN class tokens are
1416
- // rewritten (px-4 py-4 → p-4); the dynamic child is untouched by a class-only change. This is
1417
- // the real-app common case (most elements have dynamic content).
1418
- before: '<div className="px-4 py-4">{x}</div>',
1419
- after: '<div className="p-4">{x}</div>'
1420
- }
1421
- ],
1422
- // Asymmetric padding (top != bottom) cannot fold into a shorthand → left unchanged.
1423
- noMatch: ['<div className="pt-2 pr-4 pb-8 pl-4 bg-red-200">box</div>']
1424
- }
1425
- });
1426
-
1427
- // ../patterns/src/library/compress/place-shorthand.pattern.ts
1428
- init_esm_shims();
1429
- var ALIGN_ITEMS = "align-items";
1430
- var JUSTIFY_ITEMS = "justify-items";
1431
- var PLACE_ITEMS = "place-items";
1432
- var ALIGN_CONTENT = "align-content";
1433
- var JUSTIFY_CONTENT = "justify-content";
1434
- var PLACE_CONTENT = "place-content";
1435
- var BASE_KEY8 = conditionKey(BASE_CONDITION);
1436
- function samePair(a, b) {
1437
- return a !== void 0 && b !== void 0 && a.value === b.value && a.important === b.important;
1438
- }
1439
- function placeDecl(property, align) {
1440
- return {
1441
- property,
1442
- value: align.value,
1443
- important: align.important,
1444
- relativeToParent: false,
1445
- // alignment keywords (center/start/stretch/…) are not length-relative
1446
- inherited: false
1447
- // none of the place-* alignment properties are inherited
1448
- };
1449
- }
1450
- function withBaseDecls2(sm, baseDecls) {
1451
- const blocks = /* @__PURE__ */ new Map();
1452
- for (const [key, block] of sm.blocks) {
1453
- const decls = key === BASE_KEY8 ? new Map(baseDecls) : block.decls;
1454
- blocks.set(key, { condition: block.condition, decls });
1455
- }
1456
- return { blocks };
1457
- }
1458
- var placeShorthand = definePattern({
1459
- name: "place-shorthand",
1460
- category: "compress/place-shorthand",
1461
- safety: 1,
1462
- doc: {
1463
- title: "Collapse matching alignment pairs into `place-*` shorthands",
1464
- 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.",
1465
- before: '<div style="align-items:center;justify-items:center"/>',
1466
- after: '<div style="place-items:center"/>',
1467
- 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."
1468
- },
1469
- rewrite: {
1470
- rewriteClasses(computed2) {
1471
- const base = computed2.blocks.get(BASE_KEY8);
1472
- if (!base) return null;
1473
- const alignItems = base.decls.get(ALIGN_ITEMS);
1474
- const justifyItems = base.decls.get(JUSTIFY_ITEMS);
1475
- const alignContent = base.decls.get(ALIGN_CONTENT);
1476
- const justifyContent = base.decls.get(JUSTIFY_CONTENT);
1477
- const next = new Map(base.decls);
1478
- let collapsed = false;
1479
- if (samePair(alignItems, justifyItems)) {
1480
- next.delete(ALIGN_ITEMS);
1481
- next.delete(JUSTIFY_ITEMS);
1482
- next.set(PLACE_ITEMS, placeDecl(PLACE_ITEMS, alignItems));
1483
- collapsed = true;
1484
- }
1485
- if (samePair(alignContent, justifyContent)) {
1486
- next.delete(ALIGN_CONTENT);
1487
- next.delete(JUSTIFY_CONTENT);
1488
- next.set(PLACE_CONTENT, placeDecl(PLACE_CONTENT, alignContent));
1489
- collapsed = true;
1490
- }
1491
- if (!collapsed) return null;
1492
- return withBaseDecls2(computed2, next);
1493
- }
1494
- },
1495
- test: {
1496
- cases: [
1497
- {
1498
- // The matching items pair collapses to a `place-items` decl at the IR level; the minimizing
1499
- // reverse-emit picks the single utility covering both (`place-items-center`), replacing the
1500
- // `items-center`+`justify-items-center` pair. `bg-red-200` is preserved.
1501
- before: '<div className="items-center justify-items-center bg-red-200">box</div>',
1502
- after: '<div className="bg-red-200 place-items-center">box</div>'
1503
- }
1504
- ],
1505
- // Mismatched alignment (align-items != justify-items, no content pair) → nothing collapses.
1506
- noMatch: ['<div className="items-center justify-items-start bg-red-200">box</div>']
1507
- }
1508
- });
1509
-
1510
- // ../patterns/src/library/compress/scroll-margin-shorthand.pattern.ts
1511
- init_esm_shims();
1512
- var SCROLL_MARGIN_SIDES = [
1513
- "scroll-margin-top",
1514
- "scroll-margin-right",
1515
- "scroll-margin-bottom",
1516
- "scroll-margin-left"
1517
- ];
1518
- var SIDE_SET = new Set(SCROLL_MARGIN_SIDES);
1519
- var BASE_KEY9 = conditionKey(BASE_CONDITION);
1520
- var SCROLL_MARGIN = "scroll-margin";
1521
- var NON_COLLAPSIBLE_VALUES3 = /* @__PURE__ */ new Set([
1522
- "initial",
1523
- "inherit",
1524
- "unset",
1525
- "revert",
1526
- "revert-layer"
1527
- ]);
1528
- function analyzeScrollMargin(sm) {
1529
- const block = sm.blocks.get(BASE_KEY9);
1530
- if (!block) return null;
1531
- const sides = [];
1532
- for (const side of SCROLL_MARGIN_SIDES) {
1533
- const decl = block.decls.get(side);
1534
- if (!decl) return null;
1535
- sides.push(decl);
1536
- }
1537
- const important = sides[0].important;
1538
- if (!sides.every((d) => d.important === important)) return null;
1539
- const value = String(sides[0].value);
1540
- if (NON_COLLAPSIBLE_VALUES3.has(value)) return null;
1541
- if (!sides.every((d) => String(d.value) === value)) return null;
1542
- const relative = sides.some((d) => d.relativeToParent);
1543
- return { value, important, relative };
1544
- }
1545
- function withFoldedScrollMargin(sm, fold) {
1546
- const blocks = /* @__PURE__ */ new Map();
1547
- for (const [key, block] of sm.blocks) {
1548
- if (key !== BASE_KEY9) {
1549
- blocks.set(key, block);
1550
- continue;
1551
- }
1552
- const decls = /* @__PURE__ */ new Map();
1553
- for (const [prop, decl] of block.decls) {
1554
- if (SIDE_SET.has(String(prop))) continue;
1555
- decls.set(prop, decl);
1556
- }
1557
- const shorthand = {
1558
- property: SCROLL_MARGIN,
1559
- value: fold.value,
1560
- important: fold.important,
1561
- relativeToParent: fold.relative,
1562
- inherited: false
1563
- // scroll-margin is never inherited
1564
- };
1565
- decls.set(shorthand.property, shorthand);
1566
- blocks.set(key, { condition: block.condition, decls });
1567
- }
1568
- return { blocks };
1569
- }
1570
- var scrollMarginShorthand = definePattern({
1571
- name: "scroll-margin-shorthand",
1572
- category: "compress/scroll-margin-shorthand",
1573
- safety: 1,
1574
- doc: {
1575
- title: "Collapse equal scroll-margin sides into scroll-margin",
1576
- 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).",
1577
- before: '<div class="scroll-mt-4 scroll-mr-4 scroll-mb-4 scroll-ml-4"/>',
1578
- after: '<div class="scroll-m-4"/>',
1579
- 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."
1580
- },
1581
- rewrite: {
1582
- rewriteClasses(computed2) {
1583
- const fold = analyzeScrollMargin(computed2);
1584
- return fold ? withFoldedScrollMargin(computed2, fold) : null;
1585
- }
1586
- },
1587
- test: {
1588
- cases: [
1589
- {
1590
- // The four equal scroll-margin longhands collapse to a `scroll-margin` decl at the IR level; the
1591
- // minimizing reverse-emit then picks the single shortest utility (`scroll-m-4`) that reproduces
1592
- // it, replacing the four `scroll-m{t,r,b,l}-4` tokens. `bg-red-200` is preserved.
1593
- before: '<div className="scroll-mt-4 scroll-mr-4 scroll-mb-4 scroll-ml-4 bg-red-200">box</div>',
1594
- after: '<div className="bg-red-200 scroll-m-4">box</div>'
1595
- }
1596
- ],
1597
- // Sides differ (top != bottom) → no all-equal collapse.
1598
- noMatch: ['<div className="scroll-mt-2 scroll-mr-4 scroll-mb-8 scroll-ml-4 bg-red-200">box</div>']
1599
- }
1600
- });
1601
-
1602
- // ../patterns/src/library/compress/scroll-padding-shorthand.pattern.ts
1603
- init_esm_shims();
1604
- var SCROLL_PADDING_SIDES = [
1605
- "scroll-padding-top",
1606
- "scroll-padding-right",
1607
- "scroll-padding-bottom",
1608
- "scroll-padding-left"
1609
- ];
1610
- var SIDE_SET2 = new Set(SCROLL_PADDING_SIDES);
1611
- var BASE_KEY10 = conditionKey(BASE_CONDITION);
1612
- var SCROLL_PADDING = "scroll-padding";
1613
- var NON_COLLAPSIBLE_VALUES4 = /* @__PURE__ */ new Set([
1614
- "initial",
1615
- "inherit",
1616
- "unset",
1617
- "revert",
1618
- "revert-layer"
1619
- ]);
1620
- function analyzeScrollPadding(sm) {
1621
- const block = sm.blocks.get(BASE_KEY10);
1622
- if (!block) return null;
1623
- const sides = [];
1624
- for (const side of SCROLL_PADDING_SIDES) {
1625
- const decl = block.decls.get(side);
1626
- if (!decl) return null;
1627
- sides.push(decl);
1628
- }
1629
- const important = sides[0].important;
1630
- if (!sides.every((d) => d.important === important)) return null;
1631
- const value = String(sides[0].value);
1632
- if (NON_COLLAPSIBLE_VALUES4.has(value)) return null;
1633
- if (!sides.every((d) => String(d.value) === value)) return null;
1634
- const relative = sides.some((d) => d.relativeToParent);
1635
- return { value, important, relative };
1636
- }
1637
- function withFoldedScrollPadding(sm, fold) {
1638
- const blocks = /* @__PURE__ */ new Map();
1639
- for (const [key, block] of sm.blocks) {
1640
- if (key !== BASE_KEY10) {
1641
- blocks.set(key, block);
1642
- continue;
1643
- }
1644
- const decls = /* @__PURE__ */ new Map();
1645
- for (const [prop, decl] of block.decls) {
1646
- if (SIDE_SET2.has(String(prop))) continue;
1647
- decls.set(prop, decl);
1648
- }
1649
- const shorthand = {
1650
- property: SCROLL_PADDING,
1651
- value: fold.value,
1652
- important: fold.important,
1653
- relativeToParent: fold.relative,
1654
- inherited: false
1655
- // scroll-padding is never inherited
1656
- };
1657
- decls.set(shorthand.property, shorthand);
1658
- blocks.set(key, { condition: block.condition, decls });
1659
- }
1660
- return { blocks };
1661
- }
1662
- var scrollPaddingShorthand = definePattern({
1663
- name: "scroll-padding-shorthand",
1664
- category: "compress/scroll-padding-shorthand",
1665
- safety: 1,
409
+ category: "flatten/wrapper/passthrough-wrapper",
410
+ safety: 2,
1666
411
  doc: {
1667
- title: "Collapse equal scroll-padding sides into scroll-padding",
1668
- 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).",
1669
- before: '<div class="scroll-pt-4 scroll-pr-4 scroll-pb-4 scroll-pl-4"/>',
1670
- after: '<div class="scroll-p-4"/>',
1671
- 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."
412
+ title: "Flatten passthrough wrapper",
413
+ 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.",
414
+ before: "<div><Child/></div>",
415
+ after: "<Child/>",
416
+ 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."
1672
417
  },
1673
- rewrite: {
1674
- rewriteClasses(computed2) {
1675
- const fold = analyzeScrollPadding(computed2);
1676
- return fold ? withFoldedScrollPadding(computed2, fold) : null;
1677
- }
418
+ match: {
419
+ tag: "div",
420
+ onlyChild: "element",
421
+ paintsNothing: true,
422
+ where: [
423
+ not(establishesContext),
424
+ not(hasOwnAttrs2),
425
+ not(hasDynamicClasses),
426
+ not(hasSpreadAttrs2),
427
+ not(isComponentNode3),
428
+ not(targetedByStructuralPseudo3)
429
+ ]
1678
430
  },
431
+ rewrite: { flattenInto: "child" },
1679
432
  test: {
1680
433
  cases: [
1681
434
  {
1682
- // The four equal scroll-padding longhands collapse to a `scroll-padding` decl at the IR level;
1683
- // the minimizing reverse-emit then picks the single shortest utility (`scroll-p-4`) that
1684
- // reproduces it, replacing the four `scroll-p{t,r,b,l}-4` tokens. `bg-red-200` is preserved.
1685
- before: '<div className="scroll-pt-4 scroll-pr-4 scroll-pb-4 scroll-pl-4 bg-red-200">box</div>',
1686
- after: '<div className="bg-red-200 scroll-p-4">box</div>'
435
+ // A plain, style-free wrapper paints nothing and establishes no context a provably-safe
436
+ // flatten under the conservative gate: the wrapper is removed and its sole child hoisted.
437
+ before: '<div><a className="bg-red-200">Link</a></div>',
438
+ after: '<a className="bg-red-200">Link</a>'
1687
439
  }
1688
440
  ],
1689
- // Sides differ (top != bottom) → no all-equal collapse.
1690
- noMatch: ['<div className="scroll-pt-2 scroll-pr-4 scroll-pb-8 scroll-pl-4 bg-red-200">box</div>']
441
+ noMatch: [
442
+ // A ref pins the wrapper's element identity (a hard opacity barrier) → not a passthrough.
443
+ '<div ref={rootRef}><a className="bg-red-200">Link</a></div>',
444
+ // A `display:flex` wrapper establishes a formatting context, so removing its box is NOT
445
+ // provably layout-neutral → the conservative gate leaves it in place.
446
+ '<div className="flex"><a className="bg-red-200">Link</a></div>'
447
+ ]
1691
448
  }
1692
449
  });
1693
450
 
1694
- // ../patterns/src/library/compress/size-shorthand.pattern.ts
451
+ // ../patterns/src/library/wrapper/redundant-inline-wrapper.pattern.ts
1695
452
  init_esm_shims();
1696
- var WIDTH = "width";
1697
- var HEIGHT = "height";
1698
- var SIZE = "size";
1699
- var NON_COLLAPSIBLE_VALUES5 = /* @__PURE__ */ new Set(["auto", "initial", "unset"]);
1700
- function baseBlock(sm) {
1701
- return sm.blocks.get(conditionKey(BASE_CONDITION));
1702
- }
1703
- function withSizeShorthand(sm, value, important) {
1704
- const baseKey = conditionKey(BASE_CONDITION);
1705
- const blocks = /* @__PURE__ */ new Map();
1706
- for (const [key, block] of sm.blocks) {
1707
- if (key !== baseKey) {
1708
- blocks.set(key, block);
1709
- continue;
1710
- }
1711
- const decls = new Map(block.decls);
1712
- decls.delete(WIDTH);
1713
- decls.delete(HEIGHT);
1714
- for (const decl of normalizer.normalizeDeclaration(String(SIZE), value, important)) {
1715
- decls.set(decl.property, decl);
1716
- }
1717
- blocks.set(key, { condition: block.condition, decls });
1718
- }
1719
- return { blocks };
453
+ function asEl3(node) {
454
+ const n = node;
455
+ return n.kind === "element" ? n : null;
456
+ }
457
+ function metaOf3(node) {
458
+ return asEl3(node)?.meta ?? null;
1720
459
  }
1721
- var sizeShorthand = definePattern({
1722
- name: "size-shorthand",
1723
- category: "compress/size-shorthand",
460
+ var establishesContext2 = (node) => {
461
+ const m = metaOf3(node);
462
+ if (!m) return false;
463
+ return m.establishesBox || m.establishesFormattingContext || m.establishesStackingContext || m.isContainingBlock || m.declaresCustomProperties;
464
+ };
465
+ var hasSpreadAttrs3 = (node) => metaOf3(node)?.hasSpreadAttrs ?? false;
466
+ var isComponentNode4 = (node) => metaOf3(node)?.isComponent ?? false;
467
+ var hasOwnAttrs3 = (node) => {
468
+ const el = asEl3(node);
469
+ if (!el) return false;
470
+ return el.attrs.entries.size > 0 || el.attrs.spreads.length > 0;
471
+ };
472
+ var targetedByStructuralPseudo4 = (node, ctx) => {
473
+ const el = asEl3(node);
474
+ if (!el) return false;
475
+ if (el.meta.targetedByStructuralPseudo) return true;
476
+ return ctx.selectors.targetedByStructuralPseudo(el.id);
477
+ };
478
+ var DISPLAY2 = "display";
479
+ var hasNonInlineDisplay = (node, ctx) => {
480
+ const el = asEl3(node);
481
+ if (!el) return false;
482
+ const sm = ctx.computedOf(el) ?? el.computed;
483
+ for (const block of sm.blocks.values()) {
484
+ const decl = block.decls.get(DISPLAY2);
485
+ if (decl && String(decl.value) !== "inline") return true;
486
+ }
487
+ return false;
488
+ };
489
+ var redundantInlineWrapper = definePattern({
490
+ name: "redundant-inline-wrapper",
491
+ category: "flatten/wrapper/redundant-inline-wrapper",
1724
492
  safety: 2,
1725
493
  doc: {
1726
- title: "Collapse equal width/height into size-*",
1727
- 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).",
1728
- before: '<div style="width:1rem;height:1rem"/>',
1729
- after: '<div class="size-4"/>',
1730
- 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."
494
+ title: "Flatten redundant inline wrapper",
495
+ 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.",
496
+ before: "<span><Child/></span>",
497
+ after: "<Child/>",
498
+ 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."
1731
499
  },
1732
- rewrite: {
1733
- rewriteClasses(computed2) {
1734
- const base = baseBlock(computed2);
1735
- const w = base?.decls.get(WIDTH);
1736
- const h = base?.decls.get(HEIGHT);
1737
- if (!w || !h) return null;
1738
- if (w.important !== h.important) return null;
1739
- if (NON_COLLAPSIBLE_VALUES5.has(String(w.value))) return null;
1740
- if (w.value !== h.value) return null;
1741
- return withSizeShorthand(computed2, String(w.value), w.important);
1742
- }
500
+ match: {
501
+ tag: "span",
502
+ onlyChild: "element",
503
+ paintsNothing: true,
504
+ where: [
505
+ not(hasNonInlineDisplay),
506
+ not(establishesContext2),
507
+ not(hasOwnAttrs3),
508
+ not(hasDynamicClasses),
509
+ not(hasSpreadAttrs3),
510
+ not(isComponentNode4),
511
+ not(targetedByStructuralPseudo4)
512
+ ]
1743
513
  },
514
+ rewrite: { flattenInto: "child" },
1744
515
  test: {
1745
516
  cases: [
1746
517
  {
1747
- // Equal width/height collapse to a `size` decl at the IR level; the minimizing reverse-emit
1748
- // expands `size` back to width+height, finds the single utility covering both (`size-10`), and
1749
- // replaces the `h-10`+`w-10` pair with it. `bg-red-200` is preserved.
1750
- before: '<div className="h-10 w-10 bg-red-200">box</div>',
1751
- after: '<div className="bg-red-200 size-10">box</div>'
518
+ // An empty inline span paints nothing and establishes no context a provably-safe flatten:
519
+ // the span is removed and its sole child hoisted in place.
520
+ before: '<span><a className="text-blue-500">Link</a></span>',
521
+ after: '<a className="text-blue-500">Link</a>'
1752
522
  }
1753
523
  ],
1754
- // Width and height differ → no equal-axis collapse.
1755
- noMatch: ['<div className="h-10 w-20 bg-red-200">box</div>']
524
+ noMatch: [
525
+ // A ref pins the span's element identity (a hard opacity barrier) → not a passthrough.
526
+ '<span ref={spanRef}><a className="text-blue-500">Link</a></span>',
527
+ // The span paints its own background (own visual style) → kept.
528
+ '<span className="bg-green-200"><a className="text-blue-500">Link</a></span>',
529
+ // Non-inline display (inline-block) participates in layout differently → kept.
530
+ '<span className="inline-block"><a className="text-blue-500">Link</a></span>'
531
+ ]
1756
532
  }
1757
533
  });
1758
534
 
1759
535
  // ../patterns/src/_registry.generated.ts
1760
536
  init_esm_shims();
1761
537
  var builtinPatterns = [
538
+ flexCenterWrapper,
539
+ redundantFragment,
540
+ gridCenterWrapper,
1762
541
  displayContentsWrapper,
1763
542
  emptyStyleDiv,
1764
- flexCenterWrapper,
1765
- inlineFlexCenterWrapper,
1766
- nestedFlexMerge,
1767
- nestedGridMerge,
543
+ inheritedOnlyWrapper,
1768
544
  passthroughWrapper,
1769
- redundantFragment,
1770
- redundantInlineWrapper,
1771
- borderRadiusShorthand,
1772
- borderShorthand,
1773
- dedupeClasses,
1774
- gapShorthand,
1775
- insetShorthand,
1776
- marginShorthand,
1777
- overflowShorthand,
1778
- overscrollBehaviorShorthand,
1779
- paddingShorthand,
1780
- placeShorthand,
1781
- scrollMarginShorthand,
1782
- scrollPaddingShorthand,
1783
- sizeShorthand
545
+ redundantInlineWrapper
1784
546
  ];
1785
547
 
1786
548
  // ../patterns/src/index.ts
@@ -1842,14 +604,315 @@ function synthesizeResidual(remaining, ctx) {
1842
604
  // ../resolver-tailwind/src/tailwind/engine.ts
1843
605
  init_esm_shims();
1844
606
  import { createRequire } from "module";
607
+ import * as path3 from "path";
608
+
609
+ // ../resolver-tailwind/src/tailwind/engine-v4.ts
610
+ init_esm_shims();
611
+ import { readdirSync, readFileSync, statSync } from "fs";
612
+ import * as path2 from "path";
613
+
614
+ // ../resolver-tailwind/src/tailwind/v4-bridge.ts
615
+ init_esm_shims();
616
+ import { execFileSync } from "child_process";
617
+ import { mkdtempSync, rmSync, writeFileSync } from "fs";
618
+ import { tmpdir } from "os";
1845
619
  import * as path from "path";
620
+ var CHILD_SOURCE = String.raw`
621
+ import { createRequire } from 'node:module';
622
+ import { pathToFileURL } from 'node:url';
623
+ import * as fs from 'node:fs';
624
+ import * as path from 'node:path';
625
+
626
+ function out(obj) { process.stdout.write(JSON.stringify(obj)); process.exit(0); }
627
+
628
+ let payload;
629
+ try { payload = JSON.parse(fs.readFileSync(process.argv[2], 'utf8')); }
630
+ catch { out({ ok: false }); }
631
+
632
+ const projectRoot = payload.projectRoot;
633
+ const entries = payload.entries || [];
634
+ const req = createRequire(path.join(projectRoot, '__domflax_tw4__.js'));
635
+
636
+ async function importFrom(id) {
637
+ const resolved = req.resolve(id);
638
+ return import(pathToFileURL(resolved).href);
639
+ }
640
+
641
+ // Primary loader: @tailwindcss/node (the companion every v4 build tool installs). It resolves
642
+ // '@import "tailwindcss"' and @theme against the project on disk.
643
+ async function loadViaNode() {
644
+ let mod;
645
+ try { mod = await importFrom('@tailwindcss/node'); } catch { return null; }
646
+ if (!mod || typeof mod.__unstable__loadDesignSystem !== 'function') return null;
647
+ for (const e of entries) {
648
+ try { return await mod.__unstable__loadDesignSystem(e.css, { base: e.base }); } catch {}
649
+ }
650
+ return null;
651
+ }
652
+
653
+ // Secondary loader: bare 'tailwindcss' with a filesystem stylesheet resolver (best-effort).
654
+ async function loadViaCore() {
655
+ let tw;
656
+ try { tw = await importFrom('tailwindcss'); } catch { return null; }
657
+ if (!tw || typeof tw.__unstable__loadDesignSystem !== 'function') return null;
658
+ const loadStylesheet = async (id, base) => {
659
+ const r = createRequire(path.join(base, '__domflax_tw4__.js'));
660
+ let p;
661
+ const tries = id === 'tailwindcss' ? ['tailwindcss/index.css', 'tailwindcss'] : [id, id + '/index.css'];
662
+ for (const t of tries) { try { p = r.resolve(t); break; } catch {} }
663
+ if (!p) p = path.resolve(base, id);
664
+ return { path: p, base: path.dirname(p), content: fs.readFileSync(p, 'utf8') };
665
+ };
666
+ const loadModule = async (id, base) => {
667
+ const r = createRequire(path.join(base, '__domflax_tw4__.js'));
668
+ const p = r.resolve(id);
669
+ return { path: p, base: path.dirname(p), module: (await import(pathToFileURL(p).href)).default };
670
+ };
671
+ for (const e of entries) {
672
+ try { return await tw.__unstable__loadDesignSystem(e.css, { base: e.base, loadStylesheet, loadModule }); } catch {}
673
+ }
674
+ return null;
675
+ }
676
+
677
+ const ds = (await loadViaNode()) || (await loadViaCore());
678
+ if (!ds) out({ ok: false });
679
+
680
+ let names = [];
681
+ try {
682
+ names = ds.getClassList().map((e) => (Array.isArray(e) ? e[0] : e)).filter((n) => typeof n === 'string');
683
+ } catch { out({ ok: false }); }
684
+
685
+ let css = [];
686
+ try { css = ds.candidatesToCss(names); } catch { out({ ok: false }); }
687
+
688
+ const result = [];
689
+ for (let i = 0; i < names.length; i += 1) {
690
+ const c = css[i];
691
+ if (typeof c === 'string' && c.length > 0) result.push([names[i], c]);
692
+ }
693
+ out({ ok: true, entries: result });
694
+ `;
695
+ function runV4Bridge(payload) {
696
+ let dir = null;
697
+ try {
698
+ dir = mkdtempSync(path.join(tmpdir(), "domflax-tw4-"));
699
+ const scriptPath = path.join(dir, "bridge.mjs");
700
+ const payloadPath = path.join(dir, "payload.json");
701
+ writeFileSync(scriptPath, CHILD_SOURCE, "utf8");
702
+ writeFileSync(payloadPath, JSON.stringify(payload), "utf8");
703
+ const stdout = execFileSync(process.execPath, [scriptPath, payloadPath], {
704
+ cwd: payload.projectRoot,
705
+ encoding: "utf8",
706
+ timeout: 9e4,
707
+ maxBuffer: 256 * 1024 * 1024,
708
+ stdio: ["ignore", "pipe", "ignore"]
709
+ });
710
+ const parsed = JSON.parse(stdout);
711
+ if (!parsed.ok || !Array.isArray(parsed.entries) || parsed.entries.length === 0) return null;
712
+ const entries = parsed.entries.filter(
713
+ (e) => Array.isArray(e) && typeof e[0] === "string" && typeof e[1] === "string"
714
+ );
715
+ return entries.length > 0 ? { entries } : null;
716
+ } catch {
717
+ return null;
718
+ } finally {
719
+ if (dir) {
720
+ try {
721
+ rmSync(dir, { recursive: true, force: true });
722
+ } catch {
723
+ }
724
+ }
725
+ }
726
+ }
727
+
728
+ // ../resolver-tailwind/src/tailwind/v4-css.ts
729
+ init_esm_shims();
730
+ function stripComments(src) {
731
+ return src.replace(/\/\*[\s\S]*?\*\//g, "");
732
+ }
733
+ function toDecl(buffer) {
734
+ const buf = buffer.trim();
735
+ if (buf.length === 0 || buf[0] === "@") return null;
736
+ const colon = buf.indexOf(":");
737
+ if (colon <= 0) return null;
738
+ const prop = buf.slice(0, colon).trim();
739
+ let value = buf.slice(colon + 1).trim();
740
+ if (prop.length === 0 || value.length === 0) return null;
741
+ let important = false;
742
+ const bang = /!\s*important\s*$/i.exec(value);
743
+ if (bang) {
744
+ important = true;
745
+ value = value.slice(0, bang.index).trim();
746
+ }
747
+ return { type: "decl", prop, value, important };
748
+ }
749
+ function splitAtRule(prelude) {
750
+ const m = /^@([A-Za-z-]+)\s*([\s\S]*)$/.exec(prelude);
751
+ if (!m) return { name: prelude.slice(1).trim(), params: "" };
752
+ return { name: m[1].toLowerCase(), params: m[2].trim() };
753
+ }
754
+ function parseBlock(src, start) {
755
+ const nodes = [];
756
+ let buf = "";
757
+ let i = start;
758
+ while (i < src.length) {
759
+ const c = src[i];
760
+ if (c === "{") {
761
+ const prelude = buf.trim();
762
+ buf = "";
763
+ const inner = parseBlock(src, i + 1);
764
+ i = inner.next;
765
+ if (prelude.startsWith("@")) {
766
+ const { name, params } = splitAtRule(prelude);
767
+ nodes.push({ type: "atrule", name, params, nodes: inner.nodes });
768
+ } else if (prelude.length > 0) {
769
+ nodes.push({ type: "rule", selector: prelude, nodes: inner.nodes });
770
+ }
771
+ } else if (c === "}") {
772
+ const d = toDecl(buf);
773
+ if (d) nodes.push(d);
774
+ return { nodes, next: i + 1 };
775
+ } else if (c === ";") {
776
+ const d = toDecl(buf);
777
+ if (d) nodes.push(d);
778
+ buf = "";
779
+ i += 1;
780
+ } else {
781
+ buf += c;
782
+ i += 1;
783
+ }
784
+ }
785
+ const tail = toDecl(buf);
786
+ if (tail) nodes.push(tail);
787
+ return { nodes, next: i };
788
+ }
789
+ function resolveNesting(child, parent) {
790
+ const c = child.trim();
791
+ if (parent.length === 0) return c;
792
+ if (c.includes("&")) return c.split("&").join(parent);
793
+ return `${parent} ${c}`;
794
+ }
795
+ var DROP_ATRULES = /* @__PURE__ */ new Set(["property", "keyframes", "font-face", "charset", "import"]);
796
+ function flattenNodes(nodes, selector, at, out) {
797
+ const own = [];
798
+ for (const n of nodes) if (n.type === "decl") own.push(n);
799
+ if (own.length > 0 && selector.length > 0) out.push({ selector, at: [...at], decls: own });
800
+ for (const n of nodes) {
801
+ if (n.type === "rule") {
802
+ flattenNodes(n.nodes, resolveNesting(n.selector, selector), at, out);
803
+ } else if (n.type === "atrule") {
804
+ if (n.name === "media") {
805
+ flattenNodes(n.nodes, selector, [...at, { name: "media", params: n.params }], out);
806
+ } else if (n.name === "layer") {
807
+ flattenNodes(n.nodes, selector, at, out);
808
+ } else if (!DROP_ATRULES.has(n.name)) {
809
+ flattenNodes(n.nodes, selector, [...at, { name: n.name, params: n.params }], out);
810
+ }
811
+ }
812
+ }
813
+ }
814
+ function leafToNode(leaf) {
815
+ const declNodes = leaf.decls.map((d) => ({
816
+ type: "decl",
817
+ prop: d.prop,
818
+ value: d.value,
819
+ important: d.important
820
+ }));
821
+ let node = { type: "rule", selector: leaf.selector, nodes: declNodes };
822
+ for (let i = leaf.at.length - 1; i >= 0; i -= 1) {
823
+ node = { type: "atrule", name: leaf.at[i].name, params: leaf.at[i].params, nodes: [node] };
824
+ }
825
+ return node;
826
+ }
827
+ function parseUtilityCss(css) {
828
+ try {
829
+ const { nodes } = parseBlock(stripComments(css), 0);
830
+ const leaves = [];
831
+ flattenNodes(nodes, "", [], leaves);
832
+ return leaves.map(leafToNode);
833
+ } catch {
834
+ return [];
835
+ }
836
+ }
837
+
838
+ // ../resolver-tailwind/src/tailwind/engine-v4.ts
839
+ var SEARCH_DIRS = ["", "src", "app", "styles", "src/styles", "src/app", "app/styles", "assets/css", "css"];
840
+ var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "out", ".next", "coverage"]);
841
+ var ENTRY_RE = /@import\s+["']tailwindcss["']|@tailwind\b|@theme\b/;
842
+ function scanDir(dir) {
843
+ let names;
844
+ try {
845
+ names = readdirSync(dir);
846
+ } catch {
847
+ return null;
848
+ }
849
+ for (const name of names) {
850
+ if (!name.toLowerCase().endsWith(".css")) continue;
851
+ const file = path2.join(dir, name);
852
+ try {
853
+ if (!statSync(file).isFile()) continue;
854
+ const css = readFileSync(file, "utf8");
855
+ if (ENTRY_RE.test(css)) return { css, base: path2.dirname(file) };
856
+ } catch {
857
+ }
858
+ }
859
+ return null;
860
+ }
861
+ function findCssEntries(projectRoot) {
862
+ const out = [];
863
+ const seen = /* @__PURE__ */ new Set();
864
+ for (const rel of SEARCH_DIRS) {
865
+ const dir = path2.resolve(projectRoot, rel);
866
+ if (seen.has(dir) || [...SKIP_DIRS].some((s) => dir.includes(`${path2.sep}${s}`))) continue;
867
+ seen.add(dir);
868
+ const hit = scanDir(dir);
869
+ if (hit) {
870
+ out.push(hit);
871
+ break;
872
+ }
873
+ }
874
+ out.push({ css: '@import "tailwindcss";', base: projectRoot });
875
+ return out;
876
+ }
877
+ function makeV4Engine(entries, version) {
878
+ const cssByClass = new Map(entries.map(([name, css]) => [name, css]));
879
+ const nodeCache = /* @__PURE__ */ new Map();
880
+ const nodesFor = (token) => {
881
+ let cached = nodeCache.get(token);
882
+ if (!cached) {
883
+ const css = cssByClass.get(token);
884
+ cached = css ? parseUtilityCss(css) : [];
885
+ nodeCache.set(token, cached);
886
+ }
887
+ return cached;
888
+ };
889
+ return {
890
+ version,
891
+ context: {
892
+ // The resolver keeps only string entries; we hand it the concrete class names directly.
893
+ getClassList: () => [...cssByClass.keys()]
894
+ },
895
+ generate(candidates) {
896
+ const out = [];
897
+ for (const c of candidates) for (const n of nodesFor(c)) out.push(n);
898
+ return out;
899
+ }
900
+ };
901
+ }
902
+ function loadV4Engine(projectRoot, version) {
903
+ const snapshot = runV4Bridge({ projectRoot, entries: findCssEntries(projectRoot) });
904
+ if (!snapshot) return null;
905
+ return makeV4Engine(snapshot.entries, version);
906
+ }
907
+
908
+ // ../resolver-tailwind/src/tailwind/engine.ts
1846
909
  function moduleBase() {
1847
910
  return typeof __filename === "string" ? __filename : import.meta.url;
1848
911
  }
1849
912
  function projectRequire(projectRoot) {
1850
913
  const bases = [];
1851
- if (projectRoot) bases.push(path.join(projectRoot, "__domflax__.js"));
1852
- bases.push(path.join(process.cwd(), "__domflax__.js"));
914
+ if (projectRoot) bases.push(path3.join(projectRoot, "__domflax__.js"));
915
+ bases.push(path3.join(process.cwd(), "__domflax__.js"));
1853
916
  bases.push(moduleBase());
1854
917
  for (const base of bases) {
1855
918
  try {
@@ -1861,14 +924,36 @@ function projectRequire(projectRoot) {
1861
924
  }
1862
925
  return null;
1863
926
  }
927
+ var FIRST_UNSUPPORTED_MAJOR = 4;
928
+ function majorOf(version) {
929
+ const m = /^\s*(\d+)/.exec(version);
930
+ return m ? Number(m[1]) : null;
931
+ }
1864
932
  function loadEngine(options) {
1865
933
  const req = projectRequire(options.projectRoot);
1866
- if (!req) return null;
934
+ if (!req) return { engine: null, version: null, unsupportedMajor: null };
935
+ let version = null;
936
+ try {
937
+ version = req("tailwindcss/package.json").version;
938
+ } catch {
939
+ return { engine: null, version: null, unsupportedMajor: null };
940
+ }
941
+ const major = majorOf(version);
942
+ if (major !== null && major >= FIRST_UNSUPPORTED_MAJOR) {
943
+ const projectRoot = options.projectRoot ?? process.cwd();
944
+ let v4 = null;
945
+ try {
946
+ v4 = loadV4Engine(projectRoot, version);
947
+ } catch {
948
+ v4 = null;
949
+ }
950
+ if (v4) return { engine: v4, version, unsupportedMajor: null };
951
+ return { engine: null, version, unsupportedMajor: major };
952
+ }
1867
953
  try {
1868
954
  const resolveConfig = req("tailwindcss/resolveConfig.js");
1869
955
  const { createContext } = req("tailwindcss/lib/lib/setupContextUtils.js");
1870
956
  const { generateRules } = req("tailwindcss/lib/lib/generateRules.js");
1871
- const pkg = req("tailwindcss/package.json");
1872
957
  let userConfig = options.config ?? { content: [{ raw: "" }] };
1873
958
  if (options.configPath !== void 0) {
1874
959
  const loadConfig = req("tailwindcss/loadConfig.js");
@@ -1877,15 +962,19 @@ function loadEngine(options) {
1877
962
  const resolved = resolveConfig(userConfig);
1878
963
  const context = createContext(resolved);
1879
964
  return {
1880
- version: pkg.version,
1881
- context,
1882
- generate(candidates) {
1883
- const rules = generateRules(new Set(candidates), context);
1884
- return rules.map(([, node]) => node);
1885
- }
965
+ engine: {
966
+ version,
967
+ context,
968
+ generate(candidates) {
969
+ const rules = generateRules(new Set(candidates), context);
970
+ return rules.map(([, node]) => node);
971
+ }
972
+ },
973
+ version,
974
+ unsupportedMajor: null
1886
975
  };
1887
976
  } catch {
1888
- return null;
977
+ return { engine: null, version, unsupportedMajor: null };
1889
978
  }
1890
979
  }
1891
980
 
@@ -2071,22 +1160,39 @@ var DROPPABLE_USAGE = {
2071
1160
  };
2072
1161
 
2073
1162
  // ../resolver-tailwind/src/tailwind/resolver.ts
1163
+ var warnedUnsupported = /* @__PURE__ */ new Set();
2074
1164
  var TailwindResolver = class {
2075
1165
  id = "tailwind";
2076
1166
  provider;
2077
1167
  fingerprint;
1168
+ /**
1169
+ * SAFETY (Layer 1): the detected Tailwind MAJOR when the project's version is one this resolver
1170
+ * cannot drive (v4+), else `null`. When set, {@link resolve} reports every token as unknown, so
1171
+ * downstream files are left unchanged (never mis-optimized). Exposed for diagnostics/tests.
1172
+ */
1173
+ unsupportedMajor;
2078
1174
  #engine;
2079
1175
  /** Per-token extraction cache (engine output is pure for a fixed config). */
2080
1176
  #tokenCache = /* @__PURE__ */ new Map();
2081
1177
  /** Per-class-set forward-resolution cache. */
2082
1178
  #resolveCache = /* @__PURE__ */ new Map();
2083
- /** Lazily built reverse index for {@link emit}. */
1179
+ /** Lazily built reverse index for the greedy {@link emit} fallback. */
2084
1180
  #reverseIndex = null;
1181
+ /** Lazily built cover vocabulary (base-condition tuple sets) for the exact-cover engine. */
1182
+ #coverVocab = null;
2085
1183
  constructor(config = {}) {
2086
- this.#engine = loadEngine(config);
2087
- this.provider = config.provider ?? (this.#engine ? `tailwindcss@${this.#engine.version}` : "tailwindcss");
1184
+ const loaded = loadEngine(config);
1185
+ this.#engine = loaded.engine;
1186
+ this.unsupportedMajor = loaded.unsupportedMajor;
1187
+ this.provider = config.provider ?? (loaded.version ? `tailwindcss@${loaded.version}` : "tailwindcss");
2088
1188
  const seed = JSON.stringify(config.config ?? {}) + (config.configPath ?? "");
2089
1189
  this.fingerprint = config.fingerprint ?? `${this.provider}/${fnv1a(seed)}`;
1190
+ if (this.unsupportedMajor !== null && !warnedUnsupported.has(this.provider)) {
1191
+ warnedUnsupported.add(this.provider);
1192
+ console.warn(
1193
+ `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.`
1194
+ );
1195
+ }
2090
1196
  }
2091
1197
  /** Engine-backed, cached single-token extraction. */
2092
1198
  #extract(token) {
@@ -2195,9 +1301,75 @@ var TailwindResolver = class {
2195
1301
  this.#reverseIndex = index;
2196
1302
  return index;
2197
1303
  }
1304
+ /**
1305
+ * The cover vocabulary: every base-condition, plain-subject utility mapped to the {@link tupleKey}s
1306
+ * of its full normalized-longhand declaration set. Built once from a SINGLE engine `generate` over
1307
+ * the enumerable class list (grouped by selector), so it is the same cost as {@link #buildReverseIndex}.
1308
+ * This is what the provider-uniform exact-cover engine searches; the element's own droppable tokens
1309
+ * are members of it, guaranteeing feasibility. Variant / combinator / pseudo utilities are excluded
1310
+ * (their effect is not the element's own base box), so a target carrying such conditions simply finds
1311
+ * no cover and falls back to the greedy emit.
1312
+ */
1313
+ #buildCoverVocab() {
1314
+ if (this.#coverVocab) return this.#coverVocab;
1315
+ const baseCk = String(conditionKey(BASE_CONDITION));
1316
+ const out = [];
1317
+ if (this.#engine) {
1318
+ try {
1319
+ const classes = this.#engine.context.getClassList().filter((c) => typeof c === "string");
1320
+ const nodes = this.#engine.generate(classes);
1321
+ for (const node of nodes) {
1322
+ if (node.type !== "rule") continue;
1323
+ const rule = node;
1324
+ const parsed = parseSelector(rule.selector);
1325
+ if (parsed.kind !== "simple" || parsed.states.length > 0 || parsed.pseudoElement !== "") {
1326
+ continue;
1327
+ }
1328
+ const className = unescapeClass(rule.selector);
1329
+ if (className === null) continue;
1330
+ const tuples = [];
1331
+ const seen = /* @__PURE__ */ new Set();
1332
+ for (const child of rule.nodes ?? []) {
1333
+ if (child.type !== "decl") continue;
1334
+ const d = child;
1335
+ if (typeof d.value !== "string") continue;
1336
+ for (const decl of normalizer.normalizeDeclaration(d.prop, d.value, d.important === true)) {
1337
+ const k = tupleKey(baseCk, String(decl.property), String(decl.value), decl.important);
1338
+ if (!seen.has(k)) {
1339
+ seen.add(k);
1340
+ tuples.push(k);
1341
+ }
1342
+ }
1343
+ }
1344
+ if (tuples.length > 0) out.push({ token: className, tuples });
1345
+ }
1346
+ } catch {
1347
+ }
1348
+ }
1349
+ this.#coverVocab = out;
1350
+ return out;
1351
+ }
1352
+ /**
1353
+ * Try the minimal-string exact-cover engine over the WHOLE utility vocabulary. On success the chosen
1354
+ * set is verified by the mandatory CORRECTNESS BACKSTOP — re-resolve it and assert it reproduces the
1355
+ * target's tuples EXACTLY — before it is returned; any mismatch (or no cover / oversize universe)
1356
+ * yields `null` so {@link emit} uses its greedy fallback. Never returns a set that misrepresents `U`.
1357
+ */
1358
+ #tryCover(normalized, norm) {
1359
+ const universe = styleMapTuples(normalized, norm);
1360
+ if (universe.length === 0) return { classes: [], exact: true, warnings: [] };
1361
+ const chosen = minStringCover(universe, this.#buildCoverVocab());
1362
+ if (!chosen || chosen.length === 0) return null;
1363
+ const reTuples = new Set(styleMapTuples(this.resolve({ classes: chosen }).styles, norm));
1364
+ if (reTuples.size !== universe.length) return null;
1365
+ for (const t of universe) if (!reTuples.has(t)) return null;
1366
+ return { classes: chosen, exact: true, warnings: [] };
1367
+ }
2198
1368
  emit(styles, ctx) {
2199
1369
  const norm = ctx.normalizer ?? normalizer;
2200
1370
  const normalized = norm.normalizeStyleMap(styles);
1371
+ const cover = this.#tryCover(normalized, norm);
1372
+ if (cover) return cover;
2201
1373
  const base = normalized.blocks.get(conditionKey(BASE_CONDITION));
2202
1374
  if (!base || base.decls.size === 0) return { classes: [], exact: true, warnings: [] };
2203
1375
  const hasNonBase = normalized.blocks.size > 1;
@@ -2227,13 +1399,13 @@ var TailwindResolver = class {
2227
1399
  let bestCover = 0;
2228
1400
  for (const entry of candidates) {
2229
1401
  const [token, declMap] = entry;
2230
- let cover = 0;
2231
- for (const prop of declMap.keys()) if (remaining.has(prop)) cover += 1;
2232
- if (cover === 0) continue;
2233
- const better = best === null || cover > bestCover || cover === bestCover && declMap.size < best[1].size || cover === bestCover && declMap.size === best[1].size && token < best[0];
1402
+ let cover2 = 0;
1403
+ for (const prop of declMap.keys()) if (remaining.has(prop)) cover2 += 1;
1404
+ if (cover2 === 0) continue;
1405
+ const better = best === null || cover2 > bestCover || cover2 === bestCover && declMap.size < best[1].size || cover2 === bestCover && declMap.size === best[1].size && token < best[0];
2234
1406
  if (better) {
2235
1407
  best = entry;
2236
- bestCover = cover;
1408
+ bestCover = cover2;
2237
1409
  }
2238
1410
  }
2239
1411
  if (!best) break;
@@ -2312,14 +1484,14 @@ var LEGACY_PSEUDO_ELEMENTS2 = /* @__PURE__ */ new Set([
2312
1484
  // ../resolver-css/src/engine.ts
2313
1485
  init_esm_shims();
2314
1486
  import { createRequire as createRequire2 } from "module";
2315
- import * as path2 from "path";
1487
+ import * as path4 from "path";
2316
1488
  function moduleBase2() {
2317
1489
  return typeof __filename === "string" ? __filename : import.meta.url;
2318
1490
  }
2319
1491
  function loadPostcssEngine(projectRoot) {
2320
1492
  const bases = [];
2321
- if (projectRoot) bases.push(path2.join(projectRoot, "__domflax__.js"));
2322
- bases.push(path2.join(process.cwd(), "__domflax__.js"));
1493
+ if (projectRoot) bases.push(path4.join(projectRoot, "__domflax__.js"));
1494
+ bases.push(path4.join(process.cwd(), "__domflax__.js"));
2323
1495
  bases.push(moduleBase2());
2324
1496
  for (const base of bases) {
2325
1497
  try {
@@ -2374,15 +1546,15 @@ function collectDecls(rule) {
2374
1546
 
2375
1547
  // ../resolver-css/src/misc-helpers.ts
2376
1548
  init_esm_shims();
2377
- import { readFileSync } from "fs";
1549
+ import { readFileSync as readFileSync2 } from "fs";
2378
1550
  function isPlainClassToken(token) {
2379
1551
  return token.length > 0 && !/[\s.#>+~:[\]()]/.test(token);
2380
1552
  }
2381
- function readCssPath(path3) {
1553
+ function readCssPath(path5) {
2382
1554
  try {
2383
- return { id: path3, css: readFileSync(path3, "utf8") };
1555
+ return { id: path5, css: readFileSync2(path5, "utf8") };
2384
1556
  } catch (cause) {
2385
- throw new Error(`resolver-css: cannot read CSS file "${path3}"`, { cause });
1557
+ throw new Error(`resolver-css: cannot read CSS file "${path5}"`, { cause });
2386
1558
  }
2387
1559
  }
2388
1560
  function deriveFingerprint(provider, files) {
@@ -2438,6 +1610,8 @@ var CustomCSSResolver = class {
2438
1610
  /** Distinct COMPLEX selectors (combinator or structural pseudo), sorted. */
2439
1611
  #complex;
2440
1612
  #reverse = null;
1613
+ /** Lazily built cover vocabulary (full condition-keyed tuple sets) for the exact-cover engine. */
1614
+ #coverVocab = null;
2441
1615
  constructor(cssFiles = [], options = {}) {
2442
1616
  ensurePostcss(options.projectRoot);
2443
1617
  const fromDisk = (options.files ?? []).map(readCssPath);
@@ -2470,8 +1644,23 @@ var CustomCSSResolver = class {
2470
1644
  }
2471
1645
  emit(styles, ctx) {
2472
1646
  const norm = ctx.normalizer ?? normalizer;
1647
+ const normalized = norm.normalizeStyleMap(styles);
1648
+ const universe = styleMapTuples(normalized, norm);
1649
+ if (universe.length === 0) return { classes: [], exact: true, warnings: [] };
1650
+ const chosen = minStringCover(universe, this.#buildCoverVocab());
1651
+ if (chosen && chosen.length > 0) {
1652
+ const reTuples = new Set(styleMapTuples(this.resolve({ classes: chosen }).styles, norm));
1653
+ let ok = reTuples.size === universe.length;
1654
+ if (ok) {
1655
+ for (const t of universe) if (!reTuples.has(t)) {
1656
+ ok = false;
1657
+ break;
1658
+ }
1659
+ }
1660
+ if (ok) return { classes: chosen, exact: true, warnings: [] };
1661
+ }
2473
1662
  const remaining = /* @__PURE__ */ new Map();
2474
- for (const [ck, block] of norm.normalizeStyleMap(styles).blocks) {
1663
+ for (const [ck, block] of normalized.blocks) {
2475
1664
  for (const [prop, decl] of block.decls) {
2476
1665
  remaining.set(`${ck} ${prop}`, String(decl.value));
2477
1666
  }
@@ -2681,7 +1870,23 @@ var CustomCSSResolver = class {
2681
1870
  if (rawBlocks.size === 0) return emptyStyleMap();
2682
1871
  return normalizer.normalizeStyleMap({ blocks: rawBlocks });
2683
1872
  }
2684
- /** Build (once) the reverse index used by {@link emit}. */
1873
+ /**
1874
+ * Build (once) the cover vocabulary for the exact-cover engine: every forward-resolvable class
1875
+ * mapped to the {@link styleMapTuples} of its full (condition-keyed, `!important`-aware) declaration
1876
+ * set. Unlike {@link #reverseIndex} this carries ALL style conditions and the important flag, so the
1877
+ * engine can pick a custom class covering hover/media declarations too.
1878
+ */
1879
+ #buildCoverVocab() {
1880
+ if (this.#coverVocab) return this.#coverVocab;
1881
+ const out = [];
1882
+ for (const token of this.#classIndex.keys()) {
1883
+ const tuples = styleMapTuples(this.#resolveTokens([token], [token]), normalizer);
1884
+ if (tuples.length > 0) out.push({ token, tuples });
1885
+ }
1886
+ this.#coverVocab = out;
1887
+ return out;
1888
+ }
1889
+ /** Build (once) the reverse index used by the greedy {@link emit} fallback. */
2685
1890
  #reverseIndex() {
2686
1891
  if (this.#reverse) return this.#reverse;
2687
1892
  const out = [];
@@ -2896,9 +2101,10 @@ function doParse(code, ctx) {
2896
2101
  native: document
2897
2102
  };
2898
2103
  doc.sources.set(FILE_ID, sourceFile);
2899
- const resolveComputed = (tokens, tag, nodeId) => {
2104
+ const resolveComputed = (tokens, tag, nodeId, meta) => {
2900
2105
  if (tokens.length === 0) return emptyStyleMap();
2901
2106
  const res = ctx.resolver.resolve({ classes: tokens, element: { tagName: tag, namespace: "html" } });
2107
+ if (res.unknown.length > 0) meta.hasUnresolvedClasses = true;
2902
2108
  for (const w of res.warnings) {
2903
2109
  diagnostics.push({
2904
2110
  code: "DF_STYLE_CONFLICT_UNRESOLVED",
@@ -2988,7 +2194,7 @@ function doParse(code, ctx) {
2988
2194
  order.push(a.name);
2989
2195
  }
2990
2196
  const attrs = { entries, spreads: [], order };
2991
- const computed2 = resolveComputed(classTokens, tag, id);
2197
+ const computed = resolveComputed(classTokens, tag, id, meta);
2992
2198
  const children = [];
2993
2199
  if (!opaqueSubtree) {
2994
2200
  for (const c of node.childNodes ?? []) appendChild(c, id, children);
@@ -2999,7 +2205,7 @@ function doParse(code, ctx) {
2999
2205
  isComponent: false,
3000
2206
  selfClosing: loc ? loc.endTag == null : false,
3001
2207
  classes,
3002
- computed: computed2,
2208
+ computed,
3003
2209
  attrs,
3004
2210
  children,
3005
2211
  parent: parentId,
@@ -3387,7 +2593,7 @@ function doParse2(code, ctx) {
3387
2593
  break;
3388
2594
  }
3389
2595
  }
3390
- let computed2 = emptyStyleMap();
2596
+ let computed = emptyStyleMap();
3391
2597
  if (!classes.hasDynamic) {
3392
2598
  const tokens = staticTokensOf3(classes);
3393
2599
  if (tokens.length > 0) {
@@ -3395,7 +2601,8 @@ function doParse2(code, ctx) {
3395
2601
  classes: tokens,
3396
2602
  element: { tagName: tag, namespace: component ? void 0 : "html" }
3397
2603
  });
3398
- computed2 = ctx.normalizer.normalizeStyleMap(res.styles);
2604
+ computed = ctx.normalizer.normalizeStyleMap(res.styles);
2605
+ if (res.unknown.length > 0) meta.hasUnresolvedClasses = true;
3399
2606
  for (const w of res.warnings) {
3400
2607
  diagnostics.push({
3401
2608
  code: "DF_STYLE_CONFLICT_UNRESOLVED",
@@ -3413,7 +2620,7 @@ function doParse2(code, ctx) {
3413
2620
  isComponent: component,
3414
2621
  selfClosing: opening.selfClosing,
3415
2622
  classes,
3416
- computed: computed2,
2623
+ computed,
3417
2624
  attrs,
3418
2625
  children,
3419
2626
  parent: parentId,
@@ -3438,13 +2645,13 @@ function doParse2(code, ctx) {
3438
2645
  };
3439
2646
  const roots = [];
3440
2647
  traverse(ast, {
3441
- JSXElement(path3) {
3442
- roots.push(path3.node);
3443
- path3.skip();
2648
+ JSXElement(path5) {
2649
+ roots.push(path5.node);
2650
+ path5.skip();
3444
2651
  },
3445
- JSXFragment(path3) {
3446
- roots.push(path3.node);
3447
- path3.skip();
2652
+ JSXFragment(path5) {
2653
+ roots.push(path5.node);
2654
+ path5.skip();
3448
2655
  }
3449
2656
  });
3450
2657
  const rootFrag = doc.nodes.get(doc.root);
@@ -3722,28 +2929,14 @@ function createJsxBackend() {
3722
2929
  init_esm_shims();
3723
2930
 
3724
2931
  export {
2932
+ flexCenterWrapper,
2933
+ redundantFragment,
2934
+ gridCenterWrapper,
3725
2935
  displayContentsWrapper,
3726
2936
  emptyStyleDiv,
3727
- flexCenterWrapper,
3728
- inlineFlexCenterWrapper,
3729
- nestedFlexMerge,
3730
- nestedGridMerge,
2937
+ inheritedOnlyWrapper,
3731
2938
  passthroughWrapper,
3732
- redundantFragment,
3733
2939
  redundantInlineWrapper,
3734
- borderRadiusShorthand,
3735
- borderShorthand,
3736
- dedupeClasses,
3737
- gapShorthand,
3738
- insetShorthand,
3739
- marginShorthand,
3740
- overflowShorthand,
3741
- overscrollBehaviorShorthand,
3742
- paddingShorthand,
3743
- placeShorthand,
3744
- scrollMarginShorthand,
3745
- scrollPaddingShorthand,
3746
- sizeShorthand,
3747
2940
  builtinPatterns,
3748
2941
  createTailwindResolver,
3749
2942
  createCssResolver,
@@ -3752,4 +2945,4 @@ export {
3752
2945
  createJsxFrontend,
3753
2946
  createJsxBackend
3754
2947
  };
3755
- //# sourceMappingURL=chunk-3Z5ZWLXX.js.map
2948
+ //# sourceMappingURL=chunk-FPT4EJ6Q.js.map