rnwind 0.0.1 → 0.0.3

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 (330) hide show
  1. package/lib/cjs/core/parser/animation.cjs +427 -0
  2. package/lib/cjs/core/parser/animation.cjs.map +1 -0
  3. package/lib/cjs/core/parser/animation.d.ts +126 -0
  4. package/lib/cjs/core/parser/border-dispatcher.cjs +180 -0
  5. package/lib/cjs/core/parser/border-dispatcher.cjs.map +1 -0
  6. package/lib/cjs/core/parser/border-dispatcher.d.ts +15 -0
  7. package/lib/cjs/core/parser/case-convert.cjs +15 -0
  8. package/lib/cjs/core/parser/case-convert.cjs.map +1 -0
  9. package/lib/cjs/core/parser/case-convert.d.ts +6 -0
  10. package/lib/cjs/core/parser/color-properties-dispatcher.cjs +84 -0
  11. package/lib/cjs/core/parser/color-properties-dispatcher.cjs.map +1 -0
  12. package/lib/cjs/core/parser/color-properties-dispatcher.d.ts +19 -0
  13. package/lib/cjs/core/parser/color.cjs +193 -0
  14. package/lib/cjs/core/parser/color.cjs.map +1 -0
  15. package/lib/cjs/core/parser/color.d.ts +12 -0
  16. package/lib/cjs/core/parser/constants.cjs +21 -0
  17. package/lib/cjs/core/parser/constants.cjs.map +1 -0
  18. package/lib/cjs/core/parser/constants.d.ts +8 -0
  19. package/lib/cjs/core/parser/declaration.cjs +347 -0
  20. package/lib/cjs/core/parser/declaration.cjs.map +1 -0
  21. package/lib/cjs/core/parser/declaration.d.ts +15 -0
  22. package/lib/cjs/core/parser/gradient.cjs +132 -0
  23. package/lib/cjs/core/parser/gradient.cjs.map +1 -0
  24. package/lib/cjs/core/parser/gradient.d.ts +59 -0
  25. package/lib/cjs/core/parser/haptics.cjs +73 -0
  26. package/lib/cjs/core/parser/haptics.cjs.map +1 -0
  27. package/lib/cjs/core/parser/haptics.d.ts +47 -0
  28. package/lib/cjs/core/parser/index.d.ts +8 -0
  29. package/lib/cjs/core/parser/keyframes.cjs +95 -0
  30. package/lib/cjs/core/parser/keyframes.cjs.map +1 -0
  31. package/lib/cjs/core/parser/keyframes.d.ts +26 -0
  32. package/lib/cjs/core/parser/layout-dispatcher.cjs +120 -0
  33. package/lib/cjs/core/parser/layout-dispatcher.cjs.map +1 -0
  34. package/lib/cjs/core/parser/layout-dispatcher.d.ts +14 -0
  35. package/lib/cjs/core/parser/length.cjs +110 -0
  36. package/lib/cjs/core/parser/length.cjs.map +1 -0
  37. package/lib/cjs/core/parser/length.d.ts +51 -0
  38. package/lib/cjs/core/parser/motion-dispatcher.cjs +77 -0
  39. package/lib/cjs/core/parser/motion-dispatcher.cjs.map +1 -0
  40. package/lib/cjs/core/parser/motion-dispatcher.d.ts +11 -0
  41. package/lib/cjs/core/parser/property.cjs +22 -0
  42. package/lib/cjs/core/parser/property.cjs.map +1 -0
  43. package/lib/cjs/core/parser/property.d.ts +8 -0
  44. package/lib/cjs/core/parser/safe-area.cjs +404 -0
  45. package/lib/cjs/core/parser/safe-area.cjs.map +1 -0
  46. package/lib/cjs/core/parser/safe-area.d.ts +39 -0
  47. package/lib/cjs/core/parser/selector.cjs +22 -0
  48. package/lib/cjs/core/parser/selector.cjs.map +1 -0
  49. package/lib/cjs/core/parser/selector.d.ts +11 -0
  50. package/lib/cjs/core/parser/shorthand.cjs +188 -0
  51. package/lib/cjs/core/parser/shorthand.cjs.map +1 -0
  52. package/lib/cjs/core/parser/shorthand.d.ts +67 -0
  53. package/lib/cjs/core/parser/text-truncate.cjs +78 -0
  54. package/lib/cjs/core/parser/text-truncate.cjs.map +1 -0
  55. package/lib/cjs/core/parser/text-truncate.d.ts +44 -0
  56. package/lib/cjs/core/parser/theme-vars.cjs +467 -0
  57. package/lib/cjs/core/parser/theme-vars.cjs.map +1 -0
  58. package/lib/cjs/core/parser/theme-vars.d.ts +82 -0
  59. package/lib/cjs/core/parser/tokens.cjs +486 -0
  60. package/lib/cjs/core/parser/tokens.cjs.map +1 -0
  61. package/lib/cjs/core/parser/tokens.d.ts +45 -0
  62. package/lib/cjs/core/parser/transform.cjs +198 -0
  63. package/lib/cjs/core/parser/transform.cjs.map +1 -0
  64. package/lib/cjs/core/parser/transform.d.ts +36 -0
  65. package/lib/cjs/core/parser/tw-parser.cjs +1680 -0
  66. package/lib/cjs/core/parser/tw-parser.cjs.map +1 -0
  67. package/lib/cjs/core/parser/tw-parser.d.ts +210 -0
  68. package/lib/cjs/core/parser/types.d.ts +37 -0
  69. package/lib/cjs/core/parser/typography-dispatcher.cjs +108 -0
  70. package/lib/cjs/core/parser/typography-dispatcher.cjs.map +1 -0
  71. package/lib/cjs/core/parser/typography-dispatcher.d.ts +11 -0
  72. package/lib/cjs/core/parser/typography.cjs +97 -0
  73. package/lib/cjs/core/parser/typography.cjs.map +1 -0
  74. package/lib/cjs/core/parser/typography.d.ts +43 -0
  75. package/lib/cjs/core/style-builder/build-style.cjs +444 -0
  76. package/lib/cjs/core/style-builder/build-style.cjs.map +1 -0
  77. package/lib/cjs/core/style-builder/build-style.d.ts +54 -0
  78. package/lib/cjs/core/style-builder/index.d.ts +3 -0
  79. package/lib/cjs/core/style-builder/union-builder.cjs +326 -0
  80. package/lib/cjs/core/style-builder/union-builder.cjs.map +1 -0
  81. package/lib/cjs/core/style-builder/union-builder.d.ts +128 -0
  82. package/lib/cjs/core/types.d.ts +14 -0
  83. package/lib/cjs/metro/dts.cjs +127 -0
  84. package/lib/cjs/metro/dts.cjs.map +1 -0
  85. package/lib/cjs/metro/dts.d.ts +16 -0
  86. package/lib/cjs/metro/index.cjs +19 -0
  87. package/lib/cjs/metro/index.cjs.map +1 -0
  88. package/lib/cjs/metro/index.d.ts +9 -0
  89. package/lib/cjs/metro/resolver.cjs +47 -0
  90. package/lib/cjs/metro/resolver.cjs.map +1 -0
  91. package/lib/cjs/metro/resolver.d.ts +22 -0
  92. package/lib/cjs/metro/state.cjs +301 -0
  93. package/lib/cjs/metro/state.cjs.map +1 -0
  94. package/lib/cjs/metro/state.d.ts +88 -0
  95. package/lib/cjs/metro/transform-ast.cjs +1472 -0
  96. package/lib/cjs/metro/transform-ast.cjs.map +1 -0
  97. package/lib/cjs/metro/transform-ast.d.ts +88 -0
  98. package/lib/cjs/metro/transformer.cjs +372 -0
  99. package/lib/cjs/metro/transformer.cjs.map +1 -0
  100. package/lib/cjs/metro/transformer.d.ts +47 -0
  101. package/lib/cjs/metro/warn-unknown-classes.cjs +86 -0
  102. package/lib/cjs/metro/warn-unknown-classes.cjs.map +1 -0
  103. package/lib/cjs/metro/warn-unknown-classes.d.ts +21 -0
  104. package/lib/cjs/metro/with-config.cjs +196 -0
  105. package/lib/cjs/metro/with-config.cjs.map +1 -0
  106. package/lib/cjs/metro/with-config.d.ts +79 -0
  107. package/lib/cjs/runtime/chain-handlers.cjs +37 -0
  108. package/lib/cjs/runtime/chain-handlers.cjs.map +1 -0
  109. package/lib/cjs/runtime/chain-handlers.d.ts +33 -0
  110. package/lib/cjs/runtime/components/rnwind-provider.cjs +98 -0
  111. package/lib/cjs/runtime/components/rnwind-provider.cjs.map +1 -0
  112. package/lib/cjs/runtime/components/rnwind-provider.d.ts +84 -0
  113. package/lib/cjs/runtime/gradient-types.d.ts +58 -0
  114. package/lib/cjs/runtime/haptics.cjs +113 -0
  115. package/lib/cjs/runtime/haptics.cjs.map +1 -0
  116. package/lib/cjs/runtime/haptics.d.ts +48 -0
  117. package/lib/cjs/runtime/hooks/use-css.cjs +21 -0
  118. package/lib/cjs/runtime/hooks/use-css.cjs.map +1 -0
  119. package/lib/cjs/runtime/hooks/use-css.d.ts +11 -0
  120. package/lib/cjs/runtime/hooks/use-interact.cjs +46 -0
  121. package/lib/cjs/runtime/hooks/use-interact.cjs.map +1 -0
  122. package/lib/cjs/runtime/hooks/use-interact.d.ts +42 -0
  123. package/lib/cjs/runtime/hooks/use-scheme.cjs +68 -0
  124. package/lib/cjs/runtime/hooks/use-scheme.cjs.map +1 -0
  125. package/lib/cjs/runtime/hooks/use-scheme.d.ts +34 -0
  126. package/lib/cjs/runtime/index.cjs +45 -0
  127. package/lib/cjs/runtime/index.cjs.map +1 -0
  128. package/lib/cjs/runtime/index.d.ts +27 -0
  129. package/lib/cjs/runtime/interactive-box.cjs +35 -0
  130. package/lib/cjs/runtime/interactive-box.cjs.map +1 -0
  131. package/lib/cjs/runtime/interactive-box.d.ts +40 -0
  132. package/lib/cjs/runtime/lookup-css.cjs +542 -0
  133. package/lib/cjs/runtime/lookup-css.cjs.map +1 -0
  134. package/lib/cjs/runtime/lookup-css.d.ts +164 -0
  135. package/lib/cjs/runtime/types.d.ts +29 -0
  136. package/lib/cjs/testing/index.cjs +367 -0
  137. package/lib/cjs/testing/index.cjs.map +1 -0
  138. package/lib/cjs/testing/index.d.ts +145 -0
  139. package/lib/esm/core/parser/animation.d.ts +126 -0
  140. package/lib/esm/core/parser/animation.mjs +408 -0
  141. package/lib/esm/core/parser/animation.mjs.map +1 -0
  142. package/lib/esm/core/parser/border-dispatcher.d.ts +15 -0
  143. package/lib/esm/core/parser/border-dispatcher.mjs +178 -0
  144. package/lib/esm/core/parser/border-dispatcher.mjs.map +1 -0
  145. package/lib/esm/core/parser/case-convert.d.ts +6 -0
  146. package/lib/esm/core/parser/case-convert.mjs +13 -0
  147. package/lib/esm/core/parser/case-convert.mjs.map +1 -0
  148. package/lib/esm/core/parser/color-properties-dispatcher.d.ts +19 -0
  149. package/lib/esm/core/parser/color-properties-dispatcher.mjs +82 -0
  150. package/lib/esm/core/parser/color-properties-dispatcher.mjs.map +1 -0
  151. package/lib/esm/core/parser/color.d.ts +12 -0
  152. package/lib/esm/core/parser/color.mjs +191 -0
  153. package/lib/esm/core/parser/color.mjs.map +1 -0
  154. package/lib/esm/core/parser/constants.d.ts +8 -0
  155. package/lib/esm/core/parser/constants.mjs +13 -0
  156. package/lib/esm/core/parser/constants.mjs.map +1 -0
  157. package/lib/esm/core/parser/declaration.d.ts +15 -0
  158. package/lib/esm/core/parser/declaration.mjs +345 -0
  159. package/lib/esm/core/parser/declaration.mjs.map +1 -0
  160. package/lib/esm/core/parser/gradient.d.ts +59 -0
  161. package/lib/esm/core/parser/gradient.mjs +130 -0
  162. package/lib/esm/core/parser/gradient.mjs.map +1 -0
  163. package/lib/esm/core/parser/haptics.d.ts +47 -0
  164. package/lib/esm/core/parser/haptics.mjs +71 -0
  165. package/lib/esm/core/parser/haptics.mjs.map +1 -0
  166. package/lib/esm/core/parser/index.d.ts +8 -0
  167. package/lib/esm/core/parser/keyframes.d.ts +26 -0
  168. package/lib/esm/core/parser/keyframes.mjs +91 -0
  169. package/lib/esm/core/parser/keyframes.mjs.map +1 -0
  170. package/lib/esm/core/parser/layout-dispatcher.d.ts +14 -0
  171. package/lib/esm/core/parser/layout-dispatcher.mjs +118 -0
  172. package/lib/esm/core/parser/layout-dispatcher.mjs.map +1 -0
  173. package/lib/esm/core/parser/length.d.ts +51 -0
  174. package/lib/esm/core/parser/length.mjs +104 -0
  175. package/lib/esm/core/parser/length.mjs.map +1 -0
  176. package/lib/esm/core/parser/motion-dispatcher.d.ts +11 -0
  177. package/lib/esm/core/parser/motion-dispatcher.mjs +75 -0
  178. package/lib/esm/core/parser/motion-dispatcher.mjs.map +1 -0
  179. package/lib/esm/core/parser/property.d.ts +8 -0
  180. package/lib/esm/core/parser/property.mjs +20 -0
  181. package/lib/esm/core/parser/property.mjs.map +1 -0
  182. package/lib/esm/core/parser/safe-area.d.ts +39 -0
  183. package/lib/esm/core/parser/safe-area.mjs +402 -0
  184. package/lib/esm/core/parser/safe-area.mjs.map +1 -0
  185. package/lib/esm/core/parser/selector.d.ts +11 -0
  186. package/lib/esm/core/parser/selector.mjs +20 -0
  187. package/lib/esm/core/parser/selector.mjs.map +1 -0
  188. package/lib/esm/core/parser/shorthand.d.ts +67 -0
  189. package/lib/esm/core/parser/shorthand.mjs +180 -0
  190. package/lib/esm/core/parser/shorthand.mjs.map +1 -0
  191. package/lib/esm/core/parser/text-truncate.d.ts +44 -0
  192. package/lib/esm/core/parser/text-truncate.mjs +75 -0
  193. package/lib/esm/core/parser/text-truncate.mjs.map +1 -0
  194. package/lib/esm/core/parser/theme-vars.d.ts +82 -0
  195. package/lib/esm/core/parser/theme-vars.mjs +461 -0
  196. package/lib/esm/core/parser/theme-vars.mjs.map +1 -0
  197. package/lib/esm/core/parser/tokens.d.ts +45 -0
  198. package/lib/esm/core/parser/tokens.mjs +480 -0
  199. package/lib/esm/core/parser/tokens.mjs.map +1 -0
  200. package/lib/esm/core/parser/transform.d.ts +36 -0
  201. package/lib/esm/core/parser/transform.mjs +193 -0
  202. package/lib/esm/core/parser/transform.mjs.map +1 -0
  203. package/lib/esm/core/parser/tw-parser.d.ts +210 -0
  204. package/lib/esm/core/parser/tw-parser.mjs +1678 -0
  205. package/lib/esm/core/parser/tw-parser.mjs.map +1 -0
  206. package/lib/esm/core/parser/types.d.ts +37 -0
  207. package/lib/esm/core/parser/typography-dispatcher.d.ts +11 -0
  208. package/lib/esm/core/parser/typography-dispatcher.mjs +106 -0
  209. package/lib/esm/core/parser/typography-dispatcher.mjs.map +1 -0
  210. package/lib/esm/core/parser/typography.d.ts +43 -0
  211. package/lib/esm/core/parser/typography.mjs +91 -0
  212. package/lib/esm/core/parser/typography.mjs.map +1 -0
  213. package/lib/esm/core/style-builder/build-style.d.ts +54 -0
  214. package/lib/esm/core/style-builder/build-style.mjs +442 -0
  215. package/lib/esm/core/style-builder/build-style.mjs.map +1 -0
  216. package/lib/esm/core/style-builder/index.d.ts +3 -0
  217. package/lib/esm/core/style-builder/union-builder.d.ts +128 -0
  218. package/lib/esm/core/style-builder/union-builder.mjs +324 -0
  219. package/lib/esm/core/style-builder/union-builder.mjs.map +1 -0
  220. package/lib/esm/core/types.d.ts +14 -0
  221. package/lib/esm/metro/dts.d.ts +16 -0
  222. package/lib/esm/metro/dts.mjs +125 -0
  223. package/lib/esm/metro/dts.mjs.map +1 -0
  224. package/lib/esm/metro/index.d.ts +9 -0
  225. package/lib/esm/metro/index.mjs +6 -0
  226. package/lib/esm/metro/index.mjs.map +1 -0
  227. package/lib/esm/metro/resolver.d.ts +22 -0
  228. package/lib/esm/metro/resolver.mjs +43 -0
  229. package/lib/esm/metro/resolver.mjs.map +1 -0
  230. package/lib/esm/metro/state.d.ts +88 -0
  231. package/lib/esm/metro/state.mjs +291 -0
  232. package/lib/esm/metro/state.mjs.map +1 -0
  233. package/lib/esm/metro/transform-ast.d.ts +88 -0
  234. package/lib/esm/metro/transform-ast.mjs +1451 -0
  235. package/lib/esm/metro/transform-ast.mjs.map +1 -0
  236. package/lib/esm/metro/transformer.d.ts +47 -0
  237. package/lib/esm/metro/transformer.mjs +349 -0
  238. package/lib/esm/metro/transformer.mjs.map +1 -0
  239. package/lib/esm/metro/warn-unknown-classes.d.ts +21 -0
  240. package/lib/esm/metro/warn-unknown-classes.mjs +84 -0
  241. package/lib/esm/metro/warn-unknown-classes.mjs.map +1 -0
  242. package/lib/esm/metro/with-config.d.ts +79 -0
  243. package/lib/esm/metro/with-config.mjs +194 -0
  244. package/lib/esm/metro/with-config.mjs.map +1 -0
  245. package/lib/esm/runtime/chain-handlers.d.ts +33 -0
  246. package/lib/esm/runtime/chain-handlers.mjs +34 -0
  247. package/lib/esm/runtime/chain-handlers.mjs.map +1 -0
  248. package/lib/esm/runtime/components/rnwind-provider.d.ts +84 -0
  249. package/lib/esm/runtime/components/rnwind-provider.mjs +94 -0
  250. package/lib/esm/runtime/components/rnwind-provider.mjs.map +1 -0
  251. package/lib/esm/runtime/gradient-types.d.ts +58 -0
  252. package/lib/esm/runtime/haptics.d.ts +48 -0
  253. package/lib/esm/runtime/haptics.mjs +110 -0
  254. package/lib/esm/runtime/haptics.mjs.map +1 -0
  255. package/lib/esm/runtime/hooks/use-css.d.ts +11 -0
  256. package/lib/esm/runtime/hooks/use-css.mjs +19 -0
  257. package/lib/esm/runtime/hooks/use-css.mjs.map +1 -0
  258. package/lib/esm/runtime/hooks/use-interact.d.ts +42 -0
  259. package/lib/esm/runtime/hooks/use-interact.mjs +44 -0
  260. package/lib/esm/runtime/hooks/use-interact.mjs.map +1 -0
  261. package/lib/esm/runtime/hooks/use-scheme.d.ts +34 -0
  262. package/lib/esm/runtime/hooks/use-scheme.mjs +63 -0
  263. package/lib/esm/runtime/hooks/use-scheme.mjs.map +1 -0
  264. package/lib/esm/runtime/index.d.ts +27 -0
  265. package/lib/esm/runtime/index.mjs +18 -0
  266. package/lib/esm/runtime/index.mjs.map +1 -0
  267. package/lib/esm/runtime/interactive-box.d.ts +40 -0
  268. package/lib/esm/runtime/interactive-box.mjs +33 -0
  269. package/lib/esm/runtime/interactive-box.mjs.map +1 -0
  270. package/lib/esm/runtime/lookup-css.d.ts +164 -0
  271. package/lib/esm/runtime/lookup-css.mjs +531 -0
  272. package/lib/esm/runtime/lookup-css.mjs.map +1 -0
  273. package/lib/esm/runtime/types.d.ts +29 -0
  274. package/lib/esm/testing/index.d.ts +145 -0
  275. package/lib/esm/testing/index.mjs +344 -0
  276. package/lib/esm/testing/index.mjs.map +1 -0
  277. package/package.json +80 -13
  278. package/preset.css +1171 -0
  279. package/src/core/parser/animation.ts +404 -0
  280. package/src/core/parser/border-dispatcher.ts +176 -0
  281. package/src/core/parser/case-convert.ts +10 -0
  282. package/src/core/parser/color-properties-dispatcher.ts +78 -0
  283. package/src/core/parser/color.ts +191 -0
  284. package/src/core/parser/constants.ts +11 -0
  285. package/src/core/parser/declaration.ts +340 -0
  286. package/src/core/parser/gradient.ts +148 -0
  287. package/src/core/parser/haptics.ts +88 -0
  288. package/src/core/parser/index.ts +8 -0
  289. package/src/core/parser/keyframes.ts +84 -0
  290. package/src/core/parser/layout-dispatcher.ts +111 -0
  291. package/src/core/parser/length.ts +114 -0
  292. package/src/core/parser/motion-dispatcher.ts +89 -0
  293. package/src/core/parser/property.ts +15 -0
  294. package/src/core/parser/safe-area.ts +404 -0
  295. package/src/core/parser/selector.ts +17 -0
  296. package/src/core/parser/shorthand.ts +182 -0
  297. package/src/core/parser/text-truncate.ts +79 -0
  298. package/src/core/parser/theme-vars.ts +465 -0
  299. package/src/core/parser/tokens.ts +456 -0
  300. package/src/core/parser/transform.ts +195 -0
  301. package/src/core/parser/tw-parser.ts +1828 -0
  302. package/src/core/parser/types.ts +45 -0
  303. package/src/core/parser/typography-dispatcher.ts +97 -0
  304. package/src/core/parser/typography.ts +83 -0
  305. package/src/core/style-builder/build-style.ts +500 -0
  306. package/src/core/style-builder/index.ts +3 -0
  307. package/src/core/style-builder/union-builder.ts +328 -0
  308. package/src/core/types.ts +15 -0
  309. package/src/metro/dts.ts +128 -0
  310. package/src/metro/index.ts +9 -0
  311. package/src/metro/resolver.ts +42 -0
  312. package/src/metro/state.ts +305 -0
  313. package/src/metro/transform-ast.ts +1729 -0
  314. package/src/metro/transformer.ts +372 -0
  315. package/src/metro/warn-unknown-classes.ts +79 -0
  316. package/src/metro/with-config.ts +251 -0
  317. package/src/runtime/chain-handlers.ts +47 -0
  318. package/src/runtime/components/rnwind-provider.tsx +144 -0
  319. package/src/runtime/gradient-types.ts +60 -0
  320. package/src/runtime/haptics.ts +120 -0
  321. package/src/runtime/hooks/use-css.ts +16 -0
  322. package/src/runtime/hooks/use-interact.ts +65 -0
  323. package/src/runtime/hooks/use-scheme.ts +63 -0
  324. package/src/runtime/index.ts +54 -0
  325. package/src/runtime/interactive-box.tsx +57 -0
  326. package/src/runtime/lookup-css.ts +628 -0
  327. package/src/runtime/types.ts +32 -0
  328. package/src/testing/index.ts +507 -0
  329. package/src/types/tailwindcss-node.d.ts +33 -0
  330. package/src/index.ts +0 -1
@@ -0,0 +1,1828 @@
1
+ import { compile } from '@tailwindcss/node'
2
+ import { Scanner, type SourceEntry } from '@tailwindcss/oxide'
3
+ import { formatHex as culoriFormatHex } from 'culori'
4
+ import { Features, transform, type TransformOptions } from 'lightningcss'
5
+ import { declarationToRnEntries } from './declaration'
6
+ import { detectGradientAtom, type GradientAtomInfo } from './gradient'
7
+ import { detectHapticAtom, type HapticRequest } from './haptics'
8
+ import { keyframeSelectorOffset, keyframesName, pickAnimationName } from './keyframes'
9
+ import { serializeInitialValue } from './property'
10
+ import { classNameFromSelector } from './selector'
11
+ import {
12
+ BASE_SCHEME,
13
+ compileReadyTheme,
14
+ extractCustomVariantSchemes,
15
+ extractSchemeAliases,
16
+ extractThemeVars,
17
+ type ThemeSchemeTable,
18
+ } from './theme-vars'
19
+ import { serializeTokens } from './tokens'
20
+ import type { RNStyle } from './types'
21
+ import type { Declaration as LcDeclaration, TokenOrValue } from 'lightningcss'
22
+
23
+ /**
24
+ * Inferred compiler type. `@tailwindcss/node` doesn't export its
25
+ * compiler shape as a named type, so we pull it off the `compile()`
26
+ * return to stay resilient to minor upstream shape shifts.
27
+ */
28
+ type TailwindCompiler = Awaited<ReturnType<typeof compile>>
29
+
30
+ /**
31
+ * Default LightningCSS transform options for TailwindParser's visitor.
32
+ * Its taken from official Tailwind source:
33
+ * https://github.com/tailwindlabs/tailwindcss/blob/main/packages/%40tailwindcss-node/src/optimize.ts
34
+ */
35
+ const DEFAULT_TRANSFORM_OPTIONS: Partial<TransformOptions<never>> = {
36
+ drafts: {
37
+ customMedia: true,
38
+ },
39
+ nonStandard: {
40
+ deepSelectorCombinator: true,
41
+ },
42
+ include: Features.Nesting | Features.MediaQueries,
43
+ exclude: Features.LogicalProperties | Features.DirSelector | Features.LightDark,
44
+ // NOTE: deliberately no `targets`. With targets that include
45
+ // color-mix-supporting browsers (Safari 16.4+, Chrome 111+, …),
46
+ // lightningcss EVALUATES `color-mix(in oklab, var(--theme-color)
47
+ // <pct>%, transparent)` at parse time using whichever value of
48
+ // `--theme-color` it sees first in the cascade. Tailwind v4 emits
49
+ // exactly this shape for `<prop>-<themed>/<N>` utilities (e.g.
50
+ // `border-text/20`), so the resulting RGB color is locked to ONE
51
+ // scheme — every variant gets the same value. By dropping targets,
52
+ // lightningcss leaves color-mix as an unparsed function and our
53
+ // per-scheme `unparsedToEntries` substitution path runs instead,
54
+ // producing the right rgba(...) for each scheme. Targets in this
55
+ // pipeline are otherwise unused — we never re-emit CSS from the AST.
56
+ }
57
+ /** Parser configuration — one instance per Metro session, theme CSS fixed. */
58
+ export interface TailwindParserConfig {
59
+ /**
60
+ * Theme CSS passed to `@tailwindcss/node`'s compiler. Typically the
61
+ * user's `global.css`. We append `theme(inline)` to the tailwindcss
62
+ * import so Tailwind resolves every `var(--theme-token)` at compile
63
+ * time — that gives lightningcss fully-typed values (integers / rems /
64
+ * colors) instead of unresolved `var()` references.
65
+ */
66
+ themeCss: string
67
+ /**
68
+ * Glob sources the oxide Scanner walks at project-scan time
69
+ * (`parseProject()`). Typically
70
+ * `[{ base: projectRoot, pattern: '**\/*.{ts,tsx,js,jsx}', negated: false }]`
71
+ * plus negated globs for `node_modules` and the rnwind cache dir.
72
+ *
73
+ * When omitted, the scanner has no sources and `parseProject()`
74
+ * returns an empty result — `parseAtoms()` (per-file, content-driven)
75
+ * still works without sources.
76
+ */
77
+ sources?: readonly SourceEntry[]
78
+ }
79
+
80
+ /** Per-call inputs — Metro hands us file content + filename; we derive the extension. */
81
+ export interface ParseOptions {
82
+ content: string
83
+ extension: string
84
+ }
85
+
86
+ /** One parsed keyframe step — offset plus the RN style at that offset. */
87
+ export interface KeyframeStep {
88
+ offset: string
89
+ style: RNStyle
90
+ }
91
+
92
+ /** One parsed `@keyframes` animation block. */
93
+ export interface KeyframeBlock {
94
+ name: string
95
+ steps: KeyframeStep[]
96
+ }
97
+
98
+ /** Interactive variant ('active' / 'focus') an `active:`/`focus:` atom carries. */
99
+ export type InteractiveStateTag = 'active' | 'focus'
100
+
101
+ /**
102
+ * Per-scheme resolved style for a single utility class. Keys are scheme
103
+ * names declared via `@variant <name>` in the theme CSS (or the synthetic
104
+ * `'base'` scheme for themes without variants). Values are the RN style
105
+ * object under that scheme.
106
+ *
107
+ * The reserved `__state` key is set on `active:` / `focus:` atoms so
108
+ * the build-side style-builder can tag the atom for `precomputeHoist`
109
+ * — which routes interactive atoms into the stated-hoist's 4-state
110
+ * precompute. Standard atoms have no `__state`.
111
+ */
112
+ export type SchemedStyle = Readonly<Record<string, RNStyle>> & {
113
+ /** Interactive-state gate set by `active:` / `focus:` variants. */
114
+ readonly __state?: InteractiveStateTag
115
+ }
116
+
117
+ /** Full result of one `parseAtoms` call. */
118
+ export interface ParsedOutput {
119
+ /** Resolved RN style per utility class, per declared scheme. */
120
+ atoms: Map<string, SchemedStyle>
121
+ /** `@keyframes <name> { ... }` blocks the candidates pulled in. */
122
+ keyframes: Map<string, KeyframeBlock>
123
+ /** `@property --x { initial-value: y }` declared custom-property defaults. */
124
+ propertyDefaults: Map<string, string>
125
+ /**
126
+ * Gradient metadata per atom, for atoms that play a role in a
127
+ * Tailwind v4 gradient (`from-*`, `via-*`, `to-*`, `bg-gradient-to-*`,
128
+ * `bg-linear-to-*`). The transformer reads this map to extract
129
+ * `colors / start / end` props at JSX-rewrite time. Regular non-
130
+ * gradient atoms don't appear here.
131
+ */
132
+ gradientAtoms: Map<string, GradientAtomInfo>
133
+ /**
134
+ * Haptic metadata per atom for classes that emit a
135
+ * `--rnwind-haptic` marker. The transformer reads this map to
136
+ * strip the atom from the className and wire a mount-time or
137
+ * press-time call into the `onHaptics` callback registered on
138
+ * `<SchemeProvider>`.
139
+ */
140
+ hapticAtoms: Map<string, HapticRequest>
141
+ /** Candidates oxide surfaced, in document order. */
142
+ candidates: readonly string[]
143
+ /** Every scheme the theme declares (or `['base']` when there are no `@variant` blocks). */
144
+ schemes: readonly string[]
145
+ /**
146
+ * Responsive breakpoint name → minimum-width threshold (px). Pulled from
147
+ * `--breakpoint-*` tokens in the compiled `:root` block, so both
148
+ * Tailwind defaults (`sm`, `md`, `lg`, `xl`, `2xl`) and any user
149
+ * override (e.g. `--breakpoint-3xl: 120rem`) land here. Used by the
150
+ * style-builder to emit `registerBreakpoints({...})` in the manifest
151
+ * so the runtime can filter `md:*` / `lg:*` atoms based on the
152
+ * provider's `windowWidth`.
153
+ */
154
+ breakpoints: ReadonlyMap<string, number>
155
+ }
156
+
157
+ /**
158
+ * Parses one source file's Tailwind usage into RN-ready style objects.
159
+ *
160
+ * Pipeline:
161
+ * 1. `@tailwindcss/oxide` Scanner finds every Tailwind candidate.
162
+ * 2. `@tailwindcss/node` compiler emits CSS for those candidates with
163
+ * theme tokens inlined.
164
+ * 3. lightningcss `transform` + typed visitor walks the emitted CSS:
165
+ * - `style` rules → per-class RN-style object.
166
+ * - `@keyframes` rules → per-name step map (Reanimated-ready).
167
+ * - `@property` rules → custom-property initial values.
168
+ *
169
+ * One instance holds one Scanner + one lazily-built compiler so repeated
170
+ * calls share upstream state. Theme CSS is fixed at construction — theme
171
+ * edits require a new parser.
172
+ */
173
+ export class TailwindParser {
174
+ private readonly scanner: Scanner
175
+ private compiler: TailwindCompiler | undefined
176
+ private readonly themeSchemes: ThemeSchemeTable
177
+ private readonly schemeAliases: ReadonlyMap<string, string>
178
+ /**
179
+ * Scheme names declared via `@custom-variant <name> …;`. A scheme
180
+ * listed here but absent from {@link themeSchemes} (no `@variant`
181
+ * override block) draws its values from the base `@theme` — the
182
+ * standard Tailwind v4 "light defaults + dark override" shape.
183
+ */
184
+ private readonly customVariantSchemes: readonly string[]
185
+ /**
186
+ * Memoise `resolveCandidates` results by candidate-list fingerprint.
187
+ * Fast Refresh hits this on every save: oxide's scan is cheap, but
188
+ * the LightningCSS visitor walk over the compiled CSS is ~2ms per
189
+ * file. A file whose `className` literals didn't change returns the
190
+ * SAME candidate set, so the second `parseAtoms` call returns the
191
+ * cached `ParsedOutput` — zero compile, zero visitor walk.
192
+ *
193
+ * Theme CSS changes build a new `TailwindParser` (from
194
+ * `getRnwindState` detecting the hash shift), so this cache is
195
+ * naturally invalidated — no stale-theme values leak through.
196
+ */
197
+ private readonly parseCache = new Map<string, ParsedOutput>()
198
+
199
+ /**
200
+ * Build a parser bound to a theme CSS source. `@theme` and
201
+ * `@variant` blocks are extracted eagerly into a scheme table the
202
+ * visitor consults when resolving `var(--x)` references.
203
+ * @param config Parser configuration.
204
+ */
205
+ constructor(private readonly config: TailwindParserConfig) {
206
+ this.themeSchemes = extractThemeVars(config.themeCss)
207
+ this.schemeAliases = extractSchemeAliases(config.themeCss)
208
+ this.customVariantSchemes = extractCustomVariantSchemes(config.themeCss)
209
+ this.scanner = new Scanner({ sources: config.sources ? [...config.sources] : [] })
210
+ }
211
+
212
+ /**
213
+ * Schemes declared by the user — the union of every `@custom-variant
214
+ * <name>` declaration and every `@variant <name>` block, or just
215
+ * `['base']` for themes without any. Used to decide how many
216
+ * per-scheme buckets the per-atom resolver fills. Exposed publicly so
217
+ * Metro integration can hand the names to the `.d.ts` generator
218
+ * without a full parse.
219
+ *
220
+ * Both sources matter. `@variant` blocks alone miss the common
221
+ * Tailwind v4 shape where the light palette sits in the base `@theme`
222
+ * and only `@variant dark` overrides it: there `light` exists solely
223
+ * as a `@custom-variant` and would otherwise be dropped, collapsing
224
+ * every themed atom to a single bucket that can't switch.
225
+ * `@custom-variant` order wins (it's where users enumerate their
226
+ * schemes); any `@variant`-only scheme is appended after.
227
+ * @returns Scheme names.
228
+ */
229
+ public get declaredSchemes(): readonly string[] {
230
+ const ordered: string[] = []
231
+ const seen = new Set<string>()
232
+ for (const name of this.customVariantSchemes) {
233
+ if (seen.has(name)) continue
234
+ seen.add(name)
235
+ ordered.push(name)
236
+ }
237
+ for (const name of this.themeSchemes.keys()) {
238
+ if (name === BASE_SCHEME || seen.has(name)) continue
239
+ seen.add(name)
240
+ ordered.push(name)
241
+ }
242
+ return ordered.length > 0 ? ordered : [BASE_SCHEME]
243
+ }
244
+
245
+ /**
246
+ * Build an effective var table for one scheme — base vars overridden by
247
+ * variant vars. When the scheme IS `'base'` (no variants declared), the
248
+ * base table is returned unchanged.
249
+ * @param scheme Scheme name.
250
+ * @returns Effective var name → value lookup for the scheme.
251
+ */
252
+ private effectiveVars(scheme: string): ReadonlyMap<string, string> {
253
+ const base = this.themeSchemes.get(BASE_SCHEME)
254
+ const variant = scheme === BASE_SCHEME ? undefined : this.themeSchemes.get(scheme)
255
+ if (!variant) return base ?? new Map()
256
+ // eslint-disable-next-line unicorn/no-useless-collection-argument
257
+ const merged = new Map(base ?? [])
258
+ for (const [k, v] of variant) merged.set(k, v)
259
+ return merged
260
+ }
261
+
262
+ /**
263
+ * Build the Tailwind compiler on first use and cache it. The theme CSS
264
+ * gets a `theme(inline)` modifier on its `@import 'tailwindcss'` so
265
+ * lightningcss sees resolved colors/lengths instead of `var()` refs.
266
+ * @returns Cached compiler instance.
267
+ */
268
+ private async ensureCompiler(): Promise<TailwindCompiler> {
269
+ if (this.compiler) return this.compiler
270
+ const ready = compileReadyTheme(this.config.themeCss, this.themeSchemes)
271
+ try {
272
+ this.compiler = await compile(withInlineTheme(ready), {
273
+ base: process.cwd(),
274
+ onDependency: () => {},
275
+ })
276
+ } catch (error) {
277
+ throw wrapThemeError(error)
278
+ }
279
+ return this.compiler
280
+ }
281
+
282
+ /**
283
+ * Parse one file's Tailwind usage into the full typed result.
284
+ * @param options Source content + extension.
285
+ * @param options.content Raw source text to scan for Tailwind candidates.
286
+ * @param options.extension File extension (`tsx`, `ts`, `jsx`, `js`) — feeds oxide's tokenizer.
287
+ * @returns RN atoms, keyframes, property defaults, candidates list.
288
+ */
289
+ public async parseAtoms({ content, extension }: ParseOptions): Promise<ParsedOutput> {
290
+ const candidates = this.scanner.getCandidatesWithPositions({ content, extension }).map((c) => c.candidate)
291
+ const fingerprint = fingerprintCandidates(candidates)
292
+ const cached = this.parseCache.get(fingerprint)
293
+ if (cached) return cached
294
+ const result = await this.resolveCandidates(candidates)
295
+ this.parseCache.set(fingerprint, result)
296
+ return result
297
+ }
298
+
299
+ /**
300
+ * Scan every source file the Scanner was configured to watch via
301
+ * `sources` and resolve the union of candidates in one pass. Used by
302
+ * `UnionBuilder` at Metro startup (and on first worker access) to
303
+ * populate the complete atom registry before ANY per-file transform
304
+ * has run — so scheme files never ship a partial view of the theme.
305
+ *
306
+ * Hot-reload path uses `parseAtoms` for the per-file delta; this one
307
+ * only runs once per parser instance (and whenever the parser is
308
+ * rebuilt due to a theme CSS change).
309
+ * @returns Full RN atoms, keyframes, property defaults for every
310
+ * candidate discovered across the configured sources.
311
+ */
312
+ public async parseProject(): Promise<ParsedOutput> {
313
+ const candidates = this.scanner.scan()
314
+ return this.resolveCandidates(candidates)
315
+ }
316
+
317
+ /**
318
+ * Compile + typed-visit the given candidate class names. Shared
319
+ * implementation for both `parseAtoms` (single file) and
320
+ * `parseProject` (whole project).
321
+ * @param candidates Class-name candidates the oxide Scanner produced.
322
+ * @returns Fully-typed parser result.
323
+ */
324
+ private async resolveCandidates(candidates: readonly string[]): Promise<ParsedOutput> {
325
+ if (candidates.length === 0) return emptyOutput()
326
+ const compiler = await this.ensureCompiler()
327
+ let css: string
328
+ try {
329
+ css = compiler.build([...candidates])
330
+ } catch (error) {
331
+ throw wrapThemeError(error)
332
+ }
333
+ // Tailwind v4 emits opacity-suffixed themed colors as a pre-resolved
334
+ // sRGB fallback PLUS a `@supports`-gated var()-based override:
335
+ // border-color: color-mix(in srgb, #0A0A0A 20%, transparent);
336
+ // @supports (color: color-mix(in lab, red, red)) {
337
+ // border-color: color-mix(in oklab, var(--color-text) 20%, transparent);
338
+ // }
339
+ // Lightningcss takes the OUTER fallback (locked to whichever scheme
340
+ // the compiler resolved first), and our per-scheme substitution
341
+ // never gets a chance. Unwrap the @supports so the var()-based
342
+ // declaration overrides the fallback in the same rule — lightningcss
343
+ // emits the override as `unparsed` and the parser's themeVars-aware
344
+ // path produces correct rgba per scheme.
345
+ css = unwrapColorMixSupports(css)
346
+ // `compiler.build(candidates)` memoizes across calls — it returns CSS for
347
+ // every candidate the compiler has EVER seen in this process. To keep
348
+ // parser output pure per-call we restrict outputs to this call's
349
+ // candidates:
350
+ // - atoms: match class selectors against `wanted`.
351
+ // - keyframes: collect `animation-name` references during the style
352
+ // walk, then filter the visited keyframes to referenced names.
353
+ const wanted = new Set(candidates)
354
+ const schemes = this.declaredSchemes
355
+ // Tailwind's compiled CSS contains every theme token — including
356
+ // ones imported from secondary CSS files (e.g. `@import
357
+ // 'rnwind/css'`). Pull them out of the `:root` block so
358
+ // `var(--duration-normal)` style references in unparsed declarations
359
+ // resolve to literal values (`220ms`) instead of being passed through
360
+ // to RN, which can't read CSS custom properties.
361
+ const compiledTheme = extractRootCustomProperties(css)
362
+ const schemeTables = new Map<string, ReadonlyMap<string, string>>()
363
+ for (const scheme of schemes) {
364
+ const merged = new Map(compiledTheme)
365
+ for (const [k, v] of this.effectiveVars(scheme)) merged.set(k, v)
366
+ schemeTables.set(scheme, merged)
367
+ }
368
+
369
+ const atoms = new Map<string, Record<string, RNStyle>>()
370
+ const keyframes: ParsedOutput['keyframes'] = new Map()
371
+ const referencedKeyframes = new Set<string>()
372
+ const propertyDefaults: ParsedOutput['propertyDefaults'] = new Map()
373
+ const gradientAtoms: ParsedOutput['gradientAtoms'] = new Map()
374
+ const hapticAtoms: ParsedOutput['hapticAtoms'] = new Map()
375
+ const breakpoints = new Map<string, number>()
376
+ const { schemeAliases } = this
377
+
378
+ try {
379
+ transform({
380
+ ...DEFAULT_TRANSFORM_OPTIONS,
381
+ filename: 'rnwind-virtual.css',
382
+ code: Buffer.from(css),
383
+ visitor: {
384
+ Rule: {
385
+ style(rule) {
386
+ for (const selector of rule.value.selectors) {
387
+ const className = classNameFromSelector(selector)
388
+ if (!className || !wanted.has(className)) continue
389
+ processStyleRule(
390
+ rule.value.declarations.declarations,
391
+ className,
392
+ { schemes, schemeTables, atoms, referencedKeyframes, schemeAliases, breakpoints },
393
+ rule.value.rules ?? [],
394
+ )
395
+ // Gradient atoms are detected per rule: the parser's main
396
+ // RN-style path drops the `--tw-gradient-*` customs as
397
+ // unsupported, but for gradient utilities we want to
398
+ // surface their role + resolved colour so the transformer
399
+ // can rewrite `<LinearGradient className="...">` into
400
+ // `colors={...}` / `start={...}` / `end={...}` props.
401
+ const gradient = detectGradientAtom(rule.value.declarations.declarations)
402
+ if (gradient) gradientAtoms.set(className, gradient)
403
+ // Haptics may live on the rule directly OR inside a
404
+ // nested pseudo (e.g. `&:active` for `active:haptic-*`).
405
+ // Inspect both so `active:haptic-medium` registers.
406
+ const hapticDecls: LcDeclaration[] = [...rule.value.declarations.declarations]
407
+ for (const nested of rule.value.rules ?? []) hapticDecls.push(...collectNestedDecls(nested))
408
+ const haptic = detectHapticAtom(hapticDecls)
409
+ if (haptic) hapticAtoms.set(className, haptic)
410
+ }
411
+ },
412
+ keyframes(rule) {
413
+ const name = keyframesName(rule.value.name)
414
+ if (!name) return
415
+ const steps: KeyframeStep[] = []
416
+ const baseTable = schemeTables.get(BASE_SCHEME) ?? schemeTables.get(schemes[0] ?? BASE_SCHEME)
417
+ for (const frame of rule.value.keyframes) {
418
+ const offset = keyframeSelectorOffset(frame.selectors)
419
+ if (!offset) continue
420
+ const style: RNStyle = {}
421
+ const frameDecls = frame.declarations.declarations ?? []
422
+ for (const decl of frameDecls) {
423
+ for (const [key, value] of declarationToRnEntries(decl, baseTable)) style[key] = value
424
+ }
425
+ steps.push({ offset, style })
426
+ }
427
+ keyframes.set(name, { name, steps })
428
+ },
429
+ property(rule) {
430
+ const initial = serializeInitialValue(rule.value.initialValue)
431
+ if (initial !== null) propertyDefaults.set(rule.value.name, initial)
432
+ },
433
+ },
434
+ },
435
+ })
436
+ } catch (error) {
437
+ throw wrapThemeError(error)
438
+ }
439
+
440
+ // Prune keyframes to those actually referenced by this call's atoms.
441
+ for (const name of keyframes.keys()) {
442
+ if (!referencedKeyframes.has(name)) keyframes.delete(name)
443
+ }
444
+
445
+ return { atoms, keyframes, propertyDefaults, gradientAtoms, hapticAtoms, candidates: [...candidates], schemes, breakpoints }
446
+ }
447
+ }
448
+
449
+ /**
450
+ * Wrap an error from `@tailwindcss/node`'s compiler or `lightningcss`'s
451
+ * transform with a `rnwind:` prefix so the user sees a clear "this came
452
+ * from your theme CSS" signal in Metro's stack trace. Preserves the
453
+ * original error as `cause` so downstream tooling (Sentry, Metro
454
+ * symbolication) can still inspect it.
455
+ * @param error Underlying error from the compiler or transform.
456
+ * @returns Prefixed Error with the original attached as `cause`.
457
+ */
458
+ function wrapThemeError(error: unknown): Error {
459
+ const message = error instanceof Error ? error.message : String(error)
460
+ const wrapped = new Error(
461
+ `rnwind: failed to compile theme CSS — ${message}\n` +
462
+ `Check your global.css for unbalanced braces, unknown @utility / @variant declarations, ` +
463
+ `or unsupported color functions. Run \`bun run --cwd packages/rnwind code-check\` if this is the rnwind repo itself.`,
464
+ )
465
+ if (error instanceof Error) (wrapped as { cause?: unknown }).cause = error
466
+ return wrapped
467
+ }
468
+
469
+ /**
470
+ * Fingerprint a candidate list for `parseCache` lookup. Sorting gives
471
+ * the same key regardless of source order (the set of candidates is
472
+ * what drives `resolveCandidates`'s output, not their order).
473
+ * @param candidates Raw oxide-scanner output for one file.
474
+ * @returns Canonical string key.
475
+ */
476
+ function fingerprintCandidates(candidates: readonly string[]): string {
477
+ if (candidates.length === 0) return ''
478
+ if (candidates.length === 1) return candidates[0]!
479
+ return [...candidates].toSorted((a, b) => a.localeCompare(b)).join('\0')
480
+ }
481
+
482
+ /**
483
+ * Empty sentinel returned when oxide finds no candidates in the file.
484
+ * @returns Zero-atom result with only the `base` scheme declared.
485
+ */
486
+ function emptyOutput(): ParsedOutput {
487
+ return {
488
+ atoms: new Map(),
489
+ keyframes: new Map(),
490
+ propertyDefaults: new Map(),
491
+ gradientAtoms: new Map(),
492
+ hapticAtoms: new Map(),
493
+ candidates: [],
494
+ schemes: [BASE_SCHEME],
495
+ breakpoints: new Map(),
496
+ }
497
+ }
498
+
499
+ /**
500
+ * Upgrade `@import 'tailwindcss'` (single- or double-quoted, with or
501
+ * without an existing `theme(...)` clause) to `@import 'tailwindcss'
502
+ * theme(inline)`. User-authored `theme(...)` clauses are preserved so
503
+ * overrides win.
504
+ * @param css Theme CSS source.
505
+ * @returns CSS with the Tailwind import upgraded.
506
+ */
507
+ function withInlineTheme(css: string): string {
508
+ return css.replaceAll(/(@import\s+['"]tailwindcss['"])(?!\s*theme\()/g, '$1 theme(inline)')
509
+ }
510
+
511
+ /**
512
+ * Collect rule-local custom-property writes (`--tw-translate-x`,
513
+ * `--tw-scale-x`, `--tw-skew-y`, …). Tailwind v4 uses these as
514
+ * composable transform tokens that `translate: var(--tw-translate-x)
515
+ * var(--tw-translate-y)` then references. Surfacing them as theme vars
516
+ * lets the declaration converter resolve the references as if they
517
+ * were declared in `@theme`.
518
+ * @param decls All declarations from one lightningcss style rule.
519
+ * @returns Map from custom-property name (with leading `--`) to its raw value.
520
+ */
521
+ function collectRuleLocalVars(decls: readonly { property: string; value: unknown }[]): ReadonlyMap<string, string> {
522
+ const out = new Map<string, string>()
523
+ for (const decl of decls) {
524
+ if (decl.property !== 'custom') continue
525
+ const custom = decl.value as { name: { name: string } | string; value?: readonly TokenOrValue[] }
526
+ const rawName = typeof custom.name === 'string' ? custom.name : custom.name.name
527
+ if (!rawName.startsWith('--tw-')) continue
528
+ if (!custom.value) continue
529
+ const text = serializeTokens(custom.value).trim()
530
+ if (text.length > 0) out.set(rawName, text)
531
+ }
532
+ return out
533
+ }
534
+
535
+ interface StyleRuleContext {
536
+ schemes: readonly string[]
537
+ schemeTables: ReadonlyMap<string, ReadonlyMap<string, string>>
538
+ atoms: Map<string, Record<string, RNStyle>>
539
+ referencedKeyframes: Set<string>
540
+ schemeAliases: ReadonlyMap<string, string>
541
+ /**
542
+ * Mutable breakpoint registry — `applyMediaRule` populates this as it
543
+ * walks responsive variant atoms. The driving signal is the `@media
544
+ * (width >= Xrem)` condition on the nested rule. Result lives on
545
+ * {@link ParsedOutput.breakpoints}.
546
+ */
547
+ breakpoints: Map<string, number>
548
+ }
549
+
550
+ /**
551
+ * Process one matched style rule for a given class name: fold its
552
+ * declarations into the per-scheme buckets, detect referenced keyframes,
553
+ * then apply Tailwind's composable transform post-pass.
554
+ * @param declarations Declarations from one lightningcss style rule.
555
+ * @param className Class name this rule's selectors matched.
556
+ * @param ctx Parser-call-wide context (schemes, tables, output maps).
557
+ * @param nestedRules Nested style rules (Tailwind variant prefixes like `dark:`
558
+ * wrap their decls in `&:where(.<scheme>, .<scheme> *)` nested rules).
559
+ */
560
+ function processStyleRule(
561
+ declarations: readonly LcDeclaration[],
562
+ className: string,
563
+ ctx: StyleRuleContext,
564
+ nestedRules: readonly unknown[] = [],
565
+ ): void {
566
+ const bucket = ctx.atoms.get(className) ?? {}
567
+ const ruleLocalVars = collectRuleLocalVars(declarations)
568
+ const ruleSchemeTables = mergeRuleVars(ctx.schemeTables, ruleLocalVars)
569
+ // Detect active:/focus: prefix on the class name. Tailwind emits the
570
+ // actual decls inside `&:active { … }` / `&:focus { … }`, and the
571
+ // OUTER rule has zero direct decls — so the existing per-scheme phase
572
+ // below produces an empty bucket. We collect the inner decls into the
573
+ // base scheme and tag the bucket with `__state` so the style-builder
574
+ // can route the atom into `precomputeHoist`'s state buckets when the
575
+ // interactive state.
576
+ const interactiveState = classNameStateOf(className)
577
+ // Phase 1: outer declarations apply to EVERY scheme (unconditional
578
+ // utilities like `opacity-50`).
579
+ for (const decl of declarations) {
580
+ applyDeclarationToBucket(decl, bucket, ctx.schemes, ruleSchemeTables)
581
+ const animationRef = pickAnimationName(decl)
582
+ if (animationRef) ctx.referencedKeyframes.add(animationRef)
583
+ }
584
+ applyComposedTransform(bucket, ctx.schemes, ruleLocalVars)
585
+ applyComposedShadow(bucket, ctx.schemes, ruleLocalVars)
586
+ applyComposedRing(bucket, ctx.schemes, ruleLocalVars)
587
+ // Phase 2: nested rules — three orthogonal flavours, dispatched on
588
+ // the lightningcss node `type`:
589
+ // - `media`: Tailwind v4 responsive variants (`sm:`, `md:`, …) wrap
590
+ // declarations in `@media (width >= Xrem)`. Decls fold into every
591
+ // scheme; the atom's name carries the breakpoint association so
592
+ // the runtime can gate it on `windowWidth`.
593
+ // - `style` + `interactiveState`: `&:active` / `&:focus` pseudo
594
+ // decls (every scheme — interaction is theme-orthogonal).
595
+ // - `style` (default): Tailwind v4 scheme variants
596
+ // (`&:where(.<scheme>, .<scheme> *)`).
597
+ for (const scheme of ctx.schemes) bucket[scheme] = bucket[scheme] ?? {}
598
+ for (const nested of nestedRules) {
599
+ const node = nested as { type?: string }
600
+ if (node?.type === 'media') {
601
+ applyMediaRule(nested, className, bucket, ctx, ruleSchemeTables, ruleLocalVars)
602
+ continue
603
+ }
604
+ if (interactiveState) {
605
+ applyInteractiveNestedRule(nested, bucket, ctx, ruleSchemeTables, ruleLocalVars)
606
+ } else {
607
+ applyNestedSchemeRule(nested, bucket, ctx, ruleSchemeTables, ruleLocalVars)
608
+ }
609
+ }
610
+ normalizeLineHeightToPx(bucket, ctx.schemes)
611
+ if (interactiveState) (bucket as Record<string, unknown>).__state = interactiveState
612
+ ctx.atoms.set(className, bucket)
613
+ }
614
+
615
+ /** Recognised interactive variant prefixes. RN can drive both at runtime. */
616
+ const INTERACTIVE_PREFIXES = new Set(['active', 'focus'])
617
+
618
+ /**
619
+ * Inspect a class name for a leading interactive variant prefix. The
620
+ * Tailwind compiler emits `active:bg-sky-700` literally (backslash-
621
+ * escaped for CSS), and that name is the same string the parser sees.
622
+ * So a cheap `split(':')` on the head is enough — no need to parse the
623
+ * pseudo-class out of the selector itself.
624
+ * @param className Tailwind utility class (e.g. `active:bg-sky-700`).
625
+ * @returns The interactive state prefix (`'active'` / `'focus'`), or null when none.
626
+ */
627
+ function classNameStateOf(className: string): 'active' | 'focus' | null {
628
+ const colon = className.indexOf(':')
629
+ if (colon === -1) return null
630
+ const prefix = className.slice(0, colon)
631
+ if (!INTERACTIVE_PREFIXES.has(prefix)) return null
632
+ return prefix as 'active' | 'focus'
633
+ }
634
+
635
+ /**
636
+ * Pull the `min-width` threshold (px) out of a `@media (width >= Xrem)`
637
+ * lightningcss node. Tailwind v4 emits exactly this shape for every
638
+ * responsive variant — `>= 40rem` for `sm`, `>= 48rem` for `md`, etc.
639
+ * Returns null for any other media condition (`hover`, `prefers-*`,
640
+ * range bounds we don't model) so the caller can skip non-responsive
641
+ * media wrappers without breaking.
642
+ * @param nested A lightningcss `media` rule node.
643
+ * @returns Threshold in px, or null when the condition isn't a simple
644
+ * width-min check.
645
+ */
646
+ function readMediaMinWidthPx(nested: unknown): number | null {
647
+ if (typeof nested !== 'object' || nested === null) return null
648
+ const node = nested as { type?: string; value?: { query?: { mediaQueries?: readonly unknown[] } } }
649
+ if (node.type !== 'media') return null
650
+ const queries = node.value?.query?.mediaQueries
651
+ if (queries?.length !== 1) return null
652
+ const query = queries[0] as {
653
+ condition?: {
654
+ type?: string
655
+ value?: {
656
+ type?: string
657
+ name?: string
658
+ operator?: string
659
+ value?: { type?: string; value?: { type?: string; value?: { unit?: string; value?: number } } }
660
+ }
661
+ }
662
+ }
663
+ const {condition} = query
664
+ if (condition?.type !== 'feature') return null
665
+ const feature = condition.value
666
+ if (feature?.type !== 'range' || feature.name !== 'width') return null
667
+ if (feature.operator !== 'greater-than-equal') return null
668
+ const length = feature.value
669
+ if (length?.type !== 'length') return null
670
+ const inner = length.value?.value
671
+ if (!inner || typeof inner.value !== 'number') return null
672
+ if (inner.unit === 'rem') return inner.value * 16
673
+ if (inner.unit === 'px') return inner.value
674
+ return null
675
+ }
676
+
677
+ /**
678
+ * Pull the leading `prefix:` segment off a className. `md:bg-red-500`
679
+ * → `'md'`. Returns null for atoms without a colon (the common case)
680
+ * or for empty prefixes.
681
+ * @param className Atom name.
682
+ * @returns Prefix or null.
683
+ */
684
+ function leadingPrefix(className: string): string | null {
685
+ const colon = className.indexOf(':')
686
+ if (colon <= 0) return null
687
+ return className.slice(0, colon)
688
+ }
689
+
690
+ /**
691
+ * Fold a Tailwind v4 responsive `@media (width >= Xrem)` nested rule
692
+ * into every scheme's bucket and record the breakpoint threshold
693
+ * (`md` → 768) on the parser-call context. The runtime later gates the
694
+ * atom on `windowWidth` against this threshold via the prefix on the
695
+ * atom's class name.
696
+ *
697
+ * Read directly from the media condition rather than from compiled
698
+ * `--breakpoint-*` `:root` tokens because Tailwind's `theme(inline)`
699
+ * mode strips those — the only authoritative source for the actual
700
+ * thresholds Tailwind generated is the `@media` query itself.
701
+ * @param nested One nested `media` node from `rule.value.rules`.
702
+ * @param className Outer rule's class name (carries the breakpoint prefix).
703
+ * @param bucket Per-scheme style map for the atom.
704
+ * @param ctx Parser-call-wide context.
705
+ * @param ruleSchemeTables Per-scheme var tables (outer rule's merged table).
706
+ * @param ruleLocalVars Outer rule's `--tw-*` vars (inherited for inner decls).
707
+ */
708
+ function applyMediaRule(
709
+ nested: unknown,
710
+ className: string,
711
+ bucket: Record<string, RNStyle>,
712
+ ctx: StyleRuleContext,
713
+ ruleSchemeTables: ReadonlyMap<string, ReadonlyMap<string, string>>,
714
+ ruleLocalVars: ReadonlyMap<string, string>,
715
+ ): void {
716
+ const decls = collectNestedDecls(nested)
717
+ if (decls.length === 0) return
718
+ const minWidth = readMediaMinWidthPx(nested)
719
+ const prefix = leadingPrefix(className)
720
+ if (minWidth !== null && prefix !== null) ctx.breakpoints.set(prefix, minWidth)
721
+ for (const scheme of ctx.schemes) {
722
+ const table = ruleSchemeTables.get(scheme)
723
+ const schemeBucket = bucket[scheme] ?? {}
724
+ for (const decl of decls) {
725
+ for (const [key, value] of declarationToRnEntries(decl, table)) schemeBucket[key] = value
726
+ const animationRef = pickAnimationName(decl)
727
+ if (animationRef) ctx.referencedKeyframes.add(animationRef)
728
+ }
729
+ const nestedLocalVars = new Map(ruleLocalVars)
730
+ for (const [k, v] of collectRuleLocalVars(decls)) nestedLocalVars.set(k, v)
731
+ applyComposedTransformToScheme(schemeBucket, nestedLocalVars)
732
+ applyComposedShadowToScheme(schemeBucket, nestedLocalVars)
733
+ bucket[scheme] = schemeBucket
734
+ }
735
+ }
736
+
737
+ /**
738
+ * Fold one nested rule from an interactive (`active:` / `focus:`) atom
739
+ * into every scheme's bucket. The Tailwind output for `active:bg-sky-700`
740
+ * is `.active\:bg-sky-700 { &:active { background-color: oklch(...) } }`
741
+ * — the outer rule has zero
742
+ * decls; the leaf lives three levels deep through pseudo + media. We
743
+ * unwrap the pseudo and the media shell, take the inner decls, and
744
+ * apply them across every scheme — interactivity is orthogonal to
745
+ * theme. The bucket gets `__state: 'active' | 'focus'` set elsewhere.
746
+ * @param nested One nested-rule node from `rule.value.rules`.
747
+ * @param bucket Per-scheme style map for the atom.
748
+ * @param ctx Parser-call-wide context.
749
+ * @param ruleSchemeTables Per-scheme var tables (outer rule's merged table).
750
+ * @param ruleLocalVars Outer rule's `--tw-*` vars (inherited for inner decls).
751
+ */
752
+ function applyInteractiveNestedRule(
753
+ nested: unknown,
754
+ bucket: Record<string, RNStyle>,
755
+ ctx: StyleRuleContext,
756
+ ruleSchemeTables: ReadonlyMap<string, ReadonlyMap<string, string>>,
757
+ ruleLocalVars: ReadonlyMap<string, string>,
758
+ ): void {
759
+ const decls = collectNestedDecls(nested)
760
+ if (decls.length === 0) return
761
+ for (const scheme of ctx.schemes) {
762
+ const table = ruleSchemeTables.get(scheme)
763
+ const schemeBucket = bucket[scheme] ?? {}
764
+ for (const decl of decls) {
765
+ for (const [key, value] of declarationToRnEntries(decl, table)) schemeBucket[key] = value
766
+ const animationRef = pickAnimationName(decl)
767
+ if (animationRef) ctx.referencedKeyframes.add(animationRef)
768
+ }
769
+ const nestedLocalVars = new Map(ruleLocalVars)
770
+ for (const [k, v] of collectRuleLocalVars(decls)) nestedLocalVars.set(k, v)
771
+ applyComposedTransformToScheme(schemeBucket, nestedLocalVars)
772
+ applyComposedShadowToScheme(schemeBucket, nestedLocalVars)
773
+ bucket[scheme] = schemeBucket
774
+ }
775
+ }
776
+
777
+ /**
778
+ * Recursively flatten a nested style/media rule into its leaf
779
+ * declarations. Tailwind wraps interactive pseudo decls in `style`
780
+ * nodes (`&:active`, `&:focus`); the walk unwraps them and any
781
+ * `@media` shell around them.
782
+ * @param nested A nested rule node.
783
+ * @returns Every declaration found in the nested subtree.
784
+ */
785
+ function collectNestedDecls(nested: unknown): readonly LcDeclaration[] {
786
+ if (typeof nested !== 'object' || nested === null) return []
787
+ const node = nested as { type?: string; value?: NestedStyleRule & { rules?: readonly unknown[] } }
788
+ if (!node.value) return []
789
+ // `style` (with `&:active`) and `media` wrappers both surface the
790
+ // actual decls. lightningcss surfaces inner-only declarations as a
791
+ // `nested-declarations` node — flatten that too.
792
+ if (node.type === 'nested-declarations') return [...(node.value.declarations?.declarations ?? [])]
793
+ if (node.type !== 'style' && node.type !== 'media') return []
794
+ const out: LcDeclaration[] = [...(node.value.declarations?.declarations ?? [])]
795
+ for (const child of node.value.rules ?? []) out.push(...collectNestedDecls(child))
796
+ return out
797
+ }
798
+
799
+ /**
800
+ * Fold one nested style rule (Tailwind's `&:where(.<scheme>, .<scheme> *)`
801
+ * pattern) into the scheme bucket its selector targets. Rules we can't
802
+ * attribute to a single scheme are skipped — they'd only ever reach the
803
+ * bucket via CSS cascading in a browser, which doesn't translate to RN.
804
+ * @param nested One nested-rule node from `rule.value.rules`.
805
+ * @param bucket Per-scheme style map for the atom.
806
+ * @param ctx Parser-call-wide context.
807
+ * @param ruleSchemeTables Per-scheme var tables (outer rule's merged table).
808
+ * @param ruleLocalVars Outer rule's `--tw-*` vars (inherited for inner decls).
809
+ */
810
+ function applyNestedSchemeRule(
811
+ nested: unknown,
812
+ bucket: Record<string, RNStyle>,
813
+ ctx: StyleRuleContext,
814
+ ruleSchemeTables: ReadonlyMap<string, ReadonlyMap<string, string>>,
815
+ ruleLocalVars: ReadonlyMap<string, string>,
816
+ ): void {
817
+ if (typeof nested !== 'object' || nested === null) return
818
+ const node = nested as { type?: string; value?: NestedStyleRule }
819
+ if (node.type !== 'style' || !node.value) return
820
+ const targetScheme = detectNestedScheme(node.value.selectors, ctx.schemes, ctx.schemeAliases)
821
+ if (!targetScheme) return
822
+ const innerDecls = node.value.declarations?.declarations ?? []
823
+ const table = ruleSchemeTables.get(targetScheme)
824
+ const schemeBucket = bucket[targetScheme] ?? {}
825
+ for (const decl of innerDecls) {
826
+ for (const [key, value] of declarationToRnEntries(decl, table)) schemeBucket[key] = value
827
+ const animationRef = pickAnimationName(decl)
828
+ if (animationRef) ctx.referencedKeyframes.add(animationRef)
829
+ }
830
+ // Apply the composed-transform + shadow post-passes to just this one
831
+ // scheme, so nested `--tw-*` custom prop writes compose correctly.
832
+ const nestedLocalVars = new Map(ruleLocalVars)
833
+ for (const [k, v] of collectRuleLocalVars(innerDecls)) nestedLocalVars.set(k, v)
834
+ applyComposedTransformToScheme(schemeBucket, nestedLocalVars)
835
+ applyComposedShadowToScheme(schemeBucket, nestedLocalVars)
836
+ bucket[targetScheme] = schemeBucket
837
+ }
838
+
839
+ /**
840
+ * Shape of a lightningcss style rule as it appears inside `rule.value.rules`.
841
+ * Loose typing because the full type is a sprawling discriminated union
842
+ * we only need one shape from.
843
+ */
844
+ interface NestedStyleRule {
845
+ selectors: readonly (readonly unknown[])[]
846
+ declarations?: { declarations: readonly LcDeclaration[] }
847
+ }
848
+
849
+ /**
850
+ * Detect which scheme a nested `&:where(.<scheme>, .<scheme> *)`
851
+ * selector targets. Recognises both the rnwind-default literal class
852
+ * (`.dark`) and any user-declared `@custom-variant` selector class
853
+ * (`.scheme-dark`), via the `aliases` map built from the theme CSS.
854
+ * @param selectors Nested rule's selector lists.
855
+ * @param schemes Declared scheme names.
856
+ * @param aliases Class-name → scheme-name map from `@custom-variant` decls.
857
+ * @returns Matching scheme name, or null when the selector isn't scheme-scoped.
858
+ */
859
+ function detectNestedScheme(
860
+ selectors: readonly (readonly unknown[])[],
861
+ schemes: readonly string[],
862
+ aliases: ReadonlyMap<string, string>,
863
+ ): string | null {
864
+ const known = new Set(schemes)
865
+ for (const selector of selectors) {
866
+ const found = findSchemeInSelector(selector, schemes, known, aliases)
867
+ if (found) return found
868
+ }
869
+ return null
870
+ }
871
+
872
+ /**
873
+ * Inner half of {@link detectNestedScheme} — extracted so each function
874
+ * stays under the cognitive-complexity cap.
875
+ * @param selector One compound selector (sequence of simple parts).
876
+ * @param schemes Declared scheme names (for recursion).
877
+ * @param known Set form of `schemes` for O(1) lookups.
878
+ * @param aliases Class-name → scheme-name map from `@custom-variant` decls.
879
+ * @returns Matching scheme name, or null.
880
+ */
881
+ function findSchemeInSelector(
882
+ selector: readonly unknown[],
883
+ schemes: readonly string[],
884
+ known: ReadonlySet<string>,
885
+ aliases: ReadonlyMap<string, string>,
886
+ ): string | null {
887
+ for (const part of selector) {
888
+ const direct = matchSchemeClass(part, known, aliases)
889
+ if (direct) return direct
890
+ const nested = matchSchemeInWhere(part, schemes, aliases)
891
+ if (nested) return nested
892
+ }
893
+ return null
894
+ }
895
+
896
+ /**
897
+ * Match a `.scheme` class part against the declared schemes (literal
898
+ * match) or against the `@custom-variant` alias map (e.g. `.scheme-dark`
899
+ * → `dark`).
900
+ * @param part One simple selector part.
901
+ * @param known Declared scheme names.
902
+ * @param aliases Class-name → scheme-name map from `@custom-variant` decls.
903
+ * @returns Matching scheme name, or null.
904
+ */
905
+ function matchSchemeClass(part: unknown, known: ReadonlySet<string>, aliases: ReadonlyMap<string, string>): string | null {
906
+ if (typeof part !== 'object' || part === null) return null
907
+ const node = part as { type?: string; name?: string }
908
+ if (node.type !== 'class') return null
909
+ if (typeof node.name !== 'string') return null
910
+ if (known.has(node.name)) return node.name
911
+ return aliases.get(node.name) ?? null
912
+ }
913
+
914
+ /**
915
+ * Match a `:where(.scheme, …)` pseudo-class wrapper and recurse into
916
+ * its inner selectors.
917
+ * @param part One simple selector part.
918
+ * @param schemes Declared scheme names.
919
+ * @param aliases Class-name → scheme-name map from `@custom-variant` decls.
920
+ * @returns Matching scheme name from inside the `where`, or null.
921
+ */
922
+ function matchSchemeInWhere(part: unknown, schemes: readonly string[], aliases: ReadonlyMap<string, string>): string | null {
923
+ if (typeof part !== 'object' || part === null) return null
924
+ const node = part as { type?: string; kind?: string; selectors?: readonly (readonly unknown[])[] }
925
+ if (node.type !== 'pseudo-class' || node.kind !== 'where' || !node.selectors) return null
926
+ return detectNestedScheme(node.selectors, schemes, aliases)
927
+ }
928
+
929
+ /**
930
+ * Per-scheme version of `applyComposedTransform` — synthesize a
931
+ * `transform` array into a SINGLE scheme's style from its rule-local
932
+ * `--tw-*` vars.
933
+ * @param style Scheme-specific style map.
934
+ * @param ruleLocalVars Combined outer+nested `--tw-*` vars.
935
+ */
936
+ function applyComposedTransformToScheme(style: RNStyle, ruleLocalVars: ReadonlyMap<string, string>): void {
937
+ const composed = composeTransformFromVars(ruleLocalVars)
938
+ if (composed.length === 0) return
939
+ delete style.translate
940
+ delete style.scale
941
+ delete style.rotate
942
+ style.transform = composed
943
+ }
944
+
945
+ /**
946
+ * Per-scheme version of `applyComposedShadow` — synthesize RN shadow
947
+ * longhands into a SINGLE scheme's style from its `--tw-shadow` custom
948
+ * prop.
949
+ * @param style Scheme-specific style map.
950
+ * @param ruleLocalVars Combined outer+nested `--tw-*` vars.
951
+ */
952
+ function applyComposedShadowToScheme(style: RNStyle, ruleLocalVars: ReadonlyMap<string, string>): void {
953
+ const rawShadow = ruleLocalVars.get('--tw-shadow')
954
+ const rawShadowColor = ruleLocalVars.get('--tw-shadow-color')
955
+ if (!rawShadow && rawShadowColor) {
956
+ const color = resolveCustomColorString(rawShadowColor)
957
+ if (!color) return
958
+ delete style.boxShadow
959
+ style.shadowColor = color
960
+ return
961
+ }
962
+ if (!rawShadow) return
963
+ const shadow = parseFirstShadow(rawShadow)
964
+ if (!shadow) return
965
+ delete style.boxShadow
966
+ style.shadowColor = shadow.color
967
+ style.shadowOffset = { width: shadow.x, height: shadow.y }
968
+ style.shadowOpacity = shadow.opacity
969
+ style.shadowRadius = shadow.blur
970
+ style.elevation = Math.max(1, Math.min(24, Math.round(Math.max(shadow.y, shadow.blur / 2))))
971
+ }
972
+
973
+ /**
974
+ * Tailwind v4's `shadow-*` utilities write a `--tw-shadow` custom prop
975
+ * holding the actual `<x> <y> <blur> <spread> <color>` shadow values,
976
+ * then a `box-shadow: var(--tw-shadow)` declaration RN can't use. This
977
+ * post-pass parses `--tw-shadow` and emits the RN shadow longhands —
978
+ * `shadowColor` / `shadowOffset` / `shadowOpacity` / `shadowRadius` /
979
+ * `elevation` — so iOS and Android both render the shadow.
980
+ * @param bucket Per-scheme style map for the atom.
981
+ * @param schemes Scheme names active for this parse.
982
+ * @param ruleLocalVars Rule-local `--tw-*` vars.
983
+ */
984
+ function applyComposedShadow(
985
+ bucket: Record<string, RNStyle>,
986
+ schemes: readonly string[],
987
+ ruleLocalVars: ReadonlyMap<string, string>,
988
+ ): void {
989
+ const rawShadow = ruleLocalVars.get('--tw-shadow')
990
+ const rawShadowColor = ruleLocalVars.get('--tw-shadow-color')
991
+ // Color-only utility (`shadow-red-50`, `shadow-gray-200`, …): emit
992
+ // `shadowColor` + `shadowOpacity: 1` so the explicit color overrides
993
+ // the size utility's 0.1 alpha fallback (matches Tailwind v4 web,
994
+ // where setting `--tw-shadow-color` swaps in a solid color). Offset /
995
+ // blur / elevation come from the partner size utility's atom.
996
+ if (!rawShadow && rawShadowColor) {
997
+ const color = resolveCustomColorString(rawShadowColor)
998
+ if (!color) return
999
+ for (const scheme of schemes) {
1000
+ const style = bucket[scheme] ?? {}
1001
+ delete style.boxShadow
1002
+ style.shadowColor = color
1003
+ bucket[scheme] = style
1004
+ }
1005
+ return
1006
+ }
1007
+ if (!rawShadow) return
1008
+ const shadow = parseFirstShadow(rawShadow)
1009
+ if (!shadow) return
1010
+ for (const scheme of schemes) {
1011
+ const style = bucket[scheme] ?? {}
1012
+ delete style.boxShadow
1013
+ style.shadowColor = shadow.color
1014
+ style.shadowOffset = { width: shadow.x, height: shadow.y }
1015
+ style.shadowOpacity = shadow.opacity
1016
+ style.shadowRadius = shadow.blur
1017
+ style.elevation = Math.max(1, Math.min(24, Math.round(Math.max(shadow.y, shadow.blur / 2))))
1018
+ bucket[scheme] = style
1019
+ }
1020
+ }
1021
+
1022
+ /**
1023
+ * Synthesize RN ring styles from Tailwind's `--tw-ring-color` /
1024
+ * `--tw-ring-shadow` composable custom props. RN has no native ring;
1025
+ * we approximate by writing `borderColor` + `borderWidth` so the
1026
+ * outline is visible. Atoms that ALREADY set borderColor (e.g. paired
1027
+ * with `border-2`) keep their value — the ring just won't override.
1028
+ * @param bucket Per-scheme style map for the atom.
1029
+ * @param schemes Scheme names active for this parse.
1030
+ * @param ruleLocalVars Rule-local `--tw-*` vars.
1031
+ */
1032
+ function applyComposedRing(
1033
+ bucket: Record<string, RNStyle>,
1034
+ schemes: readonly string[],
1035
+ ruleLocalVars: ReadonlyMap<string, string>,
1036
+ ): void {
1037
+ const ringColor = ruleLocalVars.get('--tw-ring-color')
1038
+ if (!ringColor) return
1039
+ const color = resolveCustomColorString(ringColor)
1040
+ if (!color) return
1041
+ for (const scheme of schemes) {
1042
+ const style = bucket[scheme] ?? {}
1043
+ if (!('borderColor' in style)) style.borderColor = color
1044
+ bucket[scheme] = style
1045
+ }
1046
+ }
1047
+
1048
+ /**
1049
+ * Resolve a CSS color string (`oklch(0.971 0.013 17.38)`, `#ff0000`,
1050
+ * `rgb(0 0 0 / 0.1)`) to the hex string RN's `shadowColor` accepts.
1051
+ * Wraps culori's parser via {@link parseCssColorToHex}.
1052
+ * @param raw Raw color text from a `--tw-shadow-color` custom prop.
1053
+ * @returns `#rrggbb` string, or null when culori can't parse it.
1054
+ */
1055
+ function resolveCustomColorString(raw: string): string | null {
1056
+ const text = unwrapVariableFallback(raw).trim()
1057
+ if (text.length === 0) return null
1058
+ if (text.startsWith('#')) return text
1059
+ return parseCssColorToHex(text)
1060
+ }
1061
+
1062
+ /**
1063
+ * Parse any CSS color expression into an `#rrggbb` string via culori.
1064
+ * Falls back to null when culori doesn't recognize the format.
1065
+ * @param text CSS color value.
1066
+ * @returns Hex string, or null.
1067
+ */
1068
+ function parseCssColorToHex(text: string): string | null {
1069
+ return formatHexSafe(text)
1070
+ }
1071
+
1072
+ /**
1073
+ * Format a CSS color via culori.
1074
+ * @param text CSS color value.
1075
+ * @returns `#rrggbb` string when culori succeeds, else null.
1076
+ */
1077
+ function formatHexSafe(text: string): string | null {
1078
+ try {
1079
+ const hex = culoriFormatHex(text)
1080
+ return typeof hex === 'string' ? hex : null
1081
+ } catch {
1082
+ return null
1083
+ }
1084
+ }
1085
+
1086
+ interface ParsedShadow {
1087
+ x: number
1088
+ y: number
1089
+ blur: number
1090
+ spread: number
1091
+ color: string
1092
+ opacity: number
1093
+ }
1094
+
1095
+ /**
1096
+ * Parse the first shadow from a `--tw-shadow` custom-property value.
1097
+ * The value is a comma-separated list of shadows; each shadow is
1098
+ * `<x> <y> <blur> <spread> <color>`. RN renders only one shadow per
1099
+ * view, so we keep the first.
1100
+ * @param raw Raw `--tw-shadow` text (post-substitution).
1101
+ * @returns Parsed shadow, or null when the shape is unrecognized.
1102
+ */
1103
+ function parseFirstShadow(raw: string): ParsedShadow | null {
1104
+ // Split on top-level commas (parens-aware) so colors like `rgba(0,0,0,0.5)`
1105
+ // don't fragment the list.
1106
+ const head = topLevelSplit(raw, ',')[0]?.trim()
1107
+ if (!head) return null
1108
+ const { lengths, remainder } = extractShadowLengths(head)
1109
+ const [x = 0, y = 0, blur = 0, spread = 0] = lengths
1110
+ const { color, opacity } = parseShadowColor(remainder.trim())
1111
+ return { x, y, blur, spread, color, opacity }
1112
+ }
1113
+
1114
+ /**
1115
+ * Pull the first 4 numeric tokens out of a shadow expression and return
1116
+ * them alongside the remaining text (which is the color expression).
1117
+ * Shadow shape: `<x> <y> <blur> <spread> <color>` — tokens may be bare
1118
+ * (`0`), px-dimensioned (`1px`), or rem/em/%.
1119
+ * @param head Single shadow expression (one comma-separated entry).
1120
+ * @returns Pixel lengths + the remainder text (color expression).
1121
+ */
1122
+ function extractShadowLengths(head: string): { lengths: number[]; remainder: string } {
1123
+ // Take ONLY the leading run of length tokens, stopping at the first
1124
+ // non-length token (the color). A previous global digit-regex scanned
1125
+ // the whole string, so a <4-length shadow like `0 1px 1px rgb(0 0 0 /
1126
+ // 0.05)` stole a digit out of the color expression — corrupting the
1127
+ // alpha (opacity) or a digit-leading hex. Whitespace-splitting can't
1128
+ // reach inside the color because we break as soon as a token isn't a
1129
+ // bare/`px`/`rem`/`em`/`%` length.
1130
+ // Unambiguous integer-or-decimal (no `\d*\.?\d+` overlap) so there's no
1131
+ // super-linear backtracking on long digit runs.
1132
+ const isLength = /^-?(?:\d+(?:\.\d+)?|\.\d+)(?:px|rem|em|%)?$/
1133
+ const parts = head.split(/\s+/)
1134
+ const lengths: number[] = []
1135
+ let index = 0
1136
+ while (index < parts.length && lengths.length < 4 && isLength.test(parts[index]!)) {
1137
+ lengths.push(parseLengthToken(parts[index]!))
1138
+ index += 1
1139
+ }
1140
+ return { lengths, remainder: parts.slice(index).join(' ') }
1141
+ }
1142
+
1143
+ /**
1144
+ * Coerce one shadow length token into a pixel number. Accepts bare
1145
+ * integers (`0`), `Npx`, and `Nrem` — every shape Tailwind's
1146
+ * `--tw-shadow` value uses.
1147
+ * @param token Token text.
1148
+ * @returns Pixel number.
1149
+ */
1150
+ function parseLengthToken(token: string): number {
1151
+ if (token.endsWith('rem')) return Number(token.slice(0, -3)) * 16
1152
+ if (token.endsWith('em')) return Number(token.slice(0, -2)) * 16
1153
+ if (token.endsWith('px')) return Number(token.slice(0, -2))
1154
+ if (token.endsWith('%')) return Number(token.slice(0, -1))
1155
+ return Number(token)
1156
+ }
1157
+
1158
+ /**
1159
+ * Extract a color string + extracted alpha from a shadow's color
1160
+ * expression. Supports `rgb(...)` / `rgba(...)` / `#rrggbb` / hex with
1161
+ * alpha / theme-resolved CSS color strings.
1162
+ * @param expr Color expression text.
1163
+ * @returns Color string for `shadowColor` + alpha for `shadowOpacity`.
1164
+ */
1165
+ function parseShadowColor(expr: string): { color: string; opacity: number } {
1166
+ const working = unwrapVariableFallback(expr).trim()
1167
+ if (working.length === 0) return { color: '#000', opacity: 0.1 }
1168
+ const rgba = parseRgbaExpression(working)
1169
+ if (rgba) return rgba
1170
+ if (working.startsWith('#')) return { color: working, opacity: 1 }
1171
+ return { color: '#000', opacity: 0.1 }
1172
+ }
1173
+
1174
+ /**
1175
+ * Strip the `var(--name, fallback)` wrapper from a CSS value. Tailwind
1176
+ * wraps shadow colors as `var(--tw-shadow-color, rgb(0 0 0 / 0.1))`,
1177
+ * and when the var is unresolved we want the fallback.
1178
+ * @param expr Raw CSS value.
1179
+ * @returns Inner fallback when wrapped, otherwise the input unchanged.
1180
+ */
1181
+ function unwrapVariableFallback(expr: string): string {
1182
+ const trimmed = expr.trim()
1183
+ if (!trimmed.startsWith('var(') || !trimmed.endsWith(')')) return trimmed
1184
+ const inner = trimmed.slice(4, -1)
1185
+ let depth = 0
1186
+ for (let index = 0; index < inner.length; index += 1) {
1187
+ const ch = inner[index]
1188
+ if (ch === '(') depth += 1
1189
+ else if (ch === ')') depth -= 1
1190
+ else if (ch === ',' && depth === 0) return inner.slice(index + 1)
1191
+ }
1192
+ return trimmed
1193
+ }
1194
+
1195
+ /**
1196
+ * Parse an `rgb(r g b)` / `rgba(r,g,b,a)` / `rgb(r g b / a)` color
1197
+ * expression into a hex + alpha pair. Returns `null` when the shape
1198
+ * doesn't match.
1199
+ * @param text Expression text (already trimmed and unwrapped).
1200
+ * @returns Hex color + alpha, or null.
1201
+ */
1202
+ function parseRgbaExpression(text: string): { color: string; opacity: number } | null {
1203
+ const head = /^rgba?\(([^)]+)\)$/i.exec(text)
1204
+ if (!head) return null
1205
+ const inner = head[1]!.replaceAll(',', ' ').replaceAll('/', ' ')
1206
+ const tokens = inner.split(/\s+/).filter((part) => part.length > 0)
1207
+ if (tokens.length < 3) return null
1208
+ const [r, g, b, alphaText] = tokens
1209
+ let opacity = 1
1210
+ if (typeof alphaText === 'string') {
1211
+ opacity = alphaText.endsWith('%') ? Number(alphaText.slice(0, -1)) / 100 : Number(alphaText)
1212
+ }
1213
+ const hex = `#${[r!, g!, b!]
1214
+ .map((n) =>
1215
+ Math.max(0, Math.min(255, Math.round(Number(n))))
1216
+ .toString(16)
1217
+ .padStart(2, '0'),
1218
+ )
1219
+ .join('')}`
1220
+ return { color: hex, opacity }
1221
+ }
1222
+
1223
+ /**
1224
+ * Split `text` at top-level occurrences of `delimiter`, treating
1225
+ * parentheses as nesting. Used to safely split shadow lists without
1226
+ * fragmenting `rgb(0, 0, 0, 0.5)` on its commas.
1227
+ * @param text Source text.
1228
+ * @param delimiter Single-character delimiter to split on.
1229
+ * @returns Parts of the text between top-level delimiters.
1230
+ */
1231
+ function topLevelSplit(text: string, delimiter: string): string[] {
1232
+ const parts: string[] = []
1233
+ let depth = 0
1234
+ let start = 0
1235
+ for (let index = 0; index < text.length; index += 1) {
1236
+ const ch = text[index]
1237
+ if (ch === '(') depth += 1
1238
+ else if (ch === ')') depth -= 1
1239
+ else if (ch === delimiter && depth === 0) {
1240
+ parts.push(text.slice(start, index))
1241
+ start = index + 1
1242
+ }
1243
+ }
1244
+ parts.push(text.slice(start))
1245
+ return parts
1246
+ }
1247
+
1248
+ /**
1249
+ * Tailwind v4's `text-*` utilities emit `line-height` as a unitless
1250
+ * multiplier (`calc(2.5 / 2.25)` for `text-4xl`) that the browser
1251
+ * resolves against the element's `font-size`. RN's `lineHeight` is
1252
+ * always pixels — so when both `fontSize` and a multiplier-shaped
1253
+ * `lineHeight` (less than 10) land on the same atom, multiply through
1254
+ * to a pixel value. Atoms with only one of the two are left alone.
1255
+ * @param bucket Per-scheme style map for the atom.
1256
+ * @param schemes Scheme names active for this parse.
1257
+ */
1258
+ function normalizeLineHeightToPx(bucket: Record<string, RNStyle>, schemes: readonly string[]): void {
1259
+ for (const scheme of schemes) {
1260
+ const style = bucket[scheme]
1261
+ if (!style) continue
1262
+ const { fontSize } = style
1263
+ const { lineHeight } = style
1264
+ if (typeof fontSize !== 'number' || typeof lineHeight !== 'number') continue
1265
+ if (lineHeight >= 10) continue
1266
+ style.lineHeight = Math.round(fontSize * lineHeight * 10_000) / 10_000
1267
+ }
1268
+ }
1269
+
1270
+ /**
1271
+ * Fold one declaration's resolved entries into every scheme's bucket on
1272
+ * the target atom.
1273
+ * @param decl Lightningcss declaration to convert.
1274
+ * @param bucket Per-scheme style map for the atom.
1275
+ * @param schemes Scheme names active for this parse.
1276
+ * @param ruleSchemeTables Per-scheme var tables (with rule-local overrides folded in).
1277
+ */
1278
+ function applyDeclarationToBucket(
1279
+ decl: LcDeclaration,
1280
+ bucket: Record<string, RNStyle>,
1281
+ schemes: readonly string[],
1282
+ ruleSchemeTables: ReadonlyMap<string, ReadonlyMap<string, string>>,
1283
+ ): void {
1284
+ for (const scheme of schemes) {
1285
+ const schemeBucket = bucket[scheme] ?? {}
1286
+ for (const [key, value] of declarationToRnEntries(decl, ruleSchemeTables.get(scheme))) {
1287
+ schemeBucket[key] = value
1288
+ }
1289
+ bucket[scheme] = schemeBucket
1290
+ }
1291
+ }
1292
+
1293
+ /**
1294
+ * Apply the composed-transform post-pass for a single atom: if any of
1295
+ * Tailwind's `--tw-translate-*` / `--tw-scale-*` / `--tw-skew-*` vars
1296
+ * were written, synthesize a single `transform` array and drop the
1297
+ * intermediate `translate`/`scale`/`rotate` shorthand entries.
1298
+ * @param bucket Per-scheme style map for the atom.
1299
+ * @param schemes Scheme names active for this parse.
1300
+ * @param ruleLocalVars Rule-local `--tw-*` vars.
1301
+ */
1302
+ function applyComposedTransform(
1303
+ bucket: Record<string, RNStyle>,
1304
+ schemes: readonly string[],
1305
+ ruleLocalVars: ReadonlyMap<string, string>,
1306
+ ): void {
1307
+ const composed = composeTransformFromVars(ruleLocalVars)
1308
+ if (composed.length === 0) return
1309
+ for (const scheme of schemes) {
1310
+ const schemeBucket = bucket[scheme] ?? {}
1311
+ delete schemeBucket.translate
1312
+ delete schemeBucket.scale
1313
+ delete schemeBucket.rotate
1314
+ schemeBucket.transform = composed
1315
+ bucket[scheme] = schemeBucket
1316
+ }
1317
+ }
1318
+
1319
+ /**
1320
+ * Synthesize an RN `transform` array from Tailwind v4's composable
1321
+ * `--tw-translate-x/y`, `--tw-scale-x/y`, `--tw-skew-x/y`, and
1322
+ * `--tw-rotate-x/y/z` custom properties. Returns an empty array when
1323
+ * none of those props were written, letting the caller skip the
1324
+ * post-pass.
1325
+ * @param ruleVars Rule-local `--tw-*` vars collected from the style rule.
1326
+ * @returns RN transform operations (possibly empty).
1327
+ */
1328
+ function composeTransformFromVars(ruleVars: ReadonlyMap<string, string>): readonly Record<string, string | number>[] {
1329
+ const ops: Record<string, string | number>[] = []
1330
+ addAxisOp(ops, 'translateX', ruleVars.get('--tw-translate-x'), resolveLengthExpression)
1331
+ addAxisOp(ops, 'translateY', ruleVars.get('--tw-translate-y'), resolveLengthExpression)
1332
+ addAxisOp(ops, 'scaleX', ruleVars.get('--tw-scale-x'), resolveNumberOrPercent)
1333
+ addAxisOp(ops, 'scaleY', ruleVars.get('--tw-scale-y'), resolveNumberOrPercent)
1334
+ addAxisOp(ops, 'skewX', ruleVars.get('--tw-skew-x'), extractAngleFromSkewFunction)
1335
+ addAxisOp(ops, 'skewY', ruleVars.get('--tw-skew-y'), extractAngleFromSkewFunction)
1336
+ return ops
1337
+ }
1338
+
1339
+ /**
1340
+ * Push `{<key>: resolved(raw)}` into `ops` when `raw` is present and the
1341
+ * resolver returns non-null. Keeps {@link composeTransformFromVars}
1342
+ * below the cognitive complexity threshold.
1343
+ * @param ops Target array to mutate.
1344
+ * @param key RN transform op key (e.g. `'translateX'`).
1345
+ * @param raw Rule-local var value (possibly undefined).
1346
+ * @param resolve Value-resolver for this axis type.
1347
+ */
1348
+ function addAxisOp<T extends string | number>(
1349
+ ops: Record<string, string | number>[],
1350
+ key: string,
1351
+ raw: string | undefined,
1352
+ resolve: (text: string) => T | null,
1353
+ ): void {
1354
+ if (!raw) return
1355
+ const value = resolve(raw)
1356
+ if (value !== null) ops.push({ [key]: value })
1357
+ }
1358
+
1359
+ /**
1360
+ * Resolve a CSS length expression into the value a RN transform op
1361
+ * accepts — pixels as a number, or a percentage string preserved
1362
+ * verbatim. Supports the shapes Tailwind v4 emits into `--tw-translate-*`:
1363
+ *
1364
+ * - Direct lengths: `16px`, `1rem`, bare `42`.
1365
+ * - Percentages: `100%`, `-100%`.
1366
+ * - Flat calc: `calc(0.25rem * 52)`.
1367
+ * - Fractional calc: `calc(1 / 2 * 100%)` (→ `translate-x-1/2`).
1368
+ * - Nested calc with sign flip: `calc(calc(1 / 3 * 100%) * -1)` (→ `-translate-x-1/3`).
1369
+ *
1370
+ * Returns null when the expression mixes units (`calc(100% - 10px)` —
1371
+ * RN can't express those) or contains a token the evaluator can't
1372
+ * interpret; the transform op is simply skipped in that case.
1373
+ * @param text Length expression text.
1374
+ * @returns Pixel number, percentage string, or null when unrepresentable.
1375
+ */
1376
+ function resolveLengthExpression(text: string): number | string | null {
1377
+ const trimmed = text.trim()
1378
+ if (trimmed.length === 0) return null
1379
+ const evaluated = evaluateLengthExpr(trimmed)
1380
+ if (!evaluated) return null
1381
+ if (evaluated.unit === '%') return `${stripTrailingZeros(evaluated.value)}%`
1382
+ if (evaluated.unit === 'rem') return evaluated.value * 16
1383
+ return evaluated.value
1384
+ }
1385
+
1386
+ /** Evaluated length + its unit. `''` means px or bare number. */
1387
+ interface EvaluatedLength {
1388
+ value: number
1389
+ unit: '%' | 'rem' | ''
1390
+ }
1391
+
1392
+ /**
1393
+ * Evaluate a CSS length expression to a `{value, unit}` pair.
1394
+ *
1395
+ * Strategy: detect the (at most one) unit suffix present in the text,
1396
+ * strip every `calc(` to `(`, strip the unit suffix from numeric tokens,
1397
+ * and run a small arithmetic evaluator. Mixed-unit expressions are
1398
+ * rejected because RN has no way to express `calc(100% - 10px)` in a
1399
+ * flat transform op.
1400
+ * @param text Raw CSS length expression (already trimmed).
1401
+ * @returns Evaluated length with its unit, or `null` when invalid.
1402
+ */
1403
+ function evaluateLengthExpr(text: string): EvaluatedLength | null {
1404
+ const units = detectUnits(text)
1405
+ if (units.length > 1) return null
1406
+ const unit = (units[0] ?? '') as EvaluatedLength['unit']
1407
+ const arithmetic = stripCalcAndUnits(text)
1408
+ const value = evaluateArithmetic(arithmetic)
1409
+ if (value === null || !Number.isFinite(value)) return null
1410
+ return { value, unit }
1411
+ }
1412
+
1413
+ /**
1414
+ * Detect which length units appear in the expression. Multi-unit
1415
+ * expressions (e.g. `calc(100% - 1rem)`) aren't representable in one RN
1416
+ * transform op, so we reject them.
1417
+ * @param text Length expression.
1418
+ * @returns Sorted, deduped unit list found in the text.
1419
+ */
1420
+ function detectUnits(text: string): readonly string[] {
1421
+ const found = new Set<string>()
1422
+ if (/\d%/.test(text)) found.add('%')
1423
+ if (/[\d.]rem\b/.test(text)) found.add('rem')
1424
+ if (/[\d.]px\b/.test(text)) found.add('px')
1425
+ return [...found]
1426
+ }
1427
+
1428
+ /**
1429
+ * Strip every `calc(` wrapper to a plain `(`, and strip `%` / `rem` /
1430
+ * `px` unit suffixes from numeric tokens. Result is a plain arithmetic
1431
+ * expression the evaluator can consume.
1432
+ * @param text Length expression.
1433
+ * @returns Arithmetic text suitable for {@link evaluateArithmetic}.
1434
+ */
1435
+ function stripCalcAndUnits(text: string): string {
1436
+ // Input is Tailwind's compiled CSS, not user-controlled — no ReDoS risk.
1437
+ // eslint-disable-next-line sonarjs/slow-regex
1438
+ return text.replaceAll(/\bcalc\s*\(/g, '(').replaceAll(/([\d.]+)(?:rem|px|%)/g, '$1')
1439
+ }
1440
+
1441
+ /**
1442
+ * Format a percentage number so `50` stays `"50%"` (not `"50.00000001%"`)
1443
+ * when float drift is in the low bits. Strips trailing-zero decimals.
1444
+ * @param value Percentage magnitude.
1445
+ * @returns Integer-ish string.
1446
+ */
1447
+ function stripTrailingZeros(value: number): string {
1448
+ const rounded = Math.round(value * 1_000_000) / 1_000_000
1449
+ return String(rounded)
1450
+ }
1451
+
1452
+ /**
1453
+ * Tiny recursive-descent evaluator for CSS arithmetic. Accepts `+`, `-`,
1454
+ * `*`, `/`, parens, and decimal numbers. Returns `null` on malformed
1455
+ * input — rejects anything the tokenizer can't classify.
1456
+ * @param text Arithmetic text (post {@link stripCalcAndUnits}).
1457
+ * @returns Evaluated number, or `null`.
1458
+ */
1459
+ function evaluateArithmetic(text: string): number | null {
1460
+ const tokens = tokenizeArithmetic(text)
1461
+ if (!tokens) return null
1462
+ const cursor = { index: 0 }
1463
+ const result = parseArithmeticExpr(tokens, cursor)
1464
+ if (cursor.index !== tokens.length) return null
1465
+ return result
1466
+ }
1467
+
1468
+ /**
1469
+ * Split arithmetic text into numeric and operator tokens. Returns null
1470
+ * when the text contains any character outside the allowed set.
1471
+ * @param text Arithmetic text.
1472
+ * @returns Token list, or null on unexpected character.
1473
+ */
1474
+ function tokenizeArithmetic(text: string): readonly string[] | null {
1475
+ const tokens: string[] = []
1476
+ let index = 0
1477
+ while (index < text.length) {
1478
+ const ch = text[index]!
1479
+ if (isArithmeticWhitespace(ch)) {
1480
+ index += 1
1481
+ } else if (isArithmeticOperator(ch)) {
1482
+ tokens.push(ch)
1483
+ index += 1
1484
+ } else if (isDigitOrDot(ch)) {
1485
+ const next = consumeNumber(text, index)
1486
+ tokens.push(text.slice(index, next))
1487
+ index = next
1488
+ } else {
1489
+ return null
1490
+ }
1491
+ }
1492
+ return tokens
1493
+ }
1494
+
1495
+ /**
1496
+ * Check whether `ch` is a whitespace character the arithmetic tokenizer
1497
+ * may skip.
1498
+ * @param ch Single-character string.
1499
+ * @returns True for space / tab / newline.
1500
+ */
1501
+ function isArithmeticWhitespace(ch: string): boolean {
1502
+ return ch === ' ' || ch === '\t' || ch === '\n'
1503
+ }
1504
+
1505
+ /**
1506
+ * Check whether `ch` is one of the arithmetic operator tokens.
1507
+ * @param ch Single-character string.
1508
+ * @returns True for `(`, `)`, `+`, `-`, `*`, `/`.
1509
+ */
1510
+ function isArithmeticOperator(ch: string): boolean {
1511
+ return ch === '(' || ch === ')' || ch === '+' || ch === '-' || ch === '*' || ch === '/'
1512
+ }
1513
+
1514
+ /**
1515
+ * Check whether `ch` belongs to a numeric token.
1516
+ * @param ch Single-character string.
1517
+ * @returns True for a digit `0`–`9` or `.`.
1518
+ */
1519
+ function isDigitOrDot(ch: string): boolean {
1520
+ return (ch >= '0' && ch <= '9') || ch === '.'
1521
+ }
1522
+
1523
+ /**
1524
+ * Advance past a numeric token starting at `start`.
1525
+ * @param text Source text.
1526
+ * @param start Index of the first digit or dot.
1527
+ * @returns Index just past the last digit-or-dot.
1528
+ */
1529
+ function consumeNumber(text: string, start: number): number {
1530
+ let index = start
1531
+ while (index < text.length && isDigitOrDot(text[index]!)) index += 1
1532
+ return index
1533
+ }
1534
+
1535
+ /**
1536
+ * Parse an additive expression: `term (('+'|'-') term)*`.
1537
+ * @param tokens Token list.
1538
+ * @param cursor Mutable cursor.
1539
+ * @param cursor.index Current token index; advanced past consumed tokens.
1540
+ * @returns Evaluated number, or `null` on parse failure.
1541
+ */
1542
+ function parseArithmeticExpr(tokens: readonly string[], cursor: { index: number }): number | null {
1543
+ let left = parseArithmeticTerm(tokens, cursor)
1544
+ if (left === null) return null
1545
+ while (cursor.index < tokens.length) {
1546
+ const op = tokens[cursor.index]
1547
+ if (op !== '+' && op !== '-') break
1548
+ cursor.index += 1
1549
+ const right = parseArithmeticTerm(tokens, cursor)
1550
+ if (right === null) return null
1551
+ left = op === '+' ? left + right : left - right
1552
+ }
1553
+ return left
1554
+ }
1555
+
1556
+ /**
1557
+ * Parse a multiplicative expression: `factor (('*'|'/') factor)*`.
1558
+ * @param tokens Token list.
1559
+ * @param cursor Mutable cursor.
1560
+ * @param cursor.index Current token index; advanced past consumed tokens.
1561
+ * @returns Evaluated number, or `null`.
1562
+ */
1563
+ function parseArithmeticTerm(tokens: readonly string[], cursor: { index: number }): number | null {
1564
+ let left = parseArithmeticFactor(tokens, cursor)
1565
+ if (left === null) return null
1566
+ while (cursor.index < tokens.length) {
1567
+ const op = tokens[cursor.index]
1568
+ if (op !== '*' && op !== '/') break
1569
+ cursor.index += 1
1570
+ const right = parseArithmeticFactor(tokens, cursor)
1571
+ if (right === null) return null
1572
+ left = op === '*' ? left * right : left / right
1573
+ }
1574
+ return left
1575
+ }
1576
+
1577
+ /**
1578
+ * Parse a factor: unary minus, parenthesised expression, or number.
1579
+ * @param tokens Token list.
1580
+ * @param cursor Mutable cursor.
1581
+ * @param cursor.index Current token index; advanced past consumed tokens.
1582
+ * @returns Evaluated number, or `null`.
1583
+ */
1584
+ function parseArithmeticFactor(tokens: readonly string[], cursor: { index: number }): number | null {
1585
+ if (cursor.index >= tokens.length) return null
1586
+ const tok = tokens[cursor.index]!
1587
+ if (tok === '-') {
1588
+ cursor.index += 1
1589
+ const right = parseArithmeticFactor(tokens, cursor)
1590
+ return right === null ? null : -right
1591
+ }
1592
+ if (tok === '+') {
1593
+ cursor.index += 1
1594
+ return parseArithmeticFactor(tokens, cursor)
1595
+ }
1596
+ if (tok === '(') {
1597
+ cursor.index += 1
1598
+ const inner = parseArithmeticExpr(tokens, cursor)
1599
+ if (cursor.index >= tokens.length || tokens[cursor.index] !== ')') return null
1600
+ cursor.index += 1
1601
+ return inner
1602
+ }
1603
+ const number_ = Number(tok)
1604
+ if (!Number.isFinite(number_)) return null
1605
+ cursor.index += 1
1606
+ return number_
1607
+ }
1608
+
1609
+ /**
1610
+ * Resolve a scale factor expressed as a percentage (`150%`) or number (`1.5`).
1611
+ * @param text Raw value.
1612
+ * @returns Scale number (e.g. 1.5 for 150%), or null.
1613
+ */
1614
+ function resolveNumberOrPercent(text: string): number | null {
1615
+ const trimmed = text.trim()
1616
+ const percent = /^(-?\d+(?:\.\d+)?)%$/.exec(trimmed)
1617
+ if (percent) return Number(percent[1]) / 100
1618
+ const bare = /^-?\d+(?:\.\d+)?$/.exec(trimmed)
1619
+ if (bare) return Number(trimmed)
1620
+ return null
1621
+ }
1622
+
1623
+ /**
1624
+ * Extract the angle from Tailwind's `skewX(12deg)` / `skewY(-5deg)` /
1625
+ * `skewX(calc(6deg * -1))` custom-property value shape. Returns null
1626
+ * when the inner expression doesn't reduce to a degree value.
1627
+ *
1628
+ * Tailwind v4 emits negative skew utilities as a nested `calc()`
1629
+ * (`-skew-x-6` → `skewX(calc(6deg * -1))`), so the inner body has to be
1630
+ * evaluated as arithmetic — a bare-angle regex silently drops those.
1631
+ * @param text Raw value.
1632
+ * @returns `<N>deg` string, or null.
1633
+ */
1634
+ function extractAngleFromSkewFunction(text: string): string | null {
1635
+ const trimmed = text.trim()
1636
+ if (!trimmed.endsWith(')')) return null
1637
+ const openIdx = trimmed.indexOf('(')
1638
+ if (openIdx < 5) return null
1639
+ const head = trimmed.slice(0, openIdx)
1640
+ if (head !== 'skewX' && head !== 'skewY') return null
1641
+ const inner = trimmed.slice(openIdx + 1, -1).trim()
1642
+ return resolveAngleExpression(inner)
1643
+ }
1644
+
1645
+ /**
1646
+ * Evaluate an expression whose single unit is `deg`. Strips `calc(`
1647
+ * wrappers and `deg` suffixes, runs the arithmetic evaluator, reapplies
1648
+ * `deg`. Returns null for unit mismatches or unparseable text.
1649
+ * @param text Angle expression (e.g. `6deg`, `calc(6deg * -1)`).
1650
+ * @returns `<N>deg`, or null when not representable.
1651
+ */
1652
+ function resolveAngleExpression(text: string): string | null {
1653
+ if (!/[\d.]deg\b/.test(text)) return null
1654
+ // Input is Tailwind's compiled CSS, not user-controlled — no ReDoS risk.
1655
+ // eslint-disable-next-line sonarjs/slow-regex
1656
+ const arithmetic = text.replaceAll(/\bcalc\s*\(/g, '(').replaceAll(/([\d.]+)deg/g, '$1')
1657
+ const value = evaluateArithmetic(arithmetic)
1658
+ if (value === null || !Number.isFinite(value)) return null
1659
+ return `${stripTrailingZeros(value)}deg`
1660
+ }
1661
+
1662
+ /**
1663
+ * Scan Tailwind's compiled CSS for `:root, :host { --x: y; … }` blocks
1664
+ * and pull the custom-property declarations out. This captures every
1665
+ * theme token Tailwind resolved — including tokens imported from the
1666
+ * user's secondary `@import` files (e.g. `rnwind/css`'s
1667
+ * `--duration-normal: 220ms`) — without rnwind having to re-implement
1668
+ * `@import` resolution.
1669
+ *
1670
+ * Regex-free scanner: finds `:root` prefixes, walks forward with
1671
+ * brace-depth tracking to find the matching block close, then extracts
1672
+ * every `--name: value;` pair with a paren-balanced walker so commas
1673
+ * inside `rgb(0, 0, 0)` don't confuse the split.
1674
+ * @param css Tailwind's compiled CSS.
1675
+ * @returns Map of custom-property name → resolved value.
1676
+ */
1677
+ /**
1678
+ * Strip `\@supports (color: color-mix(in lab, red, red)) { … }` wrappers
1679
+ * from Tailwind v4's compiled CSS, hoisting their inner declarations up
1680
+ * to the parent rule.
1681
+ *
1682
+ * Tailwind emits opacity-suffixed themed colors with both a pre-resolved
1683
+ * sRGB fallback AND a var()-based override gated behind the color-mix
1684
+ * `\@supports` clause. The OUTER fallback hard-codes a single scheme's
1685
+ * value of the theme token; the inner override is var()-based and
1686
+ * substitutes correctly per scheme. By unwrapping the gate, the inner
1687
+ * declaration becomes a sibling of the fallback in the same rule body —
1688
+ * lightningcss takes the LATER one (the var()-based unparsed form), and
1689
+ * the parser's themeVars-aware path produces correct rgba per scheme.
1690
+ * Modern RN-targeted browsers all support color-mix anyway, so dropping
1691
+ * the gating is safe.
1692
+ * @param css Tailwind-compiled CSS.
1693
+ * @returns CSS with the color-mix support gates unwrapped.
1694
+ */
1695
+ function unwrapColorMixSupports(css: string): string {
1696
+ const guard = '@supports (color: color-mix(in lab, red, red))'
1697
+ let out = ''
1698
+ let cursor = 0
1699
+ while (cursor < css.length) {
1700
+ const head = css.indexOf(guard, cursor)
1701
+ if (head === -1) {
1702
+ out += css.slice(cursor)
1703
+ break
1704
+ }
1705
+ out += css.slice(cursor, head)
1706
+ const brace = css.indexOf('{', head)
1707
+ if (brace === -1) {
1708
+ out += css.slice(head)
1709
+ break
1710
+ }
1711
+ const blockEnd = findMatchingClose(css, brace + 1)
1712
+ if (blockEnd === -1) {
1713
+ out += css.slice(head)
1714
+ break
1715
+ }
1716
+ const inner = css.slice(brace + 1, blockEnd)
1717
+ // Only unwrap when the gated declaration substitutes a USER theme
1718
+ // token (`var(--color-…)`). Tailwind also gates `--tw-*` internal
1719
+ // composers (shadow color, ring color, …) on the same supports
1720
+ // clause; their outer fallback is the optimized hex/oklch value
1721
+ // the parser's own composed-prop pass needs (`applyComposedShadow`
1722
+ // reads `--tw-shadow-color` from the rule's local vars). Unwrapping
1723
+ // them would replace the resolvable color with an unresolvable
1724
+ // `color-mix(... var(--tw-shadow-alpha), transparent)` text and
1725
+ // break the composed-shadow path.
1726
+ // Keep the gate intact for non-themed colors — the outer fallback
1727
+ // wins, which is what Tailwind intended.
1728
+ out += inner.includes('var(--color-') ? inner : css.slice(head, blockEnd + 1)
1729
+ cursor = blockEnd + 1
1730
+ }
1731
+ return out
1732
+ }
1733
+
1734
+ /**
1735
+ * Extract every `--name: value` declaration from the `:root` blocks in
1736
+ * Tailwind's compiled CSS into a flat map.
1737
+ * @param css Tailwind-compiled CSS.
1738
+ * @returns Map of custom-property name → resolved value.
1739
+ */
1740
+ function extractRootCustomProperties(css: string): Map<string, string> {
1741
+ const out = new Map<string, string>()
1742
+ let cursor = 0
1743
+ while (cursor < css.length) {
1744
+ const blockEnd = consumeNextRootBlock(css, cursor, out)
1745
+ if (blockEnd === -1) break
1746
+ cursor = blockEnd + 1
1747
+ }
1748
+ return out
1749
+ }
1750
+
1751
+ /**
1752
+ * Locate the next `:root` block from `cursor`, extract its custom
1753
+ * properties into `out`, and return the index of its closing brace.
1754
+ * Split out from {@link extractRootCustomProperties} to keep complexity
1755
+ * below the cap.
1756
+ * @param css Source CSS.
1757
+ * @param cursor Start index for the search.
1758
+ * @param out Destination map, mutated.
1759
+ * @returns Index of the closing brace, or -1 when no block remains.
1760
+ */
1761
+ function consumeNextRootBlock(css: string, cursor: number, out: Map<string, string>): number {
1762
+ const head = css.indexOf(':root', cursor)
1763
+ if (head === -1) return -1
1764
+ const brace = css.indexOf('{', head)
1765
+ if (brace === -1) return -1
1766
+ const blockEnd = findMatchingClose(css, brace + 1)
1767
+ if (blockEnd === -1) return -1
1768
+ collectCustomDeclarations(css.slice(brace + 1, blockEnd), out)
1769
+ return blockEnd
1770
+ }
1771
+
1772
+ /**
1773
+ * Parse the body of a `:root` block — a `;`-separated list of `--name:
1774
+ * value` declarations — into the output map. Top-level `;` split is
1775
+ * paren-aware so `rgb(0, 0, 0)` doesn't fragment the list.
1776
+ * @param body Block body text (between braces).
1777
+ * @param out Destination map, mutated.
1778
+ */
1779
+ function collectCustomDeclarations(body: string, out: Map<string, string>): void {
1780
+ for (const declaration of topLevelSplit(body, ';')) {
1781
+ const colon = declaration.indexOf(':')
1782
+ if (colon === -1) continue
1783
+ const name = declaration.slice(0, colon).trim()
1784
+ const value = declaration.slice(colon + 1).trim()
1785
+ if (name.startsWith('--') && value.length > 0) out.set(name, value)
1786
+ }
1787
+ }
1788
+
1789
+ /**
1790
+ * Walk forward from `start` tracking brace depth; return the index of
1791
+ * the matching `}` for the opener just before `start`.
1792
+ * @param source Source text.
1793
+ * @param start Index just past the opening `{`.
1794
+ * @returns Index of matching `}`, or `-1` on imbalance.
1795
+ */
1796
+ function findMatchingClose(source: string, start: number): number {
1797
+ let depth = 1
1798
+ for (let index = start; index < source.length; index += 1) {
1799
+ const ch = source[index]
1800
+ if (ch === '{') depth += 1
1801
+ else if (ch === '}') {
1802
+ depth -= 1
1803
+ if (depth === 0) return index
1804
+ }
1805
+ }
1806
+ return -1
1807
+ }
1808
+
1809
+ /**
1810
+ * Merge rule-local custom vars into every scheme's var table. Creates
1811
+ * fresh maps so the rule pass doesn't mutate the shared parser state.
1812
+ * @param schemeTables Base per-scheme var tables.
1813
+ * @param ruleVars Rule-local `--tw-*` overrides.
1814
+ * @returns Merged per-scheme tables.
1815
+ */
1816
+ function mergeRuleVars(
1817
+ schemeTables: ReadonlyMap<string, ReadonlyMap<string, string>>,
1818
+ ruleVars: ReadonlyMap<string, string>,
1819
+ ): Map<string, ReadonlyMap<string, string>> {
1820
+ if (ruleVars.size === 0) return new Map(schemeTables)
1821
+ const out = new Map<string, ReadonlyMap<string, string>>()
1822
+ for (const [scheme, table] of schemeTables) {
1823
+ const merged = new Map(table)
1824
+ for (const [k, v] of ruleVars) merged.set(k, v)
1825
+ out.set(scheme, merged)
1826
+ }
1827
+ return out
1828
+ }