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.
- package/build-astro.ts +2 -2
- package/dist/build-static.js +7 -7
- package/dist/chunks/{chunk-UUA5LEWF.js → chunk-6IVUG7FY.js} +138 -7
- package/dist/chunks/chunk-6IVUG7FY.js.map +7 -0
- package/dist/chunks/{chunk-XSWR3QLI.js → chunk-AZQYF6KE.js} +261 -130
- package/dist/chunks/chunk-AZQYF6KE.js.map +7 -0
- package/dist/chunks/{chunk-47UNLQUU.js → chunk-CHD5UCFF.js} +57 -12
- package/dist/chunks/chunk-CHD5UCFF.js.map +7 -0
- package/dist/chunks/{chunk-FGUZOYJX.js → chunk-EQYDSPBB.js} +435 -131
- package/dist/chunks/chunk-EQYDSPBB.js.map +7 -0
- package/dist/chunks/{chunk-IF3RATBY.js → chunk-H4JSCDNW.js} +2 -2
- package/dist/chunks/{chunk-KITQJYZV.js → chunk-J23ZX5AP.js} +40 -4
- package/dist/chunks/chunk-J23ZX5AP.js.map +7 -0
- package/dist/chunks/{chunk-LJFB5EBT.js → chunk-JER5NQVM.js} +5 -5
- package/dist/chunks/{chunk-ZTKHJQ2Z.js → chunk-KPU2XHOS.js} +5 -2
- package/dist/chunks/{chunk-ZTKHJQ2Z.js.map → chunk-KPU2XHOS.js.map} +2 -2
- package/dist/chunks/{chunk-BCLGRZ3U.js → chunk-LKAGAQ3M.js} +2 -2
- package/dist/chunks/{chunk-FED5MME6.js → chunk-S2CX6HFM.js} +262 -26
- package/dist/chunks/chunk-S2CX6HFM.js.map +7 -0
- package/dist/chunks/{configService-DYCUEURL.js → configService-CCA6AIDI.js} +3 -3
- package/dist/entries/server-router.js +9 -9
- package/dist/entries/server-router.js.map +2 -2
- package/dist/lib/client/index.js +64 -20
- package/dist/lib/client/index.js.map +3 -3
- package/dist/lib/server/index.js +1737 -296
- package/dist/lib/server/index.js.map +4 -4
- package/dist/lib/shared/index.js +50 -10
- package/dist/lib/shared/index.js.map +3 -3
- package/entries/server-router.tsx +6 -2
- package/lib/client/core/ComponentBuilder.test.ts +17 -0
- package/lib/client/core/ComponentBuilder.ts +25 -1
- package/lib/client/core/builders/embedBuilder.ts +15 -2
- package/lib/client/core/builders/linkNodeBuilder.ts +15 -2
- package/lib/client/core/builders/localeListBuilder.ts +17 -6
- package/lib/client/styles/StyleInjector.ts +3 -2
- package/lib/client/theme.ts +4 -4
- package/lib/server/cssGenerator.test.ts +64 -1
- package/lib/server/cssGenerator.ts +48 -9
- package/lib/server/index.ts +1 -1
- package/lib/server/jsonLoader.test.ts +0 -17
- package/lib/server/jsonLoader.ts +0 -81
- package/lib/server/providers/fileSystemCMSProvider.test.ts +163 -0
- package/lib/server/providers/fileSystemCMSProvider.ts +200 -11
- package/lib/server/routes/api/variables.ts +4 -2
- package/lib/server/routes/index.ts +1 -1
- package/lib/server/routes/pages.ts +23 -1
- package/lib/server/services/cmsService.test.ts +246 -0
- package/lib/server/services/cmsService.ts +122 -5
- package/lib/server/services/configService.ts +5 -0
- package/lib/server/ssr/attributeBuilder.ts +41 -0
- package/lib/server/ssr/htmlGenerator.test.ts +114 -2
- package/lib/server/ssr/htmlGenerator.ts +53 -6
- package/lib/server/ssr/liveReloadIntegration.test.ts +209 -0
- package/lib/server/ssr/ssrRenderer.test.ts +362 -1
- package/lib/server/ssr/ssrRenderer.ts +216 -72
- package/lib/server/utils/jsonLineMapper.test.ts +53 -1
- package/lib/server/utils/jsonLineMapper.ts +43 -3
- package/lib/server/webflow/buildWebflow.ts +343 -123
- package/lib/server/webflow/index.ts +1 -0
- package/lib/server/webflow/nodeToWebflow.test.ts +3170 -0
- package/lib/server/webflow/nodeToWebflow.ts +2141 -129
- package/lib/server/webflow/styleMapper.test.ts +389 -0
- package/lib/server/webflow/styleMapper.ts +517 -63
- package/lib/server/webflow/templateWrapper.ts +49 -0
- package/lib/server/webflow/types.ts +218 -18
- package/lib/shared/cssGeneration.test.ts +267 -1
- package/lib/shared/cssGeneration.ts +240 -18
- package/lib/shared/cssProperties.test.ts +247 -1
- package/lib/shared/cssProperties.ts +196 -6
- package/lib/shared/elementClassName.test.ts +15 -0
- package/lib/shared/elementClassName.ts +7 -3
- package/lib/shared/interfaces/contentProvider.ts +39 -6
- package/lib/shared/pathSecurity.ts +16 -0
- package/lib/shared/registry/nodeTypes/ListNodeType.ts +1 -1
- package/lib/shared/responsiveScaling.test.ts +143 -0
- package/lib/shared/responsiveScaling.ts +253 -2
- package/lib/shared/themeDefaults.test.ts +3 -3
- package/lib/shared/themeDefaults.ts +3 -3
- package/lib/shared/types/cms.ts +28 -3
- package/lib/shared/types/index.ts +2 -0
- package/lib/shared/types/variables.ts +37 -0
- package/lib/shared/utilityClassConfig.ts +3 -0
- package/lib/shared/utilityClassMapper.test.ts +123 -0
- package/lib/shared/utilityClassMapper.ts +179 -8
- package/lib/shared/validation/schemas.ts +15 -1
- package/lib/shared/validation/validators.ts +26 -1
- package/package.json +1 -1
- package/dist/chunks/chunk-47UNLQUU.js.map +0 -7
- package/dist/chunks/chunk-FED5MME6.js.map +0 -7
- package/dist/chunks/chunk-FGUZOYJX.js.map +0 -7
- package/dist/chunks/chunk-KITQJYZV.js.map +0 -7
- package/dist/chunks/chunk-UUA5LEWF.js.map +0 -7
- package/dist/chunks/chunk-XSWR3QLI.js.map +0 -7
- /package/dist/chunks/{chunk-IF3RATBY.js.map → chunk-H4JSCDNW.js.map} +0 -0
- /package/dist/chunks/{chunk-LJFB5EBT.js.map → chunk-JER5NQVM.js.map} +0 -0
- /package/dist/chunks/{chunk-BCLGRZ3U.js.map → chunk-LKAGAQ3M.js.map} +0 -0
- /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 (
|
|
261
|
-
|
|
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
|
|
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 (
|
|
272
|
-
const
|
|
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
|
-
|
|
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
|
|