meno-core 1.0.54 → 1.1.1

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 (298) hide show
  1. package/bin/cli.ts +19 -205
  2. package/build-astro.ts +31 -27
  3. package/dist/bin/cli.js +15 -154
  4. package/dist/bin/cli.js.map +2 -2
  5. package/dist/chunks/{chunk-3XER4E5W.js → chunk-4ZRU52J2.js} +6 -5
  6. package/dist/chunks/{chunk-3XER4E5W.js.map → chunk-4ZRU52J2.js.map} +2 -2
  7. package/dist/chunks/{chunk-NVRBTSQG.js → chunk-5X4HCN7J.js} +4 -4
  8. package/dist/chunks/chunk-5X4HCN7J.js.map +7 -0
  9. package/dist/chunks/{chunk-FZITJSSS.js → chunk-7CCVOL5M.js} +14 -14
  10. package/dist/chunks/chunk-7CCVOL5M.js.map +7 -0
  11. package/dist/chunks/{chunk-AE3QK5QW.js → chunk-7ZLF4NE5.js} +31 -26
  12. package/dist/chunks/chunk-7ZLF4NE5.js.map +7 -0
  13. package/dist/chunks/{chunk-HIZMY3EP.js → chunk-AR7LHIFY.js} +8 -2
  14. package/dist/chunks/chunk-AR7LHIFY.js.map +7 -0
  15. package/dist/chunks/{chunk-7HWQUVTU.js → chunk-J4IPTP5X.js} +1839 -443
  16. package/dist/chunks/chunk-J4IPTP5X.js.map +7 -0
  17. package/dist/chunks/{chunk-5ETZFREW.js → chunk-QWTQZHG3.js} +186 -79
  18. package/dist/chunks/chunk-QWTQZHG3.js.map +7 -0
  19. package/dist/chunks/{chunk-STDY3OVM.js → chunk-XTKNX4FW.js} +3 -3
  20. package/dist/chunks/{chunk-STDY3OVM.js.map → chunk-XTKNX4FW.js.map} +2 -2
  21. package/dist/chunks/{fs-ZI5JEU7V.js → fs-G3ANWEFP.js} +2 -2
  22. package/dist/lib/client/index.js +381 -159
  23. package/dist/lib/client/index.js.map +3 -3
  24. package/dist/lib/server/index.js +14365 -6276
  25. package/dist/lib/server/index.js.map +4 -4
  26. package/dist/lib/shared/index.js +296 -87
  27. package/dist/lib/shared/index.js.map +4 -4
  28. package/dist/lib/shared/richtext/index.js +1 -1
  29. package/dist/lib/test-utils/index.js +27 -21
  30. package/dist/lib/test-utils/index.js.map +2 -2
  31. package/lib/client/ErrorBoundary.test.tsx +147 -90
  32. package/lib/client/ErrorBoundary.tsx +8 -5
  33. package/lib/client/componentRegistry.ts +0 -1
  34. package/lib/client/core/ComponentBuilder.test.ts +150 -0
  35. package/lib/client/core/ComponentBuilder.ts +112 -12
  36. package/lib/client/core/ComponentRenderer.test.tsx +1 -2
  37. package/lib/client/core/builders/embedBuilder.ts +1 -1
  38. package/lib/client/core/builders/linkBuilder.ts +2 -2
  39. package/lib/client/core/builders/linkNodeBuilder.ts +1 -1
  40. package/lib/client/core/builders/listBuilder.ts +41 -4
  41. package/lib/client/core/builders/localeListBuilder.ts +2 -2
  42. package/lib/client/core/cmsTemplateProcessor.ts +1 -2
  43. package/lib/client/hmr/HMRManager.tsx +39 -30
  44. package/lib/client/hmrCssReload.ts +35 -6
  45. package/lib/client/hmrWebSocket.ts +5 -5
  46. package/lib/client/hooks/useColorVariables.ts +12 -9
  47. package/lib/client/hooks/usePropertyAutocomplete.ts +1 -1
  48. package/lib/client/hooks/useVariables.ts +3 -1
  49. package/lib/client/meno-filter/MenoFilter.test.ts +28 -28
  50. package/lib/client/meno-filter/MenoFilter.ts +9 -6
  51. package/lib/client/meno-filter/bindings.ts +3 -3
  52. package/lib/client/meno-filter/init.ts +3 -3
  53. package/lib/client/meno-filter/renderer.ts +8 -4
  54. package/lib/client/meno-filter/ui.ts +1 -1
  55. package/lib/client/meno-filter/updates.ts +3 -2
  56. package/lib/client/meno-filter/utils.ts +1 -1
  57. package/lib/client/navigation.test.ts +40 -44
  58. package/lib/client/responsiveStyleResolver.knownTokens.test.ts +71 -0
  59. package/lib/client/responsiveStyleResolver.test.ts +5 -5
  60. package/lib/client/responsiveStyleResolver.ts +61 -4
  61. package/lib/client/routing/RouteLoader.test.ts +1 -1
  62. package/lib/client/routing/RouteLoader.ts +8 -3
  63. package/lib/client/routing/Router.tsx +181 -152
  64. package/lib/client/scripts/ScriptExecutor.ts +3 -3
  65. package/lib/client/services/PrefetchService.test.ts +1 -1
  66. package/lib/client/services/PrefetchService.ts +13 -5
  67. package/lib/client/styleProcessor.ts +9 -3
  68. package/lib/client/styles/StyleInjector.ts +1 -1
  69. package/lib/client/styles/UtilityClassCollector.ts +54 -13
  70. package/lib/client/templateEngine.test.ts +17 -23
  71. package/lib/client/templateEngine.ts +7 -13
  72. package/lib/client/theme.test.ts +1 -1
  73. package/lib/server/__integration__/server-lifecycle.test.ts +3 -1
  74. package/lib/server/__integration__/static-assets.test.ts +2 -2
  75. package/lib/server/__integration__/test-helpers.ts +3 -4
  76. package/lib/server/astro/cmsPageEmitter.ts +4 -5
  77. package/lib/server/astro/componentEmitter.ts +11 -8
  78. package/lib/server/astro/nodeToAstro.test.ts +4 -4
  79. package/lib/server/astro/nodeToAstro.ts +41 -36
  80. package/lib/server/astro/normalizeOrphanTemplateProps.test.ts +11 -11
  81. package/lib/server/astro/normalizeOrphanTemplateProps.ts +7 -6
  82. package/lib/server/astro/pageEmitter.ts +1 -1
  83. package/lib/server/astro/templateTransformer.ts +3 -3
  84. package/lib/server/createServer.ts +2 -2
  85. package/lib/server/cssGenerator.ts +2 -2
  86. package/lib/server/fileWatcher.test.ts +41 -4
  87. package/lib/server/fileWatcher.ts +102 -23
  88. package/lib/server/index.ts +7 -16
  89. package/lib/server/jsonLoader.test.ts +3 -3
  90. package/lib/server/jsonLoader.ts +8 -5
  91. package/lib/server/middleware/cors.test.ts +1 -1
  92. package/lib/server/middleware/cors.ts +2 -2
  93. package/lib/server/middleware/errorHandler.test.ts +2 -2
  94. package/lib/server/middleware/logger.test.ts +3 -3
  95. package/lib/server/middleware/logger.ts +7 -7
  96. package/lib/server/migrateTemplates.ts +3 -3
  97. package/lib/server/projectContext.ts +1 -1
  98. package/lib/server/providers/fileSystemCMSProvider.test.ts +18 -4
  99. package/lib/server/providers/fileSystemCMSProvider.ts +27 -18
  100. package/lib/server/routes/api/cms.ts +1 -1
  101. package/lib/server/routes/api/colors.test.ts +2 -2
  102. package/lib/server/routes/api/components.ts +1 -1
  103. package/lib/server/routes/api/config.ts +3 -3
  104. package/lib/server/routes/api/core-routes.ts +2 -2
  105. package/lib/server/routes/api/enums.test.ts +2 -2
  106. package/lib/server/routes/api/functions.ts +1 -1
  107. package/lib/server/routes/api/variables.test.ts +2 -2
  108. package/lib/server/routes/index.ts +2 -2
  109. package/lib/server/routes/pages.ts +1 -2
  110. package/lib/server/routes/static.ts +1 -2
  111. package/lib/server/runtime/bundler.ts +65 -43
  112. package/lib/server/runtime/fs.ts +13 -13
  113. package/lib/server/runtime/httpServer.ts +3 -3
  114. package/lib/server/services/ColorService.test.ts +3 -3
  115. package/lib/server/services/ColorService.ts +5 -2
  116. package/lib/server/services/EnumService.test.ts +2 -2
  117. package/lib/server/services/EnumService.ts +4 -1
  118. package/lib/server/services/VariableService.test.ts +7 -7
  119. package/lib/server/services/VariableService.ts +5 -2
  120. package/lib/server/services/cmsService.test.ts +44 -44
  121. package/lib/server/services/cmsService.ts +17 -97
  122. package/lib/server/services/componentService.test.ts +10 -16
  123. package/lib/server/services/componentService.ts +20 -12
  124. package/lib/server/services/configService.test.ts +13 -14
  125. package/lib/server/services/configService.ts +30 -0
  126. package/lib/server/services/fileWatcherService.ts +2 -2
  127. package/lib/server/services/pageService.test.ts +7 -7
  128. package/lib/server/services/pageService.ts +18 -5
  129. package/lib/server/ssr/attributeBuilder.ts +2 -2
  130. package/lib/server/ssr/cmsSSRProcessor.ts +1 -2
  131. package/lib/server/ssr/errorOverlay.ts +1 -1
  132. package/lib/server/ssr/htmlGenerator.nonce.test.ts +6 -5
  133. package/lib/server/ssr/htmlGenerator.test.ts +0 -1
  134. package/lib/server/ssr/htmlGenerator.ts +25 -9
  135. package/lib/server/ssr/imageMetadata.ts +2 -2
  136. package/lib/server/ssr/jsCollector.test.ts +5 -5
  137. package/lib/server/ssr/liveReloadIntegration.test.ts +7 -8
  138. package/lib/server/ssr/ssrRenderer.branches.test.ts +9 -9
  139. package/lib/server/ssr/ssrRenderer.test.ts +150 -15
  140. package/lib/server/ssr/ssrRenderer.ts +218 -40
  141. package/lib/server/ssrRenderer.test.ts +6 -6
  142. package/lib/server/utils/jsonLineMapper.ts +7 -7
  143. package/lib/server/validateStyleCoverage.ts +0 -1
  144. package/lib/server/websocketManager.ts +12 -0
  145. package/lib/shared/breakpoints.test.ts +5 -5
  146. package/lib/shared/cmsQuery.test.ts +81 -0
  147. package/lib/shared/cmsQuery.ts +92 -0
  148. package/lib/shared/cmsQueryParser.ts +11 -7
  149. package/lib/shared/colorConversions.ts +9 -9
  150. package/lib/shared/colorVariableUtils.test.ts +22 -0
  151. package/lib/shared/colorVariableUtils.ts +19 -5
  152. package/lib/shared/constants.ts +4 -0
  153. package/lib/shared/cssGeneration.test.ts +33 -0
  154. package/lib/shared/cssGeneration.ts +50 -35
  155. package/lib/shared/cssProperties.test.ts +64 -64
  156. package/lib/shared/cssProperties.ts +2 -2
  157. package/lib/shared/elementClassName.ts +2 -2
  158. package/lib/shared/errorLogger.test.ts +12 -12
  159. package/lib/shared/expressionEvaluator.test.ts +27 -1
  160. package/lib/shared/expressionEvaluator.ts +9 -0
  161. package/lib/shared/fontLoader.test.ts +13 -13
  162. package/lib/shared/fontLoader.ts +2 -2
  163. package/lib/shared/gradientUtils.test.ts +2 -2
  164. package/lib/shared/gradientUtils.ts +15 -8
  165. package/lib/shared/hrefRefs.test.ts +2 -2
  166. package/lib/shared/hrefRefs.ts +1 -1
  167. package/lib/shared/i18n.test.ts +3 -3
  168. package/lib/shared/i18n.ts +5 -4
  169. package/lib/shared/index.ts +29 -0
  170. package/lib/shared/inlineSvgStyleRules.ts +12 -9
  171. package/lib/shared/interactiveStyleMappings.test.ts +21 -21
  172. package/lib/shared/interactiveStyleMappings.ts +2 -8
  173. package/lib/shared/interactiveStyles.test.ts +3 -1
  174. package/lib/shared/itemTemplateUtils.test.ts +5 -5
  175. package/lib/shared/itemTemplateUtils.ts +9 -5
  176. package/lib/shared/jsonRepair.ts +8 -8
  177. package/lib/shared/libraryLoader.test.ts +6 -6
  178. package/lib/shared/linkUtils.ts +1 -1
  179. package/lib/shared/logger.test.ts +66 -0
  180. package/lib/shared/logger.ts +94 -0
  181. package/lib/shared/markdown.ts +25 -0
  182. package/lib/shared/nodeUtils.test.ts +13 -9
  183. package/lib/shared/nodeUtils.ts +27 -4
  184. package/lib/shared/pathSecurity.ts +1 -1
  185. package/lib/shared/pathUtils.ts +1 -2
  186. package/lib/shared/paths/PathConverter.ts +3 -3
  187. package/lib/shared/paths/PathUtils.ts +4 -3
  188. package/lib/shared/paths/PathValidator.ts +1 -1
  189. package/lib/shared/permissions.test.ts +168 -0
  190. package/lib/shared/permissions.ts +162 -0
  191. package/lib/shared/propResolver.test.ts +1 -1
  192. package/lib/shared/propResolver.ts +1 -1
  193. package/lib/shared/registry/BaseNodeTypeRegistry.test.ts +5 -3
  194. package/lib/shared/registry/ClientRegistry.ts +0 -1
  195. package/lib/shared/registry/ComponentRegistry.test.ts +3 -3
  196. package/lib/shared/registry/ComponentRegistry.ts +6 -1
  197. package/lib/shared/registry/NodeTypeDefinition.ts +2 -2
  198. package/lib/shared/registry/SSRRegistry.ts +0 -1
  199. package/lib/shared/registry/fieldPresets.ts +3 -1
  200. package/lib/shared/registry/nodeTypes/CustomNodeType.ts +103 -0
  201. package/lib/shared/registry/nodeTypes/HtmlNodeType.ts +1 -1
  202. package/lib/shared/registry/nodeTypes/IslandNodeType.ts +129 -0
  203. package/lib/shared/registry/nodeTypes/ListNodeType.ts +43 -7
  204. package/lib/shared/registry/nodeTypes/MarkdownNodeType.ts +78 -0
  205. package/lib/shared/registry/nodeTypes/SlotMarkerType.ts +6 -1
  206. package/lib/shared/registry/nodeTypes/index.ts +16 -1
  207. package/lib/shared/responsiveScaling.test.ts +1 -3
  208. package/lib/shared/responsiveScaling.ts +6 -5
  209. package/lib/shared/responsiveStyleUtils.test.ts +0 -1
  210. package/lib/shared/richtext/htmlToTiptap.test.ts +116 -116
  211. package/lib/shared/richtext/htmlToTiptap.ts +1 -1
  212. package/lib/shared/styleNodeUtils.ts +17 -14
  213. package/lib/shared/tailwindThemeScale.ts +256 -0
  214. package/lib/shared/themeDefaults.test.ts +0 -3
  215. package/lib/shared/tree/PathBuilder.test.ts +1 -7
  216. package/lib/shared/tree/PathBuilder.ts +15 -13
  217. package/lib/shared/treePathUtils.test.ts +49 -2
  218. package/lib/shared/treePathUtils.ts +40 -4
  219. package/lib/shared/types/api.ts +105 -7
  220. package/lib/shared/types/cms.test.ts +34 -0
  221. package/lib/shared/types/cms.ts +65 -13
  222. package/lib/shared/types/components.ts +24 -0
  223. package/lib/shared/types/errors.test.ts +1 -1
  224. package/lib/shared/types/index.ts +23 -0
  225. package/lib/shared/types/permissions.ts +139 -0
  226. package/lib/shared/types/variables.ts +3 -3
  227. package/lib/shared/utilityClassConfig.ts +219 -0
  228. package/lib/shared/utilityClassMapper.durability.test.ts +117 -0
  229. package/lib/shared/utilityClassMapper.test.ts +401 -26
  230. package/lib/shared/utilityClassMapper.ts +193 -29
  231. package/lib/shared/utilityClassNames.test.ts +38 -0
  232. package/lib/shared/utilityClassNames.ts +301 -23
  233. package/lib/shared/validation/index.ts +1 -0
  234. package/lib/shared/validation/permissionsValidators.test.ts +49 -0
  235. package/lib/shared/validation/permissionsValidators.ts +49 -0
  236. package/lib/shared/validation/propValidator.test.ts +1 -1
  237. package/lib/shared/validation/propValidator.ts +1 -1
  238. package/lib/shared/validation/schemas.test.ts +41 -0
  239. package/lib/shared/validation/schemas.ts +159 -13
  240. package/lib/shared/validation/validators.test.ts +2 -2
  241. package/lib/shared/viewportUnits.test.ts +13 -0
  242. package/lib/shared/viewportUnits.ts +12 -5
  243. package/lib/test-utils/factories/ConsoleMockFactory.ts +7 -7
  244. package/lib/test-utils/factories/DomMockFactory.ts +10 -4
  245. package/lib/test-utils/factories/ServerMockFactory.ts +1 -1
  246. package/lib/test-utils/factories/StoreMockFactory.ts +5 -5
  247. package/lib/test-utils/mockFactories.ts +1 -1
  248. package/lib/test-utils/mocks.ts +3 -3
  249. package/package.json +3 -5
  250. package/scripts/build-for-publish.mjs +2 -4
  251. package/scripts/build-meno-filter.ts +3 -3
  252. package/tsconfig.json +1 -0
  253. package/vite.config.ts +1 -1
  254. package/build-next.ts +0 -1374
  255. package/build-static.test.ts +0 -453
  256. package/build-static.ts +0 -1072
  257. package/dist/build-static.js +0 -37
  258. package/dist/chunks/chunk-2FN4UOVO.js +0 -6465
  259. package/dist/chunks/chunk-2FN4UOVO.js.map +0 -7
  260. package/dist/chunks/chunk-5ETZFREW.js.map +0 -7
  261. package/dist/chunks/chunk-7E4IF5L7.js +0 -245
  262. package/dist/chunks/chunk-7E4IF5L7.js.map +0 -7
  263. package/dist/chunks/chunk-7HWQUVTU.js.map +0 -7
  264. package/dist/chunks/chunk-AE3QK5QW.js.map +0 -7
  265. package/dist/chunks/chunk-F6KTJYGV.js +0 -322
  266. package/dist/chunks/chunk-F6KTJYGV.js.map +0 -7
  267. package/dist/chunks/chunk-FZITJSSS.js.map +0 -7
  268. package/dist/chunks/chunk-GSYYA5GX.js +0 -77
  269. package/dist/chunks/chunk-GSYYA5GX.js.map +0 -7
  270. package/dist/chunks/chunk-HIZMY3EP.js.map +0 -7
  271. package/dist/chunks/chunk-I2WEGYA7.js +0 -424
  272. package/dist/chunks/chunk-I2WEGYA7.js.map +0 -7
  273. package/dist/chunks/chunk-JNO3CNLJ.js +0 -464
  274. package/dist/chunks/chunk-JNO3CNLJ.js.map +0 -7
  275. package/dist/chunks/chunk-NVRBTSQG.js.map +0 -7
  276. package/dist/chunks/chunk-Q4OBWKXG.js +0 -1143
  277. package/dist/chunks/chunk-Q4OBWKXG.js.map +0 -7
  278. package/dist/chunks/chunk-QTE32Y53.js +0 -3829
  279. package/dist/chunks/chunk-QTE32Y53.js.map +0 -7
  280. package/dist/chunks/configService-PRJZF7Y6.js +0 -14
  281. package/dist/chunks/configService-PRJZF7Y6.js.map +0 -7
  282. package/dist/chunks/constants-KIQEYMAM.js +0 -46
  283. package/dist/chunks/constants-KIQEYMAM.js.map +0 -7
  284. package/dist/chunks/fs-ZI5JEU7V.js.map +0 -7
  285. package/dist/entries/server-router.js +0 -62
  286. package/dist/entries/server-router.js.map +0 -7
  287. package/entries/server-router.tsx +0 -75
  288. package/lib/server/providers/fileSystemPageProvider.test.ts +0 -84
  289. package/lib/server/providers/fileSystemPageProvider.ts +0 -179
  290. package/lib/server/webflow/buildWebflow.ts +0 -623
  291. package/lib/server/webflow/index.ts +0 -23
  292. package/lib/server/webflow/nodeToWebflow.test.ts +0 -3102
  293. package/lib/server/webflow/nodeToWebflow.ts +0 -2372
  294. package/lib/server/webflow/styleMapper.test.ts +0 -348
  295. package/lib/server/webflow/styleMapper.ts +0 -687
  296. package/lib/server/webflow/templateWrapper.ts +0 -49
  297. package/lib/server/webflow/types.ts +0 -389
  298. /package/dist/{build-static.js.map → chunks/fs-G3ANWEFP.js.map} +0 -0
@@ -4,7 +4,8 @@
4
4
  * Uses centralized configuration from utilityClassConfig.ts
5
5
  */
6
6
 
7
- import type { ResponsiveStyleObject, StyleObject, StyleMapping } from './types';
7
+ import type { ResponsiveStyleObject, StyleObject, StyleMapping, StyleValue } from './types';
8
+ import { isResponsiveStyle } from './styleUtils';
8
9
  import {
9
10
  propertyMap,
10
11
  propertyOrder,
@@ -18,6 +19,8 @@ import {
18
19
  shadowPresets,
19
20
  gradientPresets,
20
21
  borderPresets,
22
+ SPACING_SCALE_ROOTS,
23
+ SPACING_SCALE_REVERSE,
21
24
  } from './utilityClassConfig';
22
25
  import {
23
26
  camelToKebab,
@@ -26,11 +29,13 @@ import {
26
29
  parseUtilityClass,
27
30
  splitVariantPrefix,
28
31
  breakpointClassPrefix,
32
+ normalizeBreakpointVariant,
29
33
  resolveRootProperty,
30
34
  ROOT_CAMEL_OVERRIDES,
31
- isColorValue,
35
+ isBareColorTokenName,
32
36
  } from './utilityClassNames';
33
37
  import { registerStyleValue, registerDynamicStyle, getStyleValue, getDynamicStyle } from './styleValueRegistry';
38
+ import { THEME_SCALE_BY_CSSVAR } from './tailwindThemeScale';
34
39
  import {
35
40
  SCALABLE_CSS_PROPERTIES,
36
41
  buildFluidClampWithExplicitMin,
@@ -63,22 +68,29 @@ function shortHash(input: string): string {
63
68
  }
64
69
 
65
70
  /**
66
- * True for a bare Meno color token (e.g. `primary`, `background-light`) that
67
- * should resolve to `var(--token)`. Named CSS colors, hex/function colors and
68
- * values starting with digits are NOT tokens.
71
+ * Whether the natural `root-(--x)` / `root-[value]` form would round-trip to a DIFFERENT property
72
+ * than `prop`. Roots shared by a shorthand and a color/width longhand (`border` → border/borderColor,
73
+ * `outline` outline/outlineWidth/outlineColor) are disambiguated on parse by value type via
74
+ * `resolveRootProperty` — so a color-looking value (notably any opaque `var(--x)`) bound to the
75
+ * SHORTHAND reads back as the color longhand. Callers fall back to the explicit arbitrary-property
76
+ * form (`[border:var(--x)]`) when this returns true.
77
+ *
78
+ * Mirrors the inverse parser's property derivation EXACTLY (`classToStyle`): `ROOT_CAMEL_OVERRIDES`
79
+ * wins first (px → paddingInline, …), else `resolveRootProperty`. Comparing kebab CSS names instead
80
+ * would false-positive on those overridden roots (`px` resolves to `padding`, not `padding-inline`).
69
81
  */
70
- function isBareColorToken(value: string): boolean {
71
- if (!/^[a-zA-Z][\w-]*$/.test(value)) return false;
72
- if (isColorValue(value)) return false; // named colors / transparent / currentColor
73
- return true;
82
+ function rootFormMisresolves(root: string, prop: string, value: string): boolean {
83
+ const parsedProp = ROOT_CAMEL_OVERRIDES[root] ?? cssPropertyToCamel(resolveRootProperty(root, value));
84
+ return parsedProp !== prop;
74
85
  }
75
86
 
76
87
  /**
77
88
  * Converts a CSS property value to a Tailwind-style utility class name.
78
89
  * Examples:
79
- * { prop: "padding", value: "10px" } → "p-[10px]"
90
+ * { prop: "padding", value: "16px" } → "p-4" (exact scale step)
91
+ * { prop: "padding", value: "13px" } → "p-[13px]" (off-scale → arbitrary)
80
92
  * { prop: "display", value: "flex" } → "flex"
81
- * { prop: "color", value: "primary" } → "text-(--primary)"
93
+ * { prop: "color", value: "primary" } → "text-primary"
82
94
  * { prop: "width", value: "100%" } → "w-full"
83
95
  * { prop: "textOverflow", value: "ellipsis" } → "[text-overflow:ellipsis]"
84
96
  *
@@ -88,6 +100,19 @@ function isBareColorToken(value: string): boolean {
88
100
  function propertyValueToClass(prop: string, value: string | number): string | null {
89
101
  let stringValue = String(value);
90
102
 
103
+ // Tailwind theme token: a `var(--token)` value (or a legacy `var(--token, …)` with fallback)
104
+ // whose token is a known Tailwind scale var (and whose property matches) emits the compact class —
105
+ // `var(--text-lg)` → `text-lg`. Runs before the fontFamily comma-split below (which would shred a
106
+ // multi-family `var()` fallback). The parser reconstructs the value from the class, so no registry
107
+ // entry is needed.
108
+ const varRef = stringValue.match(/^var\((--[\w-]+)(?:\s*,[\s\S]*)?\)$/);
109
+ if (varRef) {
110
+ const entry = THEME_SCALE_BY_CSSVAR.get(varRef[1] ?? '');
111
+ if (entry && entry.property === camelToKebab(prop)) {
112
+ return entry.className;
113
+ }
114
+ }
115
+
91
116
  // Strip outer quotes from fontFamily values (e.g. "Space Grotesk" → Space Grotesk)
92
117
  if (prop === 'fontFamily') {
93
118
  stringValue = stringValue
@@ -110,19 +135,22 @@ function propertyValueToClass(prop: string, value: string | number): string | nu
110
135
  }
111
136
 
112
137
  // Preset values (e.g., boxShadow: "2" → shadow-2)
113
- if (prop === 'boxShadow' && shadowPresets[stringValue]) {
138
+ const shadowPreset = shadowPresets[stringValue];
139
+ if (prop === 'boxShadow' && shadowPreset) {
114
140
  const className = `shadow-${stringValue}`;
115
- registerStyleValue(className, shadowPresets[stringValue]);
141
+ registerStyleValue(className, shadowPreset);
116
142
  return className;
117
143
  }
118
- if ((prop === 'background' || prop === 'backgroundImage') && gradientPresets[stringValue]) {
144
+ const gradientPreset = gradientPresets[stringValue];
145
+ if ((prop === 'background' || prop === 'backgroundImage') && gradientPreset) {
119
146
  const className = `bg-gradient-${stringValue}`;
120
- registerStyleValue(className, gradientPresets[stringValue]);
147
+ registerStyleValue(className, gradientPreset);
121
148
  return className;
122
149
  }
123
- if (prop === 'border' && borderPresets[stringValue]) {
150
+ const borderPreset = borderPresets[stringValue];
151
+ if (prop === 'border' && borderPreset) {
124
152
  const className = `border-preset-${stringValue}`;
125
- registerStyleValue(className, borderPresets[stringValue]);
153
+ registerStyleValue(className, borderPreset);
126
154
  return className;
127
155
  }
128
156
 
@@ -170,6 +198,30 @@ function propertyValueToClass(prop: string, value: string | number): string | nu
170
198
  }
171
199
  }
172
200
 
201
+ // Named spacing scale (Tailwind steps): padding 16px → p-4, gap 24px → gap-6.
202
+ // Only EXACT steps; off-scale values fall through to the arbitrary form below.
203
+ // Keyword values (w-full, m-auto) are matched above, so they still win.
204
+ if (!className && SPACING_SCALE_ROOTS.has(root)) {
205
+ const step = SPACING_SCALE_REVERSE[stringValue];
206
+ if (step !== undefined) {
207
+ className = `${root}-${step}`;
208
+ }
209
+ }
210
+
211
+ // Tailwind grid track scale: gridTemplateColumns "repeat(3, minmax(0, 1fr))" → grid-cols-3
212
+ // (and gridTemplateRows → grid-rows-N, plus the none/subgrid keywords). Keeps grid-cols-N that
213
+ // came in from hand/AI-authored class strings round-tripping to the compact form rather than
214
+ // expanding to grid-cols-[repeat(3,_minmax(0,_1fr))]. Other track values ("1fr 1fr 1fr",
215
+ // "repeat(3, 1fr)") have no compact Tailwind spelling and stay arbitrary.
216
+ if (!className && (root === 'grid-cols' || root === 'grid-rows')) {
217
+ const repeatMatch = stringValue.match(/^repeat\(\s*([1-9]\d*)\s*,\s*minmax\(\s*0\s*,\s*1fr\s*\)\s*\)$/);
218
+ if (repeatMatch) {
219
+ className = `${root}-${repeatMatch[1]}`;
220
+ } else if (stringValue === 'none' || stringValue === 'subgrid') {
221
+ className = `${root}-${stringValue}`;
222
+ }
223
+ }
224
+
173
225
  // CSS variables: var(--x) → root-(--x). The `font-*` root is shared by font-weight and
174
226
  // font-family and is disambiguated by value type — but a `var(--x)` value is ambiguous, so
175
227
  // weight/size vars carry a Tailwind data-type hint (`number:` / `length:`) the parser reads
@@ -178,22 +230,41 @@ function propertyValueToClass(prop: string, value: string | number): string | nu
178
230
  if (!className) {
179
231
  const varMatch = stringValue.match(/^var\((--[\w-]+)\)$/);
180
232
  if (varMatch) {
233
+ const varName = (varMatch[1] ?? '').slice(2); // strip the leading `--`
181
234
  if (prop === 'fontSize') className = `text-(length:${varMatch[1]})`;
182
235
  else if (prop === 'fontWeight') className = `font-(number:${varMatch[1]})`;
183
- else className = `${root}-(${varMatch[1]})`;
236
+ // The `border`/`outline` roots are shared by a shorthand and a color longhand
237
+ // (`border` vs `borderColor`). The inverse parser disambiguates by value type, but a
238
+ // `var(--x)` value is opaque and reads as a color — so `border-(--x)` would round-trip
239
+ // to `borderColor`. When the prop is the shorthand/width longhand, fall back to the
240
+ // unambiguous arbitrary-property form (`[border:var(--x)]`), which names it explicitly.
241
+ else if (rootFormMisresolves(root, prop, stringValue)) {
242
+ className = `[${camelToKebab(prop)}:${stringValue}]`;
243
+ }
244
+ // A `var(--token)` on a color-token prop IS a Meno token reference → emit the bare token
245
+ // form (`bg-muted`), matching the bare-value path so `var(--muted)` and `muted` agree.
246
+ // (Token-aware inverse recovers it; palette-shaped names fall through to the var shorthand.)
247
+ else if (colorTokenProps.has(prop) && isBareColorTokenName(varName)) {
248
+ className = `${root}-${varName}`;
249
+ } else className = `${root}-(${varMatch[1]})`;
184
250
  }
185
251
  }
186
252
 
187
- // Bare color tokens (Meno convention): color: "primary" → text-(--primary)
188
- if (!className && colorTokenProps.has(prop) && isBareColorToken(stringValue)) {
189
- className = `${root}-(--${stringValue})`;
253
+ // Bare color tokens (Meno convention): color: "primary" → text-primary
254
+ if (!className && colorTokenProps.has(prop) && isBareColorTokenName(stringValue)) {
255
+ className = `${root}-${stringValue}`;
190
256
  }
191
257
 
192
258
  // Arbitrary value (Tailwind bracket syntax): padding: "92px 0" → p-[92px_0]
193
259
  if (!className) {
194
260
  const encoded = encodeArbitraryValue(stringValue);
195
261
  if (encoded != null) {
196
- className = `${root}-[${encoded}]`;
262
+ // Same shorthand/longhand guard as the var branch: a bare color value bound to the
263
+ // `border`/`outline` shorthand would parse back as the color longhand, so name the
264
+ // property explicitly via the arbitrary-property form instead of `root-[…]`.
265
+ className = rootFormMisresolves(root, prop, stringValue)
266
+ ? `[${camelToKebab(prop)}:${encoded}]`
267
+ : `${root}-[${encoded}]`;
197
268
  }
198
269
  }
199
270
 
@@ -235,6 +306,7 @@ export function stylesToClasses(styles: StyleObject | Record<string, string | nu
235
306
  const classes: string[] = [];
236
307
  for (const prop of sortedProps) {
237
308
  const value = styles[prop];
309
+ if (value === undefined) continue;
238
310
  // Skip StyleMapping values - they are resolved at render time, not converted to classes
239
311
  if (isStyleMapping(value)) continue;
240
312
  const className = propertyValueToClass(prop, value);
@@ -395,7 +467,10 @@ export function responsiveStylesToClasses(
395
467
  * Parse a utility class back to CSS property and value
396
468
  * Example: "p-[10px]" → { prop: "padding", value: "10px" }
397
469
  */
398
- export function classToStyle(className: string): { prop: string; value: string } | null {
470
+ export function classToStyle(
471
+ className: string,
472
+ knownTokens?: ReadonlySet<string>,
473
+ ): { prop: string; value: string } | null {
399
474
  // Remove responsive variant prefix for parsing (tablet:p-[10px] → p-[10px])
400
475
  const { base: cleanClass } = splitVariantPrefix(className);
401
476
 
@@ -407,7 +482,7 @@ export function classToStyle(className: string): { prop: string; value: string }
407
482
  return presetClassReverse[cleanClass];
408
483
  }
409
484
 
410
- const parsed = parseUtilityClass(cleanClass);
485
+ const parsed = parseUtilityClass(cleanClass, knownTokens);
411
486
 
412
487
  if (!parsed) {
413
488
  // Dynamic (arbitrary-property hash) classes registered for unknown roots
@@ -451,20 +526,26 @@ export function classToStyle(className: string): { prop: string; value: string }
451
526
  * Convert utility classes back to a style object
452
527
  * Example: ["p-[10px]", "tablet:p-[24px]"] → { base: { padding: "10px" }, tablet: { padding: "24px" } }
453
528
  */
454
- export function classesToStyles(classes: string[]): ResponsiveStyleObject | null {
529
+ export function classesToStyles(classes: string[], knownTokens?: ReadonlySet<string>): ResponsiveStyleObject | null {
455
530
  const styles: ResponsiveStyleObject = { base: {}, tablet: {}, mobile: {} };
456
531
 
457
532
  for (const className of classes) {
458
533
  const { breakpoint: variant, base: cleanClass } = splitVariantPrefix(className);
459
- const breakpoint: 'base' | 'tablet' | 'mobile' =
460
- variant === 'tablet' ? 'tablet' : variant === 'mobile' ? 'mobile' : 'base';
534
+ // Accept both the current desktop-first form (`max-tablet:`) and the legacy bare form (`tablet:`).
535
+ const bp = normalizeBreakpointVariant(variant);
536
+ const breakpoint: 'base' | 'tablet' | 'mobile' = bp === 'tablet' ? 'tablet' : bp === 'mobile' ? 'mobile' : 'base';
461
537
 
462
- const styleEntry = classToStyle(cleanClass);
538
+ const styleEntry = classToStyle(cleanClass, knownTokens);
463
539
  if (styleEntry) {
464
540
  if (!styles[breakpoint]) {
465
541
  styles[breakpoint] = {};
466
542
  }
467
543
  styles[breakpoint]![styleEntry.prop] = styleEntry.value;
544
+ // `size-*` is width AND height (no CSS shorthand) — restore both so the editor doesn't
545
+ // drop height on the next save. classToStyle returns the width half; mirror it to height.
546
+ if (cleanClass.startsWith('size-') && styleEntry.prop === 'width') {
547
+ styles[breakpoint]!.height = styleEntry.value;
548
+ }
468
549
  }
469
550
  }
470
551
 
@@ -472,5 +553,88 @@ export function classesToStyles(classes: string[]): ResponsiveStyleObject | null
472
553
  if (Object.keys(styles.tablet || {}).length === 0) delete styles.tablet;
473
554
  if (Object.keys(styles.mobile || {}).length === 0) delete styles.mobile;
474
555
 
475
- return Object.keys(styles.base || {}).length > 0 ? styles : null;
556
+ // Return the object when ANY breakpoint carries a declaration — not only base. A class string
557
+ // can legitimately style just a narrower breakpoint (e.g. "tablet:p-[16px]"), and dropping it to
558
+ // null there would lose class-string styling for that node (class-styling adapter, §11).
559
+ const hasAny =
560
+ Object.keys(styles.base || {}).length > 0 ||
561
+ Object.keys(styles.tablet || {}).length > 0 ||
562
+ Object.keys(styles.mobile || {}).length > 0;
563
+ return hasAny ? styles : null;
564
+ }
565
+
566
+ /**
567
+ * Whether a single style value can be stored as a STATIC utility class. Static strings (no
568
+ * `{{template}}`) and numbers can; template-bearing strings and `_mapping` objects cannot — a
569
+ * template would mint a broken `p-[{{x}}px]`, and a mapping is prop-driven. The latter must stay in
570
+ * the `style` object (rendered via the runtime var-bridge / `class:list`). This is the boundary that
571
+ * lets styling default to class storage while keeping dynamic values correct.
572
+ */
573
+ export function isClassableStyleValue(value: unknown): boolean {
574
+ if (typeof value === 'number') return true;
575
+ if (typeof value === 'string') return !value.includes('{{');
576
+ return false;
577
+ }
578
+
579
+ function splitStyleBucket(bucket: StyleObject): {
580
+ classable: StyleObject;
581
+ residual: StyleObject;
582
+ } {
583
+ const classable: StyleObject = {};
584
+ const residual: StyleObject = {};
585
+ for (const [k, v] of Object.entries(bucket)) {
586
+ if (isClassableStyleValue(v)) classable[k] = v as string | number;
587
+ else residual[k] = v as StyleMapping;
588
+ }
589
+ return { classable, residual };
590
+ }
591
+
592
+ /**
593
+ * Split a style value into the part storable as a utility-class string (static values) and the
594
+ * residual that must stay in the `style` object (templates + `_mapping`s). Preserves the responsive
595
+ * shape; a flat object is treated as `base`. Either side is null when it has no declarations. The
596
+ * canonical home (shared by the studio persistence boundary and the project-wide migration) for the
597
+ * "what can be a class" decision.
598
+ */
599
+ export function splitStyleByClassability(style: StyleValue | null | undefined): {
600
+ classable: ResponsiveStyleObject | null;
601
+ residual: ResponsiveStyleObject | null;
602
+ } {
603
+ if (!style || typeof style !== 'object') return { classable: null, residual: null };
604
+
605
+ if (isResponsiveStyle(style)) {
606
+ const classable: ResponsiveStyleObject = {};
607
+ const residual: ResponsiveStyleObject = {};
608
+ for (const [bp, bucket] of Object.entries(style)) {
609
+ if (!bucket || typeof bucket !== 'object') continue;
610
+ const split = splitStyleBucket(bucket);
611
+ if (Object.keys(split.classable).length > 0) classable[bp] = split.classable;
612
+ if (Object.keys(split.residual).length > 0) residual[bp] = split.residual;
613
+ }
614
+ return {
615
+ classable: Object.keys(classable).length > 0 ? classable : null,
616
+ residual: Object.keys(residual).length > 0 ? residual : null,
617
+ };
618
+ }
619
+
620
+ const split = splitStyleBucket(style as StyleObject);
621
+ return {
622
+ classable: Object.keys(split.classable).length > 0 ? { base: split.classable } : null,
623
+ residual: Object.keys(split.residual).length > 0 ? { base: split.residual } : null,
624
+ };
625
+ }
626
+
627
+ /**
628
+ * Whether a style value contains at least one prop `_mapping` (in any breakpoint) — flat or
629
+ * responsive. Prop-driven styling can't be a static utility class, so callers that gate class
630
+ * conversion (the studio panel, the project migration) check this. Single source of truth.
631
+ */
632
+ export function styleHasMapping(style: StyleValue | null | undefined): boolean {
633
+ if (!style || typeof style !== 'object') return false;
634
+ const buckets = isResponsiveStyle(style) ? Object.values(style) : [style as Record<string, unknown>];
635
+ for (const bucket of buckets) {
636
+ if (!bucket || typeof bucket !== 'object') continue;
637
+ for (const v of Object.values(bucket)) if (isStyleMapping(v)) return true;
638
+ }
639
+ return false;
476
640
  }
@@ -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
+ });