meno-core 1.0.20 → 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 (34) hide show
  1. package/lib/client/core/ComponentBuilder.test.ts +68 -56
  2. package/lib/client/core/ComponentBuilder.ts +6 -4
  3. package/lib/client/core/builders/embedBuilder.ts +10 -1
  4. package/lib/client/core/builders/index.ts +6 -2
  5. package/lib/client/core/builders/{cmsListBuilder.ts → listBuilder.ts} +227 -95
  6. package/lib/client/routing/Router.tsx +35 -7
  7. package/lib/client/templateEngine.test.ts +126 -0
  8. package/lib/client/templateEngine.ts +32 -11
  9. package/lib/server/ssr/attributeBuilder.ts +8 -0
  10. package/lib/server/ssr/index.ts +1 -1
  11. package/lib/server/ssr/ssrRenderer.ts +223 -110
  12. package/lib/server/ssrRenderer.test.ts +197 -3
  13. package/lib/shared/constants.test.ts +1 -1
  14. package/lib/shared/constants.ts +5 -1
  15. package/lib/shared/cssGeneration.test.ts +17 -0
  16. package/lib/shared/cssGeneration.ts +3 -2
  17. package/lib/shared/index.ts +3 -0
  18. package/lib/shared/itemTemplateUtils.test.ts +44 -2
  19. package/lib/shared/itemTemplateUtils.ts +15 -2
  20. package/lib/shared/nodeUtils.ts +23 -4
  21. package/lib/shared/registry/BaseNodeTypeRegistry.test.ts +2 -2
  22. package/lib/shared/registry/nodeTypes/ListNodeType.ts +186 -0
  23. package/lib/shared/registry/nodeTypes/SlotMarkerType.ts +6 -0
  24. package/lib/shared/registry/nodeTypes/index.ts +6 -5
  25. package/lib/shared/styleNodeUtils.ts +5 -5
  26. package/lib/shared/tree/PathBuilder.ts +3 -3
  27. package/lib/shared/treePathUtils.ts +7 -5
  28. package/lib/shared/types/cms.ts +4 -57
  29. package/lib/shared/types/components.ts +45 -4
  30. package/lib/shared/types/index.ts +13 -0
  31. package/lib/shared/validation/propValidator.ts +9 -1
  32. package/lib/shared/validation/schemas.ts +60 -14
  33. package/package.json +1 -1
  34. package/lib/shared/registry/nodeTypes/CMSListNodeType.ts +0 -109
@@ -16,7 +16,8 @@ import { SlotMarkerType, SlotMarkerSchema, type SlotMarker } from './SlotMarkerT
16
16
  import { EmbedNodeType, EmbedNodeSchema, type EmbedNode } from './EmbedNodeType';
17
17
  import { LinkNodeType, LinkNodeSchema, type LinkNode } from './LinkNodeType';
18
18
  import { LocaleListNodeType, LocaleListNodeSchema, type LocaleListNode } from './LocaleListNodeType';
19
- import { CMSListNodeType, CMSListNodeSchema, type CMSListNode } from './CMSListNodeType';
19
+ import { ListNodeType, ListNodeSchema, type ListNode } from './ListNodeType';
20
+
20
21
  /**
21
22
  * All built-in node type definitions
22
23
  * Type assertion needed because TypeScript sees each as a specific generic type
@@ -28,7 +29,7 @@ export const builtInNodeTypes: NodeTypeDefinition[] = [
28
29
  EmbedNodeType as NodeTypeDefinition,
29
30
  LinkNodeType as NodeTypeDefinition,
30
31
  LocaleListNodeType as NodeTypeDefinition,
31
- CMSListNodeType as NodeTypeDefinition,
32
+ ListNodeType as NodeTypeDefinition,
32
33
  ];
33
34
 
34
35
  /**
@@ -41,7 +42,7 @@ export const nodeSchemas = {
41
42
  embed: EmbedNodeSchema,
42
43
  link: LinkNodeSchema,
43
44
  localeList: LocaleListNodeSchema,
44
- cmsList: CMSListNodeSchema,
45
+ list: ListNodeSchema,
45
46
  };
46
47
 
47
48
  /**
@@ -59,7 +60,7 @@ export { SlotMarkerType, SlotMarkerSchema, type SlotMarker } from './SlotMarkerT
59
60
  export { EmbedNodeType, EmbedNodeSchema, type EmbedNode } from './EmbedNodeType';
60
61
  export { LinkNodeType, LinkNodeSchema, type LinkNode } from './LinkNodeType';
61
62
  export { LocaleListNodeType, LocaleListNodeSchema, type LocaleListNode } from './LocaleListNodeType';
62
- export { CMSListNodeType, CMSListNodeSchema, type CMSListNode } from './CMSListNodeType';
63
+ export { ListNodeType, ListNodeSchema, type ListNode } from './ListNodeType';
63
64
 
64
65
  /**
65
66
  * Discriminated union of all component node types
@@ -72,4 +73,4 @@ export type ComponentNode =
72
73
  | EmbedNode
73
74
  | LinkNode
74
75
  | LocaleListNode
75
- | CMSListNode;
76
+ | ListNode;
@@ -4,7 +4,7 @@
4
4
  */
5
5
 
6
6
  import type { ComponentNode, HtmlNode, ComponentInstanceNode, StyleValue, ResponsiveStyleObject } from './types';
7
- import { isComponentNode, isHtmlNode, isEmbedNode } from './nodeUtils';
7
+ import { isComponentNode, isHtmlNode, isEmbedNode, isListNode } from './nodeUtils';
8
8
  import { isResponsiveStyle } from './styleUtils';
9
9
 
10
10
  /**
@@ -26,8 +26,8 @@ export function applyStylesToNode(
26
26
  }
27
27
 
28
28
  node.props.style = { ...(node.props.style || {}), ...styles };
29
- } else if (isHtmlNode(node) || isEmbedNode(node)) {
30
- // HTML node and Embed node: put styles at top level
29
+ } else if (isHtmlNode(node) || isEmbedNode(node) || isListNode(node)) {
30
+ // HTML node, Embed node, and List node: put styles at top level
31
31
  node.style = styles as StyleValue;
32
32
  }
33
33
 
@@ -45,8 +45,8 @@ export function mergeNodeStyles(
45
45
  ): ComponentNode {
46
46
  if (!instanceStyles) return node;
47
47
 
48
- if (isHtmlNode(node) || isEmbedNode(node)) {
49
- // For HTML nodes and Embed nodes: merge instance styles with existing top-level styles
48
+ if (isHtmlNode(node) || isEmbedNode(node) || isListNode(node)) {
49
+ // For HTML nodes, Embed nodes, and List nodes: merge instance styles with existing top-level styles
50
50
  const existingStyle = node.style;
51
51
  if (existingStyle && typeof existingStyle === 'object') {
52
52
  node.style = {
@@ -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
 
@@ -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.20",
3
+ "version": "1.0.21",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "meno": "./bin/cli.ts"
@@ -1,109 +0,0 @@
1
- /**
2
- * CMS List Node Type Definition
3
- * Renders children for each CMS item in a collection
4
- */
5
-
6
- import { z } from 'zod';
7
- import { createElement as h } from 'react';
8
- import { StyleValueSchema, InteractiveStylesSchema, IfConditionSchema } from '../../validation/schemas';
9
- import { createNodeType } from '../createNodeType';
10
- import { NODE_TYPE } from '../../constants';
11
-
12
- // Schema is the SINGLE source of truth
13
- const CMSListNodeSchemaInternal = z.object({
14
- type: z.literal('cms-list'),
15
- label: z.string().optional(), // Custom label displayed in structure tree
16
- if: IfConditionSchema.optional(), // Conditional rendering - skip node when false
17
- style: StyleValueSchema.optional(),
18
- interactiveStyles: InteractiveStylesSchema.optional(), // Interactive CSS rules (hover, active, etc.)
19
- generateElementClass: z.boolean().optional(), // Generate element class without styles (for custom CSS)
20
- attributes: z.record(z.string(), z.union([z.string(), z.number(), z.boolean()])).optional(),
21
- /** Collection to query */
22
- collection: z.string(),
23
- /** Filter conditions */
24
- filter: z.union([
25
- z.object({
26
- field: z.string(),
27
- operator: z.enum(['eq', 'neq', 'gt', 'gte', 'lt', 'lte', 'contains', 'in']).optional(),
28
- value: z.unknown(),
29
- }),
30
- z.array(z.object({
31
- field: z.string(),
32
- operator: z.enum(['eq', 'neq', 'gt', 'gte', 'lt', 'lte', 'contains', 'in']).optional(),
33
- value: z.unknown(),
34
- })),
35
- z.record(z.unknown()),
36
- ]).optional(),
37
- /** Sort configuration */
38
- sort: z.union([
39
- z.object({
40
- field: z.string(),
41
- order: z.enum(['asc', 'desc']).optional(),
42
- }),
43
- z.array(z.object({
44
- field: z.string(),
45
- order: z.enum(['asc', 'desc']).optional(),
46
- })),
47
- ]).optional(),
48
- /** Maximum number of items to return */
49
- limit: z.number().optional(),
50
- /** Number of items to skip */
51
- offset: z.number().optional(),
52
- /** Children are repeated for each item */
53
- children: z.array(z.unknown()).optional(),
54
- }).passthrough();
55
-
56
- // TypeScript type inferred from schema
57
- export type CMSListNode = z.infer<typeof CMSListNodeSchemaInternal>;
58
-
59
- // Export schema for validation/schemas.ts
60
- export const CMSListNodeSchema = CMSListNodeSchemaInternal;
61
-
62
- export const CMSListNodeType = createNodeType({
63
- type: NODE_TYPE.CMS_LIST,
64
- displayName: 'CMS List',
65
- category: 'special',
66
- schema: CMSListNodeSchemaInternal,
67
-
68
- defaultValues: {
69
- collection: '',
70
- children: [],
71
- style: { base: {} },
72
- },
73
-
74
- treeDisplay: {
75
- icon: 'HTML_ELEMENT',
76
- getLabel: (node) => {
77
- const cmsNode = node as CMSListNode;
78
- return cmsNode.collection ? `CMS List: ${cmsNode.collection}` : 'CMS List';
79
- },
80
- },
81
-
82
- clientRenderer: (node, context) => {
83
- const cmsNode = node as CMSListNode;
84
- // In editor, shows placeholder with collection name
85
- return h('div', {
86
- key: context.key,
87
- 'data-cms-list': 'true',
88
- 'data-collection': cmsNode.collection,
89
- style: {
90
- padding: '8px 12px',
91
- background: 'rgba(139, 92, 246, 0.1)',
92
- border: '1px dashed rgba(139, 92, 246, 0.5)',
93
- borderRadius: '4px',
94
- color: '#8b5cf6',
95
- fontSize: '12px',
96
- },
97
- }, `[CMS List: ${cmsNode.collection || 'No collection'}]`);
98
- },
99
-
100
- ssrRenderer: (_node, _context) => {
101
- // Placeholder - actual SSR is handled by processCMSList in ssrRenderer.ts
102
- return '<!-- cms-list rendered by processCMSList -->';
103
- },
104
-
105
- capabilities: {
106
- canHaveChildren: true,
107
- requiresProps: ['collection'],
108
- },
109
- });