meno-core 1.0.47 → 1.0.49

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 (97) hide show
  1. package/build-astro.ts +2 -2
  2. package/dist/build-static.js +7 -7
  3. package/dist/chunks/{chunk-UUA5LEWF.js → chunk-6IVUG7FY.js} +138 -7
  4. package/dist/chunks/chunk-6IVUG7FY.js.map +7 -0
  5. package/dist/chunks/{chunk-XSWR3QLI.js → chunk-AZQYF6KE.js} +261 -130
  6. package/dist/chunks/chunk-AZQYF6KE.js.map +7 -0
  7. package/dist/chunks/{chunk-47UNLQUU.js → chunk-CHD5UCFF.js} +57 -12
  8. package/dist/chunks/chunk-CHD5UCFF.js.map +7 -0
  9. package/dist/chunks/{chunk-FGUZOYJX.js → chunk-EQYDSPBB.js} +435 -131
  10. package/dist/chunks/chunk-EQYDSPBB.js.map +7 -0
  11. package/dist/chunks/{chunk-IF3RATBY.js → chunk-H4JSCDNW.js} +2 -2
  12. package/dist/chunks/{chunk-KITQJYZV.js → chunk-J23ZX5AP.js} +40 -4
  13. package/dist/chunks/chunk-J23ZX5AP.js.map +7 -0
  14. package/dist/chunks/{chunk-LJFB5EBT.js → chunk-JER5NQVM.js} +5 -5
  15. package/dist/chunks/{chunk-ZTKHJQ2Z.js → chunk-KPU2XHOS.js} +5 -2
  16. package/dist/chunks/{chunk-ZTKHJQ2Z.js.map → chunk-KPU2XHOS.js.map} +2 -2
  17. package/dist/chunks/{chunk-BCLGRZ3U.js → chunk-LKAGAQ3M.js} +2 -2
  18. package/dist/chunks/{chunk-FED5MME6.js → chunk-S2CX6HFM.js} +262 -26
  19. package/dist/chunks/chunk-S2CX6HFM.js.map +7 -0
  20. package/dist/chunks/{configService-DYCUEURL.js → configService-CCA6AIDI.js} +3 -3
  21. package/dist/entries/server-router.js +9 -9
  22. package/dist/entries/server-router.js.map +2 -2
  23. package/dist/lib/client/index.js +64 -20
  24. package/dist/lib/client/index.js.map +3 -3
  25. package/dist/lib/server/index.js +1737 -296
  26. package/dist/lib/server/index.js.map +4 -4
  27. package/dist/lib/shared/index.js +50 -10
  28. package/dist/lib/shared/index.js.map +3 -3
  29. package/entries/server-router.tsx +6 -2
  30. package/lib/client/core/ComponentBuilder.test.ts +17 -0
  31. package/lib/client/core/ComponentBuilder.ts +25 -1
  32. package/lib/client/core/builders/embedBuilder.ts +15 -2
  33. package/lib/client/core/builders/linkNodeBuilder.ts +15 -2
  34. package/lib/client/core/builders/localeListBuilder.ts +17 -6
  35. package/lib/client/styles/StyleInjector.ts +3 -2
  36. package/lib/client/theme.ts +4 -4
  37. package/lib/server/cssGenerator.test.ts +64 -1
  38. package/lib/server/cssGenerator.ts +48 -9
  39. package/lib/server/index.ts +1 -1
  40. package/lib/server/jsonLoader.test.ts +0 -17
  41. package/lib/server/jsonLoader.ts +0 -81
  42. package/lib/server/providers/fileSystemCMSProvider.test.ts +163 -0
  43. package/lib/server/providers/fileSystemCMSProvider.ts +200 -11
  44. package/lib/server/routes/api/variables.ts +4 -2
  45. package/lib/server/routes/index.ts +1 -1
  46. package/lib/server/routes/pages.ts +23 -1
  47. package/lib/server/services/cmsService.test.ts +246 -0
  48. package/lib/server/services/cmsService.ts +122 -5
  49. package/lib/server/services/configService.ts +5 -0
  50. package/lib/server/ssr/attributeBuilder.ts +41 -0
  51. package/lib/server/ssr/htmlGenerator.test.ts +114 -2
  52. package/lib/server/ssr/htmlGenerator.ts +53 -6
  53. package/lib/server/ssr/liveReloadIntegration.test.ts +209 -0
  54. package/lib/server/ssr/ssrRenderer.test.ts +362 -1
  55. package/lib/server/ssr/ssrRenderer.ts +216 -72
  56. package/lib/server/utils/jsonLineMapper.test.ts +53 -1
  57. package/lib/server/utils/jsonLineMapper.ts +43 -3
  58. package/lib/server/webflow/buildWebflow.ts +343 -123
  59. package/lib/server/webflow/index.ts +1 -0
  60. package/lib/server/webflow/nodeToWebflow.test.ts +3170 -0
  61. package/lib/server/webflow/nodeToWebflow.ts +2141 -129
  62. package/lib/server/webflow/styleMapper.test.ts +389 -0
  63. package/lib/server/webflow/styleMapper.ts +517 -63
  64. package/lib/server/webflow/templateWrapper.ts +49 -0
  65. package/lib/server/webflow/types.ts +218 -18
  66. package/lib/shared/cssGeneration.test.ts +267 -1
  67. package/lib/shared/cssGeneration.ts +240 -18
  68. package/lib/shared/cssProperties.test.ts +247 -1
  69. package/lib/shared/cssProperties.ts +196 -6
  70. package/lib/shared/elementClassName.test.ts +15 -0
  71. package/lib/shared/elementClassName.ts +7 -3
  72. package/lib/shared/interfaces/contentProvider.ts +39 -6
  73. package/lib/shared/pathSecurity.ts +16 -0
  74. package/lib/shared/registry/nodeTypes/ListNodeType.ts +1 -1
  75. package/lib/shared/responsiveScaling.test.ts +143 -0
  76. package/lib/shared/responsiveScaling.ts +253 -2
  77. package/lib/shared/themeDefaults.test.ts +3 -3
  78. package/lib/shared/themeDefaults.ts +3 -3
  79. package/lib/shared/types/cms.ts +28 -3
  80. package/lib/shared/types/index.ts +2 -0
  81. package/lib/shared/types/variables.ts +37 -0
  82. package/lib/shared/utilityClassConfig.ts +3 -0
  83. package/lib/shared/utilityClassMapper.test.ts +123 -0
  84. package/lib/shared/utilityClassMapper.ts +179 -8
  85. package/lib/shared/validation/schemas.ts +15 -1
  86. package/lib/shared/validation/validators.ts +26 -1
  87. package/package.json +1 -1
  88. package/dist/chunks/chunk-47UNLQUU.js.map +0 -7
  89. package/dist/chunks/chunk-FED5MME6.js.map +0 -7
  90. package/dist/chunks/chunk-FGUZOYJX.js.map +0 -7
  91. package/dist/chunks/chunk-KITQJYZV.js.map +0 -7
  92. package/dist/chunks/chunk-UUA5LEWF.js.map +0 -7
  93. package/dist/chunks/chunk-XSWR3QLI.js.map +0 -7
  94. /package/dist/chunks/{chunk-IF3RATBY.js.map → chunk-H4JSCDNW.js.map} +0 -0
  95. /package/dist/chunks/{chunk-LJFB5EBT.js.map → chunk-JER5NQVM.js.map} +0 -0
  96. /package/dist/chunks/{chunk-BCLGRZ3U.js.map → chunk-LKAGAQ3M.js.map} +0 -0
  97. /package/dist/chunks/{configService-DYCUEURL.js.map → configService-CCA6AIDI.js.map} +0 -0
@@ -13,7 +13,14 @@ import {
13
13
  gradientPresets,
14
14
  borderPresets,
15
15
  } from './utilityClassConfig';
16
- import { registerStyleValue, registerDynamicStyle } from './styleValueRegistry';
16
+ import { registerStyleValue, registerDynamicStyle, getStyleValue } from './styleValueRegistry';
17
+ import {
18
+ SCALABLE_CSS_PROPERTIES,
19
+ buildFluidClampWithExplicitMin,
20
+ parseValue,
21
+ DEFAULT_FLUID_RANGE,
22
+ type ResponsiveScales,
23
+ } from './responsiveScaling';
17
24
 
18
25
  // Function name abbreviations for shorter class names
19
26
  const functionAbbreviations: Record<string, string> = {
@@ -54,6 +61,34 @@ function sanitizeClassValue(value: string): string {
54
61
  .replace(/[^\w-]/g, ''); // remove other special chars
55
62
  }
56
63
 
64
+ /**
65
+ * 32-bit FNV-1a hash, encoded as base36 (6-7 chars). Deterministic — same
66
+ * input always produces the same hash, so duplicate complex values across
67
+ * elements share a single class definition.
68
+ */
69
+ function shortHash(input: string): string {
70
+ let hash = 0x811c9dc5;
71
+ for (let i = 0; i < input.length; i++) {
72
+ hash ^= input.charCodeAt(i);
73
+ hash = Math.imul(hash, 0x01000193);
74
+ }
75
+ return (hash >>> 0).toString(36);
76
+ }
77
+
78
+ /**
79
+ * True if a class name contains characters illegal in CSS selectors. We
80
+ * generate dynamic class names from CSS values; complex values (especially
81
+ * Webflow `clamp(2*1rem, ((..2)/(90 - 20)*20)*1rem + ..*100vw, 4*1rem)`)
82
+ * end up with `*`, `(`, `)`, `+`, `,` baked into the class name, none of
83
+ * which are valid in CSS selectors. Browser silently drops the rule and
84
+ * the styled property reverts to UA defaults. Hash-fallback gives us a
85
+ * short, valid-CSS class name, with the actual value resolved via the
86
+ * style-value registry at CSS generation time.
87
+ */
88
+ function hasIllegalClassChars(className: string): boolean {
89
+ return /[^\w-]/.test(className);
90
+ }
91
+
57
92
  /**
58
93
  * Converts a CSS property value to a utility class name
59
94
  * Example: { prop: "padding", value: "10px" } → "p-10px"
@@ -191,6 +226,18 @@ function propertyValueToClass(prop: string, value: string | number): string | nu
191
226
  className = `${prefix}-${stringValue}`;
192
227
  }
193
228
 
229
+ // Hash-fallback for class names with illegal CSS selector chars. Webflow's
230
+ // fluid `clamp(2*1rem, ((2 - .5)/70*20)*1rem + ..*100vw, 4*1rem)` would
231
+ // otherwise produce `pt-cl2*1rem-((2-.5)/70*20)*1rem+..*100vw-4*1rem`,
232
+ // with `*`, `(`, `)`, `+`, `,` — none of which are valid in CSS selectors.
233
+ // Browser drops the rule, the styled element reverts to defaults
234
+ // (paddings collapse to 0, etc.). Short FNV-1a hash + style-value
235
+ // registry preserves the original value for the CSS generator without
236
+ // baking it into the selector.
237
+ if (hasIllegalClassChars(className)) {
238
+ className = `${prefix}-h${shortHash(stringValue)}`;
239
+ }
240
+
194
241
  // Register original value for CSS generation (single registration point)
195
242
  registerStyleValue(className, value);
196
243
  return className;
@@ -245,10 +292,95 @@ export function stylesToClasses(
245
292
  * Also handles flat merged style objects:
246
293
  * Example: { padding: "10px", display: "flex" } → ["p-10px", "flex"]
247
294
  */
295
+ /** Values that don't make sense in a fluid container pattern. Mirrors cssGeneration.ts. */
296
+ const CONTAINER_RESERVED_VALUES = new Set(['auto', 'inherit', 'initial', 'unset', '']);
297
+
298
+ /**
299
+ * Detect "container intent" (width === maxWidth) on a flat StyleObject and
300
+ * rewrite into the fluid container pattern. Used by `responsiveStylesToClasses`
301
+ * when its caller signals fluid mode is active.
302
+ *
303
+ * Mirrors `applyContainerPattern` in cssGeneration.ts — kept as a private
304
+ * helper here to avoid a cross-module import in this hot path.
305
+ */
306
+ function transformContainerIntent(style: StyleObject): StyleObject {
307
+ const w = style.width;
308
+ const mw = style.maxWidth;
309
+ if (w == null || mw == null) return style;
310
+ if (typeof w !== 'string' || typeof mw !== 'string') return style;
311
+ if (w !== mw) return style;
312
+ if (CONTAINER_RESERVED_VALUES.has(w.trim())) return style;
313
+ return {
314
+ ...style,
315
+ width: 'calc(100% - var(--site-margin) * 2)',
316
+ marginLeft: 'auto',
317
+ marginRight: 'auto',
318
+ };
319
+ }
320
+
321
+ /**
322
+ * In fluid mode, consume per-node mobile overrides for scalable properties
323
+ * into the base's `clamp()` value. The mobile value becomes the clamp's MIN
324
+ * (small-viewport endpoint), the base stays as MAX. The mobile property is
325
+ * then stripped from the mobile slice so no `.mob-*` @media rule is emitted
326
+ * for it — avoiding the jump at the mobile breakpoint that a hard override
327
+ * would create.
328
+ *
329
+ * Non-scalable mobile properties (display, flex-direction, etc.) are left
330
+ * alone. Tablet is also untouched — explicit per-tablet overrides keep their
331
+ * @media behavior for now.
332
+ *
333
+ * Bails out when units mismatch, parsing fails, or the registered values
334
+ * aren't plain numeric+unit strings — those cases fall back to today's
335
+ * behavior (mobile emits its own .mob-* rule).
336
+ */
337
+ function consumeMobileIntoBaseClamp(
338
+ base: StyleObject,
339
+ mobile: StyleObject,
340
+ responsiveScales: ResponsiveScales
341
+ ): { patchedBase: StyleObject; patchedMobile: StyleObject } {
342
+ const fluidRange = responsiveScales.fluidRange ?? DEFAULT_FLUID_RANGE;
343
+ const patchedBase: StyleObject = { ...base };
344
+ const patchedMobile: StyleObject = { ...mobile };
345
+
346
+ for (const prop of Object.keys(base)) {
347
+ if (!SCALABLE_CSS_PROPERTIES.has(prop)) continue;
348
+ const mobileRaw = mobile[prop];
349
+ if (mobileRaw == null || mobileRaw === '') continue;
350
+
351
+ const baseRaw = base[prop];
352
+ if (baseRaw == null || baseRaw === '') continue;
353
+ if (typeof baseRaw !== 'string' && typeof baseRaw !== 'number') continue;
354
+ if (typeof mobileRaw !== 'string' && typeof mobileRaw !== 'number') continue;
355
+
356
+ const baseParsed = parseValue(String(baseRaw));
357
+ const mobileParsed = parseValue(String(mobileRaw));
358
+ if (!baseParsed || !mobileParsed) continue;
359
+ if (baseParsed.unit !== mobileParsed.unit) continue;
360
+ if (baseParsed.unit === '%' || baseParsed.unit === 'em') continue;
361
+
362
+ const clampValue = buildFluidClampWithExplicitMin(
363
+ mobileParsed.value,
364
+ baseParsed.value,
365
+ baseParsed.unit,
366
+ fluidRange.min,
367
+ fluidRange.max
368
+ );
369
+
370
+ patchedBase[prop] = clampValue;
371
+ delete patchedMobile[prop];
372
+ }
373
+
374
+ return { patchedBase, patchedMobile };
375
+ }
376
+
248
377
  export function responsiveStylesToClasses(
249
- styles: StyleObject | ResponsiveStyleObject | null | undefined
378
+ styles: StyleObject | ResponsiveStyleObject | null | undefined,
379
+ options?: { fluidActive?: boolean; responsiveScales?: ResponsiveScales }
250
380
  ): string[] {
251
381
  if (!styles) return [];
382
+ const fluidActive = options?.fluidActive === true;
383
+ const responsiveScales = options?.responsiveScales;
252
384
 
253
385
  const classes: string[] = [];
254
386
 
@@ -256,25 +388,53 @@ export function responsiveStylesToClasses(
256
388
  if ('base' in styles || 'tablet' in styles || 'mobile' in styles) {
257
389
  const responsiveStyles = styles as ResponsiveStyleObject;
258
390
 
391
+ // In fluid mode, fold per-node scalable mobile overrides into the base's
392
+ // clamp() so they become the small-viewport endpoint instead of a hard
393
+ // @media jump. Falls back to original base/mobile when prerequisites
394
+ // aren't met.
395
+ let baseSource = responsiveStyles.base;
396
+ let mobileSource = responsiveStyles.mobile;
397
+ if (fluidActive && responsiveScales?.enabled === true && baseSource && mobileSource) {
398
+ const { patchedBase, patchedMobile } = consumeMobileIntoBaseClamp(
399
+ baseSource,
400
+ mobileSource,
401
+ responsiveScales
402
+ );
403
+ baseSource = patchedBase;
404
+ mobileSource = patchedMobile;
405
+ }
406
+
259
407
  // Base styles (no prefix)
260
- if (responsiveStyles.base) {
261
- classes.push(...stylesToClasses(responsiveStyles.base));
408
+ if (baseSource) {
409
+ const base = fluidActive
410
+ ? transformContainerIntent(baseSource)
411
+ : baseSource;
412
+ classes.push(...stylesToClasses(base));
262
413
  }
263
414
 
264
415
  // Tablet styles (t- prefix)
265
416
  if (responsiveStyles.tablet) {
266
- const tabletClasses = stylesToClasses(responsiveStyles.tablet);
417
+ const tablet = fluidActive
418
+ ? transformContainerIntent(responsiveStyles.tablet)
419
+ : responsiveStyles.tablet;
420
+ const tabletClasses = stylesToClasses(tablet);
267
421
  classes.push(...tabletClasses.map((cls) => `t-${cls}`));
268
422
  }
269
423
 
270
424
  // Mobile styles (mob- prefix to avoid conflict with margin)
271
- if (responsiveStyles.mobile) {
272
- const mobileClasses = stylesToClasses(responsiveStyles.mobile);
425
+ if (mobileSource && Object.keys(mobileSource).length > 0) {
426
+ const mobile = fluidActive
427
+ ? transformContainerIntent(mobileSource)
428
+ : mobileSource;
429
+ const mobileClasses = stylesToClasses(mobile);
273
430
  classes.push(...mobileClasses.map((cls) => `mob-${cls}`));
274
431
  }
275
432
  } else {
276
433
  // Flat style object - treat all as base classes
277
- classes.push(...stylesToClasses(styles as StyleObject));
434
+ const flat = fluidActive
435
+ ? transformContainerIntent(styles as StyleObject)
436
+ : (styles as StyleObject);
437
+ classes.push(...stylesToClasses(flat));
278
438
  }
279
439
 
280
440
  return classes;
@@ -319,6 +479,17 @@ export function classToStyle(className: string): { prop: string; value: string }
319
479
 
320
480
  if (!prop) return null;
321
481
 
482
+ // Hash-fallback class — value is `h<base36>`, the real CSS value lives in
483
+ // the style-value registry (set when the class was minted from a complex
484
+ // `clamp()` / `calc()` value that wouldn't fit in a CSS selector). Pull
485
+ // the real value back out so editor reads see the original expression.
486
+ if (/^h[0-9a-z]+$/.test(value)) {
487
+ const registered = getStyleValue(cleanClass);
488
+ if (registered != null) {
489
+ return { prop, value: String(registered) };
490
+ }
491
+ }
492
+
322
493
  // Handle CSS variables
323
494
  if (value.includes('background') || value.includes('text') || value.includes('border')) {
324
495
  return { prop, value: `var(--${value})` };
@@ -553,7 +553,7 @@ export const CMSSchemaSchema = z.object({
553
553
  }).passthrough();
554
554
 
555
555
  /**
556
- * CMS item schema (content entry)
556
+ * CMS item schema (content entry, strict — used at publish/save time).
557
557
  */
558
558
  export const CMSItemSchema = z.object({
559
559
  _id: z.string(),
@@ -562,6 +562,20 @@ export const CMSItemSchema = z.object({
562
562
  _createdAt: z.string().optional(),
563
563
  }).passthrough();
564
564
 
565
+ /**
566
+ * CMS draft item schema (loose — used at draft-write time).
567
+ *
568
+ * Drafts can be partial work-in-progress, so even system-managed identifiers
569
+ * are optional at this stage. Strict validation runs when promoting the draft
570
+ * to published (see `publishDraft` in `FileSystemCMSProvider`).
571
+ */
572
+ export const CMSDraftItemSchema = z.object({
573
+ _id: z.string().optional(),
574
+ _slug: z.string().optional(),
575
+ _filename: z.string().optional(),
576
+ _createdAt: z.string().optional(),
577
+ }).passthrough();
578
+
565
579
  /**
566
580
  * CMS filter condition schema
567
581
  */
@@ -19,6 +19,7 @@ import {
19
19
  PageMetaDataSchema,
20
20
  CMSSchemaSchema,
21
21
  CMSItemSchema,
22
+ CMSDraftItemSchema,
22
23
  } from './schemas';
23
24
 
24
25
  /**
@@ -279,7 +280,7 @@ export function validateCMSSchema(data: unknown): ValidationResult<CMSSchema> {
279
280
  }
280
281
 
281
282
  /**
282
- * Validate a CMS item
283
+ * Validate a CMS item (strict — used for published items and at publish time)
283
284
  * @returns ValidationResult with error details on failure
284
285
  */
285
286
  export function validateCMSItem(data: unknown): ValidationResult<CMSItem> {
@@ -301,4 +302,28 @@ export function validateCMSItem(data: unknown): ValidationResult<CMSItem> {
301
302
  }
302
303
  }
303
304
 
305
+ /**
306
+ * Validate a CMS draft item (loose — used at draft-write time).
307
+ * Drafts may be partial; even system identifiers are optional. Strict
308
+ * validation re-runs on Publish.
309
+ */
310
+ export function validateCMSDraftItem(data: unknown): ValidationResult<CMSItem> {
311
+ try {
312
+ const result = CMSDraftItemSchema.safeParse(data);
313
+ if (result.success) {
314
+ return { valid: true, data: result.data as CMSItem };
315
+ }
316
+ return { valid: false, errors: zodToValidationErrors(result.error) };
317
+ } catch (error) {
318
+ return {
319
+ valid: false,
320
+ errors: [{
321
+ path: 'root',
322
+ message: error instanceof Error ? error.message : 'Unknown validation error',
323
+ code: 'UNKNOWN',
324
+ }],
325
+ };
326
+ }
327
+ }
328
+
304
329
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "meno-core",
3
- "version": "1.0.47",
3
+ "version": "1.0.49",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "meno": "./dist/bin/cli.js"