meno-core 1.0.53 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (415) hide show
  1. package/.claude/settings.local.json +1 -3
  2. package/bin/cli.ts +66 -261
  3. package/build-astro.ts +150 -128
  4. package/dist/bin/cli.js +42 -185
  5. package/dist/bin/cli.js.map +2 -2
  6. package/dist/chunks/chunk-2AR55GYH.js +42 -0
  7. package/dist/chunks/chunk-2AR55GYH.js.map +7 -0
  8. package/dist/chunks/{chunk-JGWFTO6P.js → chunk-2IIQK7T3.js} +2624 -1389
  9. package/dist/chunks/chunk-2IIQK7T3.js.map +7 -0
  10. package/dist/chunks/chunk-4ZRU52J2.js +169 -0
  11. package/dist/chunks/chunk-4ZRU52J2.js.map +7 -0
  12. package/dist/chunks/{chunk-UB44F4Z2.js → chunk-5X4HCN7J.js} +5 -7
  13. package/dist/chunks/chunk-5X4HCN7J.js.map +7 -0
  14. package/dist/chunks/{chunk-WQFG7PAH.js → chunk-7CCVOL5M.js} +15 -19
  15. package/dist/chunks/chunk-7CCVOL5M.js.map +7 -0
  16. package/dist/chunks/{chunk-YBLHKYFF.js → chunk-AR7LHIFY.js} +8 -2
  17. package/dist/chunks/chunk-AR7LHIFY.js.map +7 -0
  18. package/dist/chunks/{chunk-O3NAGJP4.js → chunk-NUP7H7D3.js} +135 -41
  19. package/dist/chunks/chunk-NUP7H7D3.js.map +7 -0
  20. package/dist/chunks/{chunk-X754AHS5.js → chunk-QWTQZHG3.js} +194 -90
  21. package/dist/chunks/chunk-QWTQZHG3.js.map +7 -0
  22. package/dist/chunks/{chunk-H3GJ4H2U.js → chunk-XTKNX4FW.js} +216 -87
  23. package/dist/chunks/chunk-XTKNX4FW.js.map +7 -0
  24. package/dist/chunks/{fs-JGINUXGL.js → fs-G3ANWEFP.js} +2 -2
  25. package/dist/lib/client/index.js +1003 -475
  26. package/dist/lib/client/index.js.map +3 -3
  27. package/dist/lib/server/index.js +14393 -6371
  28. package/dist/lib/server/index.js.map +4 -4
  29. package/dist/lib/shared/index.js +342 -101
  30. package/dist/lib/shared/index.js.map +4 -4
  31. package/dist/lib/shared/richtext/index.js +1 -1
  32. package/dist/lib/test-utils/index.js +64 -80
  33. package/dist/lib/test-utils/index.js.map +2 -2
  34. package/entries/client-router.tsx +10 -8
  35. package/lib/client/ClientInitializer.ts +8 -8
  36. package/lib/client/ErrorBoundary.test.tsx +246 -186
  37. package/lib/client/ErrorBoundary.tsx +183 -132
  38. package/lib/client/componentRegistry.test.ts +96 -108
  39. package/lib/client/componentRegistry.ts +1 -3
  40. package/lib/client/contexts/ThemeContext.tsx +3 -2
  41. package/lib/client/core/ComponentBuilder.test.ts +663 -560
  42. package/lib/client/core/ComponentBuilder.ts +427 -155
  43. package/lib/client/core/ComponentRenderer.test.tsx +2 -4
  44. package/lib/client/core/ComponentRenderer.tsx +46 -33
  45. package/lib/client/core/builders/embedBuilder.ts +242 -54
  46. package/lib/client/core/builders/linkBuilder.ts +72 -45
  47. package/lib/client/core/builders/linkNodeBuilder.ts +79 -54
  48. package/lib/client/core/builders/listBuilder.ts +155 -89
  49. package/lib/client/core/builders/localeListBuilder.ts +97 -62
  50. package/lib/client/core/builders/types.ts +5 -5
  51. package/lib/client/core/cmsTemplateProcessor.ts +8 -9
  52. package/lib/client/elementRegistry.ts +3 -3
  53. package/lib/client/fontFamiliesService.test.ts +2 -10
  54. package/lib/client/fontFamiliesService.ts +3 -3
  55. package/lib/client/hmr/HMRManager.tsx +47 -30
  56. package/lib/client/hmrCssReload.ts +49 -14
  57. package/lib/client/hmrWebSocket.ts +14 -19
  58. package/lib/client/hooks/useColorVariables.test.ts +21 -21
  59. package/lib/client/hooks/useColorVariables.ts +23 -18
  60. package/lib/client/hooks/usePropertyAutocomplete.ts +4 -6
  61. package/lib/client/hooks/useVariables.ts +6 -4
  62. package/lib/client/hydration/HydrationUtils.test.ts +24 -25
  63. package/lib/client/hydration/HydrationUtils.ts +3 -4
  64. package/lib/client/i18nConfigService.test.ts +2 -7
  65. package/lib/client/i18nConfigService.ts +2 -2
  66. package/lib/client/meno-filter/MenoFilter.test.ts +47 -49
  67. package/lib/client/meno-filter/MenoFilter.ts +14 -15
  68. package/lib/client/meno-filter/bindings.ts +18 -43
  69. package/lib/client/meno-filter/init.ts +4 -4
  70. package/lib/client/meno-filter/renderer.ts +30 -32
  71. package/lib/client/meno-filter/script.generated.ts +1 -3
  72. package/lib/client/meno-filter/ui.ts +4 -6
  73. package/lib/client/meno-filter/updates.ts +18 -23
  74. package/lib/client/meno-filter/utils.ts +1 -1
  75. package/lib/client/navigation.test.ts +174 -178
  76. package/lib/client/navigation.ts +0 -1
  77. package/lib/client/responsiveStyleResolver.knownTokens.test.ts +71 -0
  78. package/lib/client/responsiveStyleResolver.test.ts +234 -232
  79. package/lib/client/responsiveStyleResolver.ts +74 -20
  80. package/lib/client/routing/RouteLoader.test.ts +24 -25
  81. package/lib/client/routing/RouteLoader.ts +27 -35
  82. package/lib/client/routing/Router.tsx +210 -153
  83. package/lib/client/scripts/ScriptExecutor.test.ts +157 -158
  84. package/lib/client/scripts/ScriptExecutor.ts +18 -36
  85. package/lib/client/services/PrefetchService.test.ts +3 -3
  86. package/lib/client/services/PrefetchService.ts +23 -29
  87. package/lib/client/styleProcessor.test.ts +9 -9
  88. package/lib/client/styleProcessor.ts +24 -15
  89. package/lib/client/styles/StyleInjector.test.ts +122 -115
  90. package/lib/client/styles/StyleInjector.ts +10 -7
  91. package/lib/client/styles/UtilityClassCollector.ts +62 -28
  92. package/lib/client/styles/cspNonce.test.ts +2 -5
  93. package/lib/client/templateEngine.test.ts +554 -488
  94. package/lib/client/templateEngine.ts +203 -73
  95. package/lib/client/theme.test.ts +1 -1
  96. package/lib/client/theme.ts +0 -1
  97. package/lib/client/utils/toast.ts +0 -1
  98. package/lib/server/__integration__/api-routes.test.ts +8 -4
  99. package/lib/server/__integration__/cms-integration.test.ts +1 -4
  100. package/lib/server/__integration__/server-lifecycle.test.ts +4 -5
  101. package/lib/server/__integration__/ssr-rendering.test.ts +47 -37
  102. package/lib/server/__integration__/static-assets.test.ts +3 -3
  103. package/lib/server/__integration__/test-helpers.ts +87 -74
  104. package/lib/server/ab/generateFunctions.ts +12 -10
  105. package/lib/server/astro/cmsPageEmitter.ts +26 -42
  106. package/lib/server/astro/componentEmitter.ts +35 -43
  107. package/lib/server/astro/cssCollector.ts +10 -26
  108. package/lib/server/astro/nodeToAstro.test.ts +1754 -34
  109. package/lib/server/astro/nodeToAstro.ts +232 -216
  110. package/lib/server/astro/normalizeOrphanTemplateProps.test.ts +115 -119
  111. package/lib/server/astro/normalizeOrphanTemplateProps.ts +11 -18
  112. package/lib/server/astro/pageEmitter.ts +9 -13
  113. package/lib/server/astro/tailwindMapper.test.ts +10 -37
  114. package/lib/server/astro/tailwindMapper.ts +33 -40
  115. package/lib/server/astro/templateTransformer.ts +17 -20
  116. package/lib/server/createServer.ts +7 -19
  117. package/lib/server/cssGenerator.test.ts +35 -44
  118. package/lib/server/cssGenerator.ts +8 -19
  119. package/lib/server/fileWatcher.test.ts +164 -13
  120. package/lib/server/fileWatcher.ts +209 -124
  121. package/lib/server/index.ts +14 -17
  122. package/lib/server/jsonLoader.test.ts +39 -2
  123. package/lib/server/jsonLoader.ts +39 -34
  124. package/lib/server/middleware/cors.test.ts +21 -21
  125. package/lib/server/middleware/cors.ts +28 -4
  126. package/lib/server/middleware/errorHandler.test.ts +7 -5
  127. package/lib/server/middleware/errorHandler.ts +3 -8
  128. package/lib/server/middleware/index.ts +0 -1
  129. package/lib/server/middleware/logger.test.ts +10 -8
  130. package/lib/server/middleware/logger.ts +17 -29
  131. package/lib/server/migrateTemplates.ts +3 -3
  132. package/lib/server/pageCache.test.ts +76 -77
  133. package/lib/server/pageCache.ts +0 -1
  134. package/lib/server/projectContext.ts +5 -4
  135. package/lib/server/providers/fileSystemCMSProvider.test.ts +142 -99
  136. package/lib/server/providers/fileSystemCMSProvider.ts +62 -38
  137. package/lib/server/routes/api/cms.test.ts +26 -14
  138. package/lib/server/routes/api/cms.ts +9 -14
  139. package/lib/server/routes/api/colors.test.ts +2 -2
  140. package/lib/server/routes/api/components.ts +31 -35
  141. package/lib/server/routes/api/config.ts +3 -4
  142. package/lib/server/routes/api/core-routes.ts +49 -76
  143. package/lib/server/routes/api/enums.test.ts +2 -2
  144. package/lib/server/routes/api/functions.ts +9 -17
  145. package/lib/server/routes/api/index.ts +3 -6
  146. package/lib/server/routes/api/pages.ts +8 -11
  147. package/lib/server/routes/api/shared.test.ts +1 -1
  148. package/lib/server/routes/api/shared.ts +12 -9
  149. package/lib/server/routes/api/variables.test.ts +3 -5
  150. package/lib/server/routes/api/variables.ts +1 -1
  151. package/lib/server/routes/index.ts +25 -28
  152. package/lib/server/routes/pages.ts +35 -31
  153. package/lib/server/routes/static.ts +17 -6
  154. package/lib/server/runtime/bundler.ts +98 -61
  155. package/lib/server/runtime/fs.ts +16 -26
  156. package/lib/server/runtime/httpServer.ts +8 -12
  157. package/lib/server/services/ColorService.test.ts +3 -3
  158. package/lib/server/services/ColorService.ts +37 -29
  159. package/lib/server/services/EnumService.test.ts +4 -8
  160. package/lib/server/services/EnumService.ts +11 -3
  161. package/lib/server/services/VariableService.test.ts +8 -12
  162. package/lib/server/services/VariableService.ts +11 -3
  163. package/lib/server/services/cmsService.test.ts +158 -120
  164. package/lib/server/services/cmsService.ts +36 -146
  165. package/lib/server/services/componentService.test.ts +125 -45
  166. package/lib/server/services/componentService.ts +70 -46
  167. package/lib/server/services/configService.test.ts +22 -45
  168. package/lib/server/services/configService.ts +50 -27
  169. package/lib/server/services/fileWatcherService.ts +42 -8
  170. package/lib/server/services/index.ts +0 -1
  171. package/lib/server/services/pageService.test.ts +8 -10
  172. package/lib/server/services/pageService.ts +32 -18
  173. package/lib/server/ssr/attributeBuilder.ts +18 -8
  174. package/lib/server/ssr/buildErrorOverlay.ts +11 -12
  175. package/lib/server/ssr/clientDataInjector.ts +7 -21
  176. package/lib/server/ssr/cmsSSRProcessor.ts +4 -8
  177. package/lib/server/ssr/cssCollector.ts +1 -1
  178. package/lib/server/ssr/errorOverlay.test.ts +1 -1
  179. package/lib/server/ssr/errorOverlay.ts +3 -9
  180. package/lib/server/ssr/htmlGenerator.nonce.test.ts +9 -14
  181. package/lib/server/ssr/htmlGenerator.test.ts +120 -44
  182. package/lib/server/ssr/htmlGenerator.ts +126 -91
  183. package/lib/server/ssr/imageMetadata.test.ts +3 -1
  184. package/lib/server/ssr/imageMetadata.ts +27 -21
  185. package/lib/server/ssr/jsCollector.test.ts +8 -18
  186. package/lib/server/ssr/jsCollector.ts +3 -8
  187. package/lib/server/ssr/liveReloadIntegration.test.ts +76 -36
  188. package/lib/server/ssr/metaTagGenerator.ts +2 -2
  189. package/lib/server/ssr/ssrRenderer.branches.test.ts +1103 -0
  190. package/lib/server/ssr/ssrRenderer.test.ts +316 -256
  191. package/lib/server/ssr/ssrRenderer.ts +795 -243
  192. package/lib/server/ssrRenderer.test.ts +1048 -954
  193. package/lib/server/utils/jsonLineMapper.test.ts +28 -28
  194. package/lib/server/utils/jsonLineMapper.ts +7 -7
  195. package/lib/server/validateStyleCoverage.ts +18 -21
  196. package/lib/server/websocketManager.ts +28 -19
  197. package/lib/shared/attributeNodeUtils.test.ts +15 -15
  198. package/lib/shared/attributeNodeUtils.ts +5 -12
  199. package/lib/shared/breakpoints.test.ts +5 -5
  200. package/lib/shared/breakpoints.ts +4 -11
  201. package/lib/shared/cmsQuery.test.ts +81 -0
  202. package/lib/shared/cmsQuery.ts +92 -0
  203. package/lib/shared/cmsQueryParser.test.ts +50 -42
  204. package/lib/shared/cmsQueryParser.ts +60 -38
  205. package/lib/shared/colorConversions.ts +9 -9
  206. package/lib/shared/colorVariableUtils.test.ts +27 -5
  207. package/lib/shared/colorVariableUtils.ts +21 -12
  208. package/lib/shared/componentRefs.ts +1 -5
  209. package/lib/shared/constants.test.ts +3 -3
  210. package/lib/shared/constants.ts +8 -8
  211. package/lib/shared/cssGeneration.test.ts +285 -144
  212. package/lib/shared/cssGeneration.ts +217 -531
  213. package/lib/shared/cssNamedColors.ts +152 -30
  214. package/lib/shared/cssProperties.test.ts +69 -70
  215. package/lib/shared/cssProperties.ts +298 -114
  216. package/lib/shared/elementClassName.test.ts +109 -109
  217. package/lib/shared/elementClassName.ts +3 -3
  218. package/lib/shared/elementUtils.ts +12 -16
  219. package/lib/shared/errorLogger.test.ts +12 -12
  220. package/lib/shared/errorLogger.ts +2 -10
  221. package/lib/shared/errors.test.ts +2 -13
  222. package/lib/shared/errors.ts +2 -8
  223. package/lib/shared/expressionEvaluator.test.ts +145 -0
  224. package/lib/shared/expressionEvaluator.ts +52 -24
  225. package/lib/shared/fontLoader.test.ts +32 -18
  226. package/lib/shared/fontLoader.ts +2 -2
  227. package/lib/shared/friendlyError.ts +2 -3
  228. package/lib/shared/gradientUtils.test.ts +3 -7
  229. package/lib/shared/gradientUtils.ts +17 -14
  230. package/lib/shared/hrefRefs.test.ts +2 -2
  231. package/lib/shared/hrefRefs.ts +2 -10
  232. package/lib/shared/i18n.test.ts +4 -4
  233. package/lib/shared/i18n.ts +18 -38
  234. package/lib/shared/index.ts +27 -0
  235. package/lib/shared/inlineSvgStyleRules.ts +14 -13
  236. package/lib/shared/interactiveStyleMappings.test.ts +32 -54
  237. package/lib/shared/interactiveStyleMappings.ts +11 -24
  238. package/lib/shared/interactiveStyles.test.ts +165 -188
  239. package/lib/shared/interfaces/contentProvider.ts +1 -1
  240. package/lib/shared/itemTemplateUtils.test.ts +11 -17
  241. package/lib/shared/itemTemplateUtils.ts +27 -39
  242. package/lib/shared/jsonRepair.ts +16 -10
  243. package/lib/shared/libraryLoader.test.ts +21 -55
  244. package/lib/shared/libraryLoader.ts +7 -22
  245. package/lib/shared/linkUtils.ts +1 -1
  246. package/lib/shared/logger.test.ts +66 -0
  247. package/lib/shared/logger.ts +94 -0
  248. package/lib/shared/markdown.ts +25 -0
  249. package/lib/shared/netlifyLocale404.test.ts +179 -0
  250. package/lib/shared/netlifyLocale404.ts +110 -0
  251. package/lib/shared/nodeUtils.test.ts +28 -16
  252. package/lib/shared/nodeUtils.ts +76 -23
  253. package/lib/shared/pathArrayUtils.test.ts +1 -2
  254. package/lib/shared/pathArrayUtils.ts +1 -1
  255. package/lib/shared/pathSecurity.ts +2 -2
  256. package/lib/shared/pathUtils.test.ts +4 -6
  257. package/lib/shared/pathUtils.ts +42 -49
  258. package/lib/shared/paths/Path.test.ts +2 -2
  259. package/lib/shared/paths/Path.ts +0 -1
  260. package/lib/shared/paths/PathConverter.test.ts +1 -1
  261. package/lib/shared/paths/PathConverter.ts +17 -20
  262. package/lib/shared/paths/PathUtils.ts +13 -13
  263. package/lib/shared/paths/PathValidator.test.ts +2 -15
  264. package/lib/shared/paths/PathValidator.ts +12 -10
  265. package/lib/shared/paths/index.ts +1 -2
  266. package/lib/shared/permissions.test.ts +168 -0
  267. package/lib/shared/permissions.ts +162 -0
  268. package/lib/shared/propResolver.test.ts +240 -244
  269. package/lib/shared/propResolver.ts +15 -26
  270. package/lib/shared/pxToRem.test.ts +7 -6
  271. package/lib/shared/pxToRem.ts +2 -5
  272. package/lib/shared/registry/BaseNodeTypeRegistry.test.ts +12 -6
  273. package/lib/shared/registry/ClientRegistry.ts +0 -2
  274. package/lib/shared/registry/ComponentRegistry.test.ts +46 -32
  275. package/lib/shared/registry/ComponentRegistry.ts +12 -9
  276. package/lib/shared/registry/NodeTypeDefinition.ts +16 -9
  277. package/lib/shared/registry/RegistryManager.ts +1 -2
  278. package/lib/shared/registry/SSRRegistry.ts +0 -2
  279. package/lib/shared/registry/createNodeType.ts +7 -9
  280. package/lib/shared/registry/defineNodeType.ts +2 -6
  281. package/lib/shared/registry/fieldPresets.ts +3 -1
  282. package/lib/shared/registry/index.ts +0 -1
  283. package/lib/shared/registry/nodeTypes/ComponentInstanceNodeType.ts +14 -15
  284. package/lib/shared/registry/nodeTypes/CustomNodeType.ts +103 -0
  285. package/lib/shared/registry/nodeTypes/EmbedNodeType.ts +18 -11
  286. package/lib/shared/registry/nodeTypes/HtmlNodeType.ts +48 -19
  287. package/lib/shared/registry/nodeTypes/IslandNodeType.ts +129 -0
  288. package/lib/shared/registry/nodeTypes/LinkNodeType.ts +21 -19
  289. package/lib/shared/registry/nodeTypes/ListNodeType.ts +117 -77
  290. package/lib/shared/registry/nodeTypes/LocaleListNodeType.ts +27 -21
  291. package/lib/shared/registry/nodeTypes/MarkdownNodeType.ts +78 -0
  292. package/lib/shared/registry/nodeTypes/SlotMarkerType.ts +12 -8
  293. package/lib/shared/registry/nodeTypes/index.ts +26 -3
  294. package/lib/shared/responsiveScaling.test.ts +16 -34
  295. package/lib/shared/responsiveScaling.ts +61 -42
  296. package/lib/shared/responsiveStyleUtils.test.ts +0 -1
  297. package/lib/shared/responsiveStyleUtils.ts +11 -13
  298. package/lib/shared/richtext/htmlToTiptap.test.ts +139 -130
  299. package/lib/shared/richtext/htmlToTiptap.ts +2 -4
  300. package/lib/shared/richtext/tiptapToHtml.test.ts +5 -6
  301. package/lib/shared/richtext/types.ts +1 -8
  302. package/lib/shared/slugTranslator.test.ts +13 -13
  303. package/lib/shared/slugTranslator.ts +12 -16
  304. package/lib/shared/slugify.ts +9 -15
  305. package/lib/shared/styleNodeUtils.test.ts +8 -8
  306. package/lib/shared/styleNodeUtils.ts +18 -20
  307. package/lib/shared/styleUtils.test.ts +87 -61
  308. package/lib/shared/styleUtils.ts +5 -6
  309. package/lib/shared/themeDefaults.test.ts +11 -14
  310. package/lib/shared/themeDefaults.ts +3 -4
  311. package/lib/shared/tree/PathBuilder.test.ts +63 -145
  312. package/lib/shared/tree/PathBuilder.ts +59 -47
  313. package/lib/shared/treePathUtils.test.ts +51 -12
  314. package/lib/shared/treePathUtils.ts +91 -60
  315. package/lib/shared/types/api.ts +105 -8
  316. package/lib/shared/types/cms.test.ts +34 -0
  317. package/lib/shared/types/cms.ts +90 -34
  318. package/lib/shared/types/comment.ts +49 -16
  319. package/lib/shared/types/components.ts +51 -25
  320. package/lib/shared/types/errors.test.ts +1 -6
  321. package/lib/shared/types/errors.ts +3 -7
  322. package/lib/shared/types/experiments.ts +28 -28
  323. package/lib/shared/types/index.ts +25 -2
  324. package/lib/shared/types/permissions.ts +139 -0
  325. package/lib/shared/types/styles.ts +0 -1
  326. package/lib/shared/types/variables.test.ts +4 -13
  327. package/lib/shared/types/variables.ts +51 -30
  328. package/lib/shared/types.ts +1 -2
  329. package/lib/shared/utilityClassConfig.ts +751 -321
  330. package/lib/shared/utilityClassMapper.durability.test.ts +117 -0
  331. package/lib/shared/utilityClassMapper.test.ts +433 -95
  332. package/lib/shared/utilityClassMapper.ts +322 -249
  333. package/lib/shared/utilityClassNames.test.ts +38 -0
  334. package/lib/shared/utilityClassNames.ts +458 -0
  335. package/lib/shared/utils.test.ts +2 -10
  336. package/lib/shared/utils.ts +19 -10
  337. package/lib/shared/validation/cmsValidators.ts +2 -1
  338. package/lib/shared/validation/commentValidators.test.ts +53 -0
  339. package/lib/shared/validation/commentValidators.ts +12 -1
  340. package/lib/shared/validation/index.ts +1 -0
  341. package/lib/shared/validation/permissionsValidators.test.ts +49 -0
  342. package/lib/shared/validation/permissionsValidators.ts +49 -0
  343. package/lib/shared/validation/propValidator.test.ts +19 -21
  344. package/lib/shared/validation/propValidator.ts +13 -18
  345. package/lib/shared/validation/schemas.test.ts +64 -32
  346. package/lib/shared/validation/schemas.ts +616 -345
  347. package/lib/shared/validation/validators.test.ts +3 -8
  348. package/lib/shared/validation/validators.ts +89 -68
  349. package/lib/shared/viewportUnits.integration.test.ts +14 -10
  350. package/lib/shared/viewportUnits.test.ts +24 -23
  351. package/lib/shared/viewportUnits.ts +12 -5
  352. package/lib/test-utils/dom-setup.ts +1 -1
  353. package/lib/test-utils/factories/ConsoleMockFactory.ts +10 -14
  354. package/lib/test-utils/factories/DomMockFactory.ts +17 -23
  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 +6 -10
  358. package/lib/test-utils/factories/StoreMockFactory.ts +18 -29
  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 +13 -19
  365. package/lib/test-utils/mocks.ts +6 -4
  366. package/package.json +3 -5
  367. package/scripts/build-for-publish.mjs +2 -4
  368. package/scripts/build-meno-filter.ts +4 -7
  369. package/tsconfig.json +1 -0
  370. package/vite.config.ts +4 -4
  371. package/build-next.ts +0 -1361
  372. package/build-static.test.ts +0 -424
  373. package/build-static.ts +0 -1074
  374. package/dist/build-static.js +0 -36
  375. package/dist/chunks/chunk-2MHDV5BF.js +0 -251
  376. package/dist/chunks/chunk-2MHDV5BF.js.map +0 -7
  377. package/dist/chunks/chunk-AZQYF6KE.js +0 -559
  378. package/dist/chunks/chunk-AZQYF6KE.js.map +0 -7
  379. package/dist/chunks/chunk-GZHGVVW3.js +0 -322
  380. package/dist/chunks/chunk-GZHGVVW3.js.map +0 -7
  381. package/dist/chunks/chunk-H3GJ4H2U.js.map +0 -7
  382. package/dist/chunks/chunk-I7YIGZXT.js +0 -467
  383. package/dist/chunks/chunk-I7YIGZXT.js.map +0 -7
  384. package/dist/chunks/chunk-IGYR22T6.js +0 -6192
  385. package/dist/chunks/chunk-IGYR22T6.js.map +0 -7
  386. package/dist/chunks/chunk-JGP5A3Y5.js +0 -1136
  387. package/dist/chunks/chunk-JGP5A3Y5.js.map +0 -7
  388. package/dist/chunks/chunk-JGWFTO6P.js.map +0 -7
  389. package/dist/chunks/chunk-O3NAGJP4.js.map +0 -7
  390. package/dist/chunks/chunk-QB2LNO4W.js +0 -77
  391. package/dist/chunks/chunk-QB2LNO4W.js.map +0 -7
  392. package/dist/chunks/chunk-R6XHAFBF.js +0 -3821
  393. package/dist/chunks/chunk-R6XHAFBF.js.map +0 -7
  394. package/dist/chunks/chunk-UB44F4Z2.js.map +0 -7
  395. package/dist/chunks/chunk-WQFG7PAH.js.map +0 -7
  396. package/dist/chunks/chunk-X754AHS5.js.map +0 -7
  397. package/dist/chunks/chunk-YBLHKYFF.js.map +0 -7
  398. package/dist/chunks/configService-R3OGU2UD.js +0 -13
  399. package/dist/chunks/configService-R3OGU2UD.js.map +0 -7
  400. package/dist/chunks/constants-STK2YBIW.js +0 -46
  401. package/dist/chunks/constants-STK2YBIW.js.map +0 -7
  402. package/dist/chunks/fs-JGINUXGL.js.map +0 -7
  403. package/dist/entries/server-router.js +0 -67
  404. package/dist/entries/server-router.js.map +0 -7
  405. package/entries/server-router.tsx +0 -81
  406. package/lib/server/providers/fileSystemPageProvider.ts +0 -160
  407. package/lib/server/webflow/buildWebflow.ts +0 -635
  408. package/lib/server/webflow/index.ts +0 -23
  409. package/lib/server/webflow/nodeToWebflow.test.ts +0 -3170
  410. package/lib/server/webflow/nodeToWebflow.ts +0 -2435
  411. package/lib/server/webflow/styleMapper.test.ts +0 -389
  412. package/lib/server/webflow/styleMapper.ts +0 -695
  413. package/lib/server/webflow/templateWrapper.ts +0 -49
  414. package/lib/server/webflow/types.ts +0 -396
  415. /package/dist/{build-static.js.map → chunks/fs-G3ANWEFP.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
+ breakpointClassPrefixes,
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,34 @@ 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++) {
38
+ const prop = propertyOrder[i];
39
+ if (prop === undefined) continue;
37
40
  // Convert camelCase key to kebab-case CSS property
38
- const kebab = props[i].replace(/([A-Z])/g, '-$1').toLowerCase();
39
- map.set(kebab, i);
41
+ const kebab = camelToKebab(prop);
42
+ if (!map.has(kebab)) map.set(kebab, i);
40
43
  }
41
44
  return map;
42
45
  })();
43
46
 
44
47
  /**
45
48
  * Get the property-order index for a utility class name.
46
- * Returns the position of the class's CSS property in propertyMap,
49
+ * Returns the position of the class's CSS property in propertyOrder,
47
50
  * or Infinity for unknown classes (placed last).
48
51
  */
49
52
  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);
53
+ const parsed = parseUtilityClass(className);
54
+ if (parsed) {
55
+ const idx = cssPropertyOrderMap.get(parsed.property);
54
56
  if (idx !== undefined) return idx;
55
57
  }
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
58
  return Infinity;
68
59
  }
69
60
 
@@ -85,18 +76,18 @@ function getResponsiveMode(scales: ResponsiveScales | undefined | null): Respons
85
76
 
86
77
  /** CSS property → responsive scale category mapping (used by generateUtilityCSS and generateSingleClassCSS) */
87
78
  const AUTO_RESPONSIVE_TYPE_MAP: Record<string, string> = {
88
- 'padding': 'padding',
79
+ padding: 'padding',
89
80
  'padding-left': 'padding',
90
81
  'padding-right': 'padding',
91
82
  'padding-top': 'padding',
92
83
  'padding-bottom': 'padding',
93
- 'margin': 'margin',
84
+ margin: 'margin',
94
85
  'margin-left': 'margin',
95
86
  'margin-right': 'margin',
96
87
  'margin-top': 'margin',
97
88
  'margin-bottom': 'margin',
98
89
  'font-size': 'fontSize',
99
- 'gap': 'gap',
90
+ gap: 'gap',
100
91
  'row-gap': 'gap',
101
92
  'column-gap': 'gap',
102
93
  'border-radius': 'borderRadius',
@@ -104,8 +95,8 @@ const AUTO_RESPONSIVE_TYPE_MAP: Record<string, string> = {
104
95
  'border-top-right-radius': 'borderRadius',
105
96
  'border-bottom-left-radius': 'borderRadius',
106
97
  'border-bottom-right-radius': 'borderRadius',
107
- 'width': 'size',
108
- 'height': 'size',
98
+ width: 'size',
99
+ height: 'size',
109
100
  'max-width': 'size',
110
101
  'max-height': 'size',
111
102
  'min-width': 'size',
@@ -113,126 +104,37 @@ const AUTO_RESPONSIVE_TYPE_MAP: Record<string, string> = {
113
104
  };
114
105
 
115
106
  /**
116
- * Escape special characters in CSS class names using backslash notation
117
- * Characters that have special meaning in CSS selectors need to be escaped
107
+ * Escape special characters in CSS class names using backslash notation.
108
+ * Tailwind-style class names carry `[ ] ( ) : % # . , / *` and more — escape
109
+ * everything outside the safe identifier charset.
118
110
  */
119
111
  function escapeCSSClassName(className: string): string {
120
- // Escape special characters with backslash
121
- // This includes: . # [ ] ( ) { } ; : , > + ~ % etc.
122
- return className.replace(/[.#[\](){};<>+~:,%\/\s]/g, '\\$&');
112
+ return className.replace(/[^a-zA-Z0-9_-]/g, '\\$&');
123
113
  }
124
114
 
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
115
  /**
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
116
+ * Predefined rules for static utilities and presets (Tailwind names):
117
+ * `flex` `display: flex;`, `hidden` `display: none;`,
118
+ * `justify-center` → `justify-content: center;`, `shadow-2` → preset value, …
119
+ * Derived from the single source of truth in utilityClassConfig.ts.
203
120
  */
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
- }
121
+ const utilityClassRules: Record<string, string> = (() => {
122
+ const rules: Record<string, string> = {};
123
+ for (const [className, entry] of Object.entries(staticUtilityReverse)) {
124
+ rules[className] = `${camelToKebab(entry.prop)}: ${entry.value};`;
223
125
  }
224
-
225
- return null;
226
- }
126
+ for (const [className, entry] of Object.entries(presetClassReverse)) {
127
+ rules[className] = `${camelToKebab(entry.prop)}: ${entry.value};`;
128
+ }
129
+ return rules;
130
+ })();
227
131
 
228
132
  /**
229
133
  * Resolve `{ property, value }` for a utility class, consulting the style-value
230
- * registry for hash-fallback classes (e.g. `fs-h1glej9a` for `222.3px`).
134
+ * registry for hash-fallback classes (e.g. `text-h1glej9a` for `222.3px`).
231
135
  *
232
136
  * 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.
137
+ * bypass `clamp()` / `@media` rewriting.
236
138
  *
237
139
  * Returns the registered value as-is (no parsing). Downstream helpers
238
140
  * (`buildFluidPropertyValue`, `scalePropertyValue`) already null out
@@ -241,48 +143,41 @@ function extractPropertyAndValue(className: string): { property: string; value:
241
143
  * `if (!fluidValue) return rule;` guards.
242
144
  */
243
145
  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;
146
+ const parsed = parseUtilityClass(className);
147
+ if (!parsed) return null;
148
+ if (parsed.value != null) return { property: parsed.property, value: parsed.value };
149
+
150
+ // Hash-fallback class: real value lives in the registry
151
+ const registered = getStyleValue(className);
152
+ if (registered == null || registered === '') return null;
153
+ const value = String(registered);
154
+ const property = parsed.root ? resolveRootProperty(parsed.root, value) : parsed.property;
155
+ return { property, value };
259
156
  }
260
157
 
158
+ /** Border-side class roots → CSS side shorthand base. */
159
+ const borderSideMap: Record<string, string> = {
160
+ 'border-t': 'border-top',
161
+ 'border-r': 'border-right',
162
+ 'border-b': 'border-bottom',
163
+ 'border-l': 'border-left',
164
+ };
165
+
261
166
  /**
262
167
  * Generate CSS rule for a utility class
263
- * Handles dynamic classes like p-10px, m-20px, fs-48px, etc.
168
+ * Handles arbitrary values (p-[10px]), variables (bg-(--primary)),
169
+ * keywords (w-full), bare numbers (z-10) and hash-fallback classes.
264
170
  */
265
- export function generateRuleForClass(className: string): string | null {
266
- // Check if it's a predefined rule
171
+ export function generateRuleForClass(className: string, knownTokens?: ReadonlySet<string>): string | null {
172
+ // Check if it's a predefined static/preset rule
267
173
  if (utilityClassRules[className]) {
268
174
  return utilityClassRules[className];
269
175
  }
270
176
 
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 = '';
275
-
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
- }
177
+ const parsed = parseUtilityClass(className, knownTokens);
283
178
 
284
- if (!prefix || !classValue) {
285
- // Check dynamic registry for classes with unknown prefixes
179
+ if (!parsed) {
180
+ // Check dynamic registry for classes with unknown roots
286
181
  const dynamicStyle = getDynamicStyle(className);
287
182
  if (dynamicStyle) {
288
183
  return `${dynamicStyle.property}: ${dynamicStyle.value};`;
@@ -290,181 +185,66 @@ export function generateRuleForClass(className: string): string | null {
290
185
  return null;
291
186
  }
292
187
 
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
188
+ // Arbitrary property syntax: [clip-path:circle(50%)] prefer the registered
189
+ // original value (exact), fall back to the decoded one.
190
+ if (parsed.kind === 'arbitrary-property') {
297
191
  const dynamicStyle = getDynamicStyle(className);
298
- if (dynamicStyle) {
299
- return `${dynamicStyle.property}: ${dynamicStyle.value};`;
300
- }
301
- return null;
192
+ const value = dynamicStyle ? dynamicStyle.value : parsed.value;
193
+ return `${parsed.property}: ${value};`;
302
194
  }
303
195
 
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};`;
320
- }
321
-
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
- }
340
-
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
- }
196
+ const root = parsed.root;
345
197
 
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'), ' ');
198
+ // Resolve the CSS value. The style-value registry holds the exact original
199
+ // for arbitrary/hash classes; variable, keyword and numeric forms decode
200
+ // deterministically from the class name itself.
201
+ let value: string;
202
+ let property = parsed.property;
203
+ if (parsed.kind === 'hash') {
204
+ const registered = getStyleValue(className);
205
+ if (registered == null) return null;
206
+ value = String(registered);
207
+ if (root) property = resolveRootProperty(root, value);
208
+ } else if (parsed.kind === 'arbitrary') {
209
+ const registered = getStyleValue(className);
210
+ value = registered !== undefined ? String(registered) : parsed.value!;
426
211
  } else {
427
- // For positive values, replace hyphens normally
428
- value = value.replace(new RegExp(`-(?=${valuePattern.source})`, 'g'), ' ');
212
+ value = parsed.value!;
429
213
  }
430
214
 
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
215
+ // Multi-property roots (Tailwind axis shorthands)
216
+ if (root === 'px') {
437
217
  return `padding-left: ${value}; padding-right: ${value};`;
438
218
  }
439
- if (prefix === 'py') {
440
- // padding-top and padding-bottom
219
+ if (root === 'py') {
441
220
  return `padding-top: ${value}; padding-bottom: ${value};`;
442
221
  }
443
- if (prefix === 'mx') {
444
- // margin-left and margin-right
222
+ if (root === 'mx') {
445
223
  return `margin-left: ${value}; margin-right: ${value};`;
446
224
  }
447
- if (prefix === 'my') {
448
- // margin-top and margin-bottom
225
+ if (root === 'my') {
449
226
  return `margin-top: ${value}; margin-bottom: ${value};`;
450
227
  }
451
228
 
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};`;
229
+ // Border-side classes (border-t-, border-r-, …): emit width/style (and color
230
+ // when present) as LONGHANDS, so a separate border-color class can still
231
+ // control the color independently (longhands sort after the side class).
232
+ if (root && borderSideMap[root] && property !== 'border-color') {
233
+ const side = borderSideMap[root];
234
+ const tokens = value.split(/\s+/).filter(Boolean);
235
+ if (tokens.length >= 2) {
236
+ const declarations = [`${side}-width: ${tokens[0]};`, `${side}-style: ${tokens[1]};`];
237
+ if (tokens[2]) {
238
+ declarations.push(`${side}-color: ${tokens.slice(2).join(' ')};`);
461
239
  }
462
- return `${cssProp}: var(--${value});`;
240
+ return declarations.join(' ');
463
241
  }
242
+ return `${side}: ${value};`;
464
243
  }
465
244
 
466
- // Standard case: prefix-value (e.g., p-10px, fs-48px, ta-center)
467
- return `${cssProp}: ${value};`;
245
+ // Standard case: property + decoded value (e.g. `padding: 10px;`,
246
+ // `color: var(--primary);`, `width: 100%;`)
247
+ return `${property}: ${value};`;
468
248
  }
469
249
 
470
250
  /**
@@ -480,7 +260,7 @@ function applyFluidToUtilityRule(
480
260
  rule: string,
481
261
  className: string,
482
262
  responsiveScales: ResponsiveScales,
483
- breakpoints: BreakpointConfig
263
+ breakpoints: BreakpointConfig,
484
264
  ): string {
485
265
  const propValue = resolveScalablePropertyValue(className);
486
266
  if (!propValue) return rule;
@@ -488,9 +268,7 @@ function applyFluidToUtilityRule(
488
268
  const category = AUTO_RESPONSIVE_TYPE_MAP[propValue.property];
489
269
  if (!category) return rule;
490
270
 
491
- const scaleConfig = responsiveScales[category as keyof ResponsiveScales] as
492
- | Record<string, number>
493
- | undefined;
271
+ const scaleConfig = responsiveScales[category as keyof ResponsiveScales] as Record<string, number> | undefined;
494
272
  if (!scaleConfig) return rule;
495
273
 
496
274
  const smallest = getSmallestBreakpointName(breakpoints);
@@ -501,13 +279,7 @@ function applyFluidToUtilityRule(
501
279
 
502
280
  const range = responsiveScales.fluidRange ?? DEFAULT_FLUID_RANGE;
503
281
  const baseRef = responsiveScales.baseReference ?? 16;
504
- const fluidValue = buildFluidPropertyValue(
505
- propValue.value,
506
- scale,
507
- range.min,
508
- range.max,
509
- baseRef
510
- );
282
+ const fluidValue = buildFluidPropertyValue(propValue.value, scale, range.min, range.max, baseRef);
511
283
  if (!fluidValue) return rule;
512
284
 
513
285
  return `${propValue.property}: ${fluidValue};`;
@@ -521,7 +293,7 @@ function applyFluidToUtilityRule(
521
293
  function applyFluidToStyle(
522
294
  style: StyleObject,
523
295
  responsiveScales: ResponsiveScales,
524
- breakpoints: BreakpointConfig
296
+ breakpoints: BreakpointConfig,
525
297
  ): StyleObject {
526
298
  const range = responsiveScales.fluidRange ?? DEFAULT_FLUID_RANGE;
527
299
  const baseRef = responsiveScales.baseReference ?? 16;
@@ -539,23 +311,13 @@ function applyFluidToStyle(
539
311
  continue;
540
312
  }
541
313
 
542
- const scale = getScaleMultiplier(
543
- responsiveScales,
544
- prop as CSSPropertyType,
545
- smallest
546
- );
314
+ const scale = getScaleMultiplier(responsiveScales, prop as CSSPropertyType, smallest);
547
315
  if (scale == null || scale === 1) {
548
316
  out[prop] = value;
549
317
  continue;
550
318
  }
551
319
 
552
- const fluidValue = buildFluidPropertyValue(
553
- String(value),
554
- scale,
555
- range.min,
556
- range.max,
557
- baseRef
558
- );
320
+ const fluidValue = buildFluidPropertyValue(String(value), scale, range.min, range.max, baseRef);
559
321
  out[prop] = fluidValue ?? value;
560
322
  }
561
323
  return out;
@@ -580,10 +342,7 @@ const CONTAINER_RESERVED_VALUES = new Set(['auto', 'inherit', 'initial', 'unset'
580
342
  * Margins are ALWAYS overwritten with `auto` (per user spec), even if the
581
343
  * caller had set explicit values.
582
344
  */
583
- export function applyContainerPattern(
584
- style: StyleObject,
585
- fluidActive: boolean
586
- ): StyleObject {
345
+ export function applyContainerPattern(style: StyleObject, fluidActive: boolean): StyleObject {
587
346
  if (!fluidActive) return style;
588
347
  const w = style.width;
589
348
  const mw = style.maxWidth;
@@ -609,7 +368,8 @@ export function generateUtilityCSS(
609
368
  usedClasses: Set<string>,
610
369
  breakpoints: BreakpointConfig = DEFAULT_BREAKPOINTS,
611
370
  responsiveScales?: ResponsiveScales,
612
- remConfig?: RemConversionConfig
371
+ remConfig?: RemConversionConfig,
372
+ knownTokens?: ReadonlySet<string>,
613
373
  ): string {
614
374
  const css: string[] = [];
615
375
  const baseClasses = new Set<string>();
@@ -619,40 +379,35 @@ export function generateUtilityCSS(
619
379
  const breakpointValues = getBreakpointValues(breakpoints);
620
380
 
621
381
  // Create a map for responsive breakpoint classes
622
- // Map from prefix (e.g., 't', 'm') to the class name and breakpoint info
382
+ // Map from variant prefix (e.g. 'tablet:', 'mobile:') to the class names and breakpoint info
623
383
  type BreakpointClassMap = Record<string, { classes: Set<string>; breakpointName: string; value: number }>;
624
384
  const responsiveClasses: BreakpointClassMap = {};
625
385
 
626
- // Initialize responsive class sets for each breakpoint
386
+ // Initialize responsive class sets for each breakpoint. Register BOTH the current desktop-first
387
+ // prefix (`max-tablet:`) and the legacy bare form (`tablet:`) so pre-`max-` projects still bucket
388
+ // into their @media block; each prefix renders its own classes verbatim (selectors stay valid).
627
389
  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
390
+ for (const prefix of breakpointClassPrefixes(breakpointName)) {
391
+ responsiveClasses[prefix] = {
392
+ classes: new Set<string>(),
393
+ breakpointName,
394
+ value: breakpointValue,
395
+ };
633
396
  }
634
-
635
- responsiveClasses[prefix] = {
636
- classes: new Set<string>(),
637
- breakpointName,
638
- value: breakpointValue
639
- };
640
397
  }
641
398
 
642
399
  // Separate classes by breakpoint
643
400
  for (const className of usedClasses) {
644
401
  let matched = false;
645
402
 
646
- // Try to match against any responsive breakpoint prefix
403
+ // Try to match against any responsive breakpoint variant prefix
647
404
  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);
650
- 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)$/)) {
655
- responsiveClasses[prefix].classes.add(potentialClass);
405
+ if (className.startsWith(prefix) && className.length > prefix.length) {
406
+ const potentialClass = className.substring(prefix.length);
407
+ const rule = generateRuleForClass(potentialClass, knownTokens);
408
+ const bucket = responsiveClasses[prefix];
409
+ if (rule && bucket) {
410
+ bucket.classes.add(potentialClass);
656
411
  matched = true;
657
412
  break;
658
413
  }
@@ -680,7 +435,7 @@ export function generateUtilityCSS(
680
435
 
681
436
  // Generate base rules — sorted so shorthands (border) appear before longhands (border-color)
682
437
  for (const className of sortClassesByPropertyOrder(baseClasses)) {
683
- let rule = generateRuleForClass(className);
438
+ let rule = generateRuleForClass(className, knownTokens);
684
439
  if (rule) {
685
440
  // In fluid mode, replace auto-responsive raw values with clamp() expressions.
686
441
  if (fluidActive && autoResponsiveClasses.has(className)) {
@@ -699,8 +454,15 @@ export function generateUtilityCSS(
699
454
 
700
455
  // Generate auto-responsive rules for classes with enabled scaling.
701
456
  // In fluid mode the base rule already encodes scaling via clamp(), so skip @media.
457
+ // Sort shorthand-before-longhand exactly like the base rules above: within a breakpoint
458
+ // block, a `margin` shorthand emitted AFTER a `margin-bottom` longhand would clobber it
459
+ // (equal specificity ⇒ source order wins), silently dropping the longhand on that
460
+ // breakpoint. The base section is sorted, so the cascade would flip ONLY inside @media —
461
+ // and the unsorted Set's order depends on collection order (SSR render vs .astro walk),
462
+ // so the same project could render correctly in one path and lose the longhand in the
463
+ // other. Sorting makes the @media cascade deterministic and consistent with base.
702
464
  if (responsiveScales?.enabled && !fluidActive) {
703
- for (const className of autoResponsiveClasses) {
465
+ for (const className of sortClassesByPropertyOrder(autoResponsiveClasses)) {
704
466
  const propValue = resolveScalablePropertyValue(className);
705
467
  if (!propValue) continue;
706
468
 
@@ -727,16 +489,17 @@ export function generateUtilityCSS(
727
489
  if (!autoResponsiveMediaQueries[breakpointName]) {
728
490
  autoResponsiveMediaQueries[breakpointName] = {
729
491
  classes: [],
730
- value: breakpointValue
492
+ value: breakpointValue,
731
493
  };
732
494
  }
733
495
 
734
- const finalScaledValue = (remConfig?.enabled && shouldConvertProperty(propValue.property))
735
- ? convertPxToRem(scaledValue, remConfig.baseFontSize)
736
- : scaledValue;
496
+ const finalScaledValue =
497
+ remConfig?.enabled && shouldConvertProperty(propValue.property)
498
+ ? convertPxToRem(scaledValue, remConfig.baseFontSize)
499
+ : scaledValue;
737
500
  autoResponsiveMediaQueries[breakpointName].classes.push({
738
501
  className: escapedClassName,
739
- rule: `${propValue.property}: ${finalScaledValue};`
502
+ rule: `${propValue.property}: ${finalScaledValue};`,
740
503
  });
741
504
  }
742
505
  }
@@ -745,14 +508,13 @@ export function generateUtilityCSS(
745
508
  // Generate media queries in two separate sections:
746
509
  // 1. Auto-responsive classes FIRST (baseline responsive behavior)
747
510
  // 2. Manual breakpoint classes AFTER (explicit overrides)
748
- // This ensures manual t-, mob- classes always override auto-responsive rules
511
+ // This ensures manual tablet:/mobile: classes always override auto-responsive rules
749
512
 
750
513
  // Sort auto-responsive media queries by breakpoint value (descending)
751
- const sortedAutoResponsive = Object.entries(autoResponsiveMediaQueries)
752
- .sort(([, a], [, b]) => b.value - a.value);
514
+ const sortedAutoResponsive = Object.entries(autoResponsiveMediaQueries).sort(([, a], [, b]) => b.value - a.value);
753
515
 
754
516
  // Generate auto-responsive media queries first
755
- for (const [breakpointName, mq] of sortedAutoResponsive) {
517
+ for (const [, mq] of sortedAutoResponsive) {
756
518
  if (mq.classes.length === 0) continue;
757
519
 
758
520
  css.push(`@media (max-width: ${mq.value}px) {`);
@@ -771,12 +533,12 @@ export function generateUtilityCSS(
771
533
  for (const [prefix, breakpointInfo] of sortedManualResponsive) {
772
534
  const rules: string[] = [];
773
535
 
774
- for (const className of breakpointInfo.classes) {
775
- const rule = generateRuleForClass(className);
536
+ for (const className of sortClassesByPropertyOrder(breakpointInfo.classes)) {
537
+ const rule = generateRuleForClass(className, knownTokens);
776
538
  if (rule) {
777
- const escapedClassName = escapeCSSClassName(className);
539
+ const escapedClassName = escapeCSSClassName(`${prefix}${className}`);
778
540
  const finalRule = applyRemConversion(rule, remConfig);
779
- rules.push(` .${prefix}-${escapedClassName} { ${finalRule} }`);
541
+ rules.push(` .${escapedClassName} { ${finalRule} }`);
780
542
  }
781
543
  }
782
544
 
@@ -802,35 +564,33 @@ export function generateSingleClassCSS(
802
564
  className: string,
803
565
  breakpoints: BreakpointConfig = DEFAULT_BREAKPOINTS,
804
566
  responsiveScales?: ResponsiveScales,
805
- remConfig?: RemConversionConfig
567
+ remConfig?: RemConversionConfig,
568
+ knownTokens?: ReadonlySet<string>,
806
569
  ): string {
807
570
  const breakpointValues = getBreakpointValues(breakpoints);
808
571
  const css: string[] = [];
809
572
 
810
- // Build responsive prefix map: prefix → { breakpointName, value }
573
+ // Build responsive variant prefix map: prefix → { breakpointName, value }. Both the current
574
+ // desktop-first (`max-tablet:`) and legacy bare (`tablet:`) forms map to the same breakpoint.
811
575
  const responsivePrefixes: Record<string, { breakpointName: string; value: number }> = {};
812
576
  for (const [breakpointName, breakpointValue] of Object.entries(breakpointValues)) {
813
- let prefix = breakpointName.charAt(0).toLowerCase();
814
- if (breakpointName.toLowerCase() === 'mobile') {
815
- prefix = 'mob';
577
+ for (const prefix of breakpointClassPrefixes(breakpointName)) {
578
+ responsivePrefixes[prefix] = { breakpointName, value: breakpointValue };
816
579
  }
817
- responsivePrefixes[prefix] = { breakpointName, value: breakpointValue };
818
580
  }
819
581
 
820
- // Check if class has a responsive prefix (e.g. "t-fs-16px", "mob-p-10px")
582
+ // Check if class has a responsive variant prefix (e.g. "tablet:text-[16px]", "mobile:p-[10px]")
821
583
  let matched = false;
822
584
  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);
825
- 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)$/)) {
585
+ if (className.startsWith(prefix) && className.length > prefix.length) {
586
+ const potentialClass = className.substring(prefix.length);
587
+ const rule = generateRuleForClass(potentialClass, knownTokens);
588
+ const responsivePrefix = responsivePrefixes[prefix];
589
+ if (rule && responsivePrefix) {
829
590
  const escapedClassName = escapeCSSClassName(className);
830
- const escapedInnerClass = escapeCSSClassName(potentialClass);
831
- const bpValue = responsivePrefixes[prefix].value;
591
+ const bpValue = responsivePrefix.value;
832
592
  const finalRule = applyRemConversion(rule, remConfig);
833
- css.push(`@media (max-width: ${bpValue}px) {\n .${prefix}-${escapedInnerClass} { ${finalRule} }\n}`);
593
+ css.push(`@media (max-width: ${bpValue}px) {\n .${escapedClassName} { ${finalRule} }\n}`);
834
594
  matched = true;
835
595
  break;
836
596
  }
@@ -839,7 +599,7 @@ export function generateSingleClassCSS(
839
599
 
840
600
  if (!matched) {
841
601
  // Base (non-responsive) class
842
- let rule = generateRuleForClass(className);
602
+ let rule = generateRuleForClass(className, knownTokens);
843
603
  if (!rule) return '';
844
604
 
845
605
  const mode = getResponsiveMode(responsiveScales);
@@ -863,13 +623,14 @@ export function generateSingleClassCSS(
863
623
  if (propValue) {
864
624
  const category = AUTO_RESPONSIVE_TYPE_MAP[propValue.property];
865
625
  if (category) {
866
- const scaleConfig = responsiveScales[category as keyof ResponsiveScales] as Record<string, number> | undefined;
626
+ const scaleConfig = responsiveScales[category as keyof ResponsiveScales] as
627
+ | Record<string, number>
628
+ | undefined;
867
629
  if (scaleConfig) {
868
630
  const baseRef = responsiveScales.baseReference || 16;
869
631
 
870
632
  // Sort breakpoints descending (largest first) for proper cascade
871
- const sortedBps = Object.entries(breakpointValues)
872
- .sort(([, a], [, b]) => b - a);
633
+ const sortedBps = Object.entries(breakpointValues).sort(([, a], [, b]) => b - a);
873
634
 
874
635
  for (const [breakpointName, breakpointValue] of sortedBps) {
875
636
  const scale = scaleConfig[breakpointName];
@@ -878,10 +639,13 @@ export function generateSingleClassCSS(
878
639
  const scaledValue = scalePropertyValue(propValue.value, baseRef, scale);
879
640
  if (!scaledValue) continue;
880
641
 
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}`);
642
+ const finalScaledValue =
643
+ remConfig?.enabled && shouldConvertProperty(propValue.property)
644
+ ? convertPxToRem(scaledValue, remConfig.baseFontSize)
645
+ : scaledValue;
646
+ css.push(
647
+ `@media (max-width: ${breakpointValue}px) {\n .${escapedClassName} { ${propValue.property}: ${finalScaledValue}; }\n}`,
648
+ );
885
649
  }
886
650
  }
887
651
  }
@@ -893,99 +657,34 @@ export function generateSingleClassCSS(
893
657
  }
894
658
 
895
659
  /**
896
- * Extract all utility classes from a rendered HTML string
897
- * Identifies utility classes by checking against known prefixes from propertyMap
660
+ * Extract all utility classes from a rendered HTML string.
661
+ * A class counts as a utility when (after stripping an optional `breakpoint:`
662
+ * variant prefix) it parses as a Meno utility class — static (`flex`,
663
+ * `justify-center`), arbitrary (`p-[10px]`), variable (`bg-(--primary)`),
664
+ * keyword (`w-full`), numeric (`z-10`), preset (`shadow-2`),
665
+ * arbitrary-property (`[clip-path:…]`) or hash-fallback — or when it was
666
+ * registered in the dynamic style registry.
898
667
  */
899
- export function extractUtilityClassesFromHTML(html: string): Set<string> {
668
+ export function extractUtilityClassesFromHTML(html: string, knownTokens?: ReadonlySet<string>): Set<string> {
900
669
  const classes = new Set<string>();
901
670
  const classRegex = /class="([^"]*)"/g;
902
- let match;
903
-
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
- while ((match = classRegex.exec(html)) !== null) {
923
- const classString = match[1];
924
- const classList = classString.split(/\s+/);
671
+ let match: RegExpExecArray | null = classRegex.exec(html);
672
+
673
+ while (match !== null) {
674
+ const classList = (match[1] ?? '').split(/\s+/);
675
+ match = classRegex.exec(html);
925
676
 
926
677
  for (const className of classList) {
927
- // Skip empty class names
928
- if (!className || className.length === 0) continue;
678
+ if (!className) continue;
929
679
 
930
- // Check if it's a special/short form class
931
- if (specialPrefixes.has(className)) {
680
+ const { base } = splitVariantPrefix(className);
681
+ if (parseUtilityClass(base, knownTokens)) {
932
682
  classes.add(className);
933
683
  continue;
934
684
  }
935
685
 
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)) {
686
+ // Dynamic classes (registered for properties without a known root)
687
+ if (isDynamicClass(className) || isDynamicClass(base)) {
989
688
  classes.add(className);
990
689
  }
991
690
  }
@@ -1002,18 +701,20 @@ export function extractUtilityClassesFromHTML(html: string): Set<string> {
1002
701
  * Check if a style object is responsive (has base/breakpoint structure)
1003
702
  */
1004
703
  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
- );
704
+ return typeof style === 'object' && style !== null && ('base' in style || 'tablet' in style || 'mobile' in style);
1010
705
  }
1011
706
 
1012
707
  /**
1013
708
  * Convert a flat style object to CSS property declarations
1014
709
  * Returns string like "padding: 10px; background-color: var(--primary);"
710
+ *
711
+ * Exported for the studio's astro-play optimistic style preview: it converts a
712
+ * single changed declaration to the same kebab-case + `var(--name)` color form
713
+ * the server's utility/theme CSS uses, so the inline preview matches the value
714
+ * the recompiled stylesheet will land with. `_mapping` (binding) values are
715
+ * skipped, so a bound property yields an empty string (no optimistic preview).
1015
716
  */
1016
- function styleObjectToCSS(style: StyleObject): string {
717
+ export function styleObjectToCSS(style: StyleObject): string {
1017
718
  const declarations: string[] = [];
1018
719
 
1019
720
  for (const [prop, value] of Object.entries(style)) {
@@ -1065,7 +766,7 @@ export function generateInteractiveCSS(
1065
766
  interactiveStyles: InteractiveStyles,
1066
767
  breakpoints: BreakpointConfig = DEFAULT_BREAKPOINTS,
1067
768
  remConfig?: RemConversionConfig,
1068
- responsiveScales?: ResponsiveScales
769
+ responsiveScales?: ResponsiveScales,
1069
770
  ): string {
1070
771
  const css: string[] = [];
1071
772
 
@@ -1082,9 +783,7 @@ export function generateInteractiveCSS(
1082
783
  const fullSelector = `${prefix || ''}.${elementClass}${postfix || ''}`;
1083
784
 
1084
785
  // Breakpoint styles (sorted by value descending)
1085
- const sortedBreakpoints = Object.entries(breakpointValues).sort(
1086
- ([, a], [, b]) => b - a
1087
- );
786
+ const sortedBreakpoints = Object.entries(breakpointValues).sort(([, a], [, b]) => b - a);
1088
787
 
1089
788
  if (isResponsiveStyle(style)) {
1090
789
  // Generate responsive rules
@@ -1120,7 +819,7 @@ export function generateInteractiveCSS(
1120
819
  responsive.base,
1121
820
  responsiveScales!,
1122
821
  breakpointName,
1123
- new Set(Object.keys(explicit ?? {}))
822
+ new Set(Object.keys(explicit ?? {})),
1124
823
  );
1125
824
  if (Object.keys(scaled).length > 0) {
1126
825
  merged = { ...(merged ?? {}), ...scaled };
@@ -1130,9 +829,7 @@ export function generateInteractiveCSS(
1130
829
  if (merged && Object.keys(merged).length > 0) {
1131
830
  const properties = applyRemConversion(styleObjectToCSS(merged), remConfig);
1132
831
  if (properties) {
1133
- css.push(
1134
- `@media (max-width: ${breakpointValue}px) { ${fullSelector} { ${properties}; } }`
1135
- );
832
+ css.push(`@media (max-width: ${breakpointValue}px) { ${fullSelector} { ${properties}; } }`);
1136
833
  }
1137
834
  }
1138
835
  }
@@ -1153,18 +850,11 @@ export function generateInteractiveCSS(
1153
850
  // Auto-scale the flat style into each enabled breakpoint — breakpoints mode only.
1154
851
  if (scalingEnabled && !fluidActive) {
1155
852
  for (const [breakpointName, breakpointValue] of sortedBreakpoints) {
1156
- const scaled = scaleStyleForBreakpoint(
1157
- flatStyle,
1158
- responsiveScales!,
1159
- breakpointName,
1160
- new Set()
1161
- );
853
+ const scaled = scaleStyleForBreakpoint(flatStyle, responsiveScales!, breakpointName, new Set());
1162
854
  if (Object.keys(scaled).length === 0) continue;
1163
855
  const scaledProps = applyRemConversion(styleObjectToCSS(scaled), remConfig);
1164
856
  if (!scaledProps) continue;
1165
- css.push(
1166
- `@media (max-width: ${breakpointValue}px) { ${fullSelector} { ${scaledProps}; } }`
1167
- );
857
+ css.push(`@media (max-width: ${breakpointValue}px) { ${fullSelector} { ${scaledProps}; } }`);
1168
858
  }
1169
859
  }
1170
860
  }
@@ -1184,7 +874,7 @@ function scaleStyleForBreakpoint(
1184
874
  baseStyle: StyleObject,
1185
875
  responsiveScales: ResponsiveScales,
1186
876
  breakpointName: string,
1187
- skipProperties: Set<string>
877
+ skipProperties: Set<string>,
1188
878
  ): StyleObject {
1189
879
  const result: StyleObject = {};
1190
880
  const baseRef = responsiveScales.baseReference ?? 16;
@@ -1197,11 +887,7 @@ function scaleStyleForBreakpoint(
1197
887
  const strValue = String(value);
1198
888
  if (strValue === '') continue;
1199
889
 
1200
- const scale = getScaleMultiplier(
1201
- responsiveScales,
1202
- property as CSSPropertyType,
1203
- breakpointName
1204
- );
890
+ const scale = getScaleMultiplier(responsiveScales, property as CSSPropertyType, breakpointName);
1205
891
  if (scale == null) continue;
1206
892
 
1207
893
  const scaledValue = scalePropertyValue(strValue, baseRef, scale);
@@ -1224,7 +910,7 @@ export function generateAllInteractiveCSS(
1224
910
  interactiveStylesMap: Map<string, InteractiveStyles>,
1225
911
  breakpoints: BreakpointConfig = DEFAULT_BREAKPOINTS,
1226
912
  remConfig?: RemConversionConfig,
1227
- responsiveScales?: ResponsiveScales
913
+ responsiveScales?: ResponsiveScales,
1228
914
  ): string {
1229
915
  const cssBlocks: string[] = [];
1230
916