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
@@ -1,29 +1,52 @@
1
1
  import { describe, test, expect } from 'bun:test';
2
- import { stylesToClasses, responsiveStylesToClasses, classToStyle, classesToStyles } from './utilityClassMapper';
2
+ import {
3
+ stylesToClasses,
4
+ responsiveStylesToClasses,
5
+ classToStyle,
6
+ classesToStyles,
7
+ splitStyleByClassability,
8
+ isClassableStyleValue,
9
+ styleHasMapping,
10
+ } from './utilityClassMapper';
3
11
  import { getStyleValue, clearRegistry } from './styleValueRegistry';
4
12
  import type { ResponsiveScales } from './responsiveScaling';
5
13
 
6
14
  describe('utilityClassMapper', () => {
7
15
  describe('stylesToClasses', () => {
8
- test('converts padding to utility class', () => {
9
- const classes = stylesToClasses({ padding: '10px' });
10
- expect(classes).toContain('p-10px');
16
+ test('converts padding on an exact scale step to a named token', () => {
17
+ expect(stylesToClasses({ padding: '16px' })).toContain('p-4');
18
+ expect(stylesToClasses({ padding: '10px' })).toContain('p-2.5');
11
19
  });
12
20
 
13
- test('converts display flex to short form', () => {
21
+ test('off-scale padding falls back to the arbitrary form', () => {
22
+ expect(stylesToClasses({ padding: '13px' })).toContain('p-[13px]');
23
+ expect(stylesToClasses({ padding: '5px' })).toContain('p-[5px]');
24
+ });
25
+
26
+ test('converts display flex to Tailwind static utility', () => {
14
27
  const classes = stylesToClasses({ display: 'flex' });
15
- expect(classes).toContain('f');
28
+ expect(classes).toContain('flex');
29
+ });
30
+
31
+ test('converts display none to hidden', () => {
32
+ const classes = stylesToClasses({ display: 'none' });
33
+ expect(classes).toContain('hidden');
16
34
  });
17
35
 
18
36
  test('converts multiple properties', () => {
19
37
  const classes = stylesToClasses({ padding: '10px', margin: '20px' });
20
- expect(classes).toContain('p-10px');
21
- expect(classes).toContain('m-20px');
38
+ expect(classes).toContain('p-2.5');
39
+ expect(classes).toContain('m-5');
22
40
  });
23
41
 
24
42
  test('handles percentage values', () => {
25
43
  const classes = stylesToClasses({ width: '50%' });
26
- expect(classes).toContain('w-50p');
44
+ expect(classes).toContain('w-[50%]');
45
+ });
46
+
47
+ test('maps width 100% to w-full', () => {
48
+ const classes = stylesToClasses({ width: '100%' });
49
+ expect(classes).toContain('w-full');
27
50
  });
28
51
 
29
52
  test('handles auto values', () => {
@@ -31,9 +54,9 @@ describe('utilityClassMapper', () => {
31
54
  expect(classes).toContain('m-auto');
32
55
  });
33
56
 
34
- test('handles values with spaces', () => {
57
+ test('handles values with spaces (underscore encoding)', () => {
35
58
  const classes = stylesToClasses({ padding: '92px 0' });
36
- expect(classes).toContain('p-92px-0');
59
+ expect(classes).toContain('p-[92px_0]');
37
60
  });
38
61
 
39
62
  test('returns empty array for null', () => {
@@ -41,60 +64,129 @@ describe('utilityClassMapper', () => {
41
64
  expect(stylesToClasses(undefined)).toEqual([]);
42
65
  });
43
66
 
44
- test('handles color variables', () => {
45
- const classes = stylesToClasses({ color: 'text' });
46
- expect(classes).toContain('c-text');
67
+ test('handles bare color tokens (Meno token form)', () => {
68
+ expect(stylesToClasses({ color: 'text' })).toContain('text-text');
69
+ expect(stylesToClasses({ backgroundColor: 'primary' })).toContain('bg-primary');
70
+ expect(stylesToClasses({ borderColor: 'border-light' })).toContain('border-border-light');
47
71
  });
48
72
 
49
- test('converts shadow preset to sh- class', () => {
73
+ test('named CSS colors / hex / Tailwind palette shapes stay arbitrary (not bare tokens)', () => {
74
+ expect(stylesToClasses({ backgroundColor: 'red' })).toContain('bg-[red]');
75
+ expect(stylesToClasses({ color: '#fff' })).toContain('text-[#fff]');
76
+ expect(stylesToClasses({ backgroundColor: 'gray-500' })).toContain('bg-[gray-500]');
77
+ });
78
+
79
+ test('handles font-size with explicit value', () => {
80
+ const classes = stylesToClasses({ fontSize: '48px' });
81
+ expect(classes).toContain('text-[48px]');
82
+ });
83
+
84
+ test('handles font-size variables with length hint', () => {
85
+ const classes = stylesToClasses({ fontSize: 'var(--font-size-lg)' });
86
+ expect(classes).toContain('text-(length:--font-size-lg)');
87
+ });
88
+
89
+ test('font-weight var carries a number hint (not misread as font-family)', () => {
90
+ // `font-*` is shared by weight/family; without the hint a var binding resolves to
91
+ // font-family (`font-family: 450`), so the weight never applies and the family changes.
92
+ expect(stylesToClasses({ fontWeight: 'var(--h-fw)' })).toContain('font-(number:--h-fw)');
93
+ expect(stylesToClasses({ fontFamily: 'var(--l-ff)' })).toContain('font-(--l-ff)');
94
+ // round-trips back to the right property
95
+ expect(classToStyle('font-(number:--h-fw)')).toEqual({ prop: 'fontWeight', value: 'var(--h-fw)' });
96
+ expect(classToStyle('font-(--l-ff)')).toEqual({ prop: 'fontFamily', value: 'var(--l-ff)' });
97
+ });
98
+
99
+ test('border/outline shorthand var binding stays the shorthand (not the color longhand)', () => {
100
+ // `border`/`outline` roots are shared by a shorthand and a color longhand. A `var(--x)` value
101
+ // is opaque and reads as a color, so the bare `border-(--x)` form would round-trip to
102
+ // `borderColor`. The shorthand must emit the explicit arbitrary-property form instead.
103
+ expect(stylesToClasses({ border: 'var(--bs)' })).toEqual(['[border:var(--bs)]']);
104
+ expect(stylesToClasses({ outline: 'var(--ol)' })).toEqual(['[outline:var(--ol)]']);
105
+ expect(stylesToClasses({ outlineWidth: 'var(--ow)' })).toEqual(['[outline-width:var(--ow)]']);
106
+ // round-trips back to the SAME property + var value
107
+ expect(classToStyle('[border:var(--bs)]')).toEqual({ prop: 'border', value: 'var(--bs)' });
108
+ expect(classToStyle('[outline:var(--ol)]')).toEqual({ prop: 'outline', value: 'var(--ol)' });
109
+ // the COLOR longhand emits the BARE token form (`var(--bs)` is a token reference); the
110
+ // token-aware inverse recovers it, and the `var(--bs)` shorthand still parses (back-compat).
111
+ expect(stylesToClasses({ borderColor: 'var(--bs)' })).toEqual(['border-bs']);
112
+ expect(classToStyle('border-bs', new Set(['bs']))).toEqual({ prop: 'borderColor', value: 'bs' });
113
+ expect(classToStyle('border-(--bs)')).toEqual({ prop: 'borderColor', value: 'bs' });
114
+ });
115
+
116
+ test('converts shadow preset to shadow- class', () => {
50
117
  const classes = stylesToClasses({ boxShadow: '2' });
51
- expect(classes).toContain('sh-2');
118
+ expect(classes).toContain('shadow-2');
52
119
  });
53
120
 
54
- test('converts gradient preset to gr- class', () => {
121
+ test('converts gradient preset to bg-gradient- class', () => {
55
122
  const classes = stylesToClasses({ background: '1' });
56
- expect(classes).toContain('gr-1');
123
+ expect(classes).toContain('bg-gradient-1');
57
124
  });
58
125
 
59
- test('converts border preset to bd- class', () => {
126
+ test('converts border preset to border-preset- class', () => {
60
127
  const classes = stylesToClasses({ border: '0' });
61
- expect(classes).toContain('bd-0');
128
+ expect(classes).toContain('border-preset-0');
62
129
  });
63
130
 
64
131
  test('handles aspectRatio with slash value', () => {
65
132
  const classes = stylesToClasses({ aspectRatio: '1/1' });
66
- expect(classes).toContain('ar-1s1');
133
+ expect(classes).toContain('aspect-[1/1]');
134
+ });
135
+
136
+ test('handles CSS function values', () => {
137
+ const classes = stylesToClasses({ transform: 'translateY(-50%)' });
138
+ expect(classes).toContain('transform-[translateY(-50%)]');
139
+ });
140
+
141
+ test('handles grid template repeat()', () => {
142
+ const classes = stylesToClasses({ gridTemplateColumns: 'repeat(3, 1fr)' });
143
+ expect(classes).toContain('grid-cols-[repeat(3,_1fr)]');
144
+ });
145
+
146
+ test('handles z-index Tailwind-style bare number', () => {
147
+ const classes = stylesToClasses({ zIndex: 10 });
148
+ expect(classes).toContain('z-10');
149
+ });
150
+
151
+ test('handles opacity on Tailwind 0-100 scale', () => {
152
+ const classes = stylesToClasses({ opacity: 0.5 });
153
+ expect(classes).toContain('opacity-50');
154
+ });
155
+
156
+ test('handles properties without a Tailwind root via arbitrary properties', () => {
157
+ const classes = stylesToClasses({ textOverflow: 'ellipsis' });
158
+ expect(classes).toContain('[text-overflow:ellipsis]');
67
159
  });
68
160
 
69
161
  test('align-items stretch maps to a distinct class from flex-start', () => {
70
- // Regression: stretch used to share the `ai-s` class with flex-start,
71
- // so imported `align-items: stretch` silently rendered as flex-start
72
- // — exactly what made Finsweet's mobile CTA buttons line up to the
73
- // left instead of stretching to full width.
162
+ // Regression: stretch must never share a class with flex-start,
163
+ // otherwise imported `align-items: stretch` silently renders as
164
+ // flex-start — exactly what made Finsweet's mobile CTA buttons line
165
+ // up to the left instead of stretching to full width.
74
166
  const stretchClasses = stylesToClasses({ alignItems: 'stretch' });
75
167
  const flexStartClasses = stylesToClasses({ alignItems: 'flex-start' });
76
- expect(stretchClasses).toContain('ai-st');
77
- expect(flexStartClasses).toContain('ai-s');
78
- expect(stretchClasses).not.toContain('ai-s');
79
- expect(classToStyle('ai-st')).toEqual({ prop: 'alignItems', value: 'stretch' });
168
+ expect(stretchClasses).toContain('items-stretch');
169
+ expect(flexStartClasses).toContain('items-start');
170
+ expect(stretchClasses).not.toContain('items-start');
171
+ expect(classToStyle('items-stretch')).toEqual({ prop: 'alignItems', value: 'stretch' });
80
172
  });
81
173
 
82
174
  test('justify-content space-evenly maps to a distinct class from flex-end', () => {
83
- // Same bug shape as align-items: `space-evenly` previously aliased to
84
- // `jc-e` (flex-end), silently changing layout.
175
+ // Same bug shape as align-items: `space-evenly` must not alias to
176
+ // flex-end, silently changing layout.
85
177
  const evenly = stylesToClasses({ justifyContent: 'space-evenly' });
86
178
  const end = stylesToClasses({ justifyContent: 'flex-end' });
87
- expect(evenly).toContain('jc-ev');
88
- expect(end).toContain('jc-e');
89
- expect(evenly).not.toContain('jc-e');
90
- expect(classToStyle('jc-ev')).toEqual({ prop: 'justifyContent', value: 'space-evenly' });
179
+ expect(evenly).toContain('justify-evenly');
180
+ expect(end).toContain('justify-end');
181
+ expect(evenly).not.toContain('justify-end');
182
+ expect(classToStyle('justify-evenly')).toEqual({ prop: 'justifyContent', value: 'space-evenly' });
91
183
  });
92
184
  });
93
185
 
94
186
  describe('responsiveStylesToClasses', () => {
95
187
  test('handles flat style object', () => {
96
188
  const classes = responsiveStylesToClasses({ padding: '10px' });
97
- expect(classes).toContain('p-10px');
189
+ expect(classes).toContain('p-2.5');
98
190
  });
99
191
 
100
192
  test('handles responsive styles with base', () => {
@@ -102,17 +194,19 @@ describe('utilityClassMapper', () => {
102
194
  base: { padding: '10px' },
103
195
  tablet: { padding: '20px' },
104
196
  });
105
- expect(classes).toContain('p-10px');
106
- expect(classes).toContain('t-p-20px');
197
+ expect(classes).toContain('p-2.5');
198
+ // Desktop-first: breakpoint overrides carry Tailwind's max-width `max-` marker.
199
+ expect(classes).toContain('max-lg:p-5');
107
200
  });
108
201
 
109
- test('handles mobile styles with mob prefix', () => {
202
+ test('handles mobile styles with mobile: variant prefix', () => {
110
203
  const classes = responsiveStylesToClasses({
111
204
  base: { padding: '10px' },
112
205
  mobile: { padding: '5px' },
113
206
  });
114
- expect(classes).toContain('p-10px');
115
- expect(classes).toContain('mob-p-5px');
207
+ expect(classes).toContain('p-2.5');
208
+ // 5px is off the scale → stays arbitrary
209
+ expect(classes).toContain('max-sm:p-[5px]');
116
210
  });
117
211
 
118
212
  test('returns empty array for null', () => {
@@ -120,6 +214,35 @@ describe('utilityClassMapper', () => {
120
214
  expect(responsiveStylesToClasses(undefined)).toEqual([]);
121
215
  });
122
216
 
217
+ test('desktop-first breakpoint prefix: emits Tailwind max-lg:/max-sm:, parses every legacy form', () => {
218
+ // Meno is desktop-first (max-width); the two default tiers carry Tailwind's canonical scale
219
+ // names in the class string — tablet → max-lg: (1024px = Tailwind lg), mobile → max-sm: (540px,
220
+ // Meno keeps its threshold). The breakpoint IDENTITY in the style object stays tablet/mobile.
221
+ const classes = responsiveStylesToClasses({
222
+ base: { padding: '16px' },
223
+ tablet: { padding: '8px' },
224
+ mobile: { padding: '4px' },
225
+ });
226
+ expect(classes).toEqual(['p-4', 'max-lg:p-2', 'max-sm:p-1']);
227
+
228
+ const expected = { base: { padding: '16px' }, tablet: { padding: '8px' }, mobile: { padding: '4px' } };
229
+ // The canonical max-lg:/max-sm: form round-trips back to the tablet/mobile buckets.
230
+ expect(classesToStyles(['p-4', 'max-lg:p-2', 'max-sm:p-1'])).toEqual(expected);
231
+ // Transitional un-aliased max-tablet:/max-mobile: form (emitted briefly before the alias) parses.
232
+ expect(classesToStyles(['p-4', 'max-tablet:p-2', 'max-mobile:p-1'])).toEqual(expected);
233
+ // Legacy bare prefixes (pre-`max-` projects) still parse to the same buckets.
234
+ expect(classesToStyles(['p-4', 'tablet:p-2', 'mobile:p-1'])).toEqual(expected);
235
+ });
236
+
237
+ test('foreign real-Tailwind min-width prefixes (bare lg:/sm:) are NOT absorbed as breakpoints', () => {
238
+ // A bare `lg:`/`sm:` is real-Tailwind MIN-width (opposite cascade) — Meno never emits it, so it
239
+ // must stay unrecognized rather than fold into the tablet/mobile buckets. (classesToStyles only
240
+ // buckets tablet/mobile; an un-mapped variant lands in base, but the studio adapter keeps such
241
+ // tokens foreign — see classAttrStyleAdapter.) Here: bare `lg:`/`sm:` do not map to tablet/mobile.
242
+ expect(classesToStyles(['lg:p-2'])?.tablet).toBeUndefined();
243
+ expect(classesToStyles(['sm:p-2'])?.mobile).toBeUndefined();
244
+ });
245
+
123
246
  describe('fluid mode — mobile-as-MIN consumption', () => {
124
247
  const fluidScales: ResponsiveScales = {
125
248
  enabled: true,
@@ -130,39 +253,39 @@ describe('utilityClassMapper', () => {
130
253
  padding: { tablet: 0.75, mobile: 0.5 },
131
254
  };
132
255
 
133
- test('consumes scalable mobile override into base clamp; no mob- class emitted', () => {
256
+ test('consumes scalable mobile override into base clamp; no mobile: class emitted', () => {
134
257
  clearRegistry();
135
258
  const classes = responsiveStylesToClasses(
136
259
  { base: { fontSize: '150px' }, mobile: { fontSize: '94px' } },
137
- { fluidActive: true, responsiveScales: fluidScales }
260
+ { fluidActive: true, responsiveScales: fluidScales },
138
261
  );
139
- // base became a hashed class wrapping clamp(94px, ..., 150px)
140
- const hashed = classes.find((c) => /^fs-h[0-9a-z]+$/.test(c));
141
- expect(hashed).toBeDefined();
142
- expect(getStyleValue(hashed!)).toMatch(/^clamp\(94px,/);
143
- expect(getStyleValue(hashed!)).toContain(', 150px)');
144
- // no .mob-fs-* emitted
145
- expect(classes.some((c) => c.startsWith('mob-fs-'))).toBe(false);
262
+ // base became an arbitrary class wrapping clamp(94px, ..., 150px)
263
+ const clamped = classes.find((c) => c.startsWith('text-[clamp('));
264
+ expect(clamped).toBeDefined();
265
+ expect(String(getStyleValue(clamped!))).toMatch(/^clamp\(94px,/);
266
+ expect(String(getStyleValue(clamped!))).toContain(', 150px)');
267
+ // no mobile:text-* emitted
268
+ expect(classes.some((c) => c.startsWith('mobile:text-'))).toBe(false);
146
269
  });
147
270
 
148
- test('mobile-only non-scalable property (display) stays as mob- class', () => {
271
+ test('mobile-only non-scalable property (display) stays as mobile: class', () => {
149
272
  clearRegistry();
150
273
  const classes = responsiveStylesToClasses(
151
274
  {
152
275
  base: { fontSize: '150px' },
153
276
  mobile: { fontSize: '94px', display: 'none' },
154
277
  },
155
- { fluidActive: true, responsiveScales: fluidScales }
278
+ { fluidActive: true, responsiveScales: fluidScales },
156
279
  );
157
280
  // fontSize consumed into base clamp
158
- const hashed = classes.find((c) => /^fs-h[0-9a-z]+$/.test(c));
159
- expect(hashed).toBeDefined();
160
- expect(getStyleValue(hashed!)).toMatch(/^clamp\(94px,/);
281
+ const clamped = classes.find((c) => c.startsWith('text-[clamp('));
282
+ expect(clamped).toBeDefined();
283
+ expect(String(getStyleValue(clamped!))).toMatch(/^clamp\(94px,/);
161
284
  // display:none survives on mobile
162
- expect(classes).toContain('mob-h');
285
+ expect(classes).toContain('max-sm:hidden');
163
286
  });
164
287
 
165
- test('tablet override stays as .t-* class (unchanged behavior)', () => {
288
+ test('tablet override stays as max-lg: class (unchanged behavior)', () => {
166
289
  clearRegistry();
167
290
  const classes = responsiveStylesToClasses(
168
291
  {
@@ -170,96 +293,132 @@ describe('utilityClassMapper', () => {
170
293
  tablet: { fontSize: '120px' },
171
294
  mobile: { fontSize: '94px' },
172
295
  },
173
- { fluidActive: true, responsiveScales: fluidScales }
296
+ { fluidActive: true, responsiveScales: fluidScales },
174
297
  );
175
- expect(classes).toContain('t-fs-120px');
298
+ expect(classes).toContain('max-lg:text-[120px]');
176
299
  // mobile consumed
177
- expect(classes.some((c) => c.startsWith('mob-fs-'))).toBe(false);
300
+ expect(classes.some((c) => c.startsWith('max-sm:text-'))).toBe(false);
178
301
  });
179
302
 
180
303
  test('no mobile override → base passes through unchanged (no consumption)', () => {
181
304
  clearRegistry();
182
305
  const classes = responsiveStylesToClasses(
183
306
  { base: { fontSize: '150px' } },
184
- { fluidActive: true, responsiveScales: fluidScales }
307
+ { fluidActive: true, responsiveScales: fluidScales },
185
308
  );
186
- expect(classes).toContain('fs-150px');
187
- expect(classes.some((c) => /^fs-h/.test(c))).toBe(false);
309
+ expect(classes).toContain('text-[150px]');
310
+ expect(classes.some((c) => c.startsWith('text-[clamp('))).toBe(false);
188
311
  });
189
312
 
190
- test('mismatched units (rem vs px) → falls back, mob- class still emitted', () => {
313
+ test('mismatched units (rem vs px) → falls back, mobile: class still emitted', () => {
191
314
  clearRegistry();
192
315
  const classes = responsiveStylesToClasses(
193
316
  { base: { fontSize: '10rem' }, mobile: { fontSize: '94px' } },
194
- { fluidActive: true, responsiveScales: fluidScales }
317
+ { fluidActive: true, responsiveScales: fluidScales },
195
318
  );
196
- expect(classes).toContain('fs-10rem');
197
- expect(classes).toContain('mob-fs-94px');
319
+ expect(classes).toContain('text-[10rem]');
320
+ expect(classes).toContain('max-sm:text-[94px]');
198
321
  });
199
322
 
200
323
  test('non-scalable mobile override (display) is not consumed', () => {
201
324
  clearRegistry();
202
325
  const classes = responsiveStylesToClasses(
203
326
  { base: { display: 'flex' }, mobile: { display: 'block' } },
204
- { fluidActive: true, responsiveScales: fluidScales }
327
+ { fluidActive: true, responsiveScales: fluidScales },
205
328
  );
206
- expect(classes).toContain('f');
207
- expect(classes).toContain('mob-b');
329
+ expect(classes).toContain('flex');
330
+ expect(classes).toContain('max-sm:block');
208
331
  });
209
332
 
210
- test('fluidActive off → no consumption, mob- class emitted', () => {
333
+ test('fluidActive off → no consumption, mobile: class emitted', () => {
211
334
  clearRegistry();
212
335
  const classes = responsiveStylesToClasses(
213
336
  { base: { fontSize: '150px' }, mobile: { fontSize: '94px' } },
214
- { fluidActive: false, responsiveScales: fluidScales }
337
+ { fluidActive: false, responsiveScales: fluidScales },
215
338
  );
216
- expect(classes).toContain('fs-150px');
217
- expect(classes).toContain('mob-fs-94px');
339
+ expect(classes).toContain('text-[150px]');
340
+ expect(classes).toContain('max-sm:text-[94px]');
218
341
  });
219
342
 
220
343
  test('fluidActive on but responsiveScales disabled → no consumption', () => {
221
344
  clearRegistry();
222
345
  const classes = responsiveStylesToClasses(
223
346
  { base: { fontSize: '150px' }, mobile: { fontSize: '94px' } },
224
- { fluidActive: true, responsiveScales: { ...fluidScales, enabled: false } }
347
+ { fluidActive: true, responsiveScales: { ...fluidScales, enabled: false } },
225
348
  );
226
- expect(classes).toContain('fs-150px');
227
- expect(classes).toContain('mob-fs-94px');
349
+ expect(classes).toContain('text-[150px]');
350
+ expect(classes).toContain('max-sm:text-[94px]');
228
351
  });
229
352
 
230
353
  test('clamp math: MIN=94px, MAX=150px, fluidRange 320–1700', () => {
231
354
  clearRegistry();
232
355
  const classes = responsiveStylesToClasses(
233
356
  { base: { fontSize: '150px' }, mobile: { fontSize: '94px' } },
234
- { fluidActive: true, responsiveScales: fluidScales }
357
+ { fluidActive: true, responsiveScales: fluidScales },
235
358
  );
236
- const hashed = classes.find((c) => /^fs-h[0-9a-z]+$/.test(c))!;
237
- const value = String(getStyleValue(hashed));
359
+ const clamped = classes.find((c) => c.startsWith('text-[clamp('))!;
360
+ const value = String(getStyleValue(clamped));
238
361
  // slope = (150-94)/(1700-320) = 56/1380 ≈ 0.04058 px/px → slopeVw ≈ 4.058vw
239
362
  // intercept = 94 - 0.04058*320 = 94 - 12.9855 ≈ 81.0145px
240
363
  expect(value).toMatch(/clamp\(94px,\s*81\.0145px\s*\+\s*4\.058vw,\s*150px\)/);
241
364
  });
242
365
  });
366
+
367
+ describe('fluid mode — container intent (width === maxWidth)', () => {
368
+ test('rewrites equal width/maxWidth into the fluid container pattern', () => {
369
+ const classes = responsiveStylesToClasses(
370
+ { base: { width: '1200px', maxWidth: '1200px' } },
371
+ { fluidActive: true },
372
+ );
373
+ // width becomes a margin-aware calc, gains mx-auto; maxWidth (the cap) is kept.
374
+ expect(classes).toContain('w-[calc(100%_-_var(--site-margin)_*_2)]');
375
+ expect(classes).toContain('ml-auto');
376
+ expect(classes).toContain('mr-auto');
377
+ expect(classes).toContain('max-w-[1200px]');
378
+ expect(classes).not.toContain('w-[1200px]');
379
+ });
380
+
381
+ test('fluidActive off → plain width/maxWidth classes, no container rewrite', () => {
382
+ expect(responsiveStylesToClasses({ base: { width: '1200px', maxWidth: '1200px' } })).toEqual([
383
+ 'w-[1200px]',
384
+ 'max-w-[1200px]',
385
+ ]);
386
+ });
387
+
388
+ test('width !== maxWidth is not container intent (left untouched even in fluid mode)', () => {
389
+ const classes = responsiveStylesToClasses(
390
+ { base: { width: '1200px', maxWidth: '100%' } },
391
+ { fluidActive: true },
392
+ );
393
+ expect(classes).toContain('w-[1200px]');
394
+ expect(classes).not.toContain('w-[calc(100%_-_var(--site-margin)_*_2)]');
395
+ });
396
+ });
243
397
  });
244
398
 
245
399
  describe('classToStyle', () => {
246
400
  test('parses padding class', () => {
247
- const style = classToStyle('p-10px');
401
+ const style = classToStyle('p-[10px]');
248
402
  expect(style).toEqual({ prop: 'padding', value: '10px' });
249
403
  });
250
404
 
251
- test('parses special case flex', () => {
252
- const style = classToStyle('f');
405
+ test('parses static flex utility', () => {
406
+ const style = classToStyle('flex');
253
407
  expect(style).toEqual({ prop: 'display', value: 'flex' });
254
408
  });
255
409
 
256
- test('handles tablet prefix', () => {
257
- const style = classToStyle('t-p-20px');
410
+ test('parses hidden utility', () => {
411
+ const style = classToStyle('hidden');
412
+ expect(style).toEqual({ prop: 'display', value: 'none' });
413
+ });
414
+
415
+ test('handles tablet variant prefix', () => {
416
+ const style = classToStyle('tablet:p-[20px]');
258
417
  expect(style).toEqual({ prop: 'padding', value: '20px' });
259
418
  });
260
419
 
261
- test('handles mobile prefix', () => {
262
- const style = classToStyle('mob-p-5px');
420
+ test('handles mobile variant prefix', () => {
421
+ const style = classToStyle('mobile:p-[5px]');
263
422
  expect(style).toEqual({ prop: 'padding', value: '5px' });
264
423
  });
265
424
 
@@ -268,32 +427,69 @@ describe('utilityClassMapper', () => {
268
427
  expect(classToStyle('unknown-class')).toBeNull();
269
428
  });
270
429
 
430
+ test('decodes underscores back to spaces', () => {
431
+ const style = classToStyle('p-[92px_0]');
432
+ expect(style).toEqual({ prop: 'padding', value: '92px 0' });
433
+ });
434
+
435
+ test('decodes color variables to bare tokens', () => {
436
+ const style = classToStyle('bg-(--primary)');
437
+ expect(style).toEqual({ prop: 'backgroundColor', value: 'primary' });
438
+ });
439
+
440
+ test('decodes non-color variables to var() values', () => {
441
+ const style = classToStyle('w-(--site-width)');
442
+ expect(style).toEqual({ prop: 'width', value: 'var(--site-width)' });
443
+ });
444
+
445
+ test('disambiguates text- by value: font-size vs color', () => {
446
+ expect(classToStyle('text-[14px]')).toEqual({ prop: 'fontSize', value: '14px' });
447
+ expect(classToStyle('text-[#fff]')).toEqual({ prop: 'color', value: '#fff' });
448
+ expect(classToStyle('text-(length:--font-size-lg)')).toEqual({
449
+ prop: 'fontSize',
450
+ value: 'var(--font-size-lg)',
451
+ });
452
+ });
453
+
454
+ test('decodes keyword utilities', () => {
455
+ expect(classToStyle('w-full')).toEqual({ prop: 'width', value: '100%' });
456
+ expect(classToStyle('h-screen')).toEqual({ prop: 'height', value: '100vh' });
457
+ expect(classToStyle('rounded-full')).toEqual({ prop: 'borderRadius', value: '9999px' });
458
+ });
459
+
460
+ test('decodes arbitrary-property classes', () => {
461
+ expect(classToStyle('[text-overflow:ellipsis]')).toEqual({ prop: 'textOverflow', value: 'ellipsis' });
462
+ });
463
+
271
464
  test('parses shadow preset class', () => {
272
- const style = classToStyle('sh-2');
465
+ const style = classToStyle('shadow-2');
273
466
  expect(style).toEqual({ prop: 'boxShadow', value: '0 4px 12px rgba(0, 0, 0, 0.1)' });
274
467
  });
275
468
 
276
469
  test('parses gradient preset class', () => {
277
- const style = classToStyle('gr-1');
278
- expect(style).toEqual({ prop: 'background', value: 'linear-gradient(90deg, rgba(10,22,40,0.85) 0%, rgba(10,22,40,0.2) 100%)' });
470
+ const style = classToStyle('bg-gradient-1');
471
+ expect(style).toEqual({
472
+ prop: 'background',
473
+ value: 'linear-gradient(90deg, rgba(10,22,40,0.85) 0%, rgba(10,22,40,0.2) 100%)',
474
+ });
279
475
  });
280
476
 
281
477
  test('parses border preset class', () => {
282
- const style = classToStyle('bd-0');
478
+ const style = classToStyle('border-preset-0');
283
479
  expect(style).toEqual({ prop: 'border', value: '1px solid var(--border)' });
284
480
  });
285
481
  });
286
482
 
287
483
  describe('classesToStyles', () => {
288
484
  test('converts base classes to style object', () => {
289
- const styles = classesToStyles(['p-10px', 'm-20px']);
485
+ const styles = classesToStyles(['p-[10px]', 'm-[20px]']);
290
486
  expect(styles).toEqual({
291
487
  base: { padding: '10px', margin: '20px' },
292
488
  });
293
489
  });
294
490
 
295
491
  test('handles responsive classes', () => {
296
- const styles = classesToStyles(['p-10px', 't-p-20px']);
492
+ const styles = classesToStyles(['p-[10px]', 'tablet:p-[20px]']);
297
493
  expect(styles).toEqual({
298
494
  base: { padding: '10px' },
299
495
  tablet: { padding: '20px' },
@@ -301,7 +497,7 @@ describe('utilityClassMapper', () => {
301
497
  });
302
498
 
303
499
  test('handles mobile classes', () => {
304
- const styles = classesToStyles(['p-10px', 'mob-p-5px']);
500
+ const styles = classesToStyles(['p-[10px]', 'mobile:p-[5px]']);
305
501
  expect(styles).toEqual({
306
502
  base: { padding: '10px' },
307
503
  mobile: { padding: '5px' },
@@ -312,11 +508,153 @@ describe('utilityClassMapper', () => {
312
508
  expect(classesToStyles([])).toBeNull();
313
509
  });
314
510
 
511
+ test('keeps a narrower-breakpoint-only class string (no base) instead of returning null', () => {
512
+ // A class can style only a narrower breakpoint; this must not collapse to null (§11 adapter).
513
+ expect(classesToStyles(['tablet:p-[16px]'])).toEqual({ base: {}, tablet: { padding: '16px' } });
514
+ expect(classesToStyles(['mobile:p-[8px]'])).toEqual({ base: {}, mobile: { padding: '8px' } });
515
+ });
516
+
315
517
  test('removes empty breakpoints', () => {
316
- const styles = classesToStyles(['p-10px']);
518
+ const styles = classesToStyles(['p-[10px]']);
317
519
  expect(styles).toEqual({ base: { padding: '10px' } });
318
520
  expect(styles?.tablet).toBeUndefined();
319
521
  expect(styles?.mobile).toBeUndefined();
320
522
  });
523
+
524
+ test('round-trips styles → classes → styles', () => {
525
+ const original = {
526
+ base: { padding: '92px 0', display: 'flex', color: 'primary', width: '100%' },
527
+ mobile: { padding: '12px 0' },
528
+ };
529
+ const classes = responsiveStylesToClasses(original);
530
+ // `color: primary` emits the bare token `text-primary`, which round-trips
531
+ // only against the project's known tokens (the new token-aware contract).
532
+ expect(classesToStyles(classes, new Set(['primary']))).toEqual(original);
533
+ });
534
+ });
535
+
536
+ describe('named spacing scale', () => {
537
+ test('forward: exact steps emit named tokens; off-scale stays arbitrary', () => {
538
+ expect(stylesToClasses({ padding: '16px' })).toEqual(['p-4']);
539
+ expect(stylesToClasses({ gap: '24px' })).toEqual(['gap-6']);
540
+ expect(stylesToClasses({ marginTop: '8px' })).toEqual(['mt-2']);
541
+ expect(stylesToClasses({ paddingInline: '16px' })).toEqual(['px-4']);
542
+ expect(stylesToClasses({ width: '256px' })).toEqual(['w-64']);
543
+ expect(stylesToClasses({ padding: '0px' })).toEqual(['p-0']);
544
+ expect(stylesToClasses({ padding: '2px' })).toEqual(['p-0.5']);
545
+ // off-scale + negatives keep the arbitrary form
546
+ expect(stylesToClasses({ padding: '13px' })).toEqual(['p-[13px]']);
547
+ expect(stylesToClasses({ margin: '-16px' })).toEqual(['m-[-16px]']);
548
+ });
549
+
550
+ test('keyword values still win over the scale', () => {
551
+ expect(stylesToClasses({ width: '100%' })).toEqual(['w-full']);
552
+ expect(stylesToClasses({ minHeight: '100vh' })).toEqual(['min-h-screen']);
553
+ expect(stylesToClasses({ margin: 'auto' })).toEqual(['m-auto']);
554
+ });
555
+
556
+ test('inverse: named steps decode back to px (durable from the name alone)', () => {
557
+ expect(classToStyle('p-4')).toEqual({ prop: 'padding', value: '16px' });
558
+ expect(classToStyle('p-2.5')).toEqual({ prop: 'padding', value: '10px' });
559
+ expect(classToStyle('gap-6')).toEqual({ prop: 'gap', value: '24px' });
560
+ expect(classToStyle('px-4')).toEqual({ prop: 'paddingInline', value: '16px' });
561
+ expect(classToStyle('w-64')).toEqual({ prop: 'width', value: '256px' });
562
+ });
563
+
564
+ test('round-trips through classesToStyles', () => {
565
+ const classes = responsiveStylesToClasses({ base: { padding: '16px', gap: '24px' }, tablet: { padding: '8px' } });
566
+ expect(classes).toEqual(['p-4', 'gap-6', 'max-lg:p-2']);
567
+ expect(classesToStyles(classes)).toEqual({ base: { padding: '16px', gap: '24px' }, tablet: { padding: '8px' } });
568
+ });
569
+
570
+ test('back-compat: the arbitrary form still parses', () => {
571
+ expect(classToStyle('p-[16px]')).toEqual({ prop: 'padding', value: '16px' });
572
+ });
573
+ });
574
+
575
+ describe('bare color tokens (token-aware)', () => {
576
+ const TOKENS = new Set(['primary', 'text', 'border', 'border-light']);
577
+
578
+ test('forward emits the bare token form', () => {
579
+ expect(stylesToClasses({ backgroundColor: 'primary' })).toEqual(['bg-primary']);
580
+ expect(stylesToClasses({ color: 'text' })).toEqual(['text-text']);
581
+ expect(stylesToClasses({ borderColor: 'border-light' })).toEqual(['border-border-light']);
582
+ });
583
+
584
+ test('a var() value on a color prop also emits the bare form (var(--muted) → bg-muted)', () => {
585
+ // The editor stores color tokens as `var(--x)`; that IS a token reference, so it emits the same
586
+ // bare form as the bare value would — `var(--muted)` and `muted` agree on `bg-muted`.
587
+ expect(stylesToClasses({ backgroundColor: 'var(--muted)' })).toEqual(['bg-muted']);
588
+ expect(stylesToClasses({ color: 'var(--text)' })).toEqual(['text-text']);
589
+ expect(stylesToClasses({ borderColor: 'var(--border)' })).toEqual(['border-border']);
590
+ expect(stylesToClasses({ outlineColor: 'var(--ring)' })).toEqual(['outline-ring']);
591
+ expect(classToStyle('bg-muted', new Set(['muted']))).toEqual({ prop: 'backgroundColor', value: 'muted' });
592
+ });
593
+
594
+ test('inverse needs the project token set; foreign classes are never tokens', () => {
595
+ expect(classToStyle('bg-primary', TOKENS)).toEqual({ prop: 'backgroundColor', value: 'primary' });
596
+ expect(classToStyle('border-border-light', TOKENS)).toEqual({ prop: 'borderColor', value: 'border-light' });
597
+ // unknown name → not a token (left for foreign-class passthrough)
598
+ expect(classToStyle('text-wrapper', TOKENS)).toBeNull();
599
+ // no token set → bare form is unrecognized (foreign-safe default)
600
+ expect(classToStyle('bg-primary')).toBeNull();
601
+ });
602
+
603
+ test('round-trips with the token set', () => {
604
+ const classes = responsiveStylesToClasses({ base: { backgroundColor: 'primary', color: 'text' } });
605
+ expect(classes).toEqual(['bg-primary', 'text-text']);
606
+ expect(classesToStyles(classes, TOKENS)).toEqual({ base: { backgroundColor: 'primary', color: 'text' } });
607
+ });
608
+
609
+ test('back-compat: the var() shorthand still parses without a token set', () => {
610
+ expect(classToStyle('bg-(--primary)')).toEqual({ prop: 'backgroundColor', value: 'primary' });
611
+ });
612
+ });
613
+
614
+ describe('splitStyleByClassability', () => {
615
+ test('isClassableStyleValue: static strings/numbers yes; templates/objects no', () => {
616
+ expect(isClassableStyleValue('24px')).toBe(true);
617
+ expect(isClassableStyleValue(100)).toBe(true);
618
+ expect(isClassableStyleValue('{{x}}px')).toBe(false);
619
+ expect(isClassableStyleValue({ _mapping: true })).toBe(false);
620
+ });
621
+
622
+ test('separates static (classable) from template/mapping (residual), preserving breakpoints', () => {
623
+ const mapping = { _mapping: true as const, prop: 'theme', values: { a: 'red' } };
624
+ expect(
625
+ splitStyleByClassability({
626
+ base: { padding: '24px', width: '{{w}}px', color: mapping },
627
+ tablet: { padding: '16px' },
628
+ }),
629
+ ).toEqual({
630
+ classable: { base: { padding: '24px' }, tablet: { padding: '16px' } },
631
+ residual: { base: { width: '{{w}}px', color: mapping } },
632
+ });
633
+ });
634
+
635
+ test('flat object splits under base; null/empty → null both sides', () => {
636
+ expect(splitStyleByClassability({ width: 100, label: '{{t}}' })).toEqual({
637
+ classable: { base: { width: 100 } },
638
+ residual: { base: { label: '{{t}}' } },
639
+ });
640
+ expect(splitStyleByClassability(null)).toEqual({ classable: null, residual: null });
641
+ expect(splitStyleByClassability({})).toEqual({ classable: null, residual: null });
642
+ });
643
+ });
644
+
645
+ describe('styleHasMapping', () => {
646
+ const mapping = { _mapping: true as const, prop: 'theme', values: { a: 'red' } };
647
+ test('detects a prop _mapping in any breakpoint (flat or responsive)', () => {
648
+ expect(styleHasMapping({ base: { color: mapping } })).toBe(true);
649
+ expect(styleHasMapping({ base: { padding: '8px' }, tablet: { color: mapping } })).toBe(true);
650
+ expect(styleHasMapping({ color: mapping })).toBe(true); // flat
651
+ });
652
+ test('false for plain / empty / nullish styles', () => {
653
+ expect(styleHasMapping({ base: { padding: '24px' } })).toBe(false);
654
+ expect(styleHasMapping({ padding: '24px' })).toBe(false);
655
+ expect(styleHasMapping({})).toBe(false);
656
+ expect(styleHasMapping(null)).toBe(false);
657
+ expect(styleHasMapping(undefined)).toBe(false);
658
+ });
321
659
  });
322
660
  });