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
@@ -0,0 +1,38 @@
1
+ import { describe, test, expect } from 'bun:test';
2
+ import { encodeArbitraryValue, decodeArbitraryValue } from './utilityClassNames';
3
+
4
+ describe('encodeArbitraryValue / decodeArbitraryValue', () => {
5
+ describe('reversible (decode ∘ encode === identity)', () => {
6
+ const cases: Array<[string, string]> = [
7
+ ['single token', '24px'],
8
+ ['spaces → underscores', '92px 0'],
9
+ ['multi-token shorthand', '1px solid red'],
10
+ ['literal underscore escaped', 'slide_in'],
11
+ ['underscore + spaces', 'fade_in 2s ease'],
12
+ ['underscore inside url() preserved', 'url(a_b.png)'],
13
+ ['calc with spaces', 'calc(100% - 24px)'],
14
+ ];
15
+ for (const [name, value] of cases) {
16
+ test(name, () => {
17
+ const encoded = encodeArbitraryValue(value);
18
+ expect(encoded).not.toBeNull();
19
+ // no raw spaces / quotes leak into the class token
20
+ expect(encoded!).not.toMatch(/[\s"']/);
21
+ expect(decodeArbitraryValue(encoded!)).toBe(value);
22
+ });
23
+ }
24
+
25
+ test('literal underscore encodes via the \\_ escape', () => {
26
+ expect(encodeArbitraryValue('slide_in')).toBe('slide\\_in');
27
+ expect(encodeArbitraryValue('fade_in 2s ease')).toBe('fade\\_in_2s_ease');
28
+ });
29
+ });
30
+
31
+ describe('un-encodable → null (defer to side-channel)', () => {
32
+ for (const value of ['"a a"', "'x'", '[grid]', 'a\\b', 'url("a b.png")']) {
33
+ test(JSON.stringify(value), () => {
34
+ expect(encodeArbitraryValue(value)).toBeNull();
35
+ });
36
+ }
37
+ });
38
+ });
@@ -0,0 +1,458 @@
1
+ /**
2
+ * Tailwind-style utility class name parsing & value encoding.
3
+ *
4
+ * Shared by the forward mapper (utilityClassMapper.ts) and the CSS generator
5
+ * (cssGeneration.ts) so class names encode and decode identically everywhere.
6
+ *
7
+ * Class name grammar (after stripping an optional `breakpoint:` variant):
8
+ * - static utilities: `flex`, `hidden`, `justify-center`, `uppercase`, …
9
+ * - presets: `shadow-2`, `bg-gradient-1`, `border-preset-0`
10
+ * - arbitrary values: `p-[40px]`, `w-[50%]`, `m-[92px_0]` (spaces → `_`)
11
+ * - CSS variables: `bg-(--primary)`, `text-(length:--font-size-lg)`
12
+ * - named keywords: `w-full`, `h-screen`, `m-auto`, `rounded-full`
13
+ * - bare numbers: `z-10`, `order-2`, `opacity-50` (= 0.5)
14
+ * - arbitrary properties: `[clip-path:circle(50%)]`, `[background:…]`
15
+ * - hash fallback: `pt-h1y9pr6i` (value lives in the style-value registry)
16
+ */
17
+
18
+ import {
19
+ prefixToCSSProperty,
20
+ staticUtilityReverse,
21
+ presetClassReverse,
22
+ keywordValues,
23
+ genericKeywordRoots,
24
+ integerValueRoots,
25
+ propertyOrder,
26
+ SPACING_SCALE,
27
+ SPACING_SCALE_ROOTS,
28
+ COLOR_TOKEN_ROOTS,
29
+ } from './utilityClassConfig';
30
+ import { isCssNamedColor } from './cssNamedColors';
31
+
32
+ /** Class roots sorted longest-first so `border-t-…` wins over `border-…`. */
33
+ export const SORTED_ROOTS: readonly string[] = Object.keys(prefixToCSSProperty).sort((a, b) => b.length - a.length);
34
+
35
+ /** Convert a camelCase style property to its kebab-case CSS name. */
36
+ export function camelToKebab(prop: string): string {
37
+ return prop.replace(/([A-Z])/g, '-$1').toLowerCase();
38
+ }
39
+
40
+ // kebab-case CSS property → camelCase style property, for all known properties.
41
+ const kebabToCamelMap: Record<string, string> = (() => {
42
+ const map: Record<string, string> = {};
43
+ for (const prop of propertyOrder) {
44
+ map[camelToKebab(prop)] = prop;
45
+ }
46
+ return map;
47
+ })();
48
+
49
+ /** Convert a kebab-case CSS property back to its camelCase style property. */
50
+ export function cssPropertyToCamel(property: string): string {
51
+ return kebabToCamelMap[property] ?? property.replace(/-([a-z])/g, (_, c: string) => c.toUpperCase());
52
+ }
53
+
54
+ /**
55
+ * Roots that decode to a different camelCase property than their kebab CSS
56
+ * property suggests (px → paddingInline, not padding).
57
+ */
58
+ export const ROOT_CAMEL_OVERRIDES: Record<string, string> = {
59
+ px: 'paddingInline',
60
+ py: 'paddingBlock',
61
+ mx: 'marginInline',
62
+ my: 'marginBlock',
63
+ };
64
+
65
+ // ---------------------------------------------------------------------------
66
+ // Arbitrary value encoding (Tailwind bracket convention: spaces → underscores)
67
+ // ---------------------------------------------------------------------------
68
+
69
+ /**
70
+ * Apply `transform` to every part of `input` that is NOT inside a `url(...)`
71
+ * segment. Tailwind preserves underscores inside url() — so do we, in both
72
+ * encode and decode directions.
73
+ */
74
+ function transformOutsideUrl(input: string, transform: (segment: string) => string): string {
75
+ let result = '';
76
+ let i = 0;
77
+ const lower = input.toLowerCase();
78
+ while (i < input.length) {
79
+ const idx = lower.indexOf('url(', i);
80
+ if (idx === -1) {
81
+ result += transform(input.slice(i));
82
+ break;
83
+ }
84
+ result += transform(input.slice(i, idx));
85
+ // Copy the url(...) segment verbatim up to its matching close paren
86
+ let depth = 0;
87
+ let j = idx + 3; // position of '('
88
+ for (; j < input.length; j++) {
89
+ if (input[j] === '(') depth++;
90
+ else if (input[j] === ')') {
91
+ depth--;
92
+ if (depth === 0) {
93
+ j++;
94
+ break;
95
+ }
96
+ }
97
+ }
98
+ result += input.slice(idx, j);
99
+ i = j;
100
+ }
101
+ return result;
102
+ }
103
+
104
+ /**
105
+ * Encode a CSS value for a Tailwind arbitrary-value class: spaces become
106
+ * underscores (`92px 0` → `92px_0`), and literal underscores are escaped as
107
+ * `\_` (`slide_in 2s` → `slide\_in_2s`) so they survive the decode — the `\_`
108
+ * escape is CSS-selector-safe (`escapeCSSClassName` turns the `\` into `\\`).
109
+ *
110
+ * Returns null when the value still can't be encoded reversibly — quotes,
111
+ * brackets, stray backslashes, or whitespace/quotes inside url(). Those callers
112
+ * fall back to a hash class + style-value registry entry (ephemeral; for the
113
+ * class-string-as-source migration they must move to a structured remainder —
114
+ * see docs/meno-class-styling-migration.md §5).
115
+ */
116
+ export function encodeArbitraryValue(value: string): string | null {
117
+ let ok = true;
118
+ const encoded = transformOutsideUrl(value, (segment) => {
119
+ // Quotes/brackets/backslash can't live safely in a class attribute or the
120
+ // `[..]` value — reject (→ side-channel). Underscores ARE representable via
121
+ // the `\_` escape, so they're allowed.
122
+ if (/["'\\[\]]/.test(segment)) ok = false;
123
+ // Escape literal underscores BEFORE collapsing whitespace, so the
124
+ // underscores we introduce for spaces aren't themselves escaped.
125
+ return segment.replace(/_/g, '\\_').replace(/\s+/g, '_');
126
+ });
127
+ if (!ok) return null;
128
+ // Strip our own `\_` escapes, then reject anything still problematic — those
129
+ // chars can only have survived verbatim from inside a url(...) segment.
130
+ const guardTarget = encoded.replace(/^\[|\]$/g, '').replace(/\\_/g, '');
131
+ if (/[\s"'\\[\]]/.test(guardTarget)) return null;
132
+ return encoded;
133
+ }
134
+
135
+ /**
136
+ * Decode a Tailwind arbitrary value: `\_` → literal underscore and bare `_` →
137
+ * space, in a single pass (so an escaped underscore isn't first read as a
138
+ * space). Content inside url(...) is left verbatim.
139
+ */
140
+ export function decodeArbitraryValue(encoded: string): string {
141
+ return transformOutsideUrl(encoded, (segment) => segment.replace(/\\_|_/g, (m) => (m === '\\_' ? '_' : ' ')));
142
+ }
143
+
144
+ // ---------------------------------------------------------------------------
145
+ // Value-type classification (for roots shared by several properties)
146
+ // ---------------------------------------------------------------------------
147
+
148
+ const FONT_SIZE_KEYWORDS = new Set([
149
+ 'xx-small',
150
+ 'x-small',
151
+ 'small',
152
+ 'medium',
153
+ 'large',
154
+ 'x-large',
155
+ 'xx-large',
156
+ 'xxx-large',
157
+ 'larger',
158
+ 'smaller',
159
+ ]);
160
+
161
+ /** True when a value reads as a color (hex, color function, named color, var()). */
162
+ export function isColorValue(value: string): boolean {
163
+ if (value.startsWith('#') || value.startsWith('var(')) return true;
164
+ if (/^(rgb|rgba|hsl|hsla|oklch|oklab|lab|lch|hwb|color|color-mix)\(/i.test(value)) return true;
165
+ if (value === 'transparent' || value === 'currentColor' || value === 'currentcolor') return true;
166
+ return isCssNamedColor(value);
167
+ }
168
+
169
+ /**
170
+ * True for a bare Meno color token (e.g. `primary`, `background-light`) that
171
+ * resolves to `var(--token)`. Rejects: named CSS colors / hex / color functions
172
+ * / `transparent` / `currentColor` (those are real colors, not tokens), and the
173
+ * Tailwind palette shape `word-<number>` (`gray-500`, `blue-600`) so foreign
174
+ * Tailwind classes carried verbatim in `class=""` aren't misread as Meno tokens.
175
+ *
176
+ * Shared by the forward mapper (token → `bg-primary`) and inverse parser
177
+ * (`bg-primary` → `var(--primary)`) so the two stay symmetric.
178
+ */
179
+ export function isBareColorTokenName(value: string): boolean {
180
+ if (!/^[a-zA-Z][\w-]*$/.test(value)) return false;
181
+ if (isColorValue(value)) return false;
182
+ if (/^[a-z]+-\d/i.test(value)) return false;
183
+ return true;
184
+ }
185
+
186
+ /** True when a `text-…` value reads as a font-size (length/number/keyword). */
187
+ function isFontSizeValue(value: string): boolean {
188
+ if (/^[\d.+-]/.test(value)) return true;
189
+ if (/^(calc|clamp|min|max)\(/i.test(value)) return true;
190
+ return FONT_SIZE_KEYWORDS.has(value);
191
+ }
192
+
193
+ /** True when a `font-…` value reads as a font-weight (numeric). */
194
+ function isFontWeightValue(value: string): boolean {
195
+ return /^\d+(\.\d+)?$/.test(value);
196
+ }
197
+
198
+ /** True when a `bg-…` value reads as a background-image (url()/gradients). */
199
+ function isImageValue(value: string): boolean {
200
+ return /^(url|linear-gradient|radial-gradient|conic-gradient|repeating-linear-gradient|repeating-radial-gradient|repeating-conic-gradient|image-set|cross-fade|element)\(/i.test(
201
+ value,
202
+ );
203
+ }
204
+
205
+ /** True for a single length token like `2px`, `0.5rem`, `-4px`. */
206
+ function isLengthValue(value: string): boolean {
207
+ return /^-?[\d.]+[a-z%]*$/i.test(value);
208
+ }
209
+
210
+ /**
211
+ * True when the value contains whitespace OUTSIDE any parentheses, i.e. it's a
212
+ * multi-token shorthand (`1px solid red`) rather than a single token. A color
213
+ * function like `rgba(0, 0, 0, 0)` or `color-mix(in srgb, …)` carries internal
214
+ * spaces but is still ONE token — those must not read as a shorthand, otherwise
215
+ * a transparent `border-color` collapses into the `border` shorthand and stops
216
+ * overriding the width/style declaration independently.
217
+ */
218
+ function hasTopLevelWhitespace(value: string): boolean {
219
+ let depth = 0;
220
+ for (let i = 0; i < value.length; i++) {
221
+ const ch = value[i] ?? '';
222
+ if (ch === '(') depth++;
223
+ else if (ch === ')') depth = Math.max(0, depth - 1);
224
+ else if (depth === 0 && /\s/.test(ch)) return true;
225
+ }
226
+ return false;
227
+ }
228
+
229
+ /**
230
+ * Resolve the CSS property for a class root, disambiguating Tailwind's shared
231
+ * roots by value type (`text-[14px]` → font-size, `text-[#fff]` → color, …).
232
+ */
233
+ export function resolveRootProperty(root: string, value: string): string {
234
+ switch (root) {
235
+ case 'text':
236
+ return isFontSizeValue(value) ? 'font-size' : 'color';
237
+ case 'font':
238
+ return isFontWeightValue(value) ? 'font-weight' : 'font-family';
239
+ case 'bg':
240
+ return isImageValue(value) ? 'background-image' : 'background-color';
241
+ case 'border':
242
+ return isColorValue(value) && !hasTopLevelWhitespace(value) ? 'border-color' : 'border';
243
+ case 'outline': {
244
+ if (hasTopLevelWhitespace(value) || value === 'none') return 'outline';
245
+ if (isColorValue(value)) return 'outline-color';
246
+ if (isLengthValue(value)) return 'outline-width';
247
+ return 'outline';
248
+ }
249
+ default:
250
+ return prefixToCSSProperty[root] ?? root;
251
+ }
252
+ }
253
+
254
+ // ---------------------------------------------------------------------------
255
+ // Breakpoint variant prefix (`tablet:p-[24px]`)
256
+ // ---------------------------------------------------------------------------
257
+
258
+ /**
259
+ * Split an optional `breakpoint:` variant prefix off a class name.
260
+ * Arbitrary-property classes (`[grid-area:hero]`) never match — their colon
261
+ * sits inside brackets, and the prefix segment must be a bare identifier.
262
+ */
263
+ export function splitVariantPrefix(className: string): { breakpoint: string | null; base: string } {
264
+ const match = className.match(/^([a-z][a-z0-9_-]*):(.+)$/);
265
+ if (match) {
266
+ return { breakpoint: match[1] ?? null, base: match[2] ?? className };
267
+ }
268
+ return { breakpoint: null, base: className };
269
+ }
270
+
271
+ /**
272
+ * Tailwind's desktop-first (max-width) variant marker. Meno breakpoints cascade desktop→mobile
273
+ * (`base` = widest, narrower breakpoints override via `max-width`), which is exactly Tailwind's
274
+ * `max-*` variant direction — so the class form is `max-<name>:` (`max-lg:p-4`), reading
275
+ * unambiguously as "this width and narrower". A bare prefix would read as Tailwind's mobile-first
276
+ * MIN-width (the opposite cascade), so the explicit `max-` keeps the class legible.
277
+ */
278
+ export const DESKTOP_FIRST_VARIANT_PREFIX = 'max-';
279
+
280
+ /**
281
+ * Class-name aliases for Meno's two default breakpoints. The breakpoint IDENTITY stays `tablet`/
282
+ * `mobile` everywhere (style model, UI labels, responsive-scales config) — only the emitted/parsed
283
+ * CLASS STRING uses Tailwind's canonical scale names: `tablet` (max-width 1024px) ⇄ `lg` (Tailwind's
284
+ * lg = 1024px, an exact match), `mobile` (max-width 540px) ⇄ `sm`. NB Meno keeps its 540px mobile
285
+ * threshold even though Tailwind's `sm` is 640px. Custom breakpoints have no alias (use their own
286
+ * name). Codec-only — nothing else in the system is renamed.
287
+ */
288
+ const BREAKPOINT_NAME_TO_CLASS: Record<string, string> = { tablet: 'lg', mobile: 'sm' };
289
+ const CLASS_NAME_TO_BREAKPOINT: Record<string, string> = { lg: 'tablet', sm: 'mobile' };
290
+
291
+ /** Build the class variant prefix for a breakpoint name: `tablet` → `max-lg:`, `mobile` → `max-sm:`. */
292
+ export function breakpointClassPrefix(breakpointName: string): string {
293
+ const lower = breakpointName.toLowerCase();
294
+ const aliased = BREAKPOINT_NAME_TO_CLASS[lower] ?? lower;
295
+ return `${DESKTOP_FIRST_VARIANT_PREFIX}${aliased}:`;
296
+ }
297
+
298
+ /**
299
+ * All class prefixes that resolve to a breakpoint, for READ-side recognition (write paths emit only
300
+ * the canonical `breakpointClassPrefix`). For `tablet` this is the canonical `max-lg:`, the
301
+ * un-aliased `max-tablet:` (emitted briefly before the alias landed), and the legacy bare `tablet:`
302
+ * (pre-`max-` projects). Deliberately NOT the bare `lg:`/`sm:` — Meno never emitted those, and a bare
303
+ * `lg:` is a foreign real-Tailwind (min-width) token we must not absorb.
304
+ */
305
+ export function breakpointClassPrefixes(breakpointName: string): string[] {
306
+ const lower = breakpointName.toLowerCase();
307
+ const aliased = BREAKPOINT_NAME_TO_CLASS[lower];
308
+ const prefixes = [`${DESKTOP_FIRST_VARIANT_PREFIX}${lower}:`, `${lower}:`]; // max-tablet:, tablet:
309
+ if (aliased) prefixes.unshift(`${DESKTOP_FIRST_VARIANT_PREFIX}${aliased}:`); // max-lg: (canonical, first)
310
+ return prefixes;
311
+ }
312
+
313
+ /**
314
+ * Normalize a parsed variant segment (from {@link splitVariantPrefix}) to the INTERNAL breakpoint
315
+ * name. The alias only applies behind the `max-` marker (a definite Meno desktop-first class):
316
+ * `max-lg`/`max-sm` → `tablet`/`mobile`, `max-tablet` → `tablet`, `max-<custom>` → `<custom>`. Bare
317
+ * segments pass through untouched — legacy Meno `tablet`/`mobile` map to themselves, and a foreign
318
+ * bare `lg`/`hover` is left alone (so it stays a non-modeled / foreign token, never mis-absorbed).
319
+ */
320
+ export function normalizeBreakpointVariant(variant: string | null): string | null {
321
+ if (variant?.startsWith(DESKTOP_FIRST_VARIANT_PREFIX)) {
322
+ const bare = variant.slice(DESKTOP_FIRST_VARIANT_PREFIX.length);
323
+ return CLASS_NAME_TO_BREAKPOINT[bare] ?? bare;
324
+ }
325
+ return variant;
326
+ }
327
+
328
+ // ---------------------------------------------------------------------------
329
+ // Class name parsing
330
+ // ---------------------------------------------------------------------------
331
+
332
+ export interface ParsedUtilityClass {
333
+ /** kebab-case CSS property the class targets */
334
+ property: string;
335
+ /** Decoded CSS value; null for hash classes (resolve via style-value registry) */
336
+ value: string | null;
337
+ /** The class root (`p`, `text`, `bg`, …); null for statics/presets/arbitrary properties */
338
+ root: string | null;
339
+ kind: 'static' | 'preset' | 'arbitrary' | 'var' | 'keyword' | 'numeric' | 'hash' | 'arbitrary-property';
340
+ }
341
+
342
+ const HASH_VALUE_RE = /^h[0-9a-z]+$/;
343
+
344
+ /**
345
+ * Parse a single (unprefixed) utility class name into its CSS property and
346
+ * decoded value. Returns null for class names that aren't Meno utilities.
347
+ *
348
+ * `knownTokens` is the project's defined design-token names (color + variable
349
+ * names). It gates the bare color-token form (`bg-primary` → `var(--primary)`):
350
+ * `bg-<name>` is only read as a token when `<name>` is in this set. Without it,
351
+ * `bg-<word>` stays UNRECOGNIZED — so foreign / authored classes (`text-wrapper`,
352
+ * imported `bg-dark`) are never misclassified. The `var(--…)` shorthand form
353
+ * (`bg-(--primary)`) is self-describing and needs no token set.
354
+ */
355
+ export function parseUtilityClass(className: string, knownTokens?: ReadonlySet<string>): ParsedUtilityClass | null {
356
+ const stat = staticUtilityReverse[className];
357
+ if (stat) {
358
+ return { property: camelToKebab(stat.prop), value: stat.value, root: null, kind: 'static' };
359
+ }
360
+
361
+ const preset = presetClassReverse[className];
362
+ if (preset) {
363
+ return { property: camelToKebab(preset.prop), value: preset.value, root: null, kind: 'preset' };
364
+ }
365
+
366
+ // Arbitrary property: [clip-path:circle(50%)], [background:linear-gradient(…)]
367
+ const arbProp = className.match(/^\[([a-z-]+):(.+)\]$/);
368
+ if (arbProp) {
369
+ return {
370
+ property: arbProp[1] ?? '',
371
+ value: decodeArbitraryValue(arbProp[2] ?? ''),
372
+ root: null,
373
+ kind: 'arbitrary-property',
374
+ };
375
+ }
376
+
377
+ for (const root of SORTED_ROOTS) {
378
+ if (!className.startsWith(`${root}-`)) continue;
379
+ const rest = className.slice(root.length + 1);
380
+ if (!rest) continue;
381
+ const parsed = parseRootValue(root, rest, knownTokens);
382
+ if (parsed) return parsed;
383
+ }
384
+
385
+ return null;
386
+ }
387
+
388
+ function parseRootValue(root: string, rest: string, knownTokens?: ReadonlySet<string>): ParsedUtilityClass | null {
389
+ // Arbitrary value: root-[value]
390
+ if (rest.startsWith('[') && rest.endsWith(']') && rest.length > 2) {
391
+ const value = decodeArbitraryValue(rest.slice(1, -1));
392
+ return { property: resolveRootProperty(root, value), value, root, kind: 'arbitrary' };
393
+ }
394
+
395
+ // CSS variable: root-(--name) / root-(length:--name)
396
+ const varMatch = rest.match(/^\((?:([a-z-]+):)?(--[\w-]+)\)$/);
397
+ if (varMatch) {
398
+ const hint = varMatch[1];
399
+ const value = `var(${varMatch[2]})`;
400
+ let property: string;
401
+ if (root === 'text') {
402
+ property = hint === 'length' ? 'font-size' : 'color';
403
+ } else if (root === 'font') {
404
+ // `font-*` is shared by font-weight and font-family; a var value is ambiguous, so the
405
+ // emitter tags weight vars with a `number:` hint (see utilityClassMapper). Default to
406
+ // font-family (the original behavior for an un-hinted var like `font-(--l-ff)`).
407
+ property = hint === 'number' ? 'font-weight' : 'font-family';
408
+ } else {
409
+ property = resolveRootProperty(root, value);
410
+ }
411
+ return { property, value, root, kind: 'var' };
412
+ }
413
+
414
+ // Named keyword: w-full, m-auto, rounded-full
415
+ const keyword = keywordValues[root]?.[rest];
416
+ if (keyword !== undefined) {
417
+ return { property: resolveRootProperty(root, keyword), value: keyword, root, kind: 'keyword' };
418
+ }
419
+
420
+ // Generic keyword roots: cursor-pointer, align-middle, origin-center, …
421
+ if (genericKeywordRoots.has(root) && /^[a-z][a-z-]*$/.test(rest)) {
422
+ return { property: resolveRootProperty(root, rest), value: rest, root, kind: 'keyword' };
423
+ }
424
+
425
+ // Named spacing scale: p-4 → 16px, gap-6 → 24px (Tailwind's step table).
426
+ if (SPACING_SCALE_ROOTS.has(root)) {
427
+ const scaleVal = SPACING_SCALE[rest];
428
+ if (scaleVal !== undefined) {
429
+ return { property: resolveRootProperty(root, scaleVal), value: scaleVal, root, kind: 'keyword' };
430
+ }
431
+ }
432
+
433
+ // Bare numbers: z-10, order-2, flex-2; opacity uses Tailwind's 0–100 scale
434
+ if (integerValueRoots.has(root) && /^-?\d+$/.test(rest)) {
435
+ return { property: prefixToCSSProperty[root] ?? root, value: rest, root, kind: 'numeric' };
436
+ }
437
+ if (root === 'opacity' && /^\d{1,3}$/.test(rest)) {
438
+ return { property: 'opacity', value: String(Number(rest) / 100), root, kind: 'numeric' };
439
+ }
440
+
441
+ // Bare design-token color (Meno convention): bg-primary → var(--primary),
442
+ // text-text → var(--text), border-border → var(--border). Gated on the
443
+ // project's `knownTokens` so `bg-<word>` is read as a token ONLY for a DEFINED
444
+ // token name — foreign / authored classes (`text-wrapper`, imported `bg-dark`)
445
+ // are left unrecognized. The token gate also resolves the `h…`-token vs
446
+ // hash-class ambiguity (a real hash class's `rest` is never a defined token).
447
+ if (COLOR_TOKEN_ROOTS.has(root) && knownTokens?.has(rest)) {
448
+ const value = `var(--${rest})`;
449
+ return { property: resolveRootProperty(root, value), value, root, kind: 'var' };
450
+ }
451
+
452
+ // Hash fallback: value lives in the style-value registry
453
+ if (HASH_VALUE_RE.test(rest)) {
454
+ return { property: prefixToCSSProperty[root] ?? root, value: null, root, kind: 'hash' };
455
+ }
456
+
457
+ return null;
458
+ }
@@ -244,18 +244,10 @@ describe('utils', () => {
244
244
 
245
245
  test('should handle complex children array', () => {
246
246
  const node = {
247
- children: [
248
- { element: 'div' },
249
- { element: 'span' },
250
- 'text',
251
- ],
247
+ children: [{ element: 'div' }, { element: 'span' }, 'text'],
252
248
  };
253
249
  normalizeChildrenArray(node);
254
- expect(node.children).toEqual([
255
- { element: 'div' },
256
- { element: 'span' },
257
- 'text',
258
- ]);
250
+ expect(node.children).toEqual([{ element: 'div' }, { element: 'span' }, 'text']);
259
251
  });
260
252
  });
261
253
  });
@@ -20,7 +20,7 @@ export function deepClone<T>(obj: T): T {
20
20
  * @param ms - Milliseconds to wait
21
21
  */
22
22
  export function wait(ms: number): Promise<void> {
23
- return new Promise(resolve => setTimeout(resolve, ms));
23
+ return new Promise((resolve) => setTimeout(resolve, ms));
24
24
  }
25
25
 
26
26
  /**
@@ -50,7 +50,20 @@ export function isEditableElement(): boolean {
50
50
  // For inputs, only block shortcuts for text-accepting types
51
51
  if (activeElement.tagName === 'INPUT') {
52
52
  const inputType = (activeElement as HTMLInputElement).type.toLowerCase();
53
- const textInputTypes = ['text', 'password', 'email', 'number', 'search', 'tel', 'url', 'date', 'time', 'datetime-local', 'month', 'week'];
53
+ const textInputTypes = [
54
+ 'text',
55
+ 'password',
56
+ 'email',
57
+ 'number',
58
+ 'search',
59
+ 'tel',
60
+ 'url',
61
+ 'date',
62
+ 'time',
63
+ 'datetime-local',
64
+ 'month',
65
+ 'week',
66
+ ];
54
67
  // Allow shortcuts for: checkbox, radio, button, submit, reset, color, file, range, hidden
55
68
  return textInputTypes.includes(inputType);
56
69
  }
@@ -59,10 +72,7 @@ export function isEditableElement(): boolean {
59
72
  // This handles rich text editors where focus may be on an internal element
60
73
  let element: Element | null = activeElement;
61
74
  while (element) {
62
- if (
63
- element.hasAttribute('contenteditable') ||
64
- (element as HTMLElement).isContentEditable
65
- ) {
75
+ if (element.hasAttribute('contenteditable') || (element as HTMLElement).isContentEditable) {
66
76
  return true;
67
77
  }
68
78
  element = element.parentElement;
@@ -75,7 +85,7 @@ export function isEditableElement(): boolean {
75
85
  * Extract the parent path from a tree path
76
86
  * Example: root_0_children_0 -> root_0, root_0_children_0_children_1 -> root_0_children_0
77
87
  * Also accepts array paths: [0,0] -> "root_0", [0,0,1] -> "root_0_children_0"
78
- *
88
+ *
79
89
  * @param treePath - The tree path (string or Path array)
80
90
  * @param rootPath - The root path constant (default: 'root_0')
81
91
  * @returns The parent path, or rootPath if no parent found
@@ -83,12 +93,12 @@ export function isEditableElement(): boolean {
83
93
  export function getParentPathFromTreePath(treePath: string | Path, rootPath: string = ROOT_0_STRING): string {
84
94
  // Convert to array if needed
85
95
  const pathArray = typeof treePath === 'string' ? stringToPath(treePath) : treePath;
86
-
96
+
87
97
  // Handle root paths
88
98
  if (isRootPathArray(pathArray)) {
89
99
  return rootPath;
90
100
  }
91
-
101
+
92
102
  // Use array operations to get parent path
93
103
  const parentArray = getParentPath(pathArray);
94
104
  return pathToLegacyString(parentArray);
@@ -109,4 +119,3 @@ export function normalizeChildrenArray(node: { children?: any[] | any }): void {
109
119
  }
110
120
  // If already an array, no change needed
111
121
  }
112
-
@@ -14,7 +14,8 @@ export const CMS_COLLECTION_ID_REGEX = /^[a-zA-Z0-9_-]+$/;
14
14
  /**
15
15
  * Zod schema for CMS collection ID validation
16
16
  */
17
- export const CMSCollectionIdSchema = z.string()
17
+ export const CMSCollectionIdSchema = z
18
+ .string()
18
19
  .min(1, 'Collection ID is required')
19
20
  .regex(CMS_COLLECTION_ID_REGEX, 'Collection ID must contain only letters, numbers, underscores, and hyphens');
20
21
 
@@ -0,0 +1,53 @@
1
+ import { describe, test, expect } from 'bun:test';
2
+ import { validateComment } from './commentValidators';
3
+
4
+ /** A minimal valid comment; callers override `anchor` to exercise schema variants. */
5
+ function baseComment(anchor: Record<string, unknown>): Record<string, unknown> {
6
+ return {
7
+ _id: 'c1',
8
+ _filename: 'c1.json',
9
+ _pagePath: 'index',
10
+ _seq: 1,
11
+ _createdAt: '2026-01-01T00:00:00.000Z',
12
+ _updatedAt: '2026-01-01T00:00:00.000Z',
13
+ _commitSha: null,
14
+ anchor,
15
+ status: 'open',
16
+ thread: [
17
+ {
18
+ id: 't1',
19
+ author: { login: 'local' },
20
+ createdAt: '2026-01-01T00:00:00.000Z',
21
+ text: 'hi',
22
+ statusChange: null,
23
+ },
24
+ ],
25
+ };
26
+ }
27
+
28
+ const pageLevelAnchor = {
29
+ nodePath: [0, 1],
30
+ nodeIdentity: { kind: 'node', name: 'div' },
31
+ offsetXPercent: 0.5,
32
+ offsetYPercent: 0.5,
33
+ };
34
+
35
+ describe('validateComment — element anchor identity', () => {
36
+ test('accepts a comment WITHOUT a cmsItemIndexPath (element not in a list)', () => {
37
+ const result = validateComment(baseComment(pageLevelAnchor));
38
+ expect(result.valid).toBe(true);
39
+ if (result.valid) expect(result.data.anchor.cmsItemIndexPath).toBeUndefined();
40
+ });
41
+
42
+ test('accepts and round-trips a comment WITH a cmsItemIndexPath', () => {
43
+ const cmsItemIndexPath = [0, 2];
44
+ const result = validateComment(baseComment({ ...pageLevelAnchor, cmsItemIndexPath }));
45
+ expect(result.valid).toBe(true);
46
+ if (result.valid) expect(result.data.anchor.cmsItemIndexPath).toEqual(cmsItemIndexPath);
47
+ });
48
+
49
+ test('rejects a malformed cmsItemIndexPath (non-number entries)', () => {
50
+ const result = validateComment(baseComment({ ...pageLevelAnchor, cmsItemIndexPath: ['a', 'b'] }));
51
+ expect(result.valid).toBe(false);
52
+ });
53
+ });
@@ -23,8 +23,19 @@ export const CommentNodeIdentitySchema = z.object({
23
23
  label: z.string().optional(),
24
24
  });
25
25
 
26
+ export const CommentNodeRefSegmentSchema = z.object({
27
+ kind: z.enum(['component', 'node']),
28
+ name: z.string().min(1),
29
+ nth: z.number().int().nonnegative(),
30
+ });
31
+
26
32
  export const CommentAnchorSchema = z.object({
33
+ // Type + occurrence path (move-resilient source of truth). Must be declared
34
+ // (z.object strips unknown keys) so it round-trips to disk.
35
+ nodeRef: z.array(CommentNodeRefSegmentSchema).optional(),
27
36
  nodePath: z.array(z.number().int().nonnegative()),
37
+ // CMS-list item path disambiguating which list copy the element belongs to.
38
+ cmsItemIndexPath: z.array(z.number().int().nonnegative()).optional(),
28
39
  nodeIdentity: CommentNodeIdentitySchema,
29
40
  offsetXPercent: z.number().min(0).max(1),
30
41
  offsetYPercent: z.number().min(0).max(1),
@@ -53,7 +64,7 @@ export const CommentSchema = z.object({
53
64
  });
54
65
 
55
66
  function zodToValidationErrors(zodError: ZodError): ValidationError[] {
56
- return zodError.errors.map(err => ({
67
+ return zodError.errors.map((err) => ({
57
68
  path: err.path.join('.') || 'root',
58
69
  message: err.message,
59
70
  code: 'INVALID_TYPE' as const,