meno-core 1.0.19 → 1.0.21

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 (50) hide show
  1. package/.claude/settings.local.json +7 -0
  2. package/lib/client/core/ComponentBuilder.test.ts +68 -56
  3. package/lib/client/core/ComponentBuilder.ts +6 -4
  4. package/lib/client/core/builders/embedBuilder.ts +10 -1
  5. package/lib/client/core/builders/index.ts +6 -2
  6. package/lib/client/core/builders/{cmsListBuilder.ts → listBuilder.ts} +227 -95
  7. package/lib/client/responsiveStyleResolver.test.ts +12 -12
  8. package/lib/client/responsiveStyleResolver.ts +19 -7
  9. package/lib/client/routing/Router.tsx +35 -7
  10. package/lib/client/templateEngine.test.ts +126 -0
  11. package/lib/client/templateEngine.ts +53 -13
  12. package/lib/server/jsonLoader.test.ts +4 -1
  13. package/lib/server/jsonLoader.ts +64 -15
  14. package/lib/server/services/configService.ts +68 -13
  15. package/lib/server/ssr/attributeBuilder.ts +8 -0
  16. package/lib/server/ssr/index.ts +1 -1
  17. package/lib/server/ssr/ssrRenderer.ts +245 -111
  18. package/lib/server/ssrRenderer.test.ts +197 -3
  19. package/lib/server/validateStyleCoverage.ts +14 -17
  20. package/lib/shared/breakpoints.test.ts +210 -23
  21. package/lib/shared/breakpoints.ts +124 -17
  22. package/lib/shared/constants.test.ts +1 -1
  23. package/lib/shared/constants.ts +5 -1
  24. package/lib/shared/cssGeneration.test.ts +17 -0
  25. package/lib/shared/cssGeneration.ts +49 -12
  26. package/lib/shared/index.ts +3 -0
  27. package/lib/shared/itemTemplateUtils.test.ts +44 -2
  28. package/lib/shared/itemTemplateUtils.ts +15 -2
  29. package/lib/shared/nodeUtils.ts +23 -4
  30. package/lib/shared/registry/BaseNodeTypeRegistry.test.ts +2 -2
  31. package/lib/shared/registry/nodeTypes/ListNodeType.ts +186 -0
  32. package/lib/shared/registry/nodeTypes/SlotMarkerType.ts +6 -0
  33. package/lib/shared/registry/nodeTypes/index.ts +6 -5
  34. package/lib/shared/responsiveScaling.test.ts +87 -0
  35. package/lib/shared/responsiveScaling.ts +33 -29
  36. package/lib/shared/responsiveStyleUtils.test.ts +7 -7
  37. package/lib/shared/responsiveStyleUtils.ts +22 -16
  38. package/lib/shared/styleNodeUtils.ts +5 -5
  39. package/lib/shared/styleValueRegistry.ts +60 -5
  40. package/lib/shared/tree/PathBuilder.ts +3 -3
  41. package/lib/shared/treePathUtils.ts +7 -5
  42. package/lib/shared/types/cms.ts +4 -57
  43. package/lib/shared/types/components.ts +45 -4
  44. package/lib/shared/types/index.ts +13 -0
  45. package/lib/shared/utilityClassConfig.ts +14 -0
  46. package/lib/shared/utilityClassMapper.ts +43 -2
  47. package/lib/shared/validation/propValidator.ts +9 -1
  48. package/lib/shared/validation/schemas.ts +60 -14
  49. package/package.json +1 -1
  50. package/lib/shared/registry/nodeTypes/CMSListNodeType.ts +0 -109
@@ -7,6 +7,10 @@
7
7
  // Global registry mapping class names to original CSS values
8
8
  const registry = new Map<string, string | number>();
9
9
 
10
+ // Registry for dynamic/fallback classes that include the CSS property name
11
+ // Used for properties not in the standard propertyMap
12
+ const dynamicRegistry = new Map<string, { property: string; value: string | number }>();
13
+
10
14
  /**
11
15
  * Register a style value for a class name
12
16
  * Called when generating utility class names from style objects
@@ -15,6 +19,14 @@ export function registerStyleValue(className: string, value: string | number): v
15
19
  registry.set(className, value);
16
20
  }
17
21
 
22
+ /**
23
+ * Register a dynamic style (property + value) for a class name
24
+ * Used for properties not in propertyMap that need dynamic class generation
25
+ */
26
+ export function registerDynamicStyle(className: string, property: string, value: string | number): void {
27
+ dynamicRegistry.set(className, { property, value });
28
+ }
29
+
18
30
  /**
19
31
  * Get the original style value for a class name
20
32
  * Returns undefined if not registered (fallback to reverse-engineering)
@@ -23,20 +35,46 @@ export function getStyleValue(className: string): string | number | undefined {
23
35
  return registry.get(className);
24
36
  }
25
37
 
38
+ /**
39
+ * Get the dynamic style (property + value) for a class name
40
+ * Returns undefined if not registered
41
+ */
42
+ export function getDynamicStyle(className: string): { property: string; value: string | number } | undefined {
43
+ return dynamicRegistry.get(className);
44
+ }
45
+
46
+ /**
47
+ * Check if a class name is registered in the dynamic registry
48
+ */
49
+ export function isDynamicClass(className: string): boolean {
50
+ return dynamicRegistry.has(className);
51
+ }
52
+
53
+ /**
54
+ * Get all registered dynamic class names
55
+ */
56
+ export function getAllDynamicClasses(): string[] {
57
+ return Array.from(dynamicRegistry.keys());
58
+ }
59
+
26
60
  /**
27
61
  * Clear all registered values
28
62
  * Useful for testing or resetting state
29
63
  */
30
64
  export function clearRegistry(): void {
31
65
  registry.clear();
66
+ dynamicRegistry.clear();
32
67
  }
33
68
 
34
69
  /**
35
70
  * Serialize registry for SSR embedding
36
- * Returns JSON string of all entries
71
+ * Returns JSON string of all entries (both standard and dynamic)
37
72
  */
38
73
  export function serializeRegistry(): string {
39
- return JSON.stringify(Array.from(registry.entries()));
74
+ return JSON.stringify({
75
+ standard: Array.from(registry.entries()),
76
+ dynamic: Array.from(dynamicRegistry.entries()),
77
+ });
40
78
  }
41
79
 
42
80
  /**
@@ -44,8 +82,25 @@ export function serializeRegistry(): string {
44
82
  * Call on client to restore server-side registered values
45
83
  */
46
84
  export function hydrateRegistry(data: string): void {
47
- const entries = JSON.parse(data) as [string, string | number][];
48
- for (const [key, value] of entries) {
49
- registry.set(key, value);
85
+ const parsed = JSON.parse(data);
86
+
87
+ // Handle both old format (array) and new format (object with standard/dynamic)
88
+ if (Array.isArray(parsed)) {
89
+ // Old format: just standard entries
90
+ for (const [key, value] of parsed as [string, string | number][]) {
91
+ registry.set(key, value);
92
+ }
93
+ } else {
94
+ // New format: { standard, dynamic }
95
+ if (parsed.standard) {
96
+ for (const [key, value] of parsed.standard as [string, string | number][]) {
97
+ registry.set(key, value);
98
+ }
99
+ }
100
+ if (parsed.dynamic) {
101
+ for (const [key, value] of parsed.dynamic as [string, { property: string; value: string | number }][]) {
102
+ dynamicRegistry.set(key, value);
103
+ }
104
+ }
50
105
  }
51
106
  }
@@ -10,7 +10,7 @@
10
10
  import type { Path } from '../pathArrayUtils';
11
11
  import type { PageData, ComponentNode } from '../types';
12
12
  import { getRootData } from '../treePathUtils';
13
- import { isComponentNode, isSlotMarker, isLocaleListNode, isCMSListNode } from '../nodeUtils';
13
+ import { isComponentNode, isSlotMarker, isLocaleListNode, isCMSListNode, isListNode } from '../nodeUtils';
14
14
 
15
15
  /**
16
16
  * Node path data containing both logical and rendered paths
@@ -289,7 +289,7 @@ export function buildTreePathsWithRendered(
289
289
  if (
290
290
  typeof child === 'object' &&
291
291
  child !== null &&
292
- ('tag' in child || 'component' in child || 'html' in child || 'src' in child || 'href' in child || isLocaleListNode(child) || isCMSListNode(child) || (cumulativeInstancePath && isSlotMarker(child)))
292
+ ('tag' in child || 'component' in child || 'html' in child || 'src' in child || 'href' in child || isLocaleListNode(child) || isCMSListNode(child) || isListNode(child) || (cumulativeInstancePath && isSlotMarker(child)))
293
293
  ) {
294
294
  const childPath = [...currentPath, index];
295
295
  traverse(child as ComponentNode, childPath);
@@ -361,7 +361,7 @@ export function buildComponentTreePaths(
361
361
  if (
362
362
  typeof child === 'object' &&
363
363
  child !== null &&
364
- ('tag' in child || 'component' in child || 'html' in child || 'src' in child || 'href' in child || isLocaleListNode(child) || isCMSListNode(child) || isSlotMarker(child))
364
+ ('tag' in child || 'component' in child || 'html' in child || 'src' in child || 'href' in child || isLocaleListNode(child) || isCMSListNode(child) || isListNode(child) || isSlotMarker(child))
365
365
  ) {
366
366
  const childPath = [...currentPath, index];
367
367
  traverse(child as ComponentNode, childPath);
@@ -293,14 +293,16 @@ export function isComponentNodeParent(
293
293
  // - SlotMarker: type === 'slot'
294
294
  // - LinkNode: type === 'link'
295
295
  // - EmbedNode: type === 'embed'
296
- // - CMSListNode: type === 'cms-list'
296
+ // - ListNode: type === 'list' (also handles legacy 'cms-list' for migration)
297
+ const nodeType = (parent as any).type;
297
298
  return 'type' in parent && (
298
299
  'tag' in parent ||
299
300
  'component' in parent ||
300
- parent.type === 'slot' ||
301
- parent.type === 'link' ||
302
- parent.type === 'embed' ||
303
- parent.type === 'cms-list'
301
+ nodeType === 'slot' ||
302
+ nodeType === 'link' ||
303
+ nodeType === 'embed' ||
304
+ nodeType === 'cms-list' || // Legacy support for migration
305
+ nodeType === 'list'
304
306
  );
305
307
  }
306
308
 
@@ -147,64 +147,11 @@ export interface CMSListQuery {
147
147
  }
148
148
 
149
149
  /**
150
- * CMSList node - a special built-in component that renders its children
151
- * for each matching CMS item. Supports filtering, sorting, and pagination.
152
- *
153
- * Context variables available in children (with named context):
154
- * - {{<itemAs>.field}} - Access CMS item fields (e.g., {{post.title}})
155
- * - {{<itemAs>Index}} - Zero-based index of current item (e.g., {{postIndex}})
156
- * - {{<itemAs>First}} - Boolean, true if first item (e.g., {{postFirst}})
157
- * - {{<itemAs>Last}} - Boolean, true if last item (e.g., {{postLast}})
158
- *
159
- * Legacy context (still supported):
160
- * - {{item.field}}, {{itemIndex}}, {{itemFirst}}, {{itemLast}}
150
+ * @deprecated CMSListNode is now replaced by ListNode with sourceType: 'collection'.
151
+ * This type alias is kept for backward compatibility during migration.
152
+ * Import ListNode from '../registry/nodeTypes/ListNodeType' instead.
161
153
  */
162
- export interface CMSListNode {
163
- type: 'cms-list';
164
- /** Custom label displayed in structure tree */
165
- label?: string;
166
- /** Conditional rendering - skip node when false */
167
- if?: { field: string; operator?: 'eq' | 'neq' | 'truthy' | 'falsy'; value?: unknown };
168
- /** Inline styles */
169
- style?: Record<string, unknown>;
170
- /** Interactive CSS rules (hover, active, etc.) */
171
- interactiveStyles?: Array<{
172
- style: Record<string, unknown>;
173
- name?: string;
174
- prefix?: string;
175
- postfix?: string;
176
- previewProp?: string;
177
- }>;
178
- /** Generate element class without styles (for custom CSS) */
179
- generateElementClass?: boolean;
180
- /** HTML attributes */
181
- attributes?: Record<string, string | number | boolean>;
182
- /** Collection to query */
183
- collection: string;
184
- /** Variable name for item in templates (default: singular of collection) */
185
- itemAs?: string;
186
- /** Direct item IDs or template expression for referenced items (e.g., "{{post.authorId}}") */
187
- items?: string | string[];
188
- /** Filter conditions */
189
- filter?: CMSFilterCondition | CMSFilterCondition[] | Record<string, unknown>;
190
- /** Sort configuration */
191
- sort?: CMSSortConfig | CMSSortConfig[];
192
- /** Maximum number of items to return */
193
- limit?: number;
194
- /** Number of items to skip */
195
- offset?: number;
196
- /** Exclude the current CMS item from the list (useful for "related items" sections) */
197
- excludeCurrentItem?: boolean;
198
- /**
199
- * Emit item template for dynamic client-side rendering.
200
- * When true, SSR emits a `<template data-meno-item>` element with unprocessed
201
- * {{item.field}} placeholders. MenoFilter uses this to render items beyond
202
- * the SSR'd limit dynamically.
203
- */
204
- emitTemplate?: boolean;
205
- /** Children are repeated for each item */
206
- children?: unknown[];
207
- }
154
+ export type CMSListNode = import('../registry/nodeTypes/ListNodeType').ListNode;
208
155
 
209
156
  /**
210
157
  * Configuration for nested CMS list placeholders.
@@ -8,7 +8,7 @@ import type { ComponentNode } from './nodes';
8
8
  /**
9
9
  * Prop type definitions
10
10
  */
11
- export type PropType = 'string' | 'select' | 'boolean' | 'number' | 'link' | 'file' | 'rich-text';
11
+ export type PropType = 'string' | 'select' | 'boolean' | 'number' | 'link' | 'file' | 'rich-text' | 'list';
12
12
 
13
13
  /**
14
14
  * Internationalization (i18n) value object
@@ -53,15 +53,56 @@ export interface LinkPropValue {
53
53
  }
54
54
 
55
55
  /**
56
- * Prop definition with improved type safety
56
+ * Base prop definition without list-specific fields
57
57
  */
58
- export interface PropDefinition {
59
- type: PropType;
58
+ export interface BasePropDefinition {
59
+ type: Exclude<PropType, 'list'>;
60
60
  default?: string | number | boolean | I18nValue | LinkPropValue;
61
61
  options?: readonly string[]; // Required for "select" type
62
62
  accept?: string; // For "file" type: MIME pattern like "image/*", "video/*"
63
63
  }
64
64
 
65
+ /**
66
+ * List item schema - defines the structure of each item in a list prop
67
+ * Uses the same prop types as component interfaces (except nested lists)
68
+ */
69
+ export type ListItemSchema = Record<string, BasePropDefinition>;
70
+
71
+ /**
72
+ * List item value type
73
+ */
74
+ export type ListItemValue = Record<string, string | number | boolean | LinkPropValue | null>;
75
+
76
+ /**
77
+ * List prop definition - for array/list data
78
+ */
79
+ export interface ListPropDefinition {
80
+ type: 'list';
81
+ /** Schema defining the structure of each list item */
82
+ itemSchema: ListItemSchema;
83
+ /** Default value is an array of items */
84
+ default?: ListItemValue[];
85
+ }
86
+
87
+ /**
88
+ * Prop definition with improved type safety
89
+ */
90
+ export type PropDefinition = BasePropDefinition | ListPropDefinition;
91
+
92
+ /**
93
+ * Type guard to check if a prop definition is a list type
94
+ */
95
+ export function isListPropDefinition(def: PropDefinition): def is ListPropDefinition {
96
+ return def.type === 'list';
97
+ }
98
+
99
+ /**
100
+ * Type guard to check if a prop definition is a base (non-list) type
101
+ */
102
+ export function isBasePropDefinition(def: PropDefinition): def is BasePropDefinition {
103
+ return def.type !== 'list';
104
+ }
105
+
65
106
  /**
66
107
  * Structured component definition
67
108
  */
@@ -22,6 +22,10 @@ export type {
22
22
  ComponentDefinition,
23
23
  StructuredComponentDefinition,
24
24
  PropDefinition,
25
+ BasePropDefinition,
26
+ ListPropDefinition,
27
+ ListItemSchema,
28
+ ListItemValue,
25
29
  PropType,
26
30
  JSONPage,
27
31
  PageData,
@@ -29,6 +33,12 @@ export type {
29
33
  I18nValue,
30
34
  I18nConfig,
31
35
  LocaleConfig,
36
+ LinkPropValue,
37
+ } from './components';
38
+
39
+ export {
40
+ isListPropDefinition,
41
+ isBasePropDefinition,
32
42
  } from './components';
33
43
 
34
44
  // Export all style types
@@ -80,6 +90,9 @@ export type {
80
90
  ReferenceLocation,
81
91
  } from './cms';
82
92
 
93
+ // Export ListNode from registry (unified list node type)
94
+ export type { ListNode } from '../registry/nodeTypes/ListNodeType';
95
+
83
96
  // Export CMS utilities
84
97
  export { singularize } from './cms';
85
98
 
@@ -76,6 +76,7 @@ export const propertyMap: Record<string, string> = {
76
76
  overflowWrap: 'ow',
77
77
  textIndent: 'ti',
78
78
  verticalAlign: 'va',
79
+ whiteSpace: 'whs',
79
80
 
80
81
  // Lists
81
82
  listStyle: 'lst',
@@ -213,6 +214,7 @@ export const prefixToCSSProperty: Record<string, string> = {
213
214
  ow: 'overflow-wrap',
214
215
  ti: 'text-indent',
215
216
  va: 'vertical-align',
217
+ whs: 'white-space',
216
218
 
217
219
  // Lists
218
220
  lst: 'list-style',
@@ -343,6 +345,13 @@ export const specialValueMappings: Record<string, Record<string, string>> = {
343
345
  text: 'us-text',
344
346
  all: 'us-all',
345
347
  },
348
+ whiteSpace: {
349
+ normal: 'whs-normal',
350
+ nowrap: 'whs-nowrap',
351
+ pre: 'whs-pre',
352
+ 'pre-wrap': 'whs-pre-wrap',
353
+ 'pre-line': 'whs-pre-line',
354
+ },
346
355
  // Note: CSS functions (blur, translateY, scale, rotate, repeat) are now handled
347
356
  // dynamically in utilityClassMapper.ts - no need to hardcode specific values here
348
357
  };
@@ -378,6 +387,11 @@ export const classToStyleSpecialCases: Record<string, { prop: string; value: str
378
387
  'us-auto': { prop: 'userSelect', value: 'auto' },
379
388
  'us-text': { prop: 'userSelect', value: 'text' },
380
389
  'us-all': { prop: 'userSelect', value: 'all' },
390
+ 'whs-normal': { prop: 'whiteSpace', value: 'normal' },
391
+ 'whs-nowrap': { prop: 'whiteSpace', value: 'nowrap' },
392
+ 'whs-pre': { prop: 'whiteSpace', value: 'pre' },
393
+ 'whs-pre-wrap': { prop: 'whiteSpace', value: 'pre-wrap' },
394
+ 'whs-pre-line': { prop: 'whiteSpace', value: 'pre-line' },
381
395
  // Grid template columns
382
396
  'gtc-2': { prop: 'gridTemplateColumns', value: 'repeat(2, 1fr)' },
383
397
  'gtc-3': { prop: 'gridTemplateColumns', value: 'repeat(3, 1fr)' },
@@ -13,7 +13,7 @@ import {
13
13
  gradientPresets,
14
14
  borderPresets,
15
15
  } from './utilityClassConfig';
16
- import { registerStyleValue } from './styleValueRegistry';
16
+ import { registerStyleValue, registerDynamicStyle } from './styleValueRegistry';
17
17
 
18
18
  // Function name abbreviations for shorter class names
19
19
  const functionAbbreviations: Record<string, string> = {
@@ -40,13 +40,54 @@ function isStyleMapping(value: unknown): value is StyleMapping {
40
40
  return typeof value === 'object' && value !== null && '_mapping' in value && (value as StyleMapping)._mapping === true;
41
41
  }
42
42
 
43
+ /**
44
+ * Sanitize a value for use in a CSS class name
45
+ * Replaces special characters with safe alternatives
46
+ */
47
+ function sanitizeClassValue(value: string): string {
48
+ return value
49
+ .replace(/\s+/g, '-') // spaces to hyphens
50
+ .replace(/[()]/g, '') // remove parentheses
51
+ .replace(/,/g, '_') // commas to underscores
52
+ .replace(/%/g, 'p') // percent to 'p'
53
+ .replace(/\./g, 'd') // dots to 'd' (for decimals)
54
+ .replace(/[^\w-]/g, ''); // remove other special chars
55
+ }
56
+
43
57
  /**
44
58
  * Converts a CSS property value to a utility class name
45
59
  * Example: { prop: "padding", value: "10px" } → "p-10px"
60
+ *
61
+ * For properties not in propertyMap, generates a dynamic class using
62
+ * a shortened property name prefix (e.g., "textOverflow" → "txov-ellipsis")
46
63
  */
47
64
  function propertyValueToClass(prop: string, value: string | number): string | null {
48
65
  const prefix = propertyMap[prop];
49
- if (!prefix) return null;
66
+
67
+ // For properties not in propertyMap, generate a dynamic fallback class
68
+ if (!prefix) {
69
+ const stringValue = String(value);
70
+ // Generate a short prefix from the property name (first 2-4 chars of each word)
71
+ // e.g., "textOverflow" → "txov", "WebkitBackgroundClip" → "wbbg"
72
+ const dynamicPrefix = prop
73
+ .replace(/([A-Z])/g, '-$1') // camelCase to kebab
74
+ .toLowerCase()
75
+ .split('-')
76
+ .filter(Boolean)
77
+ .map(word => word.slice(0, 2)) // first 2 chars of each word
78
+ .join('');
79
+
80
+ // Sanitize value for class name
81
+ const sanitizedValue = sanitizeClassValue(stringValue);
82
+ const className = `${dynamicPrefix}-${sanitizedValue}`;
83
+
84
+ // Convert camelCase to kebab-case for CSS property
85
+ const cssProperty = prop.replace(/([A-Z])/g, '-$1').toLowerCase();
86
+
87
+ // Register in dynamic registry with property info
88
+ registerDynamicStyle(className, cssProperty, value);
89
+ return className;
90
+ }
50
91
 
51
92
  const stringValue = String(value);
52
93
  let className: string | null = null;
@@ -94,7 +94,9 @@ function validateSingleProp(
94
94
  return { valid: true };
95
95
  }
96
96
 
97
- const { type, options } = propDef;
97
+ const { type } = propDef;
98
+ // Options is only available on select type (from BasePropDefinition)
99
+ const options = 'options' in propDef ? propDef.options : undefined;
98
100
 
99
101
  // Type checking and coercion
100
102
  let coercedValue: unknown;
@@ -164,6 +166,12 @@ function validateSingleProp(
164
166
  typeValid = typeof value === 'string';
165
167
  break;
166
168
 
169
+ case 'list':
170
+ // List type accepts arrays
171
+ coercedValue = value;
172
+ typeValid = Array.isArray(value);
173
+ break;
174
+
167
175
  default:
168
176
  return {
169
177
  valid: false,
@@ -13,21 +13,48 @@ import type { StyleObject, StyleMapping, LinkMapping } from '../types/styles';
13
13
  import { NODE_TYPE } from '../constants';
14
14
 
15
15
  /**
16
- * Prop type schema - validates PropType values
16
+ * Prop type schema - validates PropType values (excluding list for base definition)
17
17
  */
18
- export const PropTypeSchema = z.enum(['string', 'select', 'boolean', 'number', 'link', 'file', 'rich-text']);
18
+ export const BasePropTypeSchema = z.enum(['string', 'select', 'boolean', 'number', 'link', 'file', 'rich-text']);
19
19
 
20
20
  /**
21
- * Prop definition schema
22
- * Validates prop definitions from component interfaces
21
+ * Full prop type schema including list
22
+ */
23
+ export const PropTypeSchema = z.enum(['string', 'select', 'boolean', 'number', 'link', 'file', 'rich-text', 'list']);
24
+
25
+ /**
26
+ * Base prop definition schema (for non-list props)
23
27
  */
24
- export const PropDefinitionSchema = z.object({
25
- type: PropTypeSchema,
28
+ export const BasePropDefinitionSchema = z.object({
29
+ type: BasePropTypeSchema,
26
30
  default: z.union([z.string(), z.number(), z.boolean(), z.object({ href: z.string(), target: z.string().optional() })]).optional(),
27
31
  options: z.array(z.string()).readonly().optional(),
28
32
  accept: z.string().optional(), // For 'file' type: MIME pattern like "image/*", "video/*"
29
33
  }).passthrough();
30
34
 
35
+ /**
36
+ * List item schema - defines structure of each item in a list prop
37
+ */
38
+ export const ListItemSchemaSchema = z.record(z.string(), BasePropDefinitionSchema);
39
+
40
+ /**
41
+ * List prop definition schema
42
+ */
43
+ export const ListPropDefinitionSchema = z.object({
44
+ type: z.literal('list'),
45
+ itemSchema: ListItemSchemaSchema,
46
+ default: z.array(z.record(z.string(), z.union([z.string(), z.number(), z.boolean(), z.object({ href: z.string(), target: z.string().optional() }), z.null()]))).optional(),
47
+ }).passthrough();
48
+
49
+ /**
50
+ * Prop definition schema (union of base and list)
51
+ * Validates prop definitions from component interfaces
52
+ */
53
+ export const PropDefinitionSchema = z.union([
54
+ ListPropDefinitionSchema,
55
+ BasePropDefinitionSchema,
56
+ ]);
57
+
31
58
  /**
32
59
  * Style mapping schema
33
60
  */
@@ -124,10 +151,16 @@ export const InteractiveStylesSchema = z.array(InteractiveStyleRuleSchema);
124
151
 
125
152
  /**
126
153
  * Slot marker schema
154
+ * default: Optional default content when instance has no children
155
+ * Can be an array of ComponentNodes or a string
127
156
  */
128
- export const SlotMarkerSchema = z.object({
157
+ export const SlotMarkerSchema: z.ZodType<any> = z.lazy(() => z.object({
129
158
  type: z.literal(NODE_TYPE.SLOT),
130
- }).passthrough();
159
+ default: z.union([
160
+ z.array(z.union([ComponentNodeSchema, z.string()])),
161
+ z.string(),
162
+ ]).optional(),
163
+ }).passthrough());
131
164
 
132
165
  /**
133
166
  * Component node schema - discriminated union (forward declaration)
@@ -226,28 +259,41 @@ export const LocaleListNodeSchema: z.ZodType<any> = z.lazy(() => z.object({
226
259
  }).passthrough());
227
260
 
228
261
  /**
229
- * CMS List node schema (basic version for ComponentNodeSchema union)
230
- * Full validation with filter/sort is in CMSListNodeSchema below
262
+ * Unified List node schema (basic version for ComponentNodeSchema union)
263
+ * Handles both prop-based lists and CMS collection-based lists.
264
+ * Uses sourceType to distinguish: 'prop' (default) or 'collection'.
265
+ * Also supports legacy 'cms-list' type for backward compatibility.
231
266
  */
232
- export const CMSListNodeSchemaBasic: z.ZodType<any> = z.lazy(() => z.object({
233
- type: z.literal(NODE_TYPE.CMS_LIST),
234
- collection: z.string(),
267
+ export const ListNodeSchemaBasic: z.ZodType<any> = z.lazy(() => z.object({
268
+ type: z.union([z.literal(NODE_TYPE.LIST), z.literal('cms-list')]), // Support both for migration
269
+ sourceType: z.enum(['prop', 'collection']).optional(), // defaults to 'prop'
270
+ source: z.string().optional(), // Source prop name or collection name
271
+ collection: z.string().optional(), // Legacy field for cms-list migration
272
+ tag: z.string().optional(), // Wrapper element tag, defaults to 'div'
235
273
  label: z.string().optional(),
236
274
  if: IfConditionSchema.optional(),
237
275
  style: StyleValueSchema.optional(),
238
276
  interactiveStyles: InteractiveStylesSchema.optional(),
239
277
  generateElementClass: z.boolean().optional(),
240
278
  itemAs: z.string().optional(),
279
+ // Collection-specific options
241
280
  items: z.union([z.string(), z.array(z.string())]).optional(),
242
281
  filter: z.unknown().optional(),
243
282
  sort: z.unknown().optional(),
244
283
  limit: z.number().optional(),
245
284
  offset: z.number().optional(),
246
285
  excludeCurrentItem: z.boolean().optional(),
286
+ emitTemplate: z.boolean().optional(),
247
287
  attributes: z.record(z.string(), z.union([z.string(), z.number(), z.boolean()])).optional(),
248
288
  children: z.array(z.union([ComponentNodeSchema, z.string()])).optional(),
249
289
  }).passthrough());
250
290
 
291
+ /**
292
+ * @deprecated Use ListNodeSchemaBasic instead.
293
+ * Kept for backward compatibility during migration.
294
+ */
295
+ export const CMSListNodeSchemaBasic = ListNodeSchemaBasic;
296
+
251
297
  /**
252
298
  * Component node schema - discriminated union (now defined after dependencies)
253
299
  */
@@ -258,7 +304,7 @@ ComponentNodeSchema = z.union([
258
304
  EmbedNodeSchema,
259
305
  LinkNodeSchema,
260
306
  LocaleListNodeSchema,
261
- CMSListNodeSchemaBasic,
307
+ ListNodeSchemaBasic,
262
308
  ]);
263
309
 
264
310
  // Export ComponentNodeSchema
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "meno-core",
3
- "version": "1.0.19",
3
+ "version": "1.0.21",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "meno": "./bin/cli.ts"