meno-core 1.0.53 → 1.0.54

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 (387) hide show
  1. package/.claude/settings.local.json +1 -3
  2. package/bin/cli.ts +48 -57
  3. package/build-astro.ts +131 -113
  4. package/build-next.ts +109 -96
  5. package/build-static.test.ts +39 -10
  6. package/build-static.ts +120 -122
  7. package/dist/bin/cli.js +34 -38
  8. package/dist/bin/cli.js.map +2 -2
  9. package/dist/build-static.js +12 -11
  10. package/dist/chunks/chunk-2AR55GYH.js +42 -0
  11. package/dist/chunks/chunk-2AR55GYH.js.map +7 -0
  12. package/dist/chunks/{chunk-IGYR22T6.js → chunk-2FN4UOVO.js} +512 -239
  13. package/dist/chunks/chunk-2FN4UOVO.js.map +7 -0
  14. package/dist/chunks/chunk-3XER4E5W.js +168 -0
  15. package/dist/chunks/chunk-3XER4E5W.js.map +7 -0
  16. package/dist/chunks/{chunk-X754AHS5.js → chunk-5ETZFREW.js} +9 -12
  17. package/dist/chunks/chunk-5ETZFREW.js.map +7 -0
  18. package/dist/chunks/{chunk-2MHDV5BF.js → chunk-7E4IF5L7.js} +15 -21
  19. package/dist/chunks/chunk-7E4IF5L7.js.map +7 -0
  20. package/dist/chunks/{chunk-JGWFTO6P.js → chunk-7HWQUVTU.js} +1578 -1349
  21. package/dist/chunks/chunk-7HWQUVTU.js.map +7 -0
  22. package/dist/chunks/{chunk-O3NAGJP4.js → chunk-AE3QK5QW.js} +110 -21
  23. package/dist/chunks/chunk-AE3QK5QW.js.map +7 -0
  24. package/dist/chunks/{chunk-GZHGVVW3.js → chunk-F6KTJYGV.js} +7 -7
  25. package/dist/chunks/chunk-F6KTJYGV.js.map +7 -0
  26. package/dist/chunks/{chunk-WQFG7PAH.js → chunk-FZITJSSS.js} +2 -6
  27. package/dist/chunks/chunk-FZITJSSS.js.map +7 -0
  28. package/dist/chunks/{chunk-QB2LNO4W.js → chunk-GSYYA5GX.js} +2 -2
  29. package/dist/chunks/chunk-GSYYA5GX.js.map +7 -0
  30. package/dist/chunks/{chunk-YBLHKYFF.js → chunk-HIZMY3EP.js} +1 -1
  31. package/dist/chunks/chunk-HIZMY3EP.js.map +7 -0
  32. package/dist/chunks/{chunk-AZQYF6KE.js → chunk-I2WEGYA7.js} +41 -176
  33. package/dist/chunks/chunk-I2WEGYA7.js.map +7 -0
  34. package/dist/chunks/{chunk-I7YIGZXT.js → chunk-JNO3CNLJ.js} +6 -9
  35. package/dist/chunks/chunk-JNO3CNLJ.js.map +7 -0
  36. package/dist/chunks/{chunk-UB44F4Z2.js → chunk-NVRBTSQG.js} +2 -4
  37. package/dist/chunks/chunk-NVRBTSQG.js.map +7 -0
  38. package/dist/chunks/{chunk-JGP5A3Y5.js → chunk-Q4OBWKXG.js} +40 -33
  39. package/dist/chunks/chunk-Q4OBWKXG.js.map +7 -0
  40. package/dist/chunks/{chunk-R6XHAFBF.js → chunk-QTE32Y53.js} +250 -242
  41. package/dist/chunks/chunk-QTE32Y53.js.map +7 -0
  42. package/dist/chunks/{chunk-H3GJ4H2U.js → chunk-STDY3OVM.js} +214 -85
  43. package/dist/chunks/chunk-STDY3OVM.js.map +7 -0
  44. package/dist/chunks/configService-PRJZF7Y6.js +14 -0
  45. package/dist/chunks/{constants-STK2YBIW.js → constants-KIQEYMAM.js} +2 -2
  46. package/dist/chunks/{fs-JGINUXGL.js → fs-ZI5JEU7V.js} +2 -2
  47. package/dist/entries/server-router.js +14 -19
  48. package/dist/entries/server-router.js.map +2 -2
  49. package/dist/lib/client/index.js +638 -332
  50. package/dist/lib/client/index.js.map +2 -2
  51. package/dist/lib/server/index.js +177 -235
  52. package/dist/lib/server/index.js.map +2 -2
  53. package/dist/lib/shared/index.js +80 -44
  54. package/dist/lib/shared/index.js.map +2 -2
  55. package/dist/lib/shared/richtext/index.js +1 -1
  56. package/dist/lib/test-utils/index.js +38 -60
  57. package/dist/lib/test-utils/index.js.map +2 -2
  58. package/entries/client-router.tsx +10 -8
  59. package/entries/server-router.tsx +1 -7
  60. package/lib/client/ClientInitializer.ts +8 -8
  61. package/lib/client/ErrorBoundary.test.tsx +146 -143
  62. package/lib/client/ErrorBoundary.tsx +175 -127
  63. package/lib/client/componentRegistry.test.ts +96 -108
  64. package/lib/client/componentRegistry.ts +1 -2
  65. package/lib/client/contexts/ThemeContext.tsx +3 -2
  66. package/lib/client/core/ComponentBuilder.test.ts +513 -560
  67. package/lib/client/core/ComponentBuilder.ts +318 -146
  68. package/lib/client/core/ComponentRenderer.test.tsx +1 -2
  69. package/lib/client/core/ComponentRenderer.tsx +46 -33
  70. package/lib/client/core/builders/embedBuilder.ts +241 -53
  71. package/lib/client/core/builders/linkBuilder.ts +71 -44
  72. package/lib/client/core/builders/linkNodeBuilder.ts +78 -53
  73. package/lib/client/core/builders/listBuilder.ts +115 -86
  74. package/lib/client/core/builders/localeListBuilder.ts +95 -60
  75. package/lib/client/core/builders/types.ts +5 -5
  76. package/lib/client/core/cmsTemplateProcessor.ts +7 -7
  77. package/lib/client/elementRegistry.ts +3 -3
  78. package/lib/client/fontFamiliesService.test.ts +2 -10
  79. package/lib/client/fontFamiliesService.ts +3 -3
  80. package/lib/client/hmr/HMRManager.tsx +8 -0
  81. package/lib/client/hmrCssReload.ts +14 -8
  82. package/lib/client/hmrWebSocket.ts +9 -14
  83. package/lib/client/hooks/useColorVariables.test.ts +21 -21
  84. package/lib/client/hooks/useColorVariables.ts +12 -10
  85. package/lib/client/hooks/usePropertyAutocomplete.ts +3 -5
  86. package/lib/client/hooks/useVariables.ts +4 -4
  87. package/lib/client/hydration/HydrationUtils.test.ts +24 -25
  88. package/lib/client/hydration/HydrationUtils.ts +3 -4
  89. package/lib/client/i18nConfigService.test.ts +2 -7
  90. package/lib/client/i18nConfigService.ts +2 -2
  91. package/lib/client/meno-filter/MenoFilter.test.ts +19 -21
  92. package/lib/client/meno-filter/MenoFilter.ts +5 -9
  93. package/lib/client/meno-filter/bindings.ts +15 -40
  94. package/lib/client/meno-filter/init.ts +1 -1
  95. package/lib/client/meno-filter/renderer.ts +23 -29
  96. package/lib/client/meno-filter/script.generated.ts +1 -3
  97. package/lib/client/meno-filter/ui.ts +3 -5
  98. package/lib/client/meno-filter/updates.ts +15 -21
  99. package/lib/client/navigation.test.ts +159 -159
  100. package/lib/client/navigation.ts +0 -1
  101. package/lib/client/responsiveStyleResolver.test.ts +230 -228
  102. package/lib/client/responsiveStyleResolver.ts +13 -16
  103. package/lib/client/routing/RouteLoader.test.ts +23 -24
  104. package/lib/client/routing/RouteLoader.ts +22 -35
  105. package/lib/client/routing/Router.tsx +31 -3
  106. package/lib/client/scripts/ScriptExecutor.test.ts +157 -158
  107. package/lib/client/scripts/ScriptExecutor.ts +15 -33
  108. package/lib/client/services/PrefetchService.test.ts +2 -2
  109. package/lib/client/services/PrefetchService.ts +10 -24
  110. package/lib/client/styleProcessor.test.ts +9 -9
  111. package/lib/client/styleProcessor.ts +18 -15
  112. package/lib/client/styles/StyleInjector.test.ts +122 -115
  113. package/lib/client/styles/StyleInjector.ts +9 -6
  114. package/lib/client/styles/UtilityClassCollector.ts +19 -26
  115. package/lib/client/styles/cspNonce.test.ts +2 -5
  116. package/lib/client/templateEngine.test.ts +554 -482
  117. package/lib/client/templateEngine.ts +200 -64
  118. package/lib/client/theme.ts +0 -1
  119. package/lib/client/utils/toast.ts +0 -1
  120. package/lib/server/__integration__/api-routes.test.ts +8 -4
  121. package/lib/server/__integration__/cms-integration.test.ts +1 -4
  122. package/lib/server/__integration__/server-lifecycle.test.ts +2 -5
  123. package/lib/server/__integration__/ssr-rendering.test.ts +47 -37
  124. package/lib/server/__integration__/static-assets.test.ts +1 -1
  125. package/lib/server/__integration__/test-helpers.ts +84 -70
  126. package/lib/server/ab/generateFunctions.ts +12 -10
  127. package/lib/server/astro/cmsPageEmitter.ts +23 -38
  128. package/lib/server/astro/componentEmitter.ts +25 -36
  129. package/lib/server/astro/cssCollector.ts +10 -26
  130. package/lib/server/astro/nodeToAstro.test.ts +1750 -30
  131. package/lib/server/astro/nodeToAstro.ts +198 -187
  132. package/lib/server/astro/normalizeOrphanTemplateProps.test.ts +105 -109
  133. package/lib/server/astro/normalizeOrphanTemplateProps.ts +4 -12
  134. package/lib/server/astro/pageEmitter.ts +9 -13
  135. package/lib/server/astro/tailwindMapper.test.ts +10 -37
  136. package/lib/server/astro/tailwindMapper.ts +33 -40
  137. package/lib/server/astro/templateTransformer.ts +14 -17
  138. package/lib/server/createServer.ts +5 -17
  139. package/lib/server/cssGenerator.test.ts +35 -44
  140. package/lib/server/cssGenerator.ts +6 -17
  141. package/lib/server/fileWatcher.test.ts +124 -10
  142. package/lib/server/fileWatcher.ts +124 -118
  143. package/lib/server/index.ts +7 -1
  144. package/lib/server/jsonLoader.test.ts +39 -2
  145. package/lib/server/jsonLoader.ts +33 -31
  146. package/lib/server/middleware/cors.test.ts +20 -20
  147. package/lib/server/middleware/cors.ts +28 -4
  148. package/lib/server/middleware/errorHandler.test.ts +5 -3
  149. package/lib/server/middleware/errorHandler.ts +3 -8
  150. package/lib/server/middleware/index.ts +0 -1
  151. package/lib/server/middleware/logger.test.ts +7 -5
  152. package/lib/server/middleware/logger.ts +10 -22
  153. package/lib/server/pageCache.test.ts +76 -77
  154. package/lib/server/pageCache.ts +0 -1
  155. package/lib/server/projectContext.ts +4 -3
  156. package/lib/server/providers/fileSystemCMSProvider.test.ts +124 -95
  157. package/lib/server/providers/fileSystemCMSProvider.ts +35 -20
  158. package/lib/server/providers/fileSystemPageProvider.test.ts +84 -0
  159. package/lib/server/providers/fileSystemPageProvider.ts +31 -12
  160. package/lib/server/routes/api/cms.test.ts +26 -14
  161. package/lib/server/routes/api/cms.ts +9 -14
  162. package/lib/server/routes/api/components.ts +30 -34
  163. package/lib/server/routes/api/config.ts +0 -1
  164. package/lib/server/routes/api/core-routes.ts +47 -74
  165. package/lib/server/routes/api/functions.ts +8 -16
  166. package/lib/server/routes/api/index.ts +3 -6
  167. package/lib/server/routes/api/pages.ts +8 -11
  168. package/lib/server/routes/api/shared.test.ts +1 -1
  169. package/lib/server/routes/api/shared.ts +12 -9
  170. package/lib/server/routes/api/variables.test.ts +1 -3
  171. package/lib/server/routes/api/variables.ts +1 -1
  172. package/lib/server/routes/index.ts +23 -26
  173. package/lib/server/routes/pages.ts +34 -29
  174. package/lib/server/routes/static.ts +16 -4
  175. package/lib/server/runtime/bundler.ts +47 -32
  176. package/lib/server/runtime/fs.ts +3 -13
  177. package/lib/server/runtime/httpServer.ts +5 -9
  178. package/lib/server/services/ColorService.ts +32 -27
  179. package/lib/server/services/EnumService.test.ts +2 -6
  180. package/lib/server/services/EnumService.ts +7 -2
  181. package/lib/server/services/VariableService.test.ts +1 -5
  182. package/lib/server/services/VariableService.ts +6 -1
  183. package/lib/server/services/cmsService.test.ts +116 -78
  184. package/lib/server/services/cmsService.ts +24 -54
  185. package/lib/server/services/componentService.test.ts +122 -36
  186. package/lib/server/services/componentService.ts +54 -38
  187. package/lib/server/services/configService.test.ts +9 -31
  188. package/lib/server/services/configService.ts +20 -27
  189. package/lib/server/services/fileWatcherService.ts +40 -6
  190. package/lib/server/services/index.ts +0 -1
  191. package/lib/server/services/pageService.test.ts +1 -3
  192. package/lib/server/services/pageService.ts +14 -13
  193. package/lib/server/ssr/attributeBuilder.ts +16 -6
  194. package/lib/server/ssr/buildErrorOverlay.ts +11 -12
  195. package/lib/server/ssr/clientDataInjector.ts +7 -21
  196. package/lib/server/ssr/cmsSSRProcessor.ts +3 -6
  197. package/lib/server/ssr/cssCollector.ts +1 -1
  198. package/lib/server/ssr/errorOverlay.test.ts +1 -1
  199. package/lib/server/ssr/errorOverlay.ts +3 -9
  200. package/lib/server/ssr/htmlGenerator.nonce.test.ts +3 -9
  201. package/lib/server/ssr/htmlGenerator.test.ts +120 -43
  202. package/lib/server/ssr/htmlGenerator.ts +120 -85
  203. package/lib/server/ssr/imageMetadata.test.ts +3 -1
  204. package/lib/server/ssr/imageMetadata.ts +25 -19
  205. package/lib/server/ssr/jsCollector.test.ts +3 -13
  206. package/lib/server/ssr/jsCollector.ts +3 -8
  207. package/lib/server/ssr/liveReloadIntegration.test.ts +69 -28
  208. package/lib/server/ssr/metaTagGenerator.ts +2 -2
  209. package/lib/server/ssr/ssrRenderer.branches.test.ts +1103 -0
  210. package/lib/server/ssr/ssrRenderer.test.ts +196 -246
  211. package/lib/server/ssr/ssrRenderer.ts +609 -225
  212. package/lib/server/ssrRenderer.test.ts +1044 -950
  213. package/lib/server/utils/jsonLineMapper.test.ts +28 -28
  214. package/lib/server/utils/jsonLineMapper.ts +1 -1
  215. package/lib/server/validateStyleCoverage.ts +18 -20
  216. package/lib/server/webflow/buildWebflow.ts +41 -53
  217. package/lib/server/webflow/nodeToWebflow.test.ts +150 -218
  218. package/lib/server/webflow/nodeToWebflow.ts +195 -258
  219. package/lib/server/webflow/styleMapper.test.ts +15 -56
  220. package/lib/server/webflow/styleMapper.ts +33 -41
  221. package/lib/server/webflow/types.ts +1 -8
  222. package/lib/server/websocketManager.ts +16 -19
  223. package/lib/shared/attributeNodeUtils.test.ts +15 -15
  224. package/lib/shared/attributeNodeUtils.ts +5 -12
  225. package/lib/shared/breakpoints.ts +4 -11
  226. package/lib/shared/cmsQueryParser.test.ts +50 -42
  227. package/lib/shared/cmsQueryParser.ts +49 -31
  228. package/lib/shared/colorVariableUtils.test.ts +5 -5
  229. package/lib/shared/colorVariableUtils.ts +3 -8
  230. package/lib/shared/componentRefs.ts +1 -5
  231. package/lib/shared/constants.test.ts +3 -3
  232. package/lib/shared/constants.ts +4 -8
  233. package/lib/shared/cssGeneration.test.ts +262 -144
  234. package/lib/shared/cssGeneration.ts +189 -516
  235. package/lib/shared/cssNamedColors.ts +152 -30
  236. package/lib/shared/cssProperties.test.ts +5 -6
  237. package/lib/shared/cssProperties.ts +296 -112
  238. package/lib/shared/elementClassName.test.ts +109 -109
  239. package/lib/shared/elementClassName.ts +1 -1
  240. package/lib/shared/elementUtils.ts +12 -16
  241. package/lib/shared/errorLogger.ts +2 -10
  242. package/lib/shared/errors.test.ts +2 -13
  243. package/lib/shared/errors.ts +2 -8
  244. package/lib/shared/expressionEvaluator.test.ts +119 -0
  245. package/lib/shared/expressionEvaluator.ts +43 -24
  246. package/lib/shared/fontLoader.test.ts +19 -5
  247. package/lib/shared/friendlyError.ts +2 -3
  248. package/lib/shared/gradientUtils.test.ts +1 -5
  249. package/lib/shared/gradientUtils.ts +2 -6
  250. package/lib/shared/hrefRefs.ts +2 -10
  251. package/lib/shared/i18n.test.ts +1 -1
  252. package/lib/shared/i18n.ts +13 -34
  253. package/lib/shared/index.ts +4 -0
  254. package/lib/shared/inlineSvgStyleRules.ts +2 -4
  255. package/lib/shared/interactiveStyleMappings.test.ts +11 -33
  256. package/lib/shared/interactiveStyleMappings.ts +9 -16
  257. package/lib/shared/interactiveStyles.test.ts +165 -188
  258. package/lib/shared/interfaces/contentProvider.ts +1 -1
  259. package/lib/shared/itemTemplateUtils.test.ts +6 -12
  260. package/lib/shared/itemTemplateUtils.ts +19 -35
  261. package/lib/shared/jsonRepair.ts +8 -2
  262. package/lib/shared/libraryLoader.test.ts +15 -49
  263. package/lib/shared/libraryLoader.ts +7 -22
  264. package/lib/shared/netlifyLocale404.test.ts +179 -0
  265. package/lib/shared/netlifyLocale404.ts +110 -0
  266. package/lib/shared/nodeUtils.test.ts +24 -16
  267. package/lib/shared/nodeUtils.ts +49 -19
  268. package/lib/shared/pathArrayUtils.test.ts +1 -2
  269. package/lib/shared/pathArrayUtils.ts +1 -1
  270. package/lib/shared/pathSecurity.ts +1 -1
  271. package/lib/shared/pathUtils.test.ts +4 -6
  272. package/lib/shared/pathUtils.ts +42 -48
  273. package/lib/shared/paths/Path.test.ts +2 -2
  274. package/lib/shared/paths/Path.ts +0 -1
  275. package/lib/shared/paths/PathConverter.test.ts +1 -1
  276. package/lib/shared/paths/PathConverter.ts +14 -17
  277. package/lib/shared/paths/PathUtils.ts +9 -10
  278. package/lib/shared/paths/PathValidator.test.ts +2 -15
  279. package/lib/shared/paths/PathValidator.ts +11 -9
  280. package/lib/shared/paths/index.ts +1 -2
  281. package/lib/shared/propResolver.test.ts +240 -244
  282. package/lib/shared/propResolver.ts +14 -25
  283. package/lib/shared/pxToRem.test.ts +7 -6
  284. package/lib/shared/pxToRem.ts +2 -5
  285. package/lib/shared/registry/BaseNodeTypeRegistry.test.ts +9 -5
  286. package/lib/shared/registry/ClientRegistry.ts +0 -1
  287. package/lib/shared/registry/ComponentRegistry.test.ts +43 -29
  288. package/lib/shared/registry/ComponentRegistry.ts +9 -11
  289. package/lib/shared/registry/NodeTypeDefinition.ts +15 -8
  290. package/lib/shared/registry/RegistryManager.ts +1 -2
  291. package/lib/shared/registry/SSRRegistry.ts +0 -1
  292. package/lib/shared/registry/createNodeType.ts +7 -9
  293. package/lib/shared/registry/defineNodeType.ts +2 -6
  294. package/lib/shared/registry/index.ts +0 -1
  295. package/lib/shared/registry/nodeTypes/ComponentInstanceNodeType.ts +14 -15
  296. package/lib/shared/registry/nodeTypes/EmbedNodeType.ts +18 -11
  297. package/lib/shared/registry/nodeTypes/HtmlNodeType.ts +47 -18
  298. package/lib/shared/registry/nodeTypes/LinkNodeType.ts +21 -19
  299. package/lib/shared/registry/nodeTypes/ListNodeType.ts +78 -74
  300. package/lib/shared/registry/nodeTypes/LocaleListNodeType.ts +27 -21
  301. package/lib/shared/registry/nodeTypes/SlotMarkerType.ts +6 -7
  302. package/lib/shared/registry/nodeTypes/index.ts +10 -2
  303. package/lib/shared/responsiveScaling.test.ts +15 -31
  304. package/lib/shared/responsiveScaling.ts +55 -37
  305. package/lib/shared/responsiveStyleUtils.ts +11 -13
  306. package/lib/shared/richtext/htmlToTiptap.test.ts +23 -14
  307. package/lib/shared/richtext/htmlToTiptap.ts +1 -3
  308. package/lib/shared/richtext/tiptapToHtml.test.ts +5 -6
  309. package/lib/shared/richtext/types.ts +1 -8
  310. package/lib/shared/slugTranslator.test.ts +13 -13
  311. package/lib/shared/slugTranslator.ts +12 -16
  312. package/lib/shared/slugify.ts +9 -15
  313. package/lib/shared/styleNodeUtils.test.ts +8 -8
  314. package/lib/shared/styleNodeUtils.ts +5 -10
  315. package/lib/shared/styleUtils.test.ts +87 -61
  316. package/lib/shared/styleUtils.ts +5 -6
  317. package/lib/shared/themeDefaults.test.ts +11 -11
  318. package/lib/shared/themeDefaults.ts +3 -4
  319. package/lib/shared/tree/PathBuilder.test.ts +62 -138
  320. package/lib/shared/tree/PathBuilder.ts +49 -39
  321. package/lib/shared/treePathUtils.test.ts +2 -10
  322. package/lib/shared/treePathUtils.ts +54 -59
  323. package/lib/shared/types/api.ts +1 -2
  324. package/lib/shared/types/cms.ts +25 -21
  325. package/lib/shared/types/comment.ts +49 -16
  326. package/lib/shared/types/components.ts +27 -25
  327. package/lib/shared/types/errors.test.ts +1 -6
  328. package/lib/shared/types/errors.ts +3 -7
  329. package/lib/shared/types/experiments.ts +28 -28
  330. package/lib/shared/types/index.ts +2 -2
  331. package/lib/shared/types/styles.ts +0 -1
  332. package/lib/shared/types/variables.test.ts +4 -13
  333. package/lib/shared/types/variables.ts +48 -27
  334. package/lib/shared/types.ts +1 -2
  335. package/lib/shared/utilityClassConfig.ts +648 -321
  336. package/lib/shared/utilityClassMapper.test.ts +203 -92
  337. package/lib/shared/utilityClassMapper.ts +188 -246
  338. package/lib/shared/utilityClassNames.ts +326 -0
  339. package/lib/shared/utils.test.ts +2 -10
  340. package/lib/shared/utils.ts +19 -10
  341. package/lib/shared/validation/cmsValidators.ts +2 -1
  342. package/lib/shared/validation/commentValidators.test.ts +53 -0
  343. package/lib/shared/validation/commentValidators.ts +12 -1
  344. package/lib/shared/validation/propValidator.test.ts +18 -20
  345. package/lib/shared/validation/propValidator.ts +12 -17
  346. package/lib/shared/validation/schemas.test.ts +24 -33
  347. package/lib/shared/validation/schemas.ts +469 -344
  348. package/lib/shared/validation/validators.test.ts +1 -6
  349. package/lib/shared/validation/validators.ts +89 -68
  350. package/lib/shared/viewportUnits.integration.test.ts +14 -10
  351. package/lib/shared/viewportUnits.test.ts +11 -23
  352. package/lib/test-utils/dom-setup.ts +1 -1
  353. package/lib/test-utils/factories/ConsoleMockFactory.ts +3 -7
  354. package/lib/test-utils/factories/DomMockFactory.ts +7 -19
  355. package/lib/test-utils/factories/EventMockFactory.ts +7 -13
  356. package/lib/test-utils/factories/FetchMockFactory.ts +39 -57
  357. package/lib/test-utils/factories/ServerMockFactory.ts +5 -9
  358. package/lib/test-utils/factories/StoreMockFactory.ts +14 -25
  359. package/lib/test-utils/fixtures.ts +45 -45
  360. package/lib/test-utils/helpers/asyncHelpers.test.ts +15 -18
  361. package/lib/test-utils/helpers/asyncHelpers.ts +11 -20
  362. package/lib/test-utils/helpers.ts +1 -5
  363. package/lib/test-utils/index.ts +0 -4
  364. package/lib/test-utils/mockFactories.ts +12 -18
  365. package/lib/test-utils/mocks.ts +4 -2
  366. package/package.json +1 -1
  367. package/scripts/build-meno-filter.ts +1 -4
  368. package/vite.config.ts +4 -4
  369. package/dist/chunks/chunk-2MHDV5BF.js.map +0 -7
  370. package/dist/chunks/chunk-AZQYF6KE.js.map +0 -7
  371. package/dist/chunks/chunk-GZHGVVW3.js.map +0 -7
  372. package/dist/chunks/chunk-H3GJ4H2U.js.map +0 -7
  373. package/dist/chunks/chunk-I7YIGZXT.js.map +0 -7
  374. package/dist/chunks/chunk-IGYR22T6.js.map +0 -7
  375. package/dist/chunks/chunk-JGP5A3Y5.js.map +0 -7
  376. package/dist/chunks/chunk-JGWFTO6P.js.map +0 -7
  377. package/dist/chunks/chunk-O3NAGJP4.js.map +0 -7
  378. package/dist/chunks/chunk-QB2LNO4W.js.map +0 -7
  379. package/dist/chunks/chunk-R6XHAFBF.js.map +0 -7
  380. package/dist/chunks/chunk-UB44F4Z2.js.map +0 -7
  381. package/dist/chunks/chunk-WQFG7PAH.js.map +0 -7
  382. package/dist/chunks/chunk-X754AHS5.js.map +0 -7
  383. package/dist/chunks/chunk-YBLHKYFF.js.map +0 -7
  384. package/dist/chunks/configService-R3OGU2UD.js +0 -13
  385. /package/dist/chunks/{configService-R3OGU2UD.js.map → configService-PRJZF7Y6.js.map} +0 -0
  386. /package/dist/chunks/{constants-STK2YBIW.js.map → constants-KIQEYMAM.js.map} +0 -0
  387. /package/dist/chunks/{fs-JGINUXGL.js.map → fs-ZI5JEU7V.js.map} +0 -0
@@ -4,10 +4,16 @@
4
4
  * Used by both client and server to ensure consistent CSS output
5
5
  */
6
6
 
7
- import { prefixToCSSProperty, propertyMap } from './utilityClassConfig';
7
+ import { propertyOrder, staticUtilityReverse, presetClassReverse } from './utilityClassConfig';
8
+ import {
9
+ parseUtilityClass,
10
+ splitVariantPrefix,
11
+ breakpointClassPrefix,
12
+ resolveRootProperty,
13
+ camelToKebab,
14
+ } from './utilityClassNames';
8
15
  import { getStyleValue, getDynamicStyle, isDynamicClass } from './styleValueRegistry';
9
- import { isCssNamedColor } from './cssNamedColors';
10
- import type { BreakpointConfig, LegacyBreakpointConfig } from './breakpoints';
16
+ import type { BreakpointConfig } from './breakpoints';
11
17
  import { DEFAULT_BREAKPOINTS, getBreakpointValues } from './breakpoints';
12
18
  import type { ResponsiveScales, CSSPropertyType, ResponsiveMode } from './responsiveScaling';
13
19
  import {
@@ -21,49 +27,32 @@ import type { InteractiveStyles, StyleObject, ResponsiveStyleObject, StyleValue
21
27
  import type { RemConversionConfig } from './pxToRem';
22
28
  import { applyRemConversion, convertPxToRem, shouldConvertProperty } from './pxToRem';
23
29
 
24
- /** Sorted prefix list (longest first) — cached at module level to avoid re-sorting per call */
25
- const SORTED_PREFIXES: readonly string[] = Object.keys(prefixToCSSProperty)
26
- .sort((a, b) => b.length - a.length);
27
-
28
30
  /**
29
- * Map from CSS property (kebab-case) to its index in propertyMap.
31
+ * Map from CSS property (kebab-case) to its index in propertyOrder.
30
32
  * Shorthands like 'border' have lower indices than longhands like 'border-color',
31
33
  * so sorting by this index ensures shorthand CSS rules appear before longhand rules.
32
34
  */
33
35
  const cssPropertyOrderMap: Map<string, number> = (() => {
34
36
  const map = new Map<string, number>();
35
- const props = Object.keys(propertyMap);
36
- for (let i = 0; i < props.length; i++) {
37
+ for (let i = 0; i < propertyOrder.length; i++) {
37
38
  // Convert camelCase key to kebab-case CSS property
38
- const kebab = props[i].replace(/([A-Z])/g, '-$1').toLowerCase();
39
- map.set(kebab, i);
39
+ const kebab = camelToKebab(propertyOrder[i]);
40
+ if (!map.has(kebab)) map.set(kebab, i);
40
41
  }
41
42
  return map;
42
43
  })();
43
44
 
44
45
  /**
45
46
  * Get the property-order index for a utility class name.
46
- * Returns the position of the class's CSS property in propertyMap,
47
+ * Returns the position of the class's CSS property in propertyOrder,
47
48
  * or Infinity for unknown classes (placed last).
48
49
  */
49
50
  function getClassPropertyOrder(className: string): number {
50
- // Try dynamic prefix-based lookup
51
- const extracted = extractPropertyAndValue(className);
52
- if (extracted) {
53
- const idx = cssPropertyOrderMap.get(extracted.property);
51
+ const parsed = parseUtilityClass(className);
52
+ if (parsed) {
53
+ const idx = cssPropertyOrderMap.get(parsed.property);
54
54
  if (idx !== undefined) return idx;
55
55
  }
56
-
57
- // Try predefined rules (e.g. 'f' → display: flex)
58
- const rule = utilityClassRules[className];
59
- if (rule) {
60
- const propMatch = rule.match(/^([a-z-]+):/);
61
- if (propMatch) {
62
- const idx = cssPropertyOrderMap.get(propMatch[1]);
63
- if (idx !== undefined) return idx;
64
- }
65
- }
66
-
67
56
  return Infinity;
68
57
  }
69
58
 
@@ -85,18 +74,18 @@ function getResponsiveMode(scales: ResponsiveScales | undefined | null): Respons
85
74
 
86
75
  /** CSS property → responsive scale category mapping (used by generateUtilityCSS and generateSingleClassCSS) */
87
76
  const AUTO_RESPONSIVE_TYPE_MAP: Record<string, string> = {
88
- 'padding': 'padding',
77
+ padding: 'padding',
89
78
  'padding-left': 'padding',
90
79
  'padding-right': 'padding',
91
80
  'padding-top': 'padding',
92
81
  'padding-bottom': 'padding',
93
- 'margin': 'margin',
82
+ margin: 'margin',
94
83
  'margin-left': 'margin',
95
84
  'margin-right': 'margin',
96
85
  'margin-top': 'margin',
97
86
  'margin-bottom': 'margin',
98
87
  'font-size': 'fontSize',
99
- 'gap': 'gap',
88
+ gap: 'gap',
100
89
  'row-gap': 'gap',
101
90
  'column-gap': 'gap',
102
91
  'border-radius': 'borderRadius',
@@ -104,8 +93,8 @@ const AUTO_RESPONSIVE_TYPE_MAP: Record<string, string> = {
104
93
  'border-top-right-radius': 'borderRadius',
105
94
  'border-bottom-left-radius': 'borderRadius',
106
95
  'border-bottom-right-radius': 'borderRadius',
107
- 'width': 'size',
108
- 'height': 'size',
96
+ width: 'size',
97
+ height: 'size',
109
98
  'max-width': 'size',
110
99
  'max-height': 'size',
111
100
  'min-width': 'size',
@@ -113,126 +102,37 @@ const AUTO_RESPONSIVE_TYPE_MAP: Record<string, string> = {
113
102
  };
114
103
 
115
104
  /**
116
- * Escape special characters in CSS class names using backslash notation
117
- * Characters that have special meaning in CSS selectors need to be escaped
105
+ * Escape special characters in CSS class names using backslash notation.
106
+ * Tailwind-style class names carry `[ ] ( ) : % # . , / *` and more — escape
107
+ * everything outside the safe identifier charset.
118
108
  */
119
109
  function escapeCSSClassName(className: string): string {
120
- // Escape special characters with backslash
121
- // This includes: . # [ ] ( ) { } ; : , > + ~ % etc.
122
- return className.replace(/[.#[\](){};<>+~:,%\/\s]/g, '\\$&');
110
+ return className.replace(/[^a-zA-Z0-9_-]/g, '\\$&');
123
111
  }
124
112
 
125
- // Define all possible utility class rules
126
- const utilityClassRules: Record<string, string> = {
127
- // Display utilities (short forms from specialValueMappings)
128
- f: 'display: flex;',
129
- 'fd-col': 'flex-direction: column;',
130
- 'fd-row': 'flex-direction: row;',
131
- g: 'display: grid;',
132
- b: 'display: block;',
133
- i: 'display: inline;',
134
- ib: 'display: inline-block;',
135
- h: 'display: none;',
136
- 'if': 'display: inline-flex;',
137
- ig: 'display: inline-grid;',
138
-
139
- // Justify Content (short forms)
140
- 'jc-c': 'justify-content: center;',
141
- 'jc-s': 'justify-content: flex-start;',
142
- 'jc-e': 'justify-content: flex-end;',
143
- 'jc-b': 'justify-content: space-between;',
144
- 'jc-a': 'justify-content: space-around;',
145
- 'jc-ev': 'justify-content: space-evenly;',
146
-
147
- // Align Items (short forms)
148
- 'ai-c': 'align-items: center;',
149
- 'ai-s': 'align-items: flex-start;',
150
- 'ai-e': 'align-items: flex-end;',
151
- 'ai-st': 'align-items: stretch;',
152
- 'ai-b': 'align-items: baseline;',
153
-
154
- // Overflow (short forms)
155
- 'o-h': 'overflow: hidden;',
156
- 'o-a': 'overflow: auto;',
157
- 'o-s': 'overflow: scroll;',
158
- 'o-v': 'overflow: visible;',
159
-
160
- // Cursor (full forms)
161
- 'cursor-pointer': 'cursor: pointer;',
162
- 'cursor-default': 'cursor: default;',
163
-
164
- // Pointer events
165
- 'pe-none': 'pointer-events: none;',
166
- 'pe-auto': 'pointer-events: auto;',
167
-
168
- // User select
169
- 'us-none': 'user-select: none;',
170
- 'us-auto': 'user-select: auto;',
171
- 'us-text': 'user-select: text;',
172
- 'us-all': 'user-select: all;',
173
-
174
- // White space
175
- 'whs-normal': 'white-space: normal;',
176
- 'whs-nowrap': 'white-space: nowrap;',
177
- 'whs-pre': 'white-space: pre;',
178
- 'whs-pre-wrap': 'white-space: pre-wrap;',
179
- 'whs-pre-line': 'white-space: pre-line;',
180
-
181
- // Shadow presets
182
- 'sh-0': 'box-shadow: none;',
183
- 'sh-1': 'box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);',
184
- 'sh-2': 'box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);',
185
- 'sh-3': 'box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);',
186
- 'sh-4': 'box-shadow: 0 12px 32px rgba(0, 0, 0, 0.2);',
187
-
188
- // Gradient presets
189
- 'gr-1': 'background: linear-gradient(90deg, rgba(10,22,40,0.85) 0%, rgba(10,22,40,0.2) 100%);',
190
- 'gr-2': 'background: linear-gradient(0deg, #a9e8fd, #00aee8 25%, #0f1335 72%);',
191
-
192
- // Border presets
193
- 'bd-0': 'border: 1px solid var(--border);',
194
- 'bd-1': 'border: 1px solid var(--border-light);',
195
-
196
- // Note: CSS functions (blur, translateY, scale, rotate, repeat) are now handled
197
- // dynamically via the styleValueRegistry - no need to hardcode specific values here
198
- };
199
-
200
113
  /**
201
- * Extract CSS property and value from a utility class
202
- * Returns { property: CSS property name, value: CSS value } or null if not a dynamic class
114
+ * Predefined rules for static utilities and presets (Tailwind names):
115
+ * `flex` `display: flex;`, `hidden` `display: none;`,
116
+ * `justify-center` → `justify-content: center;`, `shadow-2` → preset value, …
117
+ * Derived from the single source of truth in utilityClassConfig.ts.
203
118
  */
204
- function extractPropertyAndValue(className: string): { property: string; value: string } | null {
205
- // Parse prefix-value pattern (e.g., "p-10px", "fs-48px")
206
- for (const knownPrefix of SORTED_PREFIXES) {
207
- if (className.startsWith(knownPrefix + '-')) {
208
- const classValue = className.substring(knownPrefix.length + 1);
209
- const cssProp = prefixToCSSProperty[knownPrefix];
210
- if (cssProp) {
211
- // Hash-fallback class (e.g. `pt-h1y9pr6i`): the part after the
212
- // prefix is a hash, not a real CSS value. Returning `value: 'h1y9pr6i'`
213
- // would make auto-responsive scaling and other downstream consumers
214
- // emit broken rules like `padding-top: h1y9pr6i;`. The class IS
215
- // resolvable via the style-value registry — `generateRuleForClass`
216
- // handles it directly. Skip the dynamic-extraction path here.
217
- if (/^h[0-9a-z]+$/.test(classValue)) {
218
- return null;
219
- }
220
- return { property: cssProp, value: classValue };
221
- }
222
- }
119
+ const utilityClassRules: Record<string, string> = (() => {
120
+ const rules: Record<string, string> = {};
121
+ for (const [className, entry] of Object.entries(staticUtilityReverse)) {
122
+ rules[className] = `${camelToKebab(entry.prop)}: ${entry.value};`;
223
123
  }
224
-
225
- return null;
226
- }
124
+ for (const [className, entry] of Object.entries(presetClassReverse)) {
125
+ rules[className] = `${camelToKebab(entry.prop)}: ${entry.value};`;
126
+ }
127
+ return rules;
128
+ })();
227
129
 
228
130
  /**
229
131
  * Resolve `{ property, value }` for a utility class, consulting the style-value
230
- * registry for hash-fallback classes (e.g. `fs-h1glej9a` for `222.3px`).
132
+ * registry for hash-fallback classes (e.g. `text-h1glej9a` for `222.3px`).
231
133
  *
232
134
  * Used by the auto-responsive scaling path so hashed classes don't silently
233
- * bypass `clamp()` / `@media` rewriting. `extractPropertyAndValue` itself
234
- * intentionally returns null for hashed classes because its other consumers
235
- * would emit broken `${prop}: ${hash};` rules.
135
+ * bypass `clamp()` / `@media` rewriting.
236
136
  *
237
137
  * Returns the registered value as-is (no parsing). Downstream helpers
238
138
  * (`buildFluidPropertyValue`, `scalePropertyValue`) already null out
@@ -241,48 +141,41 @@ function extractPropertyAndValue(className: string): { property: string; value:
241
141
  * `if (!fluidValue) return rule;` guards.
242
142
  */
243
143
  function resolveScalablePropertyValue(className: string): { property: string; value: string } | null {
244
- const direct = extractPropertyAndValue(className);
245
- if (direct) return direct;
246
-
247
- for (const knownPrefix of SORTED_PREFIXES) {
248
- if (!className.startsWith(knownPrefix + '-')) continue;
249
- const classValue = className.substring(knownPrefix.length + 1);
250
- if (!/^h[0-9a-z]+$/.test(classValue)) continue;
251
- const cssProp = prefixToCSSProperty[knownPrefix];
252
- if (!cssProp) continue;
253
- const registered = getStyleValue(className);
254
- if (registered == null || registered === '') continue;
255
- return { property: cssProp, value: String(registered) };
256
- }
257
-
258
- return null;
144
+ const parsed = parseUtilityClass(className);
145
+ if (!parsed) return null;
146
+ if (parsed.value != null) return { property: parsed.property, value: parsed.value };
147
+
148
+ // Hash-fallback class: real value lives in the registry
149
+ const registered = getStyleValue(className);
150
+ if (registered == null || registered === '') return null;
151
+ const value = String(registered);
152
+ const property = parsed.root ? resolveRootProperty(parsed.root, value) : parsed.property;
153
+ return { property, value };
259
154
  }
260
155
 
156
+ /** Border-side class roots → CSS side shorthand base. */
157
+ const borderSideMap: Record<string, string> = {
158
+ 'border-t': 'border-top',
159
+ 'border-r': 'border-right',
160
+ 'border-b': 'border-bottom',
161
+ 'border-l': 'border-left',
162
+ };
163
+
261
164
  /**
262
165
  * Generate CSS rule for a utility class
263
- * Handles dynamic classes like p-10px, m-20px, fs-48px, etc.
166
+ * Handles arbitrary values (p-[10px]), variables (bg-(--primary)),
167
+ * keywords (w-full), bare numbers (z-10) and hash-fallback classes.
264
168
  */
265
169
  export function generateRuleForClass(className: string): string | null {
266
- // Check if it's a predefined rule
170
+ // Check if it's a predefined static/preset rule
267
171
  if (utilityClassRules[className]) {
268
172
  return utilityClassRules[className];
269
173
  }
270
174
 
271
- // Parse prefix-value pattern (e.g., "p-10px", "fs-48px", "p-92px-0", "bgc-background-light")
272
- // Try to match known prefixes first by checking longest prefixes first
273
- let prefix = '';
274
- let classValue = '';
175
+ const parsed = parseUtilityClass(className);
275
176
 
276
- for (const knownPrefix of SORTED_PREFIXES) {
277
- if (className.startsWith(knownPrefix + '-')) {
278
- prefix = knownPrefix;
279
- classValue = className.substring(knownPrefix.length + 1); // +1 for the hyphen
280
- break;
281
- }
282
- }
283
-
284
- if (!prefix || !classValue) {
285
- // Check dynamic registry for classes with unknown prefixes
177
+ if (!parsed) {
178
+ // Check dynamic registry for classes with unknown roots
286
179
  const dynamicStyle = getDynamicStyle(className);
287
180
  if (dynamicStyle) {
288
181
  return `${dynamicStyle.property}: ${dynamicStyle.value};`;
@@ -290,181 +183,66 @@ export function generateRuleForClass(className: string): string | null {
290
183
  return null;
291
184
  }
292
185
 
293
- // Look up the CSS property from prefix
294
- const cssProp = prefixToCSSProperty[prefix];
295
- if (!cssProp) {
296
- // Check dynamic registry for classes with unknown prefixes
186
+ // Arbitrary property syntax: [clip-path:circle(50%)] prefer the registered
187
+ // original value (exact), fall back to the decoded one.
188
+ if (parsed.kind === 'arbitrary-property') {
297
189
  const dynamicStyle = getDynamicStyle(className);
298
- if (dynamicStyle) {
299
- return `${dynamicStyle.property}: ${dynamicStyle.value};`;
300
- }
301
- return null;
302
- }
303
-
304
- // Handle border-side classes (bt-, bb-, bl-, border-r-) with special syntax FIRST
305
- // Generate ONLY width and style, NOT color - allows bc- to control color independently
306
- // e.g., bt-1px-solid → "border-top-width: 1px; border-top-style: solid;"
307
- // This must come BEFORE registry check to ensure consistent output
308
- const borderSideMap: Record<string, string> = {
309
- 'bt': 'border-top',
310
- 'bb': 'border-bottom',
311
- 'bl': 'border-left',
312
- 'border-r': 'border-right',
313
- };
314
- if (borderSideMap[prefix] && classValue.includes('-')) {
315
- const parts = classValue.split('-');
316
- const width = parts[0];
317
- const borderStyle = parts[1] || 'solid';
318
- const side = borderSideMap[prefix];
319
- return `${side}-width: ${width}; ${side}-style: ${borderStyle};`;
190
+ const value = dynamicStyle ? dynamicStyle.value : parsed.value;
191
+ return `${parsed.property}: ${value};`;
320
192
  }
321
193
 
322
- // Check registry for original value first (avoids lossy reverse-engineering)
323
- const originalValue = getStyleValue(className);
324
- if (originalValue !== undefined) {
325
- return `${cssProp}: ${originalValue};`;
326
- }
327
-
328
- // FALLBACK: Reverse-engineer value from class name for backward compatibility
329
- // (for classes extracted from HTML that weren't registered)
330
-
331
- // Handle border with special syntax BEFORE hyphen conversion (e.g., b-1px-solid-text)
332
- // This needs the original hyphenated format to parse correctly
333
- if (prefix === 'b' && classValue.includes('-')) {
334
- const parts = classValue.split('-');
335
- const width = parts[0];
336
- const borderStyle = parts[1] || 'solid';
337
- const borderColor = parts[2] ? `var(--${parts[2]})` : 'currentColor';
338
- return `border: ${width} ${borderStyle} ${borderColor};`;
339
- }
194
+ const root = parsed.root;
340
195
 
341
- // Handle border-radius with CSS variables (e.g., br-background)
342
- if (prefix === 'br' && classValue.includes('-') && !classValue.match(/^\d+px$/)) {
343
- return `border-radius: var(--${classValue});`;
344
- }
345
-
346
- // Convert hyphenated values back to spaces, but preserve negative numbers
347
- // Handle cases like:
348
- // - "92px-0" "92px 0" (space between values)
349
- // - "-0.015" → "-0.015" (negative number)
350
- // - "0-5px" "0 5px" (space between values)
351
- // - "1px-solid" "1px solid" (border shorthand)
352
- // - "translateY(-100vh)" → "translateY(-100vh)" (preserve negatives in functions)
353
- let value = classValue;
354
-
355
- // Pattern to match CSS values that should be preceded by a space instead of hyphen
356
- // Includes: digits, auto, inherit, initial, unset, border style keywords, and CSS function names
357
- const valuePattern = /\d|auto|inherit|initial|unset|solid|dashed|dotted|double|groove|ridge|inset|outset|none|hidden|minmax|repeat|clamp|calc|min|max|fit-content|var/;
358
-
359
- // Only convert hyphens that are NOT inside parentheses (function arguments)
360
- // This preserves negative values like translateY(-100vh) while still handling
361
- // multi-value shorthand like "10px-20px" → "10px 20px"
362
- const hasParentheses = value.includes('(');
363
-
364
- if (hasParentheses) {
365
- // Convert hyphens to spaces, handling both inside and outside parentheses
366
- // Inside parentheses: convert hyphens after commas (they were space separators)
367
- // - "repeat(3,-1fr)" → "repeat(3, 1fr)" (single hyphen = space)
368
- // - "clamp(-10px,--5px,-10px)" → "clamp(-10px, -5px, 10px)" (double hyphen = space + negative)
369
- let result = '';
370
- let depth = 0;
371
- let afterComma = false;
372
- for (let i = 0; i < value.length; i++) {
373
- const char = value[i];
374
- if (char === '(') {
375
- depth++;
376
- result += char;
377
- afterComma = false;
378
- } else if (char === ')') {
379
- depth--;
380
- result += char;
381
- afterComma = false;
382
- } else if (char === ',') {
383
- result += char;
384
- afterComma = true;
385
- } else if (char === '-' && afterComma && depth > 0) {
386
- // Hyphen after comma inside parentheses - convert to space
387
- // Check if next char is also hyphen (indicating actual negative number)
388
- if (value[i + 1] === '-') {
389
- // Double hyphen: ",--" means ", -" (space followed by negative)
390
- result += ' ';
391
- // Don't skip the next hyphen - let it be added normally
392
- } else {
393
- // Single hyphen: ",-" means ", " (just a space separator)
394
- result += ' ';
395
- }
396
- afterComma = false;
397
- } else if (char === '-' && depth > 0 && i > 0) {
398
- // Hyphens inside parentheses (not after comma) - convert if followed by digit/value
399
- // This handles cases like "#0f1442-94%" → "#0f1442 94%"
400
- const rest = value.substring(i + 1);
401
- if (valuePattern.test(rest[0] || '')) {
402
- result += ' ';
403
- } else {
404
- result += char;
405
- }
406
- afterComma = false;
407
- } else if (char === '-' && depth === 0 && i > 0) {
408
- // Hyphens outside parentheses - convert if followed by value pattern
409
- const rest = value.substring(i + 1);
410
- if (valuePattern.test(rest[0] || '')) {
411
- result += ' ';
412
- } else {
413
- result += char;
414
- }
415
- afterComma = false;
416
- } else {
417
- result += char;
418
- afterComma = false;
419
- }
420
- }
421
- value = result;
422
- } else if (value.startsWith('-')) {
423
- // For negative values, only replace hyphens that come after the first character
424
- // This way "-0.015" stays as is, but "-10px-5px" becomes "-10px 5px"
425
- value = value[0] + value.substring(1).replace(new RegExp(`-(?=${valuePattern.source})`, 'g'), ' ');
196
+ // Resolve the CSS value. The style-value registry holds the exact original
197
+ // for arbitrary/hash classes; variable, keyword and numeric forms decode
198
+ // deterministically from the class name itself.
199
+ let value: string;
200
+ let property = parsed.property;
201
+ if (parsed.kind === 'hash') {
202
+ const registered = getStyleValue(className);
203
+ if (registered == null) return null;
204
+ value = String(registered);
205
+ if (root) property = resolveRootProperty(root, value);
206
+ } else if (parsed.kind === 'arbitrary') {
207
+ const registered = getStyleValue(className);
208
+ value = registered !== undefined ? String(registered) : parsed.value!;
426
209
  } else {
427
- // For positive values, replace hyphens normally
428
- value = value.replace(new RegExp(`-(?=${valuePattern.source})`, 'g'), ' ');
210
+ value = parsed.value!;
429
211
  }
430
212
 
431
- // Convert 'p' suffix to '%' for percentage values (e.g., "50p" → "50%")
432
- value = value.replace(/(\d+)p(?!\w)/g, '$1%');
433
-
434
- // Handle special multi-property prefixes
435
- if (prefix === 'px') {
436
- // padding-left and padding-right
213
+ // Multi-property roots (Tailwind axis shorthands)
214
+ if (root === 'px') {
437
215
  return `padding-left: ${value}; padding-right: ${value};`;
438
216
  }
439
- if (prefix === 'py') {
440
- // padding-top and padding-bottom
217
+ if (root === 'py') {
441
218
  return `padding-top: ${value}; padding-bottom: ${value};`;
442
219
  }
443
- if (prefix === 'mx') {
444
- // margin-left and margin-right
220
+ if (root === 'mx') {
445
221
  return `margin-left: ${value}; margin-right: ${value};`;
446
222
  }
447
- if (prefix === 'my') {
448
- // margin-top and margin-bottom
223
+ if (root === 'my') {
449
224
  return `margin-top: ${value}; margin-bottom: ${value};`;
450
225
  }
451
226
 
452
- // Handle CSS variables for color properties
453
- // For color (c), background-color (bgc), and border-color (bc), treat as CSS variables
454
- // unless it's a hex color (#...) or RGB value
455
- if ((prefix === 'bgc' || prefix === 'bg' || prefix === 'c' || prefix === 'bc')) {
456
- // If it's not a hex color, RGB value, or pixel value, treat it as a CSS variable name
457
- // Unless it's a CSS named color (red, blue, transparent, etc.) — use directly
458
- if (!value.startsWith('#') && !value.includes('rgb') && !value.includes('px')) {
459
- if (isCssNamedColor(value)) {
460
- return `${cssProp}: ${value};`;
227
+ // Border-side classes (border-t-, border-r-, …): emit width/style (and color
228
+ // when present) as LONGHANDS, so a separate border-color class can still
229
+ // control the color independently (longhands sort after the side class).
230
+ if (root && borderSideMap[root] && property !== 'border-color') {
231
+ const side = borderSideMap[root];
232
+ const tokens = value.split(/\s+/).filter(Boolean);
233
+ if (tokens.length >= 2) {
234
+ const declarations = [`${side}-width: ${tokens[0]};`, `${side}-style: ${tokens[1]};`];
235
+ if (tokens[2]) {
236
+ declarations.push(`${side}-color: ${tokens.slice(2).join(' ')};`);
461
237
  }
462
- return `${cssProp}: var(--${value});`;
238
+ return declarations.join(' ');
463
239
  }
240
+ return `${side}: ${value};`;
464
241
  }
465
242
 
466
- // Standard case: prefix-value (e.g., p-10px, fs-48px, ta-center)
467
- return `${cssProp}: ${value};`;
243
+ // Standard case: property + decoded value (e.g. `padding: 10px;`,
244
+ // `color: var(--primary);`, `width: 100%;`)
245
+ return `${property}: ${value};`;
468
246
  }
469
247
 
470
248
  /**
@@ -480,7 +258,7 @@ function applyFluidToUtilityRule(
480
258
  rule: string,
481
259
  className: string,
482
260
  responsiveScales: ResponsiveScales,
483
- breakpoints: BreakpointConfig
261
+ breakpoints: BreakpointConfig,
484
262
  ): string {
485
263
  const propValue = resolveScalablePropertyValue(className);
486
264
  if (!propValue) return rule;
@@ -488,9 +266,7 @@ function applyFluidToUtilityRule(
488
266
  const category = AUTO_RESPONSIVE_TYPE_MAP[propValue.property];
489
267
  if (!category) return rule;
490
268
 
491
- const scaleConfig = responsiveScales[category as keyof ResponsiveScales] as
492
- | Record<string, number>
493
- | undefined;
269
+ const scaleConfig = responsiveScales[category as keyof ResponsiveScales] as Record<string, number> | undefined;
494
270
  if (!scaleConfig) return rule;
495
271
 
496
272
  const smallest = getSmallestBreakpointName(breakpoints);
@@ -501,13 +277,7 @@ function applyFluidToUtilityRule(
501
277
 
502
278
  const range = responsiveScales.fluidRange ?? DEFAULT_FLUID_RANGE;
503
279
  const baseRef = responsiveScales.baseReference ?? 16;
504
- const fluidValue = buildFluidPropertyValue(
505
- propValue.value,
506
- scale,
507
- range.min,
508
- range.max,
509
- baseRef
510
- );
280
+ const fluidValue = buildFluidPropertyValue(propValue.value, scale, range.min, range.max, baseRef);
511
281
  if (!fluidValue) return rule;
512
282
 
513
283
  return `${propValue.property}: ${fluidValue};`;
@@ -521,7 +291,7 @@ function applyFluidToUtilityRule(
521
291
  function applyFluidToStyle(
522
292
  style: StyleObject,
523
293
  responsiveScales: ResponsiveScales,
524
- breakpoints: BreakpointConfig
294
+ breakpoints: BreakpointConfig,
525
295
  ): StyleObject {
526
296
  const range = responsiveScales.fluidRange ?? DEFAULT_FLUID_RANGE;
527
297
  const baseRef = responsiveScales.baseReference ?? 16;
@@ -539,23 +309,13 @@ function applyFluidToStyle(
539
309
  continue;
540
310
  }
541
311
 
542
- const scale = getScaleMultiplier(
543
- responsiveScales,
544
- prop as CSSPropertyType,
545
- smallest
546
- );
312
+ const scale = getScaleMultiplier(responsiveScales, prop as CSSPropertyType, smallest);
547
313
  if (scale == null || scale === 1) {
548
314
  out[prop] = value;
549
315
  continue;
550
316
  }
551
317
 
552
- const fluidValue = buildFluidPropertyValue(
553
- String(value),
554
- scale,
555
- range.min,
556
- range.max,
557
- baseRef
558
- );
318
+ const fluidValue = buildFluidPropertyValue(String(value), scale, range.min, range.max, baseRef);
559
319
  out[prop] = fluidValue ?? value;
560
320
  }
561
321
  return out;
@@ -580,10 +340,7 @@ const CONTAINER_RESERVED_VALUES = new Set(['auto', 'inherit', 'initial', 'unset'
580
340
  * Margins are ALWAYS overwritten with `auto` (per user spec), even if the
581
341
  * caller had set explicit values.
582
342
  */
583
- export function applyContainerPattern(
584
- style: StyleObject,
585
- fluidActive: boolean
586
- ): StyleObject {
343
+ export function applyContainerPattern(style: StyleObject, fluidActive: boolean): StyleObject {
587
344
  if (!fluidActive) return style;
588
345
  const w = style.width;
589
346
  const mw = style.maxWidth;
@@ -609,7 +366,7 @@ export function generateUtilityCSS(
609
366
  usedClasses: Set<string>,
610
367
  breakpoints: BreakpointConfig = DEFAULT_BREAKPOINTS,
611
368
  responsiveScales?: ResponsiveScales,
612
- remConfig?: RemConversionConfig
369
+ remConfig?: RemConversionConfig,
613
370
  ): string {
614
371
  const css: string[] = [];
615
372
  const baseClasses = new Set<string>();
@@ -619,23 +376,17 @@ export function generateUtilityCSS(
619
376
  const breakpointValues = getBreakpointValues(breakpoints);
620
377
 
621
378
  // Create a map for responsive breakpoint classes
622
- // Map from prefix (e.g., 't', 'm') to the class name and breakpoint info
379
+ // Map from variant prefix (e.g. 'tablet:', 'mobile:') to the class names and breakpoint info
623
380
  type BreakpointClassMap = Record<string, { classes: Set<string>; breakpointName: string; value: number }>;
624
381
  const responsiveClasses: BreakpointClassMap = {};
625
382
 
626
383
  // Initialize responsive class sets for each breakpoint
627
384
  for (const [breakpointName, breakpointValue] of Object.entries(breakpointValues)) {
628
- // Generate prefix from breakpoint name, avoiding conflicts with property prefixes
629
- // For 'mobile', use 'mob' to avoid conflict with 'margin' (m-), etc.
630
- let prefix = breakpointName.charAt(0).toLowerCase();
631
- if (breakpointName.toLowerCase() === 'mobile') {
632
- prefix = 'mob'; // Use 'mob' instead of 'm' to avoid margin conflict
633
- }
634
-
385
+ const prefix = breakpointClassPrefix(breakpointName);
635
386
  responsiveClasses[prefix] = {
636
387
  classes: new Set<string>(),
637
388
  breakpointName,
638
- value: breakpointValue
389
+ value: breakpointValue,
639
390
  };
640
391
  }
641
392
 
@@ -643,15 +394,12 @@ export function generateUtilityCSS(
643
394
  for (const className of usedClasses) {
644
395
  let matched = false;
645
396
 
646
- // Try to match against any responsive breakpoint prefix
397
+ // Try to match against any responsive breakpoint variant prefix
647
398
  for (const prefix of Object.keys(responsiveClasses)) {
648
- if (className.startsWith(`${prefix}-`) && className.length > prefix.length + 1) {
649
- const potentialClass = className.substring(prefix.length + 1);
399
+ if (className.startsWith(prefix) && className.length > prefix.length) {
400
+ const potentialClass = className.substring(prefix.length);
650
401
  const rule = generateRuleForClass(potentialClass);
651
-
652
- // If it generates a valid rule and doesn't look like a margin value, treat as breakpoint class
653
- // Margin values are typically: auto, 0, or pixel/percentage values (like 10px, 92px, 50p)
654
- if (rule && !potentialClass.match(/^(auto|0|[\d.]+px|[\d.]+p)$/)) {
402
+ if (rule) {
655
403
  responsiveClasses[prefix].classes.add(potentialClass);
656
404
  matched = true;
657
405
  break;
@@ -699,8 +447,15 @@ export function generateUtilityCSS(
699
447
 
700
448
  // Generate auto-responsive rules for classes with enabled scaling.
701
449
  // In fluid mode the base rule already encodes scaling via clamp(), so skip @media.
450
+ // Sort shorthand-before-longhand exactly like the base rules above: within a breakpoint
451
+ // block, a `margin` shorthand emitted AFTER a `margin-bottom` longhand would clobber it
452
+ // (equal specificity ⇒ source order wins), silently dropping the longhand on that
453
+ // breakpoint. The base section is sorted, so the cascade would flip ONLY inside @media —
454
+ // and the unsorted Set's order depends on collection order (SSR render vs .astro walk),
455
+ // so the same project could render correctly in one path and lose the longhand in the
456
+ // other. Sorting makes the @media cascade deterministic and consistent with base.
702
457
  if (responsiveScales?.enabled && !fluidActive) {
703
- for (const className of autoResponsiveClasses) {
458
+ for (const className of sortClassesByPropertyOrder(autoResponsiveClasses)) {
704
459
  const propValue = resolveScalablePropertyValue(className);
705
460
  if (!propValue) continue;
706
461
 
@@ -727,16 +482,17 @@ export function generateUtilityCSS(
727
482
  if (!autoResponsiveMediaQueries[breakpointName]) {
728
483
  autoResponsiveMediaQueries[breakpointName] = {
729
484
  classes: [],
730
- value: breakpointValue
485
+ value: breakpointValue,
731
486
  };
732
487
  }
733
488
 
734
- const finalScaledValue = (remConfig?.enabled && shouldConvertProperty(propValue.property))
735
- ? convertPxToRem(scaledValue, remConfig.baseFontSize)
736
- : scaledValue;
489
+ const finalScaledValue =
490
+ remConfig?.enabled && shouldConvertProperty(propValue.property)
491
+ ? convertPxToRem(scaledValue, remConfig.baseFontSize)
492
+ : scaledValue;
737
493
  autoResponsiveMediaQueries[breakpointName].classes.push({
738
494
  className: escapedClassName,
739
- rule: `${propValue.property}: ${finalScaledValue};`
495
+ rule: `${propValue.property}: ${finalScaledValue};`,
740
496
  });
741
497
  }
742
498
  }
@@ -745,14 +501,13 @@ export function generateUtilityCSS(
745
501
  // Generate media queries in two separate sections:
746
502
  // 1. Auto-responsive classes FIRST (baseline responsive behavior)
747
503
  // 2. Manual breakpoint classes AFTER (explicit overrides)
748
- // This ensures manual t-, mob- classes always override auto-responsive rules
504
+ // This ensures manual tablet:/mobile: classes always override auto-responsive rules
749
505
 
750
506
  // Sort auto-responsive media queries by breakpoint value (descending)
751
- const sortedAutoResponsive = Object.entries(autoResponsiveMediaQueries)
752
- .sort(([, a], [, b]) => b.value - a.value);
507
+ const sortedAutoResponsive = Object.entries(autoResponsiveMediaQueries).sort(([, a], [, b]) => b.value - a.value);
753
508
 
754
509
  // Generate auto-responsive media queries first
755
- for (const [breakpointName, mq] of sortedAutoResponsive) {
510
+ for (const [, mq] of sortedAutoResponsive) {
756
511
  if (mq.classes.length === 0) continue;
757
512
 
758
513
  css.push(`@media (max-width: ${mq.value}px) {`);
@@ -771,12 +526,12 @@ export function generateUtilityCSS(
771
526
  for (const [prefix, breakpointInfo] of sortedManualResponsive) {
772
527
  const rules: string[] = [];
773
528
 
774
- for (const className of breakpointInfo.classes) {
529
+ for (const className of sortClassesByPropertyOrder(breakpointInfo.classes)) {
775
530
  const rule = generateRuleForClass(className);
776
531
  if (rule) {
777
- const escapedClassName = escapeCSSClassName(className);
532
+ const escapedClassName = escapeCSSClassName(`${prefix}${className}`);
778
533
  const finalRule = applyRemConversion(rule, remConfig);
779
- rules.push(` .${prefix}-${escapedClassName} { ${finalRule} }`);
534
+ rules.push(` .${escapedClassName} { ${finalRule} }`);
780
535
  }
781
536
  }
782
537
 
@@ -802,35 +557,28 @@ export function generateSingleClassCSS(
802
557
  className: string,
803
558
  breakpoints: BreakpointConfig = DEFAULT_BREAKPOINTS,
804
559
  responsiveScales?: ResponsiveScales,
805
- remConfig?: RemConversionConfig
560
+ remConfig?: RemConversionConfig,
806
561
  ): string {
807
562
  const breakpointValues = getBreakpointValues(breakpoints);
808
563
  const css: string[] = [];
809
564
 
810
- // Build responsive prefix map: prefix → { breakpointName, value }
565
+ // Build responsive variant prefix map: prefix → { breakpointName, value }
811
566
  const responsivePrefixes: Record<string, { breakpointName: string; value: number }> = {};
812
567
  for (const [breakpointName, breakpointValue] of Object.entries(breakpointValues)) {
813
- let prefix = breakpointName.charAt(0).toLowerCase();
814
- if (breakpointName.toLowerCase() === 'mobile') {
815
- prefix = 'mob';
816
- }
817
- responsivePrefixes[prefix] = { breakpointName, value: breakpointValue };
568
+ responsivePrefixes[breakpointClassPrefix(breakpointName)] = { breakpointName, value: breakpointValue };
818
569
  }
819
570
 
820
- // Check if class has a responsive prefix (e.g. "t-fs-16px", "mob-p-10px")
571
+ // Check if class has a responsive variant prefix (e.g. "tablet:text-[16px]", "mobile:p-[10px]")
821
572
  let matched = false;
822
573
  for (const prefix of Object.keys(responsivePrefixes)) {
823
- if (className.startsWith(`${prefix}-`) && className.length > prefix.length + 1) {
824
- const potentialClass = className.substring(prefix.length + 1);
574
+ if (className.startsWith(prefix) && className.length > prefix.length) {
575
+ const potentialClass = className.substring(prefix.length);
825
576
  const rule = generateRuleForClass(potentialClass);
826
-
827
- // Only treat as responsive if it generates a valid rule and isn't a margin value
828
- if (rule && !potentialClass.match(/^(auto|0|[\d.]+px|[\d.]+p)$/)) {
577
+ if (rule) {
829
578
  const escapedClassName = escapeCSSClassName(className);
830
- const escapedInnerClass = escapeCSSClassName(potentialClass);
831
579
  const bpValue = responsivePrefixes[prefix].value;
832
580
  const finalRule = applyRemConversion(rule, remConfig);
833
- css.push(`@media (max-width: ${bpValue}px) {\n .${prefix}-${escapedInnerClass} { ${finalRule} }\n}`);
581
+ css.push(`@media (max-width: ${bpValue}px) {\n .${escapedClassName} { ${finalRule} }\n}`);
834
582
  matched = true;
835
583
  break;
836
584
  }
@@ -863,13 +611,14 @@ export function generateSingleClassCSS(
863
611
  if (propValue) {
864
612
  const category = AUTO_RESPONSIVE_TYPE_MAP[propValue.property];
865
613
  if (category) {
866
- const scaleConfig = responsiveScales[category as keyof ResponsiveScales] as Record<string, number> | undefined;
614
+ const scaleConfig = responsiveScales[category as keyof ResponsiveScales] as
615
+ | Record<string, number>
616
+ | undefined;
867
617
  if (scaleConfig) {
868
618
  const baseRef = responsiveScales.baseReference || 16;
869
619
 
870
620
  // Sort breakpoints descending (largest first) for proper cascade
871
- const sortedBps = Object.entries(breakpointValues)
872
- .sort(([, a], [, b]) => b - a);
621
+ const sortedBps = Object.entries(breakpointValues).sort(([, a], [, b]) => b - a);
873
622
 
874
623
  for (const [breakpointName, breakpointValue] of sortedBps) {
875
624
  const scale = scaleConfig[breakpointName];
@@ -878,10 +627,13 @@ export function generateSingleClassCSS(
878
627
  const scaledValue = scalePropertyValue(propValue.value, baseRef, scale);
879
628
  if (!scaledValue) continue;
880
629
 
881
- const finalScaledValue = (remConfig?.enabled && shouldConvertProperty(propValue.property))
882
- ? convertPxToRem(scaledValue, remConfig.baseFontSize)
883
- : scaledValue;
884
- css.push(`@media (max-width: ${breakpointValue}px) {\n .${escapedClassName} { ${propValue.property}: ${finalScaledValue}; }\n}`);
630
+ const finalScaledValue =
631
+ remConfig?.enabled && shouldConvertProperty(propValue.property)
632
+ ? convertPxToRem(scaledValue, remConfig.baseFontSize)
633
+ : scaledValue;
634
+ css.push(
635
+ `@media (max-width: ${breakpointValue}px) {\n .${escapedClassName} { ${propValue.property}: ${finalScaledValue}; }\n}`,
636
+ );
885
637
  }
886
638
  }
887
639
  }
@@ -893,99 +645,33 @@ export function generateSingleClassCSS(
893
645
  }
894
646
 
895
647
  /**
896
- * Extract all utility classes from a rendered HTML string
897
- * Identifies utility classes by checking against known prefixes from propertyMap
648
+ * Extract all utility classes from a rendered HTML string.
649
+ * A class counts as a utility when (after stripping an optional `breakpoint:`
650
+ * variant prefix) it parses as a Meno utility class — static (`flex`,
651
+ * `justify-center`), arbitrary (`p-[10px]`), variable (`bg-(--primary)`),
652
+ * keyword (`w-full`), numeric (`z-10`), preset (`shadow-2`),
653
+ * arbitrary-property (`[clip-path:…]`) or hash-fallback — or when it was
654
+ * registered in the dynamic style registry.
898
655
  */
899
656
  export function extractUtilityClassesFromHTML(html: string): Set<string> {
900
657
  const classes = new Set<string>();
901
658
  const classRegex = /class="([^"]*)"/g;
902
659
  let match;
903
660
 
904
- // Get all known utility prefixes from propertyMap
905
- const knownPrefixes = new Set(Object.values(propertyMap));
906
- // Also include special/short form prefixes from utilityClassRules
907
- const specialPrefixes = new Set([
908
- // Display short forms
909
- 'f', 'fd-col', 'fd-row', 'g', 'b', 'i', 'ib', 'h', 'if', 'ig',
910
- // Justify content
911
- 'jc-c', 'jc-s', 'jc-e', 'jc-b', 'jc-a',
912
- // Align items
913
- 'ai-c', 'ai-s', 'ai-e', 'ai-b',
914
- // Overflow
915
- 'o-h', 'o-a', 'o-s', 'o-v',
916
- // Cursor
917
- 'cursor-pointer', 'cursor-default',
918
- // White space
919
- 'whs-normal', 'whs-nowrap', 'whs-pre', 'whs-pre-wrap', 'whs-pre-line',
920
- ]);
921
-
922
661
  while ((match = classRegex.exec(html)) !== null) {
923
- const classString = match[1];
924
- const classList = classString.split(/\s+/);
662
+ const classList = match[1].split(/\s+/);
925
663
 
926
664
  for (const className of classList) {
927
- // Skip empty class names
928
- if (!className || className.length === 0) continue;
665
+ if (!className) continue;
929
666
 
930
- // Check if it's a special/short form class
931
- if (specialPrefixes.has(className)) {
667
+ const { base } = splitVariantPrefix(className);
668
+ if (parseUtilityClass(base)) {
932
669
  classes.add(className);
933
670
  continue;
934
671
  }
935
672
 
936
- // Check for responsive prefix (like 't-', 'mob-', 's-', etc.) and process accordingly
937
- let classToCheck = className;
938
- let hasResponsivePrefix = false;
939
-
940
- // Extract responsive prefix if present
941
- // Check for multi-letter prefix first (e.g., 'mob-' for mobile)
942
- if (className.startsWith('mob-') && className.length > 4) {
943
- classToCheck = className.substring(4); // Remove 'mob-' prefix
944
- hasResponsivePrefix = true;
945
- }
946
- // Then check for single-letter prefixes (t-, s-, l-, x-, u-)
947
- else if (className.length > 2 && className.charAt(1) === '-' && className.match(/^[a-z]-/)) {
948
- const firstChar = className.charAt(0);
949
- // Only treat as responsive prefix if it looks like a breakpoint indicator
950
- // Common breakpoint prefixes: t (tablet), s (small), x (extra), u (ultra)
951
- // NOTE: We exclude 'm' because it conflicts with margin prefix - use 'mob' instead
952
- // NOTE: We exclude 'l' because it conflicts with left property prefix - use 'lg' instead
953
- if (['t', 's', 'x', 'u'].includes(firstChar)) {
954
- classToCheck = className.substring(2); // Remove responsive prefix
955
- hasResponsivePrefix = true;
956
- }
957
- }
958
-
959
- // Check if it starts with a known prefix
960
- // Handle both single-letter (like 'p-', 'w-') and multi-letter (like 'jc-', 'ai-')
961
- for (const prefix of knownPrefixes) {
962
- if (classToCheck === prefix) {
963
- // Exact match (like 'f' for flex, 'g' for grid)
964
- classes.add(className);
965
- break;
966
- } else if (classToCheck.startsWith(prefix + '-')) {
967
- // Prefix with hyphen separator (like 'p-10px', 'fs-16px', or 't-p-10px')
968
- classes.add(className);
969
- break;
970
- }
971
- }
972
-
973
- // Also check special prefixes for responsive classes
974
- if (hasResponsivePrefix && !classes.has(className)) {
975
- for (const specialPrefix of specialPrefixes) {
976
- if (classToCheck === specialPrefix) {
977
- classes.add(className);
978
- break;
979
- } else if (classToCheck.startsWith(specialPrefix + '-')) {
980
- classes.add(className);
981
- break;
982
- }
983
- }
984
- }
985
-
986
- // Check if it's a dynamic class (registered in the dynamic registry)
987
- // This handles classes for properties not in propertyMap
988
- if (!classes.has(className) && isDynamicClass(className)) {
673
+ // Dynamic classes (registered for properties without a known root)
674
+ if (isDynamicClass(className) || isDynamicClass(base)) {
989
675
  classes.add(className);
990
676
  }
991
677
  }
@@ -1002,18 +688,20 @@ export function extractUtilityClassesFromHTML(html: string): Set<string> {
1002
688
  * Check if a style object is responsive (has base/breakpoint structure)
1003
689
  */
1004
690
  function isResponsiveStyle(style: StyleValue): style is ResponsiveStyleObject {
1005
- return (
1006
- typeof style === 'object' &&
1007
- style !== null &&
1008
- ('base' in style || 'tablet' in style || 'mobile' in style)
1009
- );
691
+ return typeof style === 'object' && style !== null && ('base' in style || 'tablet' in style || 'mobile' in style);
1010
692
  }
1011
693
 
1012
694
  /**
1013
695
  * Convert a flat style object to CSS property declarations
1014
696
  * Returns string like "padding: 10px; background-color: var(--primary);"
697
+ *
698
+ * Exported for the studio's astro-play optimistic style preview: it converts a
699
+ * single changed declaration to the same kebab-case + `var(--name)` color form
700
+ * the server's utility/theme CSS uses, so the inline preview matches the value
701
+ * the recompiled stylesheet will land with. `_mapping` (binding) values are
702
+ * skipped, so a bound property yields an empty string (no optimistic preview).
1015
703
  */
1016
- function styleObjectToCSS(style: StyleObject): string {
704
+ export function styleObjectToCSS(style: StyleObject): string {
1017
705
  const declarations: string[] = [];
1018
706
 
1019
707
  for (const [prop, value] of Object.entries(style)) {
@@ -1065,7 +753,7 @@ export function generateInteractiveCSS(
1065
753
  interactiveStyles: InteractiveStyles,
1066
754
  breakpoints: BreakpointConfig = DEFAULT_BREAKPOINTS,
1067
755
  remConfig?: RemConversionConfig,
1068
- responsiveScales?: ResponsiveScales
756
+ responsiveScales?: ResponsiveScales,
1069
757
  ): string {
1070
758
  const css: string[] = [];
1071
759
 
@@ -1082,9 +770,7 @@ export function generateInteractiveCSS(
1082
770
  const fullSelector = `${prefix || ''}.${elementClass}${postfix || ''}`;
1083
771
 
1084
772
  // Breakpoint styles (sorted by value descending)
1085
- const sortedBreakpoints = Object.entries(breakpointValues).sort(
1086
- ([, a], [, b]) => b - a
1087
- );
773
+ const sortedBreakpoints = Object.entries(breakpointValues).sort(([, a], [, b]) => b - a);
1088
774
 
1089
775
  if (isResponsiveStyle(style)) {
1090
776
  // Generate responsive rules
@@ -1120,7 +806,7 @@ export function generateInteractiveCSS(
1120
806
  responsive.base,
1121
807
  responsiveScales!,
1122
808
  breakpointName,
1123
- new Set(Object.keys(explicit ?? {}))
809
+ new Set(Object.keys(explicit ?? {})),
1124
810
  );
1125
811
  if (Object.keys(scaled).length > 0) {
1126
812
  merged = { ...(merged ?? {}), ...scaled };
@@ -1130,9 +816,7 @@ export function generateInteractiveCSS(
1130
816
  if (merged && Object.keys(merged).length > 0) {
1131
817
  const properties = applyRemConversion(styleObjectToCSS(merged), remConfig);
1132
818
  if (properties) {
1133
- css.push(
1134
- `@media (max-width: ${breakpointValue}px) { ${fullSelector} { ${properties}; } }`
1135
- );
819
+ css.push(`@media (max-width: ${breakpointValue}px) { ${fullSelector} { ${properties}; } }`);
1136
820
  }
1137
821
  }
1138
822
  }
@@ -1153,18 +837,11 @@ export function generateInteractiveCSS(
1153
837
  // Auto-scale the flat style into each enabled breakpoint — breakpoints mode only.
1154
838
  if (scalingEnabled && !fluidActive) {
1155
839
  for (const [breakpointName, breakpointValue] of sortedBreakpoints) {
1156
- const scaled = scaleStyleForBreakpoint(
1157
- flatStyle,
1158
- responsiveScales!,
1159
- breakpointName,
1160
- new Set()
1161
- );
840
+ const scaled = scaleStyleForBreakpoint(flatStyle, responsiveScales!, breakpointName, new Set());
1162
841
  if (Object.keys(scaled).length === 0) continue;
1163
842
  const scaledProps = applyRemConversion(styleObjectToCSS(scaled), remConfig);
1164
843
  if (!scaledProps) continue;
1165
- css.push(
1166
- `@media (max-width: ${breakpointValue}px) { ${fullSelector} { ${scaledProps}; } }`
1167
- );
844
+ css.push(`@media (max-width: ${breakpointValue}px) { ${fullSelector} { ${scaledProps}; } }`);
1168
845
  }
1169
846
  }
1170
847
  }
@@ -1184,7 +861,7 @@ function scaleStyleForBreakpoint(
1184
861
  baseStyle: StyleObject,
1185
862
  responsiveScales: ResponsiveScales,
1186
863
  breakpointName: string,
1187
- skipProperties: Set<string>
864
+ skipProperties: Set<string>,
1188
865
  ): StyleObject {
1189
866
  const result: StyleObject = {};
1190
867
  const baseRef = responsiveScales.baseReference ?? 16;
@@ -1197,11 +874,7 @@ function scaleStyleForBreakpoint(
1197
874
  const strValue = String(value);
1198
875
  if (strValue === '') continue;
1199
876
 
1200
- const scale = getScaleMultiplier(
1201
- responsiveScales,
1202
- property as CSSPropertyType,
1203
- breakpointName
1204
- );
877
+ const scale = getScaleMultiplier(responsiveScales, property as CSSPropertyType, breakpointName);
1205
878
  if (scale == null) continue;
1206
879
 
1207
880
  const scaledValue = scalePropertyValue(strValue, baseRef, scale);
@@ -1224,7 +897,7 @@ export function generateAllInteractiveCSS(
1224
897
  interactiveStylesMap: Map<string, InteractiveStyles>,
1225
898
  breakpoints: BreakpointConfig = DEFAULT_BREAKPOINTS,
1226
899
  remConfig?: RemConversionConfig,
1227
- responsiveScales?: ResponsiveScales
900
+ responsiveScales?: ResponsiveScales,
1228
901
  ): string {
1229
902
  const cssBlocks: string[] = [];
1230
903