@tenphi/tasty 0.0.0-snapshot.1c5dff6 → 0.0.0-snapshot.1f1c00a

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 (331) hide show
  1. package/README.md +377 -238
  2. package/dist/babel-rZcEwPLM.d.ts +82 -0
  3. package/dist/chunks/async-storage-CG5HnUT5.js +20 -0
  4. package/dist/chunks/async-storage-CG5HnUT5.js.map +1 -0
  5. package/dist/chunks/build-config-CL4eRu2z.js +42 -0
  6. package/dist/chunks/build-config-CL4eRu2z.js.map +1 -0
  7. package/dist/chunks/collector-D2ARGT2L.js +208 -0
  8. package/dist/chunks/collector-D2ARGT2L.js.map +1 -0
  9. package/dist/chunks/config-engine-DEmmPPdp.js +272 -0
  10. package/dist/chunks/config-engine-DEmmPPdp.js.map +1 -0
  11. package/dist/chunks/css-definitions-DotDkIXt.js +876 -0
  12. package/dist/chunks/css-definitions-DotDkIXt.js.map +1 -0
  13. package/dist/chunks/css-resources-P7IBUUZs.js +142 -0
  14. package/dist/chunks/css-resources-P7IBUUZs.js.map +1 -0
  15. package/dist/chunks/debug-D8pg9RGI.js +502 -0
  16. package/dist/chunks/debug-D8pg9RGI.js.map +1 -0
  17. package/dist/chunks/dsl-DRhFWQzo.js +1378 -0
  18. package/dist/chunks/dsl-DRhFWQzo.js.map +1 -0
  19. package/dist/chunks/hydration-TRcz5vnk.js +41 -0
  20. package/dist/chunks/hydration-TRcz5vnk.js.map +1 -0
  21. package/dist/chunks/react-runtime-BpxlOTbO.js +562 -0
  22. package/dist/chunks/react-runtime-BpxlOTbO.js.map +1 -0
  23. package/dist/chunks/runtime-engine-CKFXUH-A.js +2416 -0
  24. package/dist/chunks/runtime-engine-CKFXUH-A.js.map +1 -0
  25. package/dist/chunks/runtime-state-BxaLhVdP.js +127 -0
  26. package/dist/chunks/runtime-state-BxaLhVdP.js.map +1 -0
  27. package/dist/{utils/merge-styles.js → chunks/shared-utils-y-HjvPN4.js} +61 -58
  28. package/dist/chunks/shared-utils-y-HjvPN4.js.map +1 -0
  29. package/dist/chunks/style-engine-BBbBuSkT.js +5260 -0
  30. package/dist/chunks/style-engine-BBbBuSkT.js.map +1 -0
  31. package/dist/{zero/extractor.js → chunks/zero-engine-gJrKBRiw.js} +126 -66
  32. package/dist/chunks/zero-engine-gJrKBRiw.js.map +1 -0
  33. package/dist/collector-w4mBj5gd.d.ts +164 -0
  34. package/dist/config-CZWCzBr6.d.ts +1349 -0
  35. package/dist/core/index.d.ts +5 -33
  36. package/dist/core/index.js +9 -26
  37. package/dist/index-G9JuwM9p.d.ts +1920 -0
  38. package/dist/index.d.ts +5 -48
  39. package/dist/index.js +11 -33
  40. package/dist/{utils/merge-styles.d.ts → merge-styles-VXwJ0Frr.d.ts} +3 -4
  41. package/dist/ssr/astro-client.d.ts +1 -0
  42. package/dist/ssr/astro-client.js +6 -0
  43. package/dist/ssr/astro-client.js.map +1 -0
  44. package/dist/ssr/astro-middleware-extract-static.d.ts +10 -0
  45. package/dist/ssr/astro-middleware-extract-static.js +9 -0
  46. package/dist/ssr/astro-middleware-extract-static.js.map +1 -0
  47. package/dist/ssr/astro-middleware-extract.d.ts +10 -0
  48. package/dist/ssr/astro-middleware-extract.js +9 -0
  49. package/dist/ssr/astro-middleware-extract.js.map +1 -0
  50. package/dist/ssr/astro-middleware-static.d.ts +17 -0
  51. package/dist/ssr/astro-middleware-static.js +7 -0
  52. package/dist/ssr/astro-middleware-static.js.map +1 -0
  53. package/dist/ssr/astro-middleware.d.ts +18 -0
  54. package/dist/ssr/astro-middleware.js +7 -0
  55. package/dist/ssr/astro-middleware.js.map +1 -0
  56. package/dist/ssr/astro.d.ts +127 -0
  57. package/dist/ssr/astro.js +221 -0
  58. package/dist/ssr/astro.js.map +1 -0
  59. package/dist/ssr/index.d.ts +36 -0
  60. package/dist/ssr/index.js +10 -0
  61. package/dist/ssr/index.js.map +1 -0
  62. package/dist/ssr/next-config.d.ts +64 -0
  63. package/dist/ssr/next-config.js +128 -0
  64. package/dist/ssr/next-config.js.map +1 -0
  65. package/dist/ssr/next.d.ts +46 -0
  66. package/dist/ssr/next.js +60 -0
  67. package/dist/ssr/next.js.map +1 -0
  68. package/dist/static/index.d.ts +90 -5
  69. package/dist/static/index.js +31 -4
  70. package/dist/static/index.js.map +1 -0
  71. package/dist/static/inject.d.ts +4 -0
  72. package/dist/static/inject.js +17 -0
  73. package/dist/static/inject.js.map +1 -0
  74. package/dist/types-JEY8PVYP.d.ts +1567 -0
  75. package/dist/zero/babel.d.ts +2 -113
  76. package/dist/zero/babel.js +230 -106
  77. package/dist/zero/babel.js.map +1 -1
  78. package/dist/zero/index.d.ts +80 -3
  79. package/dist/zero/index.js +2 -4
  80. package/dist/zero/next.d.ts +57 -33
  81. package/dist/zero/next.js +93 -59
  82. package/dist/zero/next.js.map +1 -1
  83. package/docs/README.md +39 -0
  84. package/docs/adoption.md +323 -0
  85. package/docs/ai-agents.md +250 -0
  86. package/docs/comparison.md +448 -0
  87. package/docs/configuration.md +684 -56
  88. package/docs/debug.md +170 -343
  89. package/docs/design-system.md +455 -0
  90. package/docs/dsl.md +1065 -0
  91. package/docs/getting-started.md +240 -0
  92. package/docs/injector.md +159 -53
  93. package/docs/methodology.md +648 -0
  94. package/docs/migration-v3.md +285 -0
  95. package/docs/pipeline.md +765 -0
  96. package/docs/plugins.md +353 -0
  97. package/docs/react-api.md +683 -0
  98. package/docs/runtime-benchmarks.md +388 -0
  99. package/docs/ssr.md +610 -0
  100. package/docs/styles.md +211 -63
  101. package/docs/tasty-static.md +273 -67
  102. package/package.json +182 -58
  103. package/tasty.config.ts +6 -0
  104. package/dist/_virtual/_rolldown/runtime.js +0 -8
  105. package/dist/chunks/cacheKey.js +0 -70
  106. package/dist/chunks/cacheKey.js.map +0 -1
  107. package/dist/chunks/definitions.d.ts +0 -37
  108. package/dist/chunks/definitions.js +0 -260
  109. package/dist/chunks/definitions.js.map +0 -1
  110. package/dist/chunks/renderChunk.js +0 -61
  111. package/dist/chunks/renderChunk.js.map +0 -1
  112. package/dist/config.d.ts +0 -288
  113. package/dist/config.js +0 -403
  114. package/dist/config.js.map +0 -1
  115. package/dist/debug.d.ts +0 -204
  116. package/dist/debug.js +0 -733
  117. package/dist/debug.js.map +0 -1
  118. package/dist/hooks/useGlobalStyles.d.ts +0 -27
  119. package/dist/hooks/useGlobalStyles.js +0 -56
  120. package/dist/hooks/useGlobalStyles.js.map +0 -1
  121. package/dist/hooks/useKeyframes.d.ts +0 -56
  122. package/dist/hooks/useKeyframes.js +0 -54
  123. package/dist/hooks/useKeyframes.js.map +0 -1
  124. package/dist/hooks/useProperty.d.ts +0 -79
  125. package/dist/hooks/useProperty.js +0 -91
  126. package/dist/hooks/useProperty.js.map +0 -1
  127. package/dist/hooks/useRawCSS.d.ts +0 -53
  128. package/dist/hooks/useRawCSS.js +0 -28
  129. package/dist/hooks/useRawCSS.js.map +0 -1
  130. package/dist/hooks/useStyles.d.ts +0 -40
  131. package/dist/hooks/useStyles.js +0 -169
  132. package/dist/hooks/useStyles.js.map +0 -1
  133. package/dist/injector/index.d.ts +0 -157
  134. package/dist/injector/index.js +0 -154
  135. package/dist/injector/index.js.map +0 -1
  136. package/dist/injector/injector.d.ts +0 -139
  137. package/dist/injector/injector.js +0 -417
  138. package/dist/injector/injector.js.map +0 -1
  139. package/dist/injector/sheet-manager.d.ts +0 -135
  140. package/dist/injector/sheet-manager.js +0 -728
  141. package/dist/injector/sheet-manager.js.map +0 -1
  142. package/dist/injector/types.d.ts +0 -144
  143. package/dist/keyframes/index.js +0 -206
  144. package/dist/keyframes/index.js.map +0 -1
  145. package/dist/parser/classify.js +0 -319
  146. package/dist/parser/classify.js.map +0 -1
  147. package/dist/parser/const.js +0 -33
  148. package/dist/parser/const.js.map +0 -1
  149. package/dist/parser/lru.js +0 -109
  150. package/dist/parser/lru.js.map +0 -1
  151. package/dist/parser/parser.d.ts +0 -25
  152. package/dist/parser/parser.js +0 -116
  153. package/dist/parser/parser.js.map +0 -1
  154. package/dist/parser/tokenizer.js +0 -69
  155. package/dist/parser/tokenizer.js.map +0 -1
  156. package/dist/parser/types.d.ts +0 -51
  157. package/dist/parser/types.js +0 -46
  158. package/dist/parser/types.js.map +0 -1
  159. package/dist/pipeline/conditions.d.ts +0 -134
  160. package/dist/pipeline/conditions.js +0 -406
  161. package/dist/pipeline/conditions.js.map +0 -1
  162. package/dist/pipeline/exclusive.js +0 -231
  163. package/dist/pipeline/exclusive.js.map +0 -1
  164. package/dist/pipeline/index.d.ts +0 -53
  165. package/dist/pipeline/index.js +0 -660
  166. package/dist/pipeline/index.js.map +0 -1
  167. package/dist/pipeline/materialize.js +0 -856
  168. package/dist/pipeline/materialize.js.map +0 -1
  169. package/dist/pipeline/parseStateKey.d.ts +0 -15
  170. package/dist/pipeline/parseStateKey.js +0 -451
  171. package/dist/pipeline/parseStateKey.js.map +0 -1
  172. package/dist/pipeline/simplify.js +0 -516
  173. package/dist/pipeline/simplify.js.map +0 -1
  174. package/dist/pipeline/warnings.js +0 -18
  175. package/dist/pipeline/warnings.js.map +0 -1
  176. package/dist/plugins/okhsl-plugin.d.ts +0 -35
  177. package/dist/plugins/okhsl-plugin.js +0 -371
  178. package/dist/plugins/okhsl-plugin.js.map +0 -1
  179. package/dist/plugins/types.d.ts +0 -69
  180. package/dist/properties/index.js +0 -221
  181. package/dist/properties/index.js.map +0 -1
  182. package/dist/properties/property-type-resolver.d.ts +0 -24
  183. package/dist/properties/property-type-resolver.js +0 -83
  184. package/dist/properties/property-type-resolver.js.map +0 -1
  185. package/dist/states/index.d.ts +0 -49
  186. package/dist/states/index.js +0 -416
  187. package/dist/states/index.js.map +0 -1
  188. package/dist/static/tastyStatic.d.ts +0 -46
  189. package/dist/static/tastyStatic.js +0 -31
  190. package/dist/static/tastyStatic.js.map +0 -1
  191. package/dist/static/types.d.ts +0 -49
  192. package/dist/static/types.js +0 -24
  193. package/dist/static/types.js.map +0 -1
  194. package/dist/styles/align.d.ts +0 -15
  195. package/dist/styles/align.js +0 -14
  196. package/dist/styles/align.js.map +0 -1
  197. package/dist/styles/border.d.ts +0 -25
  198. package/dist/styles/border.js +0 -114
  199. package/dist/styles/border.js.map +0 -1
  200. package/dist/styles/color.d.ts +0 -14
  201. package/dist/styles/color.js +0 -23
  202. package/dist/styles/color.js.map +0 -1
  203. package/dist/styles/createStyle.js +0 -77
  204. package/dist/styles/createStyle.js.map +0 -1
  205. package/dist/styles/dimension.js +0 -97
  206. package/dist/styles/dimension.js.map +0 -1
  207. package/dist/styles/display.d.ts +0 -37
  208. package/dist/styles/display.js +0 -67
  209. package/dist/styles/display.js.map +0 -1
  210. package/dist/styles/fade.d.ts +0 -15
  211. package/dist/styles/fade.js +0 -58
  212. package/dist/styles/fade.js.map +0 -1
  213. package/dist/styles/fill.d.ts +0 -42
  214. package/dist/styles/fill.js +0 -52
  215. package/dist/styles/fill.js.map +0 -1
  216. package/dist/styles/flow.d.ts +0 -16
  217. package/dist/styles/flow.js +0 -12
  218. package/dist/styles/flow.js.map +0 -1
  219. package/dist/styles/gap.d.ts +0 -31
  220. package/dist/styles/gap.js +0 -37
  221. package/dist/styles/gap.js.map +0 -1
  222. package/dist/styles/height.d.ts +0 -17
  223. package/dist/styles/height.js +0 -20
  224. package/dist/styles/height.js.map +0 -1
  225. package/dist/styles/index.d.ts +0 -2
  226. package/dist/styles/index.js +0 -9
  227. package/dist/styles/index.js.map +0 -1
  228. package/dist/styles/inset.d.ts +0 -52
  229. package/dist/styles/inset.js +0 -150
  230. package/dist/styles/inset.js.map +0 -1
  231. package/dist/styles/justify.d.ts +0 -15
  232. package/dist/styles/justify.js +0 -14
  233. package/dist/styles/justify.js.map +0 -1
  234. package/dist/styles/list.d.ts +0 -16
  235. package/dist/styles/list.js +0 -98
  236. package/dist/styles/list.js.map +0 -1
  237. package/dist/styles/margin.d.ts +0 -24
  238. package/dist/styles/margin.js +0 -104
  239. package/dist/styles/margin.js.map +0 -1
  240. package/dist/styles/outline.d.ts +0 -29
  241. package/dist/styles/outline.js +0 -65
  242. package/dist/styles/outline.js.map +0 -1
  243. package/dist/styles/padding.d.ts +0 -24
  244. package/dist/styles/padding.js +0 -104
  245. package/dist/styles/padding.js.map +0 -1
  246. package/dist/styles/predefined.d.ts +0 -73
  247. package/dist/styles/predefined.js +0 -241
  248. package/dist/styles/predefined.js.map +0 -1
  249. package/dist/styles/preset.d.ts +0 -47
  250. package/dist/styles/preset.js +0 -126
  251. package/dist/styles/preset.js.map +0 -1
  252. package/dist/styles/radius.d.ts +0 -14
  253. package/dist/styles/radius.js +0 -51
  254. package/dist/styles/radius.js.map +0 -1
  255. package/dist/styles/scrollbar.d.ts +0 -23
  256. package/dist/styles/scrollbar.js +0 -34
  257. package/dist/styles/scrollbar.js.map +0 -1
  258. package/dist/styles/shadow.d.ts +0 -14
  259. package/dist/styles/shadow.js +0 -24
  260. package/dist/styles/shadow.js.map +0 -1
  261. package/dist/styles/styledScrollbar.d.ts +0 -47
  262. package/dist/styles/styledScrollbar.js +0 -38
  263. package/dist/styles/styledScrollbar.js.map +0 -1
  264. package/dist/styles/transition.d.ts +0 -14
  265. package/dist/styles/transition.js +0 -158
  266. package/dist/styles/transition.js.map +0 -1
  267. package/dist/styles/types.d.ts +0 -498
  268. package/dist/styles/width.d.ts +0 -17
  269. package/dist/styles/width.js +0 -20
  270. package/dist/styles/width.js.map +0 -1
  271. package/dist/tasty.d.ts +0 -982
  272. package/dist/tasty.js +0 -206
  273. package/dist/tasty.js.map +0 -1
  274. package/dist/tokens/typography.d.ts +0 -19
  275. package/dist/tokens/typography.js +0 -237
  276. package/dist/tokens/typography.js.map +0 -1
  277. package/dist/types.d.ts +0 -184
  278. package/dist/utils/cache-wrapper.js +0 -26
  279. package/dist/utils/cache-wrapper.js.map +0 -1
  280. package/dist/utils/case-converter.js +0 -8
  281. package/dist/utils/case-converter.js.map +0 -1
  282. package/dist/utils/colors.d.ts +0 -5
  283. package/dist/utils/colors.js +0 -9
  284. package/dist/utils/colors.js.map +0 -1
  285. package/dist/utils/css-types.d.ts +0 -7
  286. package/dist/utils/dotize.d.ts +0 -26
  287. package/dist/utils/dotize.js +0 -122
  288. package/dist/utils/dotize.js.map +0 -1
  289. package/dist/utils/filter-base-props.d.ts +0 -15
  290. package/dist/utils/filter-base-props.js +0 -45
  291. package/dist/utils/filter-base-props.js.map +0 -1
  292. package/dist/utils/get-display-name.d.ts +0 -7
  293. package/dist/utils/get-display-name.js +0 -10
  294. package/dist/utils/get-display-name.js.map +0 -1
  295. package/dist/utils/hsl-to-rgb.js +0 -38
  296. package/dist/utils/hsl-to-rgb.js.map +0 -1
  297. package/dist/utils/is-dev-env.js +0 -19
  298. package/dist/utils/is-dev-env.js.map +0 -1
  299. package/dist/utils/is-valid-element-type.js +0 -15
  300. package/dist/utils/is-valid-element-type.js.map +0 -1
  301. package/dist/utils/merge-styles.js.map +0 -1
  302. package/dist/utils/mod-attrs.d.ts +0 -8
  303. package/dist/utils/mod-attrs.js +0 -21
  304. package/dist/utils/mod-attrs.js.map +0 -1
  305. package/dist/utils/okhsl-to-rgb.js +0 -296
  306. package/dist/utils/okhsl-to-rgb.js.map +0 -1
  307. package/dist/utils/process-tokens.d.ts +0 -31
  308. package/dist/utils/process-tokens.js +0 -171
  309. package/dist/utils/process-tokens.js.map +0 -1
  310. package/dist/utils/resolve-recipes.d.ts +0 -17
  311. package/dist/utils/resolve-recipes.js +0 -147
  312. package/dist/utils/resolve-recipes.js.map +0 -1
  313. package/dist/utils/selector-transform.js +0 -32
  314. package/dist/utils/selector-transform.js.map +0 -1
  315. package/dist/utils/string.js +0 -8
  316. package/dist/utils/string.js.map +0 -1
  317. package/dist/utils/styles.d.ts +0 -177
  318. package/dist/utils/styles.js +0 -730
  319. package/dist/utils/styles.js.map +0 -1
  320. package/dist/utils/typography.d.ts +0 -36
  321. package/dist/utils/typography.js +0 -53
  322. package/dist/utils/typography.js.map +0 -1
  323. package/dist/utils/warnings.d.ts +0 -16
  324. package/dist/utils/warnings.js +0 -16
  325. package/dist/utils/warnings.js.map +0 -1
  326. package/dist/zero/css-writer.d.ts +0 -45
  327. package/dist/zero/css-writer.js +0 -74
  328. package/dist/zero/css-writer.js.map +0 -1
  329. package/dist/zero/extractor.d.ts +0 -24
  330. package/dist/zero/extractor.js.map +0 -1
  331. package/docs/usage.md +0 -695
@@ -0,0 +1,765 @@
1
+ # Tasty Style Rendering Pipeline
2
+
3
+ This document describes the style rendering pipeline that transforms style objects into CSS rules. The pipeline ensures that each style value is applied to exactly one condition through exclusive condition building, boolean simplification, and intelligent CSS generation.
4
+
5
+ **Implementation:** [`src/pipeline/`](../src/pipeline/) — TypeScript file names below are relative to that directory.
6
+
7
+ ## Overview
8
+
9
+ The pipeline takes a `Styles` object and produces an array of `CSSRule` objects ready for injection into the DOM. The entry point is `renderStyles`, which has two overloads: called with a selector or class (`renderStyles(styles, '.t1')`) it returns the rules directly; called without one (`renderStyles(styles)`) it returns `{ rules }` for a caller that will allocate the class name itself. The per-handler flow is:
10
+
11
+ ```
12
+ Input: Styles Object
13
+ ↓
14
+ ┌──────────────────────────────────────────┐
15
+ │ 0. PRE-PARSE NORMALIZATION │
16
+ │ extractCompoundStates │
17
+ │ (drop don't-care AND atoms) │
18
+ └──────────────────────────────────────────┘
19
+ ↓
20
+ ┌──────────────────────────────────────────┐
21
+ │ 1. PARSE CONDITIONS │
22
+ │ parseStyleEntries + parseStateKey │
23
+ └──────────────────────────────────────────┘
24
+ ↓
25
+ ┌──────────────────────────────────────────┐
26
+ │ 1b. MERGE ENTRIES BY VALUE │
27
+ │ mergeEntriesByValue │
28
+ │ (collapse same-value non-defaults) │
29
+ └──────────────────────────────────────────┘
30
+ ↓
31
+ ┌──────────────────────────────────────────┐
32
+ │ 2a. EXPAND USER OR BRANCHES │
33
+ │ expandOrConditions │
34
+ │ (A | B | C → A, B&!A, C&!A&!B) │
35
+ └──────────────────────────────────────────┘
36
+ ↓
37
+ ┌──────────────────────────────────────────┐
38
+ │ 2b. BUILD EXCLUSIVE CONDITIONS │
39
+ │ Negate higher-priority entries │
40
+ └──────────────────────────────────────────┘
41
+ ↓
42
+ ┌──────────────────────────────────────────┐
43
+ │ 3. EXPAND DE MORGAN OR BRANCHES │
44
+ │ expandExclusiveOrs │
45
+ │ (only for at-rule ORs from negation) │
46
+ └──────────────────────────────────────────┘
47
+ ↓
48
+ ┌──────────────────────────────────────────┐
49
+ │ 4. COMPUTE STATE COMBINATIONS │
50
+ │ Cartesian product across styles │
51
+ └──────────────────────────────────────────┘
52
+ ↓
53
+ ┌──────────────────────────────────────────┐
54
+ │ 5. CALL HANDLERS │
55
+ │ Compute CSS declarations │
56
+ └──────────────────────────────────────────┘
57
+ ↓
58
+ ┌──────────────────────────────────────────┐
59
+ │ 6. MERGE BY VALUE │
60
+ │ Combine rules with same output │
61
+ └──────────────────────────────────────────┘
62
+ ↓
63
+ ┌──────────────────────────────────────────┐
64
+ │ 7. MATERIALIZE CSS │
65
+ │ Condition → selectors + at-rules │
66
+ └──────────────────────────────────────────┘
67
+ ↓
68
+ ┌──────────────────────────────────────────┐
69
+ │ runPipeline post-pass: │
70
+ │ - dedupe identical rules │
71
+ │ - emit @starting-style rules last │
72
+ └──────────────────────────────────────────┘
73
+ ↓
74
+ Output: CSSRule[]
75
+ ```
76
+
77
+ **Simplification** (`simplifyCondition` in `simplify.ts`) is not a separate numbered stage. It runs inside OR expansion, exclusive building, `expandExclusiveOrs` branch cleanup, combination ANDs, merge-by-value ORs, and materialization. Every call is cached by condition unique-id, so the repetition is cheap.
78
+
79
+ **Post-pass:** After `processStyles` collects rules from every handler, `runPipeline` (`index.ts`) filters duplicates using a key of `selector|declarations|atRules|rootPrefix|startingStyle`, then **stable-sorts rules by their cascade `order` hint (ascending source priority)** so lower-priority rules come first and higher-priority rules win, and finally emits every `@starting-style` rule **after** all normal rules. The `order` hint is the highest source priority among the entries that produced each rule (threaded `ComputedRule → CSSRule`). This ordering is cascade-critical because Stage 7 equalizes specificity with `:where()`: once every state selector carries zero specificity, source order alone decides which of two overlapping rules wins — most importantly the `_` fallback floor (low order, emitted first) versus the states layered over it, and `@starting-style` versus its normal counterpart. Mutually-exclusive rules are unaffected by ordering (they never both match), so reordering them is safe.
80
+
81
+ ---
82
+
83
+ ## Stage 0: Pre-parse Normalization
84
+
85
+ **File:** `exclusive.ts` (`extractCompoundStates`)
86
+
87
+ ### What It Does
88
+
89
+ Runs on each style's value map **before** any parsing, when the map has at least three keys and one of them uses `&`. An atom is a **don't-care** when flipping it never changes which value the map resolves to; every key that mentions a don't-care atom is dropped.
90
+
91
+ ### How It Works
92
+
93
+ 1. Split each key into its top-level `&` atoms. Keys containing `|`, `^`, or `,` at top level are a single opaque atom. The `_` floor (and a misused `_` key) is kept verbatim and left out of the analysis.
94
+ 2. Treat the map as the cascade it is: the last authored key whose atoms all hold wins, and `''` is always the lowest priority.
95
+ 3. An atom is a don't-care when, in every state, the cascade resolves to the same value with the atom present as with it absent. Only the states `(K₁ minus the atom) ∪ K₀`, for every key `K₁` that contains the atom and every key `K₀` that does not (or none), can tell the two apart, so the check is quadratic in the number of keys rather than exponential in atoms.
96
+ 4. Drop every key that contains a don't-care atom. With the atom forced absent those keys never match, and by definition the result is unchanged.
97
+
98
+ The check is over the cascade, not over pairs of keys. In `{ '': A, checked: B, 'invalid & checked': C, disabled: A }`, `disabled` has the default's value, but it outranks `checked`: a checked, disabled element gets `A`, so `disabled` is not a don't-care. (An earlier pairwise rule — "every key with the atom has a same-value partner without it" — folded `disabled` into `''` here and lost that priority.)
99
+
100
+ Atoms are compared as strings and treated as independent. Real atoms can be related (`theme=dark` vs `theme=light`, nested media ranges); that only rules states out, so a don't-care over independent atoms is one for every real state too. Two guards keep the rewrite safe: a map with more than 32 distinct atoms is left unreduced, and a reduction that would leave the `_` floor alone with `''` (which means "use the floor", see Stage 1) is skipped.
101
+
102
+ ### Why
103
+
104
+ Removing don't-care dimensions before parsing prevents combinatorial blowup in later stages. `mergeEntriesByValue`, `buildExclusiveConditions`, and materialization all see fewer entries and fewer spurious conditions.
105
+
106
+ ### Example
107
+
108
+ ```typescript
109
+ // Input (value map)
110
+ { '': 'A', '@dark': 'B', '@hc': 'A', '@dark & @hc': 'B' }
111
+ // @hc is a don't-care: its presence never changes the value.
112
+
113
+ // Output
114
+ { '': 'A', '@dark': 'B' }
115
+ ```
116
+
117
+ ---
118
+
119
+ ## Stage 1: Parse Conditions
120
+
121
+ **Files:** `exclusive.ts` (`parseStyleEntries`), `parseStateKey.ts` (`parseStateKey`)
122
+
123
+ ### What It Does
124
+
125
+ Converts each state key in a style value map (like `'hovered & !disabled'`, `'@media(w < 768px)'`) into `ConditionNode` trees. `parseStyleEntries` walks the object keys in source order and assigns priorities; `parseStateKey` parses a single key string.
126
+
127
+ `parseStyleEntries` also handles the bare `''` default and the standalone `_` fallback floor before the priority order is finalized. The bare `''` default only behaves correctly as the lowest-priority state, so a misplaced one is moved to the front (`MISPLACED_DEFAULT_STATE` warning). The `_` key is pulled out as a separate floor entry (`floor: true`, `TrueCondition`) and always assigned the lowest priority. If a map defines only `_` and a bare `''` (no other states), the `''` default is redundant — it is dropped in favor of the `_` value (`REDUNDANT_DEFAULT_STATE` warning). A `_` combined with state logic (`_ & hovered`, `_ | x`) is ignored (`INVALID_FALLBACK_KEY` warning).
128
+
129
+ ### How It Works
130
+
131
+ 1. **Tokenization**: The state key is split into tokens using a regex pattern that recognizes:
132
+ - Operators: `&` (AND), `|` (OR), `!` (NOT), `^` (XOR)
133
+ - Parentheses for grouping
134
+ - State tokens: `@media(...)`, `@root(...)`, `@parent(...)`, `@own(...)`, `@supports(...)`, `@(...)`, `@starting`, predefined states, modifiers, pseudo-classes
135
+
136
+ 2. **Recursive Descent Parsing**: Tokens are parsed with operator precedence:
137
+ ```
138
+ ! (NOT) > ^ (XOR) > | (OR) > & (AND)
139
+ ```
140
+
141
+ 3. **State Token Interpretation**: Each state token is converted to a specific condition type:
142
+ - `hovered` → `ModifierCondition` with `attribute: 'data-hovered'`
143
+ - `theme=dark` → `ModifierCondition` with `attribute: 'data-theme', value: 'dark'`
144
+ - `:hover` → `PseudoCondition`
145
+ - `@media(w < 768px)` → `MediaCondition` (`subtype: 'dimension'`) with bounds
146
+ - `@media(prefers-color-scheme: dark)` → `MediaCondition` (`subtype: 'feature'`, `feature` + `featureValue`)
147
+ - `@root(schema=dark)` → `RootCondition` wrapping the inner condition
148
+ - `@parent(hovered)` → `ParentCondition` (optional `direct` for immediate parent)
149
+ - `@own(hovered)` → `OwnCondition` wrapping the parsed inner condition
150
+ - `@supports(display: grid)` → `SupportsCondition`
151
+ - `@(w < 600px)` → `ContainerCondition` (dimension, style, or raw subtypes)
152
+ - `@mobile` → Resolved via predefined states, then parsed recursively
153
+
154
+ Pipeline warnings for invalid inputs (e.g. bad `$` selector affix) are emitted from `warnings.ts`.
155
+
156
+ ### Why
157
+
158
+ The condition tree representation enables:
159
+ - Boolean algebra operations (simplification, negation)
160
+ - Semantic analysis (detect contradictions)
161
+ - Flexible CSS generation (different output for media vs. selectors)
162
+
163
+ ### Example
164
+
165
+ ```typescript
166
+ // Input
167
+ 'hovered & @media(w < 768px)'
168
+
169
+ // Output ConditionNode
170
+ {
171
+ kind: 'compound',
172
+ operator: 'AND',
173
+ children: [
174
+ { kind: 'state', type: 'modifier', attribute: 'data-hovered', ... },
175
+ { kind: 'state', type: 'media', subtype: 'dimension', upperBound: { value: '768px', ... }, ... }
176
+ ]
177
+ }
178
+ ```
179
+
180
+ ---
181
+
182
+ ## Stage 1b: Merge Entries By Value
183
+
184
+ **File:** `exclusive.ts` (`mergeEntriesByValue`)
185
+
186
+ ### What It Does
187
+
188
+ Collapses parsed entries that share the same value when doing so provably preserves the authored cascade. Only **non-default** entries are merged — an entry with the default state (`''` → `TrueCondition`) is never merged with a non-default entry.
189
+
190
+ ### How It Works
191
+
192
+ Entries arrive sorted highest-priority-first. For each entry the pass walks already-emitted entries from most-recent backward, looking for the nearest same-value entry where merging is safe. If found, the two entries collapse into one (priority becomes the maximum of the pair, condition becomes the simplified `OR`). Default entries are emitted as singletons and never participate in a merge.
193
+
194
+ ### The Safety Condition
195
+
196
+ Merging two same-value entries with conditions `C_h` (higher priority) and `C_l` (lower priority) lifts the lower one up to `p_h` and changes the higher-priority "blocker" for every intermediate-priority entry from `!C_h` to `!(C_h | C_l) = !C_h & !C_l`. The added `!C_l` constraint can incorrectly block an intermediate entry that should have won.
197
+
198
+ The merge is safe iff for every entry `e_m` strictly between them in priority with a different value,
199
+
200
+ simplify(C_m & C_l & !C_h) = FALSE
201
+
202
+ i.e. there is no scenario where the intermediate state could have matched, the lower same-value entry would also have matched, and the higher one would not. This is the only way the merge could leak through and shadow the intermediate state.
203
+
204
+ An intermediate whose condition always holds (a tautological key, or same-value keys that already merged into one, like `beta | !beta`) is checked like any other. Only the `''` default and the `_` floor are exempt from the check, and they are always the lowest priorities, so they are never intermediates.
205
+
206
+ ### Why
207
+
208
+ Without this pass a value map like `{ '@dark': 'red', '@dark & @hc': 'red' }` would create two separate entries that later produce two CSS rules with identical output. Merging before exclusive building keeps the exclusive condition algebra small and avoids duplicate CSS. The safety check ensures we never break the cascade in service of this optimization.
209
+
210
+ **Why defaults are kept separate:** merging `TRUE | X` collapses to `TRUE`, destroying X's participation in the exclusive cascade. Intermediate-priority states would then lose their `:not(X)` negation, producing overlapping CSS rules.
211
+
212
+ ### Example — Safe merge (still collapses)
213
+
214
+ ```typescript
215
+ // { '@dark & @hc': 'red', '@dark': 'red' }
216
+ // Input entries (highest priority first), no intermediate different-value entry
217
+ [
218
+ { stateKey: '@dark & @hc', value: 'red', condition: dark & hc, priority: 1 },
219
+ { stateKey: '@dark', value: 'red', condition: dark, priority: 0 },
220
+ ]
221
+
222
+ // Safe: no intermediates. Output: one merged entry
223
+ [
224
+ { stateKey: '@dark & @hc | @dark', value: 'red',
225
+ condition: simplify((dark & hc) | dark) = dark, priority: 1 }
226
+ ]
227
+ ```
228
+
229
+ The compound dark/HC dedup pattern `{ '': light, '@dark': dark, '@hc': hc, '@dark & @hc': dark }` also collapses cleanly because `@hc & @dark & !(@dark & @hc)` simplifies to FALSE — the intermediate `@hc` is structurally blocked by the contradiction.
230
+
231
+ ### Example — Unsafe merge (must NOT collapse)
232
+
233
+ ```typescript
234
+ // { hovered: 'red', pressed: 'blue', disabled: 'red' }
235
+ // Authored cascade: disabled > pressed > hovered.
236
+ // Merging hovered (priority 0) with disabled (priority 2) would lift
237
+ // hovered to priority 2 and rewrite `pressed`'s exclusive from
238
+ // `pressed & !disabled` to `pressed & !disabled & !hovered`, making
239
+ // `pressed + hovered` resolve to red instead of blue.
240
+
241
+ // `simplify(pressed & hovered & !disabled)` is not FALSE — three
242
+ // independent modifiers can all be active — so the entries are kept
243
+ // separate. Stage 6 `mergeByValue` will later combine the two red
244
+ // CSS rules into one selector group, but only after the cascade is
245
+ // correctly resolved.
246
+ ```
247
+
248
+ ---
249
+
250
+ ## Stage 2a: Expand User OR Branches
251
+
252
+ **File:** `exclusive.ts` (`expandOrConditions`)
253
+
254
+ ### What It Does
255
+
256
+ Runs **before** `buildExclusiveConditions`. Splits any user-authored OR in a parsed entry's condition into multiple sibling entries, each made exclusive against the OR branches that come before it.
257
+
258
+ ### How It Works
259
+
260
+ For an entry with condition `A | B | C`:
261
+
262
+ ```
263
+ A (first branch, no prior)
264
+ B & !A (second branch exclusive from first)
265
+ C & !A & !B (third branch exclusive from first two)
266
+ ```
267
+
268
+ Each expanded branch gets a `stateKey` suffix like `[0]`, `[1]`, `[2]`. Branches that simplify to `FALSE` are dropped. Branches inherit the original entry's priority.
269
+
270
+ This pass does **not** sort branches — user ORs are authored in the natural order they appear and aren't the product of De Morgan negation, so at-rule-aware sorting isn't required here (that's Stage 3's job).
271
+
272
+ ### Why
273
+
274
+ Running this before exclusive building means the Stage 2b negation cascade sees one branch per entry and never has to reason about nested ORs while computing `!prior`. It also avoids emitting overlapping CSS rules: `{ 'compact | @media(dark)': 'red' }` becomes two mutually exclusive entries rather than one rule whose two branches could both match simultaneously.
275
+
276
+ ---
277
+
278
+ ## Stage 2b: Build Exclusive Conditions
279
+
280
+ **File:** `exclusive.ts` (`buildExclusiveConditions`)
281
+
282
+ ### What It Does
283
+
284
+ Ensures each style entry applies in exactly one scenario by ANDing each condition with the negation of all higher-priority conditions.
285
+
286
+ ### How It Works
287
+
288
+ Given entries ordered by priority (highest first):
289
+ ```
290
+ A: value1 (priority 2)
291
+ B: value2 (priority 1)
292
+ C: value3 (priority 0)
293
+ ```
294
+
295
+ Produces:
296
+ ```
297
+ A: A (highest priority, no negation needed)
298
+ B: B & !A (applies only when A doesn't)
299
+ C: C & !A & !B (applies only when neither A nor B)
300
+ ```
301
+
302
+ Each exclusive condition is passed through `simplifyCondition`. Entries that simplify to `FALSE` (impossible) are filtered out.
303
+
304
+ Every entry is added to the "prior" list, including one whose condition is `TRUE`. That can be a tautological key, or same-value keys that merged into one in Stage 1b (`alpha: X, '!alpha': X` → `TRUE`). Such an entry always applies at its priority, so negating it correctly makes every lower entry `FALSE`. The bare `''` default is `TRUE` too, but it is always the lowest priority, so no entry comes after it.
305
+
306
+ ### `_` fallback floor
307
+
308
+ An entry flagged `floor: true` (from the standalone `_` key) is the one
309
+ exception to the negation cascade above. `buildExclusiveConditions` **pulls the
310
+ floor entries out** before the negation pass, builds the remaining entries with a
311
+ plain unconditional negation loop, then re-appends each floor with its own
312
+ (`TRUE`) condition as the exclusive condition. So the floor **always applies**,
313
+ never receives `!prior`, and never negates lower-priority entries. Because its
314
+ condition is `TRUE`, Stage 1 `mergeEntriesByValue` already leaves it untouched
315
+ (same guard as the bare `''` default), so no floor-specific merge handling is
316
+ needed.
317
+
318
+ This exists to fix the *unknown-query hole*: CSS three-valued logic makes
319
+ `not(unknown) = unknown`, so a negated `@supports(...)` / container-query default
320
+ branch silently never applies. A `_` floor sidesteps negation entirely and lets
321
+ the positive override layer on top via the cascade (see Stage 7's `:where()`
322
+ equalization and the ascending-priority post-pass).
323
+
324
+ Because the floor is **additive** (always emitted, never negated) rather than
325
+ mutually exclusive, it differs observably from the `''` default in two ways
326
+ beyond the unknown-query fix: an "empty" higher-priority state (one that emits no
327
+ declarations) cannot unset the property — the floor still applies — and when
328
+ states emit different declaration sets, the floor's declarations bleed through
329
+ wherever a winning state does not override the same property. The `''` default,
330
+ being negated, swaps out cleanly. See the DSL guide's "`_` vs the `''` default".
331
+
332
+ ### Why
333
+
334
+ This eliminates CSS specificity wars. Instead of relying on cascade order, each CSS rule matches in exactly one scenario. Benefits:
335
+ - Predictable styling regardless of rule order
336
+ - No conflicts from overlapping conditions
337
+ - Easier debugging (each rule is mutually exclusive)
338
+
339
+ ### Example
340
+
341
+ ```typescript
342
+ // Style value mapping
343
+ { padding: { '': '2x', 'compact': '1x', '@media(w < 768px)': '0.5x' } }
344
+
345
+ // After exclusive building (highest priority first):
346
+ // @media(w < 768px): applies when media matches
347
+ // compact & !@media(w < 768px): applies when compact but NOT media
348
+ // !compact & !@media(w < 768px): default, applies when neither
349
+ ```
350
+
351
+ ---
352
+
353
+ ## Stage 3: Expand De Morgan OR Branches
354
+
355
+ **File:** `exclusive.ts` (`expandExclusiveOrs`, `sortOrBranchesForExpansion`)
356
+
357
+ ### What It Does
358
+
359
+ Runs **after** `buildExclusiveConditions`. Handles ORs that arise **during** exclusive building from De Morgan negation — e.g. when a higher-priority condition `A & B` gets negated into the next entry's exclusive as `!(A & B) = !A | !B`. When such an OR mixes **at-rule** context (`media`, `container`, `supports`, `starting`) with other branches, each branch needs to keep its own at-rule wrapping.
360
+
361
+ This is the companion to **Stage 2a** (user-OR expansion). The split exists because the two passes have different data and different correctness needs:
362
+
363
+ | Stage | Runs on | Sees ORs from | Sorts branches? |
364
+ |---|---|---|---|
365
+ | 2a `expandOrConditions` | `ParsedStyleEntry.condition` | User-authored `|` in state keys | No — user order is stable |
366
+ | 3 `expandExclusiveOrs` | `ExclusiveStyleEntry.exclusiveCondition` | De Morgan negation inside `buildExclusiveConditions` | Yes — at-rule branches first |
367
+
368
+ ### How It Works
369
+
370
+ 1. Collect top-level OR branches of `exclusiveCondition`.
371
+ 2. If there is no OR (single branch), the entry is unchanged. Pure selector ORs with no at-rule context are also left alone (materialization handles them via `:is()` / variant merging).
372
+ 3. Otherwise `sortOrBranchesForExpansion` reorders branches by a three-tier rank — `@supports` branches first, then other at-rule branches (media / container / starting), then pure-selector branches. This is load-bearing for correctness (see below).
373
+ 4. Each branch is made exclusive against prior branches: `branch & !prior[0] & !prior[1] & ...`, then simplified.
374
+ 5. Impossible branches are dropped; expanded entries get a synthetic `stateKey` suffix like `[or:0]`.
375
+
376
+ ### Why the sort matters
377
+
378
+ **At-rules before selectors.** Consider `!A | !B` where A is an at-rule (e.g. `@supports(grid)`) and B is a modifier (e.g. `:has(foo)`):
379
+
380
+ - **With at-rule-first sort** (`!A`, then `!B & A`): the first branch emits "outside `@supports`", the second emits "inside `@supports` with `:not(:has(foo))`". Full coverage.
381
+ - **Without the sort** (`!B`, then `!A & B`): the first branch emits `:not(:has(foo))` as a bare selector with no at-rule context — leaking the rule outside `@supports`. The second is incomplete.
382
+
383
+ **`@supports` before other at-rules (Kleene logic).** A `@supports` feature query is special: anything ANDed with it can become *unknown* — not simply false — when the feature is unsupported. The canonical case is a feature guard over a dependent query, e.g. `@supports(container-type: scroll-state) & @(scroll-state(scrolled: block-end))` (`S & C`). The default's exclusive is `!(S & C) = !S | !C`, which `simplify` sorts alphabetically into `[!C, !S]`.
384
+
385
+ - **Expanding `!C` first** emits a bare `@container (not scroll-state(...))` rule. In a browser without `scroll-state` support that query is unknown (not false), so it matches nothing and the default `inset` is never applied — a coverage hole.
386
+ - **Expanding `!S` first** yields `!S | (S & !C)`: a bare `@supports (not (container-type: scroll-state))` fallback carrying the default, plus the dependent negation nested inside the supported scope. Full coverage, and the unsupported path never references the guarded query.
387
+
388
+ Because `!S | (S & !R) ≡ !S | !R` in two-valued logic, this reordering is a no-op for cases without an `@supports` guard. The same supports-first ordering is also applied in `makeOrBranchesExclusive` (`materialize.ts`) for ORs that survive nested inside an AND (e.g. `compact & (!S | !C)`).
389
+
390
+ The pre-build Stage 2a pass doesn't need this because user-authored ORs aren't produced by negation and their branches are expected to apply in each branch's own scope.
391
+
392
+ ### Example (conceptual)
393
+
394
+ See the comment block on `expandExclusiveOrs` in `exclusive.ts`: a default value whose higher-priority sibling is `@supports(...) & :has(...)` gets an exclusive of `!@supports | !:has`. Expansion yields one branch under `@supports (not ...)` and another under `@supports (...) { :not(:has()) }` instead of a bare `:not(:has())` rule.
395
+
396
+ ---
397
+
398
+ ## Stage 4: Compute State Combinations
399
+
400
+ **File:** `index.ts` (`computeStateCombinations`)
401
+
402
+ ### What It Does
403
+
404
+ Computes the Cartesian product of all style entries for a handler, creating snapshots of which value each style has for each possible state combination.
405
+
406
+ When every input is a direct value rather than a state map, the pipeline skips stages 0–4 and passes one unconditional snapshot directly to the handler. This avoids building exclusive entries and a Cartesian product for ordinary state-free styles.
407
+
408
+ ### How It Works
409
+
410
+ 1. Collect exclusive entries for each style the handler uses
411
+ 2. Compute Cartesian product: every combination of entries
412
+ 3. For each combination:
413
+ - AND all `exclusiveCondition` values together
414
+ - `simplifyCondition` the result
415
+ - Skip if simplified to `FALSE`
416
+ - Record the values for each style
417
+
418
+ ### Why
419
+
420
+ Style handlers often depend on multiple style properties (e.g., `padding` might look at both `padding` and `gap`). By computing all valid combinations, we can call the handler once per unique state and get the correct CSS output.
421
+
422
+ ### Example
423
+
424
+ ```typescript
425
+ // Handler looks up: ['padding', 'size']
426
+ // padding has entries: [{ value: '2x', condition: A }, { value: '1x', condition: B }]
427
+ // size has entries: [{ value: 'large', condition: C }, { value: 'small', condition: D }]
428
+
429
+ // Combinations:
430
+ // { padding: '2x', size: 'large', condition: A & C }
431
+ // { padding: '2x', size: 'small', condition: A & D }
432
+ // { padding: '1x', size: 'large', condition: B & C }
433
+ // { padding: '1x', size: 'small', condition: B & D }
434
+ ```
435
+
436
+ ---
437
+
438
+ ## Stage 5: Call Handlers
439
+
440
+ **File:** `index.ts` (within `processStyles`)
441
+
442
+ ### What It Does
443
+
444
+ Invokes style handlers with computed value snapshots to produce CSS declarations.
445
+
446
+ ### How It Works
447
+
448
+ 1. For each state snapshot (condition + values):
449
+ - Call the handler with the values
450
+ - Handler returns CSS properties (e.g., `{ 'padding-top': '16px', 'padding-bottom': '16px' }`)
451
+ - Handler may also return `$` (selector suffix) for pseudo-elements
452
+ 2. Create computed rules with the condition, declarations, and selector suffix
453
+
454
+ ### Why
455
+
456
+ Style handlers encapsulate the logic for translating design tokens (like `'2x'`) to actual CSS values (like `'16px'`). They can also handle complex multi-property styles (e.g., `padding` → `padding-top`, `padding-right`, etc.).
457
+
458
+ ---
459
+
460
+ ## Stage 6: Merge By Value
461
+
462
+ **File:** `index.ts` (`mergeByValue`)
463
+
464
+ ### What It Does
465
+
466
+ Combines rules that have identical CSS output into a single rule with an OR condition.
467
+ Zero- and one-rule inputs return unchanged because no merge is possible.
468
+
469
+ ### How It Works
470
+
471
+ 1. Group rules by `selectorSuffix` plus a stable string for declarations (JSON via an internal `declStringCache` `WeakMap` on declaration objects)
472
+ 2. For rules in the same group:
473
+ - Merge their conditions with OR
474
+ - `simplifyCondition` the resulting condition
475
+ 3. Output one rule per group
476
+
477
+ ### Why
478
+
479
+ Different state combinations might produce the same CSS output. Rather than emitting duplicate CSS, we combine them into a single rule. This reduces CSS size and improves performance.
480
+
481
+ ### Example
482
+
483
+ ```typescript
484
+ // Before merging:
485
+ // condition: A → { color: 'red' }
486
+ // condition: B → { color: 'red' }
487
+
488
+ // After merging:
489
+ // condition: A | B → { color: 'red' }
490
+ ```
491
+
492
+ ---
493
+
494
+ ## Stage 7: Materialize CSS
495
+
496
+ **File:** `materialize.ts` (`conditionToCSS`, `materializeComputedRule` in `index.ts`)
497
+
498
+ ### What It Does
499
+
500
+ Converts condition trees into actual CSS selectors and at-rules.
501
+
502
+ ### How It Works
503
+
504
+ 1. **Condition to CSS components** (`conditionToCSS`): Walk the condition tree and build `SelectorVariant` data:
505
+ - `ModifierCondition` → attribute selectors (e.g. `[data-hovered]`); optional `operator` (`=`, `^=`, `$=`, `*=`)
506
+ - `PseudoCondition` → pseudo-class (e.g. `:hover`)
507
+ - `MediaCondition` → `@media` (dimension, feature, or type)
508
+ - `ContainerCondition` → `@container` (dimension, style query, or raw)
509
+ - `RootCondition` → `rootGroups` / root prefix fragments
510
+ - `ParentCondition` → `parentGroups` / ancestor selectors (`direct` → child combinator path)
511
+ - `OwnCondition` → `ownGroups` on the **styled** element (sub-element / `&` scope), optimized with `optimizeGroups`
512
+ - `SupportsCondition` → `@supports` at-rules
513
+ - `StartingCondition` → `@starting-style` wrapper
514
+
515
+ 2. **AND / OR on variants**: AND merges variant dimensions; OR yields multiple variants (later merged into `:is()` / `:not()` groups where appropriate).
516
+
517
+ 3. **Contradiction detection**: During variant merging, impossible combinations are dropped (e.g. conflicting media, root, or modifier negations).
518
+
519
+ 4. **`materializeComputedRule`**: Groups variants by sorted at-rules plus root-prefix key; within each group, `mergeVariantsIntoSelectorGroups` merges variants that differ only in flat modifier/pseudo parts; builds selector strings and emits one or more `CSSRule` objects. The rule's `order` hint (cascade priority) is propagated here for the post-pass sort.
520
+
521
+ 5. **`:where()` specificity equalization**: Every stateful part of the selector is wrapped in `:where(...)` so it contributes **zero specificity**:
522
+ - the flat root modifiers + pseudos and the root element's `:is()`/`:not()` groups are combined into one `:where(...)` (`buildSelectorFromVariant`);
523
+ - the sub-element (`@own`) groups are wrapped in a **separate** `:where(...)` so the sub-element's structural `[data-element="X"]` specificity is preserved while its states are zeroed;
524
+ - `@parent` ancestors (`parentGroupsToCSS`) become `:where(... *)` / `:where(:not(... *))`;
525
+ - the `@root` context prefix (`rootGroupsToCSS`) becomes `:where(:root...)` (zeroing the `:root` pseudo too).
526
+
527
+ The only specificity anchors that remain are the doubled component class (`.tXX.tXX`, added by the injector) and sub-element `[data-element]` attributes. Because Stage 2b already makes ordinary rules mutually exclusive, zeroing specificity never changes *which* rule matches — it only makes additive layering (`_` fallback floor, `@starting-style`) resolve deterministically by source order (see the post-pass). Canonical sorting/dedup/subsumption still run first; `:where()` is only the final wrapper, and both output paths (injector and direct-selector / SSR) consume the same wrapped fragments.
528
+
529
+ ### Why
530
+
531
+ CSS has different mechanisms for different condition types:
532
+ - Modifiers → attribute selectors
533
+ - Media queries → `@media` blocks
534
+ - Container queries → `@container` blocks
535
+ - Root state → `:root` / root groups
536
+ - Supports → `@supports` blocks
537
+
538
+ The materialization layer handles these differences while maintaining the logical semantics of the condition tree.
539
+
540
+ ### Output Structure
541
+
542
+ ```typescript
543
+ interface CSSRule {
544
+ selector: string | string[]; // Selector fragment(s); array when OR’d selector branches
545
+ declarations: string; // CSS declarations (e.g. 'color: red;')
546
+ atRules?: string[]; // Wrapping at-rules
547
+ rootPrefix?: string; // Root state prefix
548
+ startingStyle?: boolean; // Wrap declarations in @starting-style
549
+ order?: number; // Internal cascade order hint (ascending source priority); stripped before injection
550
+ }
551
+ ```
552
+
553
+ When `renderStyles` runs **without** a selector, returned rules include `needsClassName: true` (compatibility field for the injector); that flag is not part of `CSSRule` inside `materialize.ts`.
554
+
555
+ ---
556
+
557
+ ## Condition Types
558
+
559
+ **File:** `conditions.ts`
560
+
561
+ ### ConditionNode Hierarchy
562
+
563
+ ```
564
+ ConditionNode
565
+ ├── TrueCondition (matches everything)
566
+ ├── FalseCondition (matches nothing)
567
+ ├── CompoundCondition (AND/OR of children)
568
+ └── StateCondition
569
+ ├── ModifierCondition (data attributes; optional value + match operator)
570
+ ├── PseudoCondition (CSS pseudo-classes: :hover)
571
+ ├── MediaCondition (subtype: dimension | feature | type)
572
+ ├── ContainerCondition (subtype: dimension | style | raw)
573
+ ├── RootCondition (inner condition under :root)
574
+ ├── ParentCondition (@parent(...); optional direct parent)
575
+ ├── OwnCondition (@own(...); scoped to styled / sub-element)
576
+ ├── SupportsCondition (@supports(...))
577
+ └── StartingCondition (@starting-style wrapper)
578
+ ```
579
+
580
+ ### Key Operations
581
+
582
+ - `and(...conditions)`: Create AND with short-circuit and flattening
583
+ - `or(...conditions)`: Create OR with short-circuit and flattening
584
+ - `not(condition)`: Negate with De Morgan's law support
585
+ - `getConditionUniqueId(condition)`: Get canonical ID for comparison
586
+
587
+ ---
588
+
589
+ ## Simplification
590
+
591
+ **File:** `simplify.ts`
592
+
593
+ ### What It Does
594
+
595
+ Applies boolean algebra rules to reduce condition complexity and detect impossible combinations.
596
+
597
+ ### Rules Applied
598
+
599
+ 1. **Identity Laws**:
600
+ - `A & TRUE = A`
601
+ - `A | FALSE = A`
602
+
603
+ 2. **Annihilator Laws**:
604
+ - `A & FALSE = FALSE`
605
+ - `A | TRUE = TRUE`
606
+
607
+ 3. **Contradiction Detection**:
608
+ - `A & !A = FALSE`
609
+
610
+ 4. **Tautology Detection**:
611
+ - `A | !A = TRUE`
612
+
613
+ 5. **Idempotent Laws** (via deduplication):
614
+ - `A & A = A`
615
+ - `A | A = A`
616
+
617
+ 6. **Absorption Laws**:
618
+ - `A & (A | B) = A`
619
+ - `A | (A & B) = A`
620
+
621
+ 7. **Range intersection**: For **media and container** dimension queries, impossible ranges simplify to `FALSE` (e.g. `@media(w > 400px) & @media(w < 300px)`). Ranges with compatible bounds are also merged in place (`w >= 400 & w <= 800` → a single bounded range).
622
+
623
+ 8. **Container style queries**: Conflicting or redundant `@container` style conditions on the same property can be reduced (see `simplify.ts` around the container-style conflict pass).
624
+
625
+ 9. **Attribute conflict detection**:
626
+ - `[data-theme="dark"] & [data-theme="light"] = FALSE`
627
+
628
+ 10. **Complementary factoring** (OR context): `(A & B) | (A & !B) = A`. Also works on **compound complements** — if two AND-clauses differ only by a child that is a compound negation of the other (e.g. `X` vs `!X` where X is itself `(P & Q)`), the clauses factor correctly.
629
+
630
+ 11. **Consensus / resolution** (AND context, dual of #10): `(A | B) & (A | !B) = A`. Added in commit f9038bd to eliminate overlapping CSS selectors from compound-state OR branches.
631
+
632
+ ### Why
633
+
634
+ Simplification reduces CSS output size and catches impossible combinations early, preventing invalid CSS rules from being generated. Every `simplifyCondition` call is memoized by the condition's unique id, so the cost of running it many times across stages is negligible after the first hit.
635
+
636
+ ---
637
+
638
+ ## Caching Strategy
639
+
640
+ LRU and small auxiliary caches:
641
+
642
+ | Cache | Size | Key | Purpose |
643
+ |-------|------|-----|---------|
644
+ | `pipelineCache` | 5000 | `pipelineCacheKey \|\| stringifyStyles(styles)` | Skip full pipeline for identical styles |
645
+ | `parseCache` | 5000 | `trimmedStateKey + '\\0' + isSubElement + '\\0' + JSON.stringify(localPredefinedStates)` | Skip re-parsing identical state keys in context |
646
+ | `simplifyCache` | 5000 | `getConditionUniqueId(node)` | Skip re-simplifying identical conditions |
647
+ | `conditionCache` | 3000 | `getConditionUniqueId(node)` in `conditionToCSS` | Skip re-materializing identical conditions |
648
+ | `variantKeyCache` | — | `WeakMap<SelectorVariant, string>` | Stable string keys for variants during materialization |
649
+ | `declStringCache` | — | `WeakMap<Record<string,string>, string>` | Stable JSON keys for declaration objects in `mergeByValue` |
650
+
651
+ ---
652
+
653
+ ## Example Walkthrough
654
+
655
+ ### Input
656
+
657
+ ```typescript
658
+ const styles = {
659
+ color: {
660
+ '': '#white',
661
+ '@media(prefers-color-scheme: dark)': '#dark',
662
+ hovered: '#highlight',
663
+ },
664
+ };
665
+ ```
666
+
667
+ ### Stage 1: Parse Conditions
668
+
669
+ ```
670
+ '' → TrueCondition
671
+ '@media(prefers-color-scheme: dark)' → MediaCondition(subtype: 'feature', feature: 'prefers-color-scheme', featureValue: 'dark')
672
+ 'hovered' → ModifierCondition(attribute: 'data-hovered')
673
+ ```
674
+
675
+ ### Stage 0 + 1b: Normalization
676
+
677
+ No compound AND keys, no same-value duplicates — the value map is unchanged.
678
+
679
+ ### Stage 1 + 2a: Parse and expand user ORs
680
+
681
+ No user ORs — three entries pass through unchanged.
682
+
683
+ ### Stage 2b + 3: Exclusive conditions + De Morgan expansion
684
+
685
+ Processing order (highest priority first): `hovered`, `@media(dark)`, default.
686
+
687
+ ```
688
+ hovered: [data-hovered]
689
+ @media(dark) & !hovered: @media(dark) & :not([data-hovered])
690
+ !hovered & !@media(dark): :not([data-hovered]) & not @media(dark)
691
+ ```
692
+
693
+ The default entry's exclusive is `!hovered & !@media(dark)` — no top-level OR, so Stage 3 expansion does nothing. If a higher-priority entry had been `@media(dark) & :has(foo)`, the default's exclusive would have expanded via De Morgan into two at-rule-aware branches (see Stage 3 for that scenario).
694
+
695
+ ### Stages 4–5: Compute combinations and call handler
696
+
697
+ Single style, three snapshots; the `color` handler emits `color` plus a `--current-color` variable.
698
+
699
+ ### Stage 6: Merge by value
700
+
701
+ Each snapshot yields distinct declarations; no merge.
702
+
703
+ ### Stage 7: Materialize CSS
704
+
705
+ Using `renderStyles(styles, '.t1')` (the class is doubled — `.t1.t1` — so Tasty owns specificity without `!important`):
706
+
707
+ ```css
708
+ .t1[data-hovered] {
709
+ color: var(--highlight-color);
710
+ --current-color: var(--highlight-color);
711
+ }
712
+ @media (prefers-color-scheme: dark) {
713
+ .t1:not([data-hovered]) {
714
+ color: var(--dark-color);
715
+ --current-color: var(--dark-color);
716
+ }
717
+ }
718
+ @media (not (prefers-color-scheme: dark)) {
719
+ .t1:not([data-hovered]) {
720
+ color: var(--white-color);
721
+ --current-color: var(--white-color);
722
+ }
723
+ }
724
+ ```
725
+
726
+ ---
727
+
728
+ ## Key Design Decisions
729
+
730
+ ### 1. Exclusive Conditions Over CSS Specificity
731
+
732
+ Rather than relying on CSS cascade rules, we generate mutually exclusive selectors. This makes styling predictable and debuggable.
733
+
734
+ ### 2. OR Handling in Three Layers
735
+
736
+ Boolean OR appears in three different shapes during the pipeline, and each is handled where it's cheapest to get right:
737
+
738
+ 1. **User-authored ORs in state keys** (Stage 2a, `expandOrConditions`): A user-authored condition like `'compact | @media(w < 768px)'` is split into multiple exclusive entries **before** exclusive building so the negation cascade doesn't have to reason about nested ORs.
739
+
740
+ 2. **De Morgan ORs from negation** (Stage 3, `expandExclusiveOrs`): When `buildExclusiveConditions` negates a higher-priority compound like `A & B`, the result is `!A | !B`. If branches involve at-rules, they're split with `sortOrBranchesForExpansion` so at-rule context is preserved per branch.
741
+
742
+ 3. **Pure selector ORs** (materialization): ORs that only mention modifiers/pseudos are kept intact until the `conditionToCSS` layer, where they're merged into `:is()` / `:not()` groups or emitted as comma-separated selectors. There's no gain from expanding these earlier — CSS already has compact syntax for selector-only disjunction.
743
+
744
+ Ultimately every emitted CSS rule corresponds to one conjunctive clause (DNF), produced by whichever of the three paths handled the OR.
745
+
746
+ ### 3. Early Contradiction Detection
747
+
748
+ Impossible combinations are detected at multiple levels (simplification, variant merging) to avoid generating invalid CSS.
749
+
750
+ ### 4. Aggressive Caching
751
+
752
+ Parse, simplify, condition-to-CSS, and full-pipeline results are cached independently, enabling fast re-rendering when only parts of the style object change.
753
+
754
+ Cache keys must respect key order where it carries meaning. A state map resolves by key order, so `{ '': A, checked: B, disabled: C }` and `{ '': A, disabled: C, checked: B }` are different styles and must get different keys (and classes). Only the style keys of a styles object, including a sub-element's, are order-insensitive, so they may be sorted. `stringifyStyles` (pipeline cache) and `generateChunkCacheKey` (class allocation) both follow this rule.
755
+
756
+ ---
757
+
758
+ ## Testing Priority
759
+
760
+ Every stage above is an optimization that must leave one thing unchanged: in any state, the value that applies is the value of the last authored key whose condition holds. `src/test/state-oracle.ts` states that rule directly, with none of the pipeline's machinery, and two suites compare the pipeline against it:
761
+
762
+ - `src/pipeline/exclusive.test.ts` (Node) evaluates the conditions after each of Stages 0–3 on thousands of generated maps, and names the stage that disagrees.
763
+ - `src/pipeline/state-priority.test.ts` (browser) renders hand-written, permuted, and generated maps to CSS, applies every combination of states (modifiers, value modifiers, `:has()`, `@root`, `@parent`, `@own`, media, supports), and compares what the browser computes against the oracle. It also checks that rules stay mutually exclusive, which a correct computed value alone can hide. It runs through the pipeline, the runtime injector, and the SSR collector, with all maps mounted at once so a cache-key collision shows up.
764
+
765
+ A change to any stage should keep both green. When a generated map fails, the message includes the map, the failing states, and the CSS; the map is usually worth adding to the hand-written cases once it is reduced.