meno-core 1.0.1 → 1.0.3
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/lib/client/core/ComponentBuilder.ts +347 -769
- package/lib/client/core/builders/cmsListBuilder.ts +193 -0
- package/lib/client/core/builders/embedBuilder.ts +92 -0
- package/lib/client/core/builders/index.ts +26 -0
- package/lib/client/core/builders/linkBuilder.ts +118 -0
- package/lib/client/core/builders/localeListBuilder.ts +145 -0
- package/lib/client/core/builders/objectLinkBuilder.ts +90 -0
- package/lib/client/core/builders/types.ts +89 -0
- package/lib/client/hmrWebSocket.ts +6 -2
- package/lib/client/routing/RouteLoader.ts +4 -2
- package/lib/client/scripts/ScriptExecutor.ts +9 -4
- package/lib/client/styles/StyleInjector.ts +7 -3
- package/lib/server/errors.ts +65 -0
- package/lib/server/ssr/attributeBuilder.ts +78 -0
- package/lib/server/ssr/cmsSSRProcessor.ts +100 -0
- package/lib/server/ssr/cssCollector.ts +33 -0
- package/lib/server/ssr/htmlGenerator.ts +147 -0
- package/lib/server/ssr/imageMetadata.ts +117 -0
- package/lib/server/ssr/index.ts +33 -0
- package/lib/server/ssr/jsCollector.ts +89 -0
- package/lib/server/ssr/metaTagGenerator.ts +106 -0
- package/lib/server/ssr/ssrRenderer.ts +991 -0
- package/lib/server/ssrRenderer.ts +7 -1491
- package/lib/shared/errorLogger.test.ts +128 -0
- package/lib/shared/errorLogger.ts +87 -0
- package/lib/shared/errors.test.ts +99 -0
- package/lib/shared/errors.ts +50 -0
- package/lib/shared/index.ts +4 -0
- package/lib/shared/propResolver.test.ts +108 -3
- package/lib/shared/propResolver.ts +8 -6
- package/lib/shared/validation/propValidator.test.ts +40 -0
- package/lib/shared/validation/propValidator.ts +6 -1
- package/package.json +1 -1
|
@@ -14,28 +14,37 @@ import { NODE_TYPE } from "../../shared/constants";
|
|
|
14
14
|
import { ErrorBoundary } from "../ErrorBoundary";
|
|
15
15
|
import type { ComponentNode, StyleValue } from "../../shared/types";
|
|
16
16
|
import { isComponentNode, extractNodeProperties, isSlotMarker, isEmbedNode, isObjectLinkNode, isLocaleListNode, isCMSListNode, markAsSlotContent } from "../../shared/nodeUtils";
|
|
17
|
-
import DOMPurify from "isomorphic-dompurify";
|
|
18
17
|
import { mergeNodeStyles } from "../../shared/styleNodeUtils";
|
|
19
18
|
import { extractAttributesFromNode } from "../../shared/attributeNodeUtils";
|
|
20
|
-
import { resolvePropsFromDefinition
|
|
19
|
+
import { resolvePropsFromDefinition } from "../../shared/propResolver";
|
|
21
20
|
import { ElementRegistry } from "../elementRegistry";
|
|
22
21
|
import { InteractiveStylesRegistry } from "../InteractiveStylesRegistry";
|
|
23
22
|
import type { Path } from "../../shared/pathArrayUtils";
|
|
24
23
|
import type { I18nConfig } from "../../shared/types/components";
|
|
25
|
-
import type {
|
|
26
|
-
import {
|
|
27
|
-
import { processItemTemplate, processItemPropsTemplate, hasItemTemplates, buildTemplateContext, resolveItemsTemplate, type ValueResolver } from "../../shared/itemTemplateUtils";
|
|
24
|
+
import type { ItemContext, TemplateContext } from "../../shared/types/cms";
|
|
25
|
+
import { processItemTemplate, processItemPropsTemplate, hasItemTemplates, type ValueResolver } from "../../shared/itemTemplateUtils";
|
|
28
26
|
import { DEFAULT_I18N_CONFIG, resolveI18nValue } from "../../shared/i18n";
|
|
29
27
|
import { getChildPath, pathToString } from "../../shared/pathArrayUtils";
|
|
30
|
-
import { navigateTo } from "../navigation";
|
|
31
28
|
import { responsiveStylesToClasses } from "../../shared/utilityClassMapper";
|
|
32
29
|
import { processCMSTemplate, processCMSPropsTemplate } from "./cmsTemplateProcessor";
|
|
33
30
|
import type { PrefetchService } from "../services/PrefetchService";
|
|
34
|
-
import type { PrefetchStrategy } from "../../shared/types/prefetch";
|
|
35
31
|
import { generateElementClassName, type ElementClassContext } from "../../shared/elementClassName";
|
|
36
32
|
import type { InteractiveStyles } from "../../shared/types/styles";
|
|
37
33
|
import { extractInteractiveStyleMappings, resolveExtractedMappings, hasInteractiveStyleMappings } from "../../shared/interactiveStyleMappings";
|
|
38
34
|
|
|
35
|
+
// Extracted builders
|
|
36
|
+
import {
|
|
37
|
+
type BuildChildrenContext,
|
|
38
|
+
type BuildComponentOptions,
|
|
39
|
+
type BuildResult,
|
|
40
|
+
type BuilderContext,
|
|
41
|
+
buildEmbed,
|
|
42
|
+
buildObjectLink,
|
|
43
|
+
buildLocaleList,
|
|
44
|
+
buildCMSList,
|
|
45
|
+
buildLink,
|
|
46
|
+
} from "./builders";
|
|
47
|
+
|
|
39
48
|
type ComponentProps = Record<string, unknown>;
|
|
40
49
|
|
|
41
50
|
export interface ComponentBuilderConfig {
|
|
@@ -49,54 +58,8 @@ export interface ComponentBuilderConfig {
|
|
|
49
58
|
getCurrentFileType?: () => 'page' | 'component';
|
|
50
59
|
}
|
|
51
60
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
*/
|
|
55
|
-
export interface BuildChildrenContext {
|
|
56
|
-
elementPath: Path;
|
|
57
|
-
parentComponentName: string | null;
|
|
58
|
-
viewportWidth: number;
|
|
59
|
-
componentContext: string | null;
|
|
60
|
-
/** Path where the current component started (for calculating component-relative paths) */
|
|
61
|
-
componentRootPath?: Path;
|
|
62
|
-
locale?: string;
|
|
63
|
-
i18nConfig?: I18nConfig;
|
|
64
|
-
cmsContext?: Record<string, unknown> | null;
|
|
65
|
-
cmsLocale?: string | null;
|
|
66
|
-
collectionItemsMap?: Record<string, CMSItem[]>;
|
|
67
|
-
itemContext?: ItemContext | null;
|
|
68
|
-
cmsItemIndexPath?: number[] | null;
|
|
69
|
-
templateContext?: TemplateContext | null;
|
|
70
|
-
/** Resolved component props for interactive style mapping resolution */
|
|
71
|
-
componentResolvedProps?: Record<string, unknown> | null;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export interface BuildComponentOptions {
|
|
75
|
-
node: ComponentNode | ComponentNode[] | string | number | null | undefined;
|
|
76
|
-
key?: number;
|
|
77
|
-
customProps?: ComponentProps;
|
|
78
|
-
elementPath?: Path;
|
|
79
|
-
parentComponentName?: string | null;
|
|
80
|
-
viewportWidth?: number;
|
|
81
|
-
componentContext?: string | null;
|
|
82
|
-
/** Path where the current component started (for calculating component-relative paths) */
|
|
83
|
-
componentRootPath?: Path;
|
|
84
|
-
locale?: string;
|
|
85
|
-
i18nConfig?: I18nConfig;
|
|
86
|
-
cmsContext?: Record<string, unknown> | null;
|
|
87
|
-
/** CMS locale for i18n field resolution */
|
|
88
|
-
cmsLocale?: string | null;
|
|
89
|
-
/** Map of collection name to items for CMS List rendering */
|
|
90
|
-
collectionItemsMap?: Record<string, CMSItem[]>;
|
|
91
|
-
/** Item context for rendering inside CMS List (contains current item data) */
|
|
92
|
-
itemContext?: ItemContext | null;
|
|
93
|
-
/** Template context with named variables for nested CMS lists */
|
|
94
|
-
templateContext?: TemplateContext | null;
|
|
95
|
-
/** CMS item index path for elements inside CMS List(s) - supports nested lists (e.g., [0, 2] for outer item 0, inner item 2) */
|
|
96
|
-
cmsItemIndexPath?: number[] | null;
|
|
97
|
-
/** Resolved component props for interactive style mapping resolution */
|
|
98
|
-
componentResolvedProps?: Record<string, unknown> | null;
|
|
99
|
-
}
|
|
61
|
+
// Re-export types for backward compatibility
|
|
62
|
+
export type { BuildChildrenContext, BuildComponentOptions };
|
|
100
63
|
|
|
101
64
|
/**
|
|
102
65
|
* ComponentBuilder class for building React elements from component tree nodes
|
|
@@ -141,7 +104,6 @@ export class ComponentBuilder {
|
|
|
141
104
|
|
|
142
105
|
/**
|
|
143
106
|
* Determines the parent component name for nested component instances.
|
|
144
|
-
* Priority: componentContext (if inside component definition) > parentComponentName (if inside another instance) > componentName (fallback for page-level components)
|
|
145
107
|
*/
|
|
146
108
|
private getParentComponentNameForNestedComponent(
|
|
147
109
|
componentContext: string | null,
|
|
@@ -153,7 +115,6 @@ export class ComponentBuilder {
|
|
|
153
115
|
|
|
154
116
|
/**
|
|
155
117
|
* Determines the effective parent component name when building children.
|
|
156
|
-
* Uses componentContext if available, otherwise falls back to parentComponentName.
|
|
157
118
|
*/
|
|
158
119
|
private getEffectiveParentComponentName(
|
|
159
120
|
componentContext: string | null,
|
|
@@ -164,12 +125,9 @@ export class ComponentBuilder {
|
|
|
164
125
|
|
|
165
126
|
/**
|
|
166
127
|
* Find the path to the slot marker in a component structure
|
|
167
|
-
* This helps determine where children should be placed and what paths they should have
|
|
168
128
|
*/
|
|
169
129
|
private findSlotMarkerPath(structure: ComponentNode | undefined, currentPath: Path = [0]): Path | null {
|
|
170
130
|
if (!structure) return null;
|
|
171
|
-
|
|
172
|
-
// Check if this node has children (SlotMarker doesn't have children)
|
|
173
131
|
if (isSlotMarker(structure)) return null;
|
|
174
132
|
|
|
175
133
|
const nodeChildren = (structure as any).children;
|
|
@@ -177,13 +135,9 @@ export class ComponentBuilder {
|
|
|
177
135
|
|
|
178
136
|
for (let i = 0; i < nodeChildren.length; i++) {
|
|
179
137
|
const child = nodeChildren[i];
|
|
180
|
-
|
|
181
|
-
// Check if this is a slot marker
|
|
182
138
|
if (isSlotMarker(child)) {
|
|
183
139
|
return getChildPath(currentPath, i);
|
|
184
140
|
}
|
|
185
|
-
|
|
186
|
-
// Recursively search in nested structures
|
|
187
141
|
if (typeof child === 'object' && child !== null && !Array.isArray(child) && 'type' in child) {
|
|
188
142
|
const nestedPath = this.findSlotMarkerPath(child as ComponentNode, getChildPath(currentPath, i));
|
|
189
143
|
if (nestedPath) return nestedPath;
|
|
@@ -254,9 +208,8 @@ export class ComponentBuilder {
|
|
|
254
208
|
|
|
255
209
|
/**
|
|
256
210
|
* Component builder from JSON tree with support for custom components
|
|
257
|
-
* viewportWidth is passed down to resolve responsive styles based on actual viewport
|
|
258
211
|
*/
|
|
259
|
-
buildComponent(options: BuildComponentOptions):
|
|
212
|
+
buildComponent(options: BuildComponentOptions): BuildResult {
|
|
260
213
|
const {
|
|
261
214
|
node,
|
|
262
215
|
key = 0,
|
|
@@ -279,797 +232,436 @@ export class ComponentBuilder {
|
|
|
279
232
|
|
|
280
233
|
if (!node) return null;
|
|
281
234
|
|
|
282
|
-
//
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
result = processCMSTemplate(result, cmsContext, cmsLocale || locale);
|
|
289
|
-
}
|
|
235
|
+
// Build context for specialized builders
|
|
236
|
+
const ctx: BuilderContext = {
|
|
237
|
+
key, elementPath, parentComponentName, viewportWidth, componentContext, componentRootPath,
|
|
238
|
+
locale, i18nConfig, cmsContext, cmsLocale, collectionItemsMap,
|
|
239
|
+
itemContext, cmsItemIndexPath, templateContext, componentResolvedProps
|
|
240
|
+
};
|
|
290
241
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
? (value: unknown) => resolveI18nValue(value, effectiveLocale, config)
|
|
300
|
-
: undefined;
|
|
301
|
-
result = processItemTemplate(result, effectiveTemplateContext, i18nResolver);
|
|
302
|
-
}
|
|
242
|
+
// Dependencies for specialized builders
|
|
243
|
+
const builderDeps = {
|
|
244
|
+
elementRegistry: this.elementRegistry,
|
|
245
|
+
prefetchService: this.prefetchService,
|
|
246
|
+
getCachedStyleClasses: this.getCachedStyleClasses.bind(this),
|
|
247
|
+
buildChildren: this.buildChildren.bind(this),
|
|
248
|
+
getEffectiveParentComponentName: this.getEffectiveParentComponentName.bind(this),
|
|
249
|
+
};
|
|
303
250
|
|
|
304
|
-
|
|
251
|
+
// Handle text nodes - process CMS templates and item templates
|
|
252
|
+
if (typeof node === 'string') {
|
|
253
|
+
return this.processTextNode(node, ctx);
|
|
305
254
|
}
|
|
306
255
|
|
|
307
256
|
if (typeof node === 'number') {
|
|
308
257
|
return node;
|
|
309
258
|
}
|
|
310
|
-
|
|
259
|
+
|
|
311
260
|
if (Array.isArray(node)) {
|
|
312
|
-
|
|
313
|
-
const childPath = getChildPath(elementPath, index);
|
|
314
|
-
return this.buildComponent({
|
|
315
|
-
node: child,
|
|
316
|
-
key: index,
|
|
317
|
-
customProps,
|
|
318
|
-
elementPath: childPath,
|
|
319
|
-
parentComponentName,
|
|
320
|
-
viewportWidth,
|
|
321
|
-
componentContext,
|
|
322
|
-
locale,
|
|
323
|
-
i18nConfig,
|
|
324
|
-
cmsContext,
|
|
325
|
-
cmsLocale,
|
|
326
|
-
collectionItemsMap,
|
|
327
|
-
itemContext,
|
|
328
|
-
cmsItemIndexPath,
|
|
329
|
-
templateContext,
|
|
330
|
-
componentResolvedProps
|
|
331
|
-
});
|
|
332
|
-
}).filter(
|
|
333
|
-
(item): item is ReactElement | string | number => item !== null
|
|
334
|
-
);
|
|
335
|
-
return result as ReactElement[];
|
|
261
|
+
return this.buildNodeArray(node, options);
|
|
336
262
|
}
|
|
337
|
-
|
|
338
|
-
//
|
|
263
|
+
|
|
264
|
+
// Get node type
|
|
339
265
|
const nodeType = typeof node === 'object' && node !== null && 'type' in node ? node.type : undefined;
|
|
340
266
|
const children = typeof node === 'object' && node !== null && 'children' in node ? (node.children || []) : [];
|
|
341
267
|
|
|
342
|
-
// Handle
|
|
268
|
+
// Handle specialized node types using extracted builders
|
|
343
269
|
if (nodeType === NODE_TYPE.EMBED && isEmbedNode(node)) {
|
|
344
|
-
|
|
345
|
-
// Script tags and event handlers are still removed for security
|
|
346
|
-
const sanitizedHtml = DOMPurify.sanitize(node.html, {
|
|
347
|
-
ALLOWED_TAGS: ['svg', 'path', 'circle', 'rect', 'line', 'polyline', 'polygon', 'g', 'text', 'tspan', 'image', 'defs', 'use', 'linearGradient', 'radialGradient', 'stop', 'clipPath', 'mask', 'pattern', 'marker', 'symbol', 'a', 'div', 'span', 'p', 'br', 'button', 'img', 'iframe', 'video', 'audio', 'source', 'canvas', 'b', 'i', 'u', 'strong', 'em', 'sub', 'sup', 'mark', 's', 'small', 'del', 'ins', 'q', 'abbr', 'code', 'pre', 'blockquote', 'ul', 'ol', 'li', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'],
|
|
348
|
-
ALLOWED_ATTR: ['class', 'id', 'style', 'width', 'height', 'viewBox', 'xmlns', 'fill', 'stroke', 'stroke-width', 'stroke-linecap', 'stroke-linejoin', 'stroke-dasharray', 'stroke-dashoffset', 'd', 'cx', 'cy', 'r', 'x', 'y', 'x1', 'y1', 'x2', 'y2', 'points', 'href', 'src', 'alt', 'target', 'rel', 'data-*', 'aria-*', 'transform', 'opacity', 'fill-opacity', 'stroke-opacity', 'font-size', 'font-family', 'text-anchor', 'dominant-baseline', 'offset', 'stop-color', 'stop-opacity'],
|
|
349
|
-
KEEP_CONTENT: true
|
|
350
|
-
});
|
|
351
|
-
const effectiveParentComponentName = this.getEffectiveParentComponentName(componentContext, parentComponentName);
|
|
352
|
-
|
|
353
|
-
// Extract attributes from node
|
|
354
|
-
const extractedAttributes = extractAttributesFromNode(node);
|
|
355
|
-
|
|
356
|
-
// Build embed node props with all features
|
|
357
|
-
const embedProps: Record<string, any> = {
|
|
358
|
-
key,
|
|
359
|
-
'data-element-path': pathToString(elementPath),
|
|
360
|
-
'data-embed-node': 'true',
|
|
361
|
-
ref: (el: HTMLElement | null) => this.elementRegistry.register(elementPath, el, effectiveParentComponentName, false),
|
|
362
|
-
dangerouslySetInnerHTML: { __html: sanitizedHtml }
|
|
363
|
-
};
|
|
364
|
-
|
|
365
|
-
// Add CMS item index path for elements inside CMS lists
|
|
366
|
-
if (cmsItemIndexPath && cmsItemIndexPath.length > 0) {
|
|
367
|
-
embedProps['data-cms-item-index'] = cmsItemIndexPath.join('.');
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
// Convert embed styles to utility classes
|
|
371
|
-
if (node.style) {
|
|
372
|
-
const utilityClasses = responsiveStylesToClasses(node.style);
|
|
373
|
-
if (utilityClasses.length > 0) {
|
|
374
|
-
const existingClassName = (extractedAttributes.className || '') as string;
|
|
375
|
-
const classArray = existingClassName ? existingClassName.split(/\s+/) : [];
|
|
376
|
-
embedProps.className = [...classArray, ...utilityClasses].filter(Boolean).join(' ');
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
// Add extracted attributes (like class, id, data-*, aria-*, etc.)
|
|
381
|
-
if (Object.keys(extractedAttributes).length > 0) {
|
|
382
|
-
Object.assign(embedProps, extractedAttributes);
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
// Add parent component context
|
|
386
|
-
if (effectiveParentComponentName) {
|
|
387
|
-
embedProps['data-parent-component'] = effectiveParentComponentName;
|
|
388
|
-
}
|
|
389
|
-
if (componentContext) {
|
|
390
|
-
embedProps['data-component-context'] = componentContext;
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
// Use span for inline embeds (rich-text content), div for block embeds (SVG, etc.)
|
|
394
|
-
const embedTag = (node as any).inline ? 'span' : 'div';
|
|
395
|
-
return h(embedTag, embedProps);
|
|
270
|
+
return buildEmbed(node, ctx, builderDeps);
|
|
396
271
|
}
|
|
397
272
|
|
|
398
|
-
// Handle object link nodes (render as div in editor)
|
|
399
273
|
if (nodeType === NODE_TYPE.OBJECT_LINK && isObjectLinkNode(node)) {
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
// Extract attributes
|
|
403
|
-
const extractedAttributes = extractAttributesFromNode(node);
|
|
404
|
-
|
|
405
|
-
// Build object link props (renders as div in editor)
|
|
406
|
-
const objectLinkProps: Record<string, any> = {
|
|
407
|
-
key,
|
|
408
|
-
'data-element-path': pathToString(elementPath),
|
|
409
|
-
'data-object-link-node': 'true',
|
|
410
|
-
ref: (el: HTMLElement | null) => this.elementRegistry.register(elementPath, el, effectiveParentComponentName, false)
|
|
411
|
-
};
|
|
412
|
-
|
|
413
|
-
// Add CMS item index path for elements inside CMS lists
|
|
414
|
-
if (cmsItemIndexPath && cmsItemIndexPath.length > 0) {
|
|
415
|
-
objectLinkProps['data-cms-item-index'] = cmsItemIndexPath.join('.');
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
// Convert object link styles to utility classes
|
|
419
|
-
if (node.style) {
|
|
420
|
-
const utilityClasses = responsiveStylesToClasses(node.style);
|
|
421
|
-
if (utilityClasses.length > 0) {
|
|
422
|
-
const existingClassName = (extractedAttributes.className || '') as string;
|
|
423
|
-
const classArray = existingClassName ? existingClassName.split(/\s+/) : [];
|
|
424
|
-
objectLinkProps.className = [...classArray, ...utilityClasses].filter(Boolean).join(' ');
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
// Add extracted attributes (like class, id, data-*, aria-*, etc.)
|
|
429
|
-
if (Object.keys(extractedAttributes).length > 0) {
|
|
430
|
-
Object.assign(objectLinkProps, extractedAttributes);
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
// Add parent component context
|
|
434
|
-
if (effectiveParentComponentName) {
|
|
435
|
-
objectLinkProps['data-parent-component'] = effectiveParentComponentName;
|
|
436
|
-
}
|
|
437
|
-
if (componentContext) {
|
|
438
|
-
objectLinkProps['data-component-context'] = componentContext;
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
// Build children recursively
|
|
442
|
-
const objectLinkChildren = this.buildChildren(children, {
|
|
443
|
-
elementPath, parentComponentName: effectiveParentComponentName, viewportWidth, componentContext, componentRootPath,
|
|
444
|
-
locale, i18nConfig, cmsContext, cmsLocale, collectionItemsMap, itemContext, cmsItemIndexPath, templateContext, componentResolvedProps
|
|
445
|
-
});
|
|
446
|
-
|
|
447
|
-
return h('div', objectLinkProps, objectLinkChildren);
|
|
274
|
+
return buildObjectLink(node, children, ctx, builderDeps);
|
|
448
275
|
}
|
|
449
276
|
|
|
450
|
-
// Handle locale-list nodes (render placeholder in editor)
|
|
451
277
|
if (nodeType === NODE_TYPE.LOCALE_LIST && isLocaleListNode(node)) {
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
// Extract attributes
|
|
455
|
-
const extractedAttributes = extractAttributesFromNode(node);
|
|
456
|
-
|
|
457
|
-
// Build locale list props
|
|
458
|
-
const localeListProps: Record<string, any> = {
|
|
459
|
-
key,
|
|
460
|
-
'data-element-path': pathToString(elementPath),
|
|
461
|
-
'data-locale-list': 'true',
|
|
462
|
-
ref: (el: HTMLElement | null) => this.elementRegistry.register(elementPath, el, effectiveParentComponentName, false)
|
|
463
|
-
};
|
|
464
|
-
|
|
465
|
-
// Add CMS item index path for elements inside CMS lists
|
|
466
|
-
if (cmsItemIndexPath && cmsItemIndexPath.length > 0) {
|
|
467
|
-
localeListProps['data-cms-item-index'] = cmsItemIndexPath.join('.');
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
// Convert container styles to utility classes
|
|
471
|
-
if (node.style) {
|
|
472
|
-
const utilityClasses = responsiveStylesToClasses(node.style);
|
|
473
|
-
if (utilityClasses.length > 0) {
|
|
474
|
-
const existingClassName = (extractedAttributes.className || '') as string;
|
|
475
|
-
const classArray = existingClassName ? existingClassName.split(/\s+/) : [];
|
|
476
|
-
localeListProps.className = [...classArray, ...utilityClasses].filter(Boolean).join(' ');
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
// Add extracted attributes
|
|
481
|
-
if (Object.keys(extractedAttributes).length > 0) {
|
|
482
|
-
Object.assign(localeListProps, extractedAttributes);
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
// Add parent component context
|
|
486
|
-
if (effectiveParentComponentName) {
|
|
487
|
-
localeListProps['data-parent-component'] = effectiveParentComponentName;
|
|
488
|
-
}
|
|
489
|
-
if (componentContext) {
|
|
490
|
-
localeListProps['data-component-context'] = componentContext;
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
// Build locale links for editor preview using actual i18n config
|
|
494
|
-
const showCurrent = node.showCurrent !== false;
|
|
495
|
-
const showSeparator = node.showSeparator !== false;
|
|
496
|
-
const showFlag = node.showFlag !== false;
|
|
497
|
-
// Use actual locales from i18nConfig (LocaleConfig[])
|
|
498
|
-
const configLocales = i18nConfig?.locales || [];
|
|
499
|
-
const currentLocaleCode = locale || i18nConfig?.defaultLocale || 'en';
|
|
500
|
-
|
|
501
|
-
// Convert item styles, active item styles, separator styles, and flag styles to utility classes
|
|
502
|
-
const itemClasses = node.itemStyle ? responsiveStylesToClasses(node.itemStyle) : [];
|
|
503
|
-
const activeItemClasses = node.activeItemStyle ? responsiveStylesToClasses(node.activeItemStyle) : [];
|
|
504
|
-
const separatorClasses = node.separatorStyle ? responsiveStylesToClasses(node.separatorStyle) : [];
|
|
505
|
-
const flagClasses = node.flagStyle ? responsiveStylesToClasses(node.flagStyle) : [];
|
|
506
|
-
|
|
507
|
-
// Build locale links from config
|
|
508
|
-
const linkElements: ReactElement[] = [];
|
|
509
|
-
for (let i = 0; i < configLocales.length; i++) {
|
|
510
|
-
const localeConfig = configLocales[i];
|
|
511
|
-
const isCurrent = localeConfig.code === currentLocaleCode;
|
|
512
|
-
if (!showCurrent && isCurrent) continue;
|
|
513
|
-
|
|
514
|
-
// Add separator between links (empty span styled via separatorStyle)
|
|
515
|
-
if (showSeparator && linkElements.length > 0) {
|
|
516
|
-
linkElements.push(h('span', {
|
|
517
|
-
key: `sep-${i}`,
|
|
518
|
-
className: separatorClasses.length > 0 ? separatorClasses.join(' ') : undefined
|
|
519
|
-
}));
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
// Build link element content - flag icon (if enabled and exists) + nativeName in span
|
|
523
|
-
const linkContent: (ReactElement | string)[] = [];
|
|
524
|
-
if (showFlag && localeConfig.icon) {
|
|
525
|
-
linkContent.push(h('img', {
|
|
526
|
-
key: 'flag',
|
|
527
|
-
src: localeConfig.icon,
|
|
528
|
-
alt: `${localeConfig.name} flag`,
|
|
529
|
-
className: flagClasses.length > 0 ? flagClasses.join(' ') : undefined
|
|
530
|
-
}));
|
|
531
|
-
}
|
|
532
|
-
// Get display text based on displayType (mirrors SSR logic)
|
|
533
|
-
const displayType = node.displayType || 'nativeName';
|
|
534
|
-
let displayText: string;
|
|
535
|
-
switch (displayType) {
|
|
536
|
-
case 'code':
|
|
537
|
-
displayText = localeConfig.code.toUpperCase();
|
|
538
|
-
break;
|
|
539
|
-
case 'name':
|
|
540
|
-
displayText = localeConfig.name;
|
|
541
|
-
break;
|
|
542
|
-
case 'nativeName':
|
|
543
|
-
default:
|
|
544
|
-
displayText = localeConfig.nativeName;
|
|
545
|
-
break;
|
|
546
|
-
}
|
|
547
|
-
linkContent.push(h('div', { key: 'text' }, displayText));
|
|
548
|
-
|
|
549
|
-
// Current item gets both itemClasses + activeItemClasses (additive/override)
|
|
550
|
-
const linkClasses = isCurrent ? [...itemClasses, ...activeItemClasses] : itemClasses;
|
|
551
|
-
linkElements.push(h('div', {
|
|
552
|
-
key: `locale-${localeConfig.code}`,
|
|
553
|
-
'data-current': isCurrent ? 'true' : 'false',
|
|
554
|
-
'data-locale': localeConfig.code,
|
|
555
|
-
className: linkClasses.length > 0 ? linkClasses.join(' ') : undefined,
|
|
556
|
-
style: { cursor: 'pointer' }
|
|
557
|
-
}, ...linkContent));
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
return h('div', localeListProps, linkElements);
|
|
278
|
+
return buildLocaleList(node, ctx, builderDeps);
|
|
561
279
|
}
|
|
562
280
|
|
|
563
|
-
// Handle cms-list nodes (render children for each CMS item)
|
|
564
281
|
if (nodeType === NODE_TYPE.CMS_LIST && isCMSListNode(node)) {
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
// Extract attributes
|
|
568
|
-
const extractedAttributes = extractAttributesFromNode(node);
|
|
282
|
+
return buildCMSList(node, children, ctx, builderDeps);
|
|
283
|
+
}
|
|
569
284
|
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
key,
|
|
573
|
-
'data-element-path': pathToString(elementPath),
|
|
574
|
-
'data-cms-list': 'true',
|
|
575
|
-
'data-collection': node.collection || '',
|
|
576
|
-
ref: (el: HTMLElement | null) => this.elementRegistry.register(elementPath, el, effectiveParentComponentName, false)
|
|
577
|
-
};
|
|
285
|
+
// Extract node properties
|
|
286
|
+
const { tag, componentName, props: nodeProps } = extractNodeProperties(node);
|
|
578
287
|
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
const utilityClasses = this.getCachedStyleClasses(node.style);
|
|
582
|
-
if (utilityClasses.length > 0) {
|
|
583
|
-
const existingClassName = (extractedAttributes.className || '') as string;
|
|
584
|
-
const classArray = existingClassName ? existingClassName.split(/\s+/) : [];
|
|
585
|
-
cmsListProps.className = [...classArray, ...utilityClasses].filter(Boolean).join(' ');
|
|
586
|
-
}
|
|
587
|
-
}
|
|
288
|
+
// Filter internal props
|
|
289
|
+
const props = this.filterInternalProps(nodeProps, tag);
|
|
588
290
|
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
Object.assign(cmsListProps, extractedAttributes);
|
|
592
|
-
}
|
|
291
|
+
// Process templates in props
|
|
292
|
+
const processedProps = this.processPropsTemplates(props, ctx);
|
|
593
293
|
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
cmsListProps['data-parent-component'] = effectiveParentComponentName;
|
|
597
|
-
}
|
|
598
|
-
if (componentContext) {
|
|
599
|
-
cmsListProps['data-component-context'] = componentContext;
|
|
600
|
-
}
|
|
294
|
+
// Convert styles to utility classes
|
|
295
|
+
const propsWithClasses = this.applyStyleClasses(processedProps, node);
|
|
601
296
|
|
|
602
|
-
|
|
603
|
-
|
|
297
|
+
// Apply interactive styles
|
|
298
|
+
const propsWithInteractive = this.applyInteractiveStyles(propsWithClasses, node, ctx);
|
|
604
299
|
|
|
605
|
-
|
|
606
|
-
|
|
300
|
+
// Extract and merge attributes
|
|
301
|
+
const finalProps = this.mergeAttributes(propsWithInteractive, node, ctx);
|
|
607
302
|
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
return ids
|
|
613
|
-
.filter(Boolean)
|
|
614
|
-
.map(id => itemMap.get(id) || filenameMap.get(id))
|
|
615
|
-
.filter((item): item is CMSItem => item !== undefined);
|
|
616
|
-
};
|
|
303
|
+
// Handle custom component
|
|
304
|
+
if (nodeType === NODE_TYPE.COMPONENT && componentName && this.componentRegistry.has(componentName)) {
|
|
305
|
+
return this.buildCustomComponent(componentName, nodeProps, children, finalProps, options);
|
|
306
|
+
}
|
|
617
307
|
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
// If items is a template expression, resolve it from current context
|
|
623
|
-
if (typeof node.items === 'string' && node.items.startsWith('{{')) {
|
|
624
|
-
const effectiveTemplateContext = templateContext || {};
|
|
625
|
-
const resolvedIds = resolveItemsTemplate(node.items, effectiveTemplateContext);
|
|
626
|
-
if (resolvedIds) {
|
|
627
|
-
const ids = Array.isArray(resolvedIds) ? resolvedIds : [resolvedIds];
|
|
628
|
-
itemsToRender = lookupItemsByIds(ids, collectionItems);
|
|
629
|
-
}
|
|
630
|
-
} else {
|
|
631
|
-
// Direct IDs (string or array)
|
|
632
|
-
const ids = Array.isArray(node.items) ? node.items : [node.items];
|
|
633
|
-
itemsToRender = lookupItemsByIds(ids, collectionItems);
|
|
634
|
-
}
|
|
635
|
-
} else {
|
|
636
|
-
// Get items for this collection from the map
|
|
637
|
-
const collectionItems = node.collection ? (collectionItemsMap[node.collection] || []) : [];
|
|
308
|
+
// Handle Link component
|
|
309
|
+
if (tag === 'Link') {
|
|
310
|
+
return buildLink(finalProps, children, ctx, builderDeps);
|
|
311
|
+
}
|
|
638
312
|
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
itemsToRender = itemsToRender.slice(node.offset);
|
|
643
|
-
}
|
|
644
|
-
if (node.limit) {
|
|
645
|
-
itemsToRender = itemsToRender.slice(0, node.limit);
|
|
646
|
-
}
|
|
647
|
-
}
|
|
313
|
+
// Build regular HTML element
|
|
314
|
+
return this.buildHtmlElement(tag, finalProps, children, customProps, options);
|
|
315
|
+
}
|
|
648
316
|
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
padding: '12px',
|
|
655
|
-
background: 'rgba(139, 92, 246, 0.05)',
|
|
656
|
-
border: '1px dashed rgba(139, 92, 246, 0.3)',
|
|
657
|
-
borderRadius: '4px',
|
|
658
|
-
color: '#8b5cf6',
|
|
659
|
-
fontSize: '12px',
|
|
660
|
-
fontFamily: 'system-ui, sans-serif',
|
|
661
|
-
textAlign: 'center'
|
|
662
|
-
}
|
|
663
|
-
}, `CMS List: ${node.collection || 'No collection'} - No items`);
|
|
664
|
-
return h('div', cmsListProps, emptyState);
|
|
665
|
-
}
|
|
317
|
+
/**
|
|
318
|
+
* Process text node with CMS and item templates
|
|
319
|
+
*/
|
|
320
|
+
private processTextNode(text: string, ctx: BuilderContext): string {
|
|
321
|
+
let result = text;
|
|
666
322
|
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
// Build template context with named variable (preserves parent context for nested lists)
|
|
672
|
-
const newTemplateContext = buildTemplateContext(
|
|
673
|
-
variableName,
|
|
674
|
-
item,
|
|
675
|
-
index,
|
|
676
|
-
itemsToRender.length,
|
|
677
|
-
templateContext || undefined
|
|
678
|
-
);
|
|
679
|
-
|
|
680
|
-
// Build children with templateContext (contains both named and legacy context)
|
|
681
|
-
// Append current index to existing path to support nested CMS lists
|
|
682
|
-
const newIndexPath = [...(cmsItemIndexPath || []), index];
|
|
683
|
-
const itemChildren = this.buildChildren(children, {
|
|
684
|
-
elementPath, parentComponentName: effectiveParentComponentName, viewportWidth, componentContext, componentRootPath,
|
|
685
|
-
locale, i18nConfig, cmsContext, cmsLocale, collectionItemsMap,
|
|
686
|
-
itemContext: null, // itemContext no longer needed - templateContext has all data
|
|
687
|
-
cmsItemIndexPath: newIndexPath, // Pass updated path - will be applied to all descendants via ref callback
|
|
688
|
-
templateContext: newTemplateContext, // Pass named template context for nested lists
|
|
689
|
-
componentResolvedProps
|
|
690
|
-
});
|
|
691
|
-
|
|
692
|
-
// Add children directly (no wrapper div)
|
|
693
|
-
// Each child element will have data-cms-item-index attribute via ref callback
|
|
694
|
-
if (itemChildren === null) continue;
|
|
695
|
-
if (Array.isArray(itemChildren)) {
|
|
696
|
-
// Add unique keys by combining original key with item index
|
|
697
|
-
for (const child of itemChildren) {
|
|
698
|
-
if (typeof child === 'object' && child !== null && 'key' in child) {
|
|
699
|
-
renderedItems.push({ ...child, key: `${child.key}-item-${index}` } as ReactElement);
|
|
700
|
-
} else {
|
|
701
|
-
renderedItems.push(child);
|
|
702
|
-
}
|
|
703
|
-
}
|
|
704
|
-
} else if (typeof itemChildren === 'object' && itemChildren !== null && 'key' in itemChildren) {
|
|
705
|
-
// Single child - ensure unique key
|
|
706
|
-
renderedItems.push({ ...itemChildren, key: `${(itemChildren as ReactElement).key}-item-${index}` } as ReactElement);
|
|
707
|
-
} else {
|
|
708
|
-
renderedItems.push(itemChildren);
|
|
709
|
-
}
|
|
710
|
-
}
|
|
323
|
+
// Process CMS templates
|
|
324
|
+
if (ctx.cmsContext && result.includes('{{cms.')) {
|
|
325
|
+
result = processCMSTemplate(result, ctx.cmsContext, ctx.cmsLocale || ctx.locale);
|
|
326
|
+
}
|
|
711
327
|
|
|
712
|
-
|
|
328
|
+
// Process item templates
|
|
329
|
+
const effectiveTemplateContext = ctx.templateContext || ctx.itemContext;
|
|
330
|
+
if (effectiveTemplateContext && hasItemTemplates(result)) {
|
|
331
|
+
const effectiveLocale = ctx.cmsLocale || ctx.locale;
|
|
332
|
+
const config = ctx.i18nConfig || DEFAULT_I18N_CONFIG;
|
|
333
|
+
const i18nResolver: ValueResolver | undefined = effectiveLocale
|
|
334
|
+
? (value: unknown) => resolveI18nValue(value, effectiveLocale, config)
|
|
335
|
+
: undefined;
|
|
336
|
+
result = processItemTemplate(result, effectiveTemplateContext, i18nResolver);
|
|
713
337
|
}
|
|
714
338
|
|
|
715
|
-
|
|
716
|
-
|
|
339
|
+
return result;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Build array of nodes
|
|
344
|
+
*/
|
|
345
|
+
private buildNodeArray(nodes: ComponentNode[], options: BuildComponentOptions): ReactElement[] {
|
|
346
|
+
const {
|
|
347
|
+
customProps, elementPath = [0], parentComponentName = null, viewportWidth = 1920,
|
|
348
|
+
componentContext = null, locale, i18nConfig, cmsContext = null, cmsLocale = null,
|
|
349
|
+
collectionItemsMap = {}, itemContext = null, cmsItemIndexPath = null,
|
|
350
|
+
templateContext = null, componentResolvedProps = null
|
|
351
|
+
} = options;
|
|
717
352
|
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
353
|
+
return nodes.map((child, index) => {
|
|
354
|
+
const childPath = getChildPath(elementPath, index);
|
|
355
|
+
return this.buildComponent({
|
|
356
|
+
node: child,
|
|
357
|
+
key: index,
|
|
358
|
+
customProps,
|
|
359
|
+
elementPath: childPath,
|
|
360
|
+
parentComponentName,
|
|
361
|
+
viewportWidth,
|
|
362
|
+
componentContext,
|
|
363
|
+
locale,
|
|
364
|
+
i18nConfig,
|
|
365
|
+
cmsContext,
|
|
366
|
+
cmsLocale,
|
|
367
|
+
collectionItemsMap,
|
|
368
|
+
itemContext,
|
|
369
|
+
cmsItemIndexPath,
|
|
370
|
+
templateContext,
|
|
371
|
+
componentResolvedProps
|
|
372
|
+
});
|
|
373
|
+
}).filter((item): item is ReactElement => item !== null && typeof item === 'object');
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Filter internal props from node props
|
|
378
|
+
*/
|
|
379
|
+
private filterInternalProps(nodeProps: Record<string, unknown>, tag: string | undefined): Record<string, unknown> {
|
|
721
380
|
const imageOnlyProps = ['src', 'alt', 'loading', 'width', 'height', 'sizes', 'srcset'];
|
|
722
381
|
const internalProps = ['type', 'tag', 'component', 'props', 'children', 'html', 'style', ...imageOnlyProps];
|
|
723
|
-
|
|
382
|
+
const props: Record<string, unknown> = {};
|
|
383
|
+
|
|
724
384
|
for (const [key, value] of Object.entries(nodeProps)) {
|
|
725
385
|
if (internalProps.includes(key)) {
|
|
726
|
-
// Keep image properties if this is an img tag
|
|
727
386
|
if (tag === 'img' && imageOnlyProps.includes(key)) {
|
|
728
387
|
props[key] = value;
|
|
729
388
|
}
|
|
730
|
-
// else skip - it's an internal prop
|
|
731
389
|
} else {
|
|
732
390
|
props[key] = value;
|
|
733
391
|
}
|
|
734
392
|
}
|
|
735
393
|
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
394
|
+
return props;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Process CMS and item templates in props
|
|
399
|
+
*/
|
|
400
|
+
private processPropsTemplates(props: Record<string, unknown>, ctx: BuilderContext): Record<string, unknown> {
|
|
401
|
+
let result = props;
|
|
402
|
+
|
|
403
|
+
// Process CMS templates
|
|
404
|
+
if (ctx.cmsContext && Object.keys(result).length > 0) {
|
|
405
|
+
result = processCMSPropsTemplate(result, ctx.cmsContext, ctx.cmsLocale || ctx.locale);
|
|
739
406
|
}
|
|
740
407
|
|
|
741
|
-
// Process item templates
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
const
|
|
746
|
-
const config = i18nConfig || DEFAULT_I18N_CONFIG;
|
|
408
|
+
// Process item templates
|
|
409
|
+
const effectiveItemContext = ctx.templateContext || ctx.itemContext;
|
|
410
|
+
if (effectiveItemContext && Object.keys(result).length > 0) {
|
|
411
|
+
const effectiveLocale = ctx.cmsLocale || ctx.locale;
|
|
412
|
+
const config = ctx.i18nConfig || DEFAULT_I18N_CONFIG;
|
|
747
413
|
const i18nResolver: ValueResolver | undefined = effectiveLocale
|
|
748
414
|
? (value: unknown) => resolveI18nValue(value, effectiveLocale, config)
|
|
749
415
|
: undefined;
|
|
750
|
-
|
|
416
|
+
result = processItemPropsTemplate(result, effectiveItemContext, i18nResolver);
|
|
751
417
|
}
|
|
752
418
|
|
|
753
|
-
|
|
419
|
+
return result;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Apply style classes to props
|
|
424
|
+
*/
|
|
425
|
+
private applyStyleClasses(props: Record<string, unknown>, node: ComponentNode): Record<string, unknown> {
|
|
754
426
|
const nodeStyle = node.style || (isComponentNode(node) ? node.props?.style : undefined);
|
|
755
427
|
|
|
756
428
|
if (nodeStyle && typeof nodeStyle === 'object') {
|
|
757
|
-
// Use cached utility classes when possible
|
|
758
429
|
const utilityClasses = this.getCachedStyleClasses(nodeStyle);
|
|
759
430
|
if (utilityClasses.length > 0) {
|
|
760
431
|
const existingClassName = (props.className || '') as string;
|
|
761
432
|
const classArray = existingClassName ? existingClassName.split(/\s+/) : [];
|
|
762
|
-
props
|
|
433
|
+
return { ...props, className: [...classArray, ...utilityClasses].filter(Boolean).join(' ') };
|
|
763
434
|
}
|
|
764
435
|
}
|
|
765
436
|
|
|
766
|
-
|
|
437
|
+
return props;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Apply interactive styles and generate element class
|
|
442
|
+
*/
|
|
443
|
+
private applyInteractiveStyles(props: Record<string, unknown>, node: ComponentNode, ctx: BuilderContext): Record<string, unknown> {
|
|
767
444
|
const nodeInteractiveStyles = (node as any).interactiveStyles as InteractiveStyles | undefined;
|
|
768
445
|
const nodeGenerateElementClass = (node as any).generateElementClass as boolean | undefined;
|
|
769
446
|
const nodeTreeNodeName = (node as any).treeNodeName as string | undefined;
|
|
770
447
|
|
|
771
|
-
if ((nodeInteractiveStyles && nodeInteractiveStyles.length > 0) || nodeGenerateElementClass) {
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
const isSlotContent = (node as any)._isSlotContent === true;
|
|
775
|
-
|
|
776
|
-
// When inside a component (componentContext is set) and NOT slot content, use component-based class names
|
|
777
|
-
// Slot content uses page context since it's defined in the page, not the component
|
|
778
|
-
const useComponentContext = componentContext && !isSlotContent;
|
|
779
|
-
const fileType = useComponentContext ? 'component' : (this.getCurrentFileType?.() || 'page');
|
|
780
|
-
const fileName = useComponentContext ? componentContext : (this.getCurrentPageName ? this.getCurrentPageName() : 'page');
|
|
781
|
-
// For component elements, use relative path (from component root) for stable class names
|
|
782
|
-
// For slot content and page elements, use full path
|
|
783
|
-
const pathForClass = useComponentContext && componentRootPath
|
|
784
|
-
? elementPath.slice(componentRootPath.length)
|
|
785
|
-
: elementPath;
|
|
786
|
-
const elementClassCtx: ElementClassContext = {
|
|
787
|
-
fileType,
|
|
788
|
-
fileName,
|
|
789
|
-
treeNodeName: nodeTreeNodeName,
|
|
790
|
-
path: pathForClass,
|
|
791
|
-
};
|
|
792
|
-
const elementClass = generateElementClassName(elementClassCtx);
|
|
793
|
-
|
|
794
|
-
// Prepend element class to existing classes for proper CSS specificity
|
|
795
|
-
const existingClassName = (props.className || '') as string;
|
|
796
|
-
props.className = [elementClass, existingClassName].filter(Boolean).join(' ');
|
|
797
|
-
|
|
798
|
-
// Register interactive styles for CSS generation
|
|
799
|
-
// StyleInjector will read from the registry to generate interactive CSS
|
|
800
|
-
if (nodeInteractiveStyles && nodeInteractiveStyles.length > 0) {
|
|
801
|
-
// Check if interactive styles contain mappings that need resolution
|
|
802
|
-
if (componentResolvedProps && hasInteractiveStyleMappings(nodeInteractiveStyles)) {
|
|
803
|
-
// Extract mappings and replace with CSS variable references
|
|
804
|
-
const { resolvedStyles, mappings } = extractInteractiveStyleMappings(nodeInteractiveStyles);
|
|
805
|
-
|
|
806
|
-
// Resolve CSS variable values from component props
|
|
807
|
-
const cssVariables = resolveExtractedMappings(mappings, componentResolvedProps);
|
|
808
|
-
|
|
809
|
-
// Store resolved styles (with var() references) for CSS generation
|
|
810
|
-
InteractiveStylesRegistry.set(elementClass, resolvedStyles);
|
|
448
|
+
if (!((nodeInteractiveStyles && nodeInteractiveStyles.length > 0) || nodeGenerateElementClass)) {
|
|
449
|
+
return props;
|
|
450
|
+
}
|
|
811
451
|
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
452
|
+
// Check if slot content
|
|
453
|
+
const isSlotContent = (node as any)._isSlotContent === true;
|
|
454
|
+
const useComponentContext = ctx.componentContext && !isSlotContent;
|
|
455
|
+
const fileType = useComponentContext ? 'component' : (this.getCurrentFileType?.() || 'page');
|
|
456
|
+
const fileName = useComponentContext ? ctx.componentContext! : (this.getCurrentPageName ? this.getCurrentPageName() : 'page');
|
|
457
|
+
const pathForClass = useComponentContext && ctx.componentRootPath
|
|
458
|
+
? ctx.elementPath.slice(ctx.componentRootPath.length)
|
|
459
|
+
: ctx.elementPath;
|
|
460
|
+
|
|
461
|
+
const elementClassCtx: ElementClassContext = {
|
|
462
|
+
fileType,
|
|
463
|
+
fileName,
|
|
464
|
+
treeNodeName: nodeTreeNodeName,
|
|
465
|
+
path: pathForClass,
|
|
466
|
+
};
|
|
467
|
+
const elementClass = generateElementClassName(elementClassCtx);
|
|
468
|
+
|
|
469
|
+
// Prepend element class
|
|
470
|
+
const existingClassName = (props.className || '') as string;
|
|
471
|
+
const result = { ...props, className: [elementClass, existingClassName].filter(Boolean).join(' ') };
|
|
472
|
+
|
|
473
|
+
// Register interactive styles
|
|
474
|
+
if (nodeInteractiveStyles && nodeInteractiveStyles.length > 0) {
|
|
475
|
+
if (ctx.componentResolvedProps && hasInteractiveStyleMappings(nodeInteractiveStyles)) {
|
|
476
|
+
const { resolvedStyles, mappings } = extractInteractiveStyleMappings(nodeInteractiveStyles);
|
|
477
|
+
const cssVariables = resolveExtractedMappings(mappings, ctx.componentResolvedProps);
|
|
478
|
+
InteractiveStylesRegistry.set(elementClass, resolvedStyles);
|
|
479
|
+
if (Object.keys(cssVariables).length > 0) {
|
|
480
|
+
InteractiveStylesRegistry.setVariables(ctx.elementPath, cssVariables);
|
|
819
481
|
}
|
|
482
|
+
} else {
|
|
483
|
+
InteractiveStylesRegistry.set(elementClass, nodeInteractiveStyles);
|
|
820
484
|
}
|
|
821
485
|
}
|
|
822
486
|
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
487
|
+
return result;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Extract and merge attributes from node
|
|
492
|
+
*/
|
|
493
|
+
private mergeAttributes(props: Record<string, unknown>, node: ComponentNode, ctx: BuilderContext): Record<string, unknown> {
|
|
826
494
|
let extractedAttributes = extractAttributesFromNode(node);
|
|
827
495
|
|
|
828
|
-
// Process item templates in attributes
|
|
829
|
-
|
|
496
|
+
// Process item templates in attributes
|
|
497
|
+
const effectiveItemContext = ctx.templateContext || ctx.itemContext;
|
|
830
498
|
if (effectiveItemContext && Object.keys(extractedAttributes).length > 0) {
|
|
831
|
-
const effectiveLocale = cmsLocale || locale;
|
|
832
|
-
const config = i18nConfig || DEFAULT_I18N_CONFIG;
|
|
499
|
+
const effectiveLocale = ctx.cmsLocale || ctx.locale;
|
|
500
|
+
const config = ctx.i18nConfig || DEFAULT_I18N_CONFIG;
|
|
833
501
|
const i18nResolver: ValueResolver | undefined = effectiveLocale
|
|
834
502
|
? (value: unknown) => resolveI18nValue(value, effectiveLocale, config)
|
|
835
503
|
: undefined;
|
|
836
504
|
extractedAttributes = processItemPropsTemplate(extractedAttributes, effectiveItemContext, i18nResolver);
|
|
837
505
|
}
|
|
838
506
|
|
|
839
|
-
if (Object.keys(extractedAttributes).length
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
const attrClass = (extractedAttributes.class || extractedAttributes.className || '') as string;
|
|
843
|
-
const existingClassName = (props.className || '') as string;
|
|
844
|
-
|
|
845
|
-
// Merge: existing utility classes + attribute classes
|
|
846
|
-
props.className = [existingClassName, attrClass].filter(Boolean).join(' ');
|
|
507
|
+
if (Object.keys(extractedAttributes).length === 0) {
|
|
508
|
+
return props;
|
|
509
|
+
}
|
|
847
510
|
|
|
848
|
-
|
|
849
|
-
delete extractedAttributes.class;
|
|
850
|
-
delete extractedAttributes.className;
|
|
851
|
-
}
|
|
511
|
+
let result = { ...props };
|
|
852
512
|
|
|
853
|
-
|
|
513
|
+
// Handle class/className specially
|
|
514
|
+
if (extractedAttributes.class || extractedAttributes.className) {
|
|
515
|
+
const attrClass = (extractedAttributes.class || extractedAttributes.className || '') as string;
|
|
516
|
+
const existingClassName = (result.className || '') as string;
|
|
517
|
+
result.className = [existingClassName, attrClass].filter(Boolean).join(' ');
|
|
518
|
+
delete extractedAttributes.class;
|
|
519
|
+
delete extractedAttributes.className;
|
|
854
520
|
}
|
|
855
521
|
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
const componentDef = this.componentRegistry.get(componentName);
|
|
859
|
-
|
|
860
|
-
if (!componentDef) return null;
|
|
861
|
-
|
|
862
|
-
try {
|
|
863
|
-
// Extract structured component definition
|
|
864
|
-
const structuredComponentDef = componentDef.component;
|
|
865
|
-
if (!structuredComponentDef) {
|
|
866
|
-
return h(ErrorBoundary, {
|
|
867
|
-
key,
|
|
868
|
-
componentName: componentName,
|
|
869
|
-
level: 'component',
|
|
870
|
-
}, null);
|
|
871
|
-
}
|
|
872
|
-
|
|
873
|
-
// Resolve props with defaults from interface definition (with i18n support)
|
|
874
|
-
// Use nodeProps (original extracted props) instead of filtered props
|
|
875
|
-
// Filtering is for HTML DOM attributes, not component prop templates
|
|
876
|
-
let resolvedProps = resolvePropsFromDefinition(
|
|
877
|
-
structuredComponentDef,
|
|
878
|
-
nodeProps,
|
|
879
|
-
children,
|
|
880
|
-
locale,
|
|
881
|
-
i18nConfig
|
|
882
|
-
);
|
|
883
|
-
|
|
884
|
-
// Process CMS templates in resolved component props
|
|
885
|
-
if (cmsContext && Object.keys(resolvedProps).length > 0) {
|
|
886
|
-
resolvedProps = processCMSPropsTemplate(resolvedProps, cmsContext, cmsLocale || locale);
|
|
887
|
-
}
|
|
522
|
+
return { ...result, ...extractedAttributes };
|
|
523
|
+
}
|
|
888
524
|
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
525
|
+
/**
|
|
526
|
+
* Build a custom component
|
|
527
|
+
*/
|
|
528
|
+
private buildCustomComponent(
|
|
529
|
+
componentName: string,
|
|
530
|
+
nodeProps: Record<string, unknown>,
|
|
531
|
+
children: unknown,
|
|
532
|
+
finalProps: Record<string, unknown>,
|
|
533
|
+
options: BuildComponentOptions
|
|
534
|
+
): ReactElement | null {
|
|
535
|
+
const {
|
|
536
|
+
key = 0, elementPath = [0], parentComponentName = null, viewportWidth = 1920,
|
|
537
|
+
componentContext = null, locale, i18nConfig, cmsContext = null, cmsLocale = null,
|
|
538
|
+
collectionItemsMap = {}, itemContext = null, cmsItemIndexPath = null,
|
|
539
|
+
templateContext = null
|
|
540
|
+
} = options;
|
|
899
541
|
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
structuredComponentDef.structure,
|
|
908
|
-
{ props: resolvedProps, componentDef: structuredComponentDef },
|
|
909
|
-
viewportWidth,
|
|
910
|
-
markedChildren
|
|
911
|
-
);
|
|
912
|
-
|
|
913
|
-
// Type guard: ensure processedStructure is a ComponentNode
|
|
914
|
-
if (!processedStructure || typeof processedStructure === 'string' || typeof processedStructure === 'number' || Array.isArray(processedStructure)) {
|
|
915
|
-
return null;
|
|
916
|
-
}
|
|
917
|
-
|
|
918
|
-
// Merge instance style overrides (from component instance props) with structure styles
|
|
919
|
-
if (props.style && typeof props.style === 'object') {
|
|
920
|
-
mergeNodeStyles(processedStructure, props.style as StyleValue, viewportWidth);
|
|
921
|
-
}
|
|
922
|
-
|
|
923
|
-
// Add event handlers to props (only for component instances)
|
|
924
|
-
if (props.onClick && isComponentNode(processedStructure)) {
|
|
925
|
-
if (!processedStructure.props) {
|
|
926
|
-
processedStructure.props = {};
|
|
927
|
-
}
|
|
928
|
-
processedStructure.props.onClick = this.createOnClickHandler(props.onClick);
|
|
929
|
-
}
|
|
930
|
-
|
|
931
|
-
// Build the component instance's structure with proper component context tracking
|
|
932
|
-
// Elements inside this component will have parentComponentName set via closure variables
|
|
933
|
-
// Component root will have props stored in registry (no DOM attributes needed)
|
|
934
|
-
const nestedParentComponentName = this.getParentComponentNameForNestedComponent(
|
|
935
|
-
componentContext,
|
|
936
|
-
parentComponentName,
|
|
937
|
-
componentName
|
|
938
|
-
);
|
|
939
|
-
|
|
940
|
-
const componentElement = this.buildComponent({
|
|
941
|
-
node: processedStructure,
|
|
942
|
-
key,
|
|
943
|
-
customProps: {
|
|
944
|
-
// Store resolved props in a special key to pass to registry (not added to DOM)
|
|
945
|
-
'__componentProps': resolvedProps
|
|
946
|
-
},
|
|
947
|
-
elementPath,
|
|
948
|
-
parentComponentName: nestedParentComponentName,
|
|
949
|
-
viewportWidth,
|
|
950
|
-
componentContext: componentName,
|
|
951
|
-
// Set componentRootPath to track where this component starts for relative path calculation
|
|
952
|
-
componentRootPath: elementPath,
|
|
953
|
-
locale,
|
|
954
|
-
i18nConfig,
|
|
955
|
-
cmsContext,
|
|
956
|
-
cmsLocale,
|
|
957
|
-
collectionItemsMap,
|
|
958
|
-
itemContext,
|
|
959
|
-
cmsItemIndexPath,
|
|
960
|
-
// Pass resolved props for interactive style mapping resolution
|
|
961
|
-
componentResolvedProps: resolvedProps
|
|
962
|
-
});
|
|
963
|
-
return h(ErrorBoundary, {
|
|
964
|
-
key,
|
|
965
|
-
componentName: componentName,
|
|
966
|
-
level: 'component',
|
|
967
|
-
}, componentElement);
|
|
968
|
-
|
|
969
|
-
} catch (error) {
|
|
970
|
-
return h(ErrorBoundary, {
|
|
971
|
-
key,
|
|
972
|
-
componentName: tag,
|
|
973
|
-
level: 'component',
|
|
974
|
-
}, null);
|
|
542
|
+
const componentDef = this.componentRegistry.get(componentName);
|
|
543
|
+
if (!componentDef) return null;
|
|
544
|
+
|
|
545
|
+
try {
|
|
546
|
+
const structuredComponentDef = componentDef.component;
|
|
547
|
+
if (!structuredComponentDef) {
|
|
548
|
+
return h(ErrorBoundary, { key, componentName, level: 'component' }, null);
|
|
975
549
|
}
|
|
976
|
-
}
|
|
977
550
|
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
551
|
+
// Resolve props
|
|
552
|
+
let resolvedProps = resolvePropsFromDefinition(
|
|
553
|
+
structuredComponentDef,
|
|
554
|
+
nodeProps,
|
|
555
|
+
children,
|
|
556
|
+
locale,
|
|
557
|
+
i18nConfig
|
|
558
|
+
);
|
|
982
559
|
|
|
983
|
-
//
|
|
984
|
-
|
|
560
|
+
// Process templates
|
|
561
|
+
if (cmsContext) {
|
|
562
|
+
resolvedProps = processCMSPropsTemplate(resolvedProps, cmsContext, cmsLocale || locale);
|
|
563
|
+
}
|
|
985
564
|
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
565
|
+
const effectiveItemContext = templateContext || itemContext;
|
|
566
|
+
if (effectiveItemContext) {
|
|
567
|
+
const effectiveLocale = cmsLocale || locale;
|
|
568
|
+
const config = i18nConfig || DEFAULT_I18N_CONFIG;
|
|
569
|
+
const i18nResolver: ValueResolver | undefined = effectiveLocale
|
|
570
|
+
? (value: unknown) => resolveI18nValue(value, effectiveLocale, config)
|
|
571
|
+
: undefined;
|
|
572
|
+
resolvedProps = processItemPropsTemplate(resolvedProps, effectiveItemContext, i18nResolver);
|
|
573
|
+
}
|
|
993
574
|
|
|
994
|
-
//
|
|
995
|
-
const
|
|
996
|
-
const
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
// viewport and load strategies are handled via ref callback below
|
|
575
|
+
// Process structure
|
|
576
|
+
const markedChildren = children ? markAsSlotContent(children) : undefined;
|
|
577
|
+
const processedStructure = processStructure(
|
|
578
|
+
structuredComponentDef.structure,
|
|
579
|
+
{ props: resolvedProps, componentDef: structuredComponentDef },
|
|
580
|
+
viewportWidth,
|
|
581
|
+
markedChildren
|
|
582
|
+
);
|
|
583
|
+
|
|
584
|
+
if (!processedStructure || typeof processedStructure === 'string' || typeof processedStructure === 'number' || Array.isArray(processedStructure)) {
|
|
585
|
+
return null;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// Merge styles
|
|
589
|
+
if (finalProps.style && typeof finalProps.style === 'object') {
|
|
590
|
+
mergeNodeStyles(processedStructure, finalProps.style as StyleValue, viewportWidth);
|
|
1011
591
|
}
|
|
1012
592
|
|
|
1013
|
-
|
|
1014
|
-
|
|
593
|
+
// Add onClick
|
|
594
|
+
if (finalProps.onClick && isComponentNode(processedStructure)) {
|
|
595
|
+
if (!processedStructure.props) processedStructure.props = {};
|
|
596
|
+
processedStructure.props.onClick = this.createOnClickHandler(finalProps.onClick);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
const nestedParentComponentName = this.getParentComponentNameForNestedComponent(
|
|
600
|
+
componentContext,
|
|
601
|
+
parentComponentName,
|
|
602
|
+
componentName
|
|
603
|
+
);
|
|
604
|
+
|
|
605
|
+
const componentElement = this.buildComponent({
|
|
606
|
+
node: processedStructure,
|
|
1015
607
|
key,
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
}
|
|
608
|
+
customProps: { '__componentProps': resolvedProps },
|
|
609
|
+
elementPath,
|
|
610
|
+
parentComponentName: nestedParentComponentName,
|
|
611
|
+
viewportWidth,
|
|
612
|
+
componentContext: componentName,
|
|
613
|
+
componentRootPath: elementPath,
|
|
614
|
+
locale,
|
|
615
|
+
i18nConfig,
|
|
616
|
+
cmsContext,
|
|
617
|
+
cmsLocale,
|
|
618
|
+
collectionItemsMap,
|
|
619
|
+
itemContext,
|
|
620
|
+
cmsItemIndexPath,
|
|
621
|
+
componentResolvedProps: resolvedProps
|
|
622
|
+
});
|
|
1032
623
|
|
|
1033
|
-
|
|
624
|
+
return h(ErrorBoundary, { key, componentName, level: 'component' }, componentElement);
|
|
1034
625
|
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
const strategy = (prefetchAttr as PrefetchStrategy) || this.prefetchService.getDefaultStrategy();
|
|
1038
|
-
if (strategy === 'viewport') {
|
|
1039
|
-
this.prefetchService.observeLink(el as HTMLAnchorElement, pathKey);
|
|
1040
|
-
}
|
|
1041
|
-
}
|
|
1042
|
-
},
|
|
1043
|
-
onClick: navigationOnClick,
|
|
1044
|
-
...prefetchHandlers,
|
|
1045
|
-
style: {
|
|
1046
|
-
textDecoration: 'none',
|
|
1047
|
-
color: '#0070f3',
|
|
1048
|
-
cursor: 'pointer',
|
|
1049
|
-
...(props.style && typeof props.style === 'object' ? props.style : {}),
|
|
1050
|
-
}
|
|
1051
|
-
}, this.buildChildren(children, {
|
|
1052
|
-
elementPath, parentComponentName: effectiveParentComponentName, viewportWidth, componentContext, componentRootPath,
|
|
1053
|
-
locale, i18nConfig, cmsContext, cmsLocale, collectionItemsMap, itemContext, cmsItemIndexPath, templateContext, componentResolvedProps
|
|
1054
|
-
}));
|
|
626
|
+
} catch (error) {
|
|
627
|
+
return h(ErrorBoundary, { key, componentName, level: 'component' }, null);
|
|
1055
628
|
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* Build a regular HTML element
|
|
633
|
+
*/
|
|
634
|
+
private buildHtmlElement(
|
|
635
|
+
tag: string | undefined,
|
|
636
|
+
props: Record<string, unknown>,
|
|
637
|
+
children: unknown,
|
|
638
|
+
customProps: ComponentProps,
|
|
639
|
+
options: BuildComponentOptions
|
|
640
|
+
): ReactElement | null {
|
|
641
|
+
const {
|
|
642
|
+
key = 0, elementPath = [0], parentComponentName = null, viewportWidth = 1920,
|
|
643
|
+
componentContext = null, componentRootPath, locale, i18nConfig,
|
|
644
|
+
cmsContext = null, cmsLocale = null, collectionItemsMap = {},
|
|
645
|
+
itemContext = null, cmsItemIndexPath = null, templateContext = null,
|
|
646
|
+
componentResolvedProps = null
|
|
647
|
+
} = options;
|
|
1056
648
|
|
|
1057
|
-
|
|
1058
|
-
|
|
649
|
+
const finalTag = tag || 'div';
|
|
650
|
+
const effectiveParentComponentName = this.getEffectiveParentComponentName(componentContext, parentComponentName);
|
|
651
|
+
|
|
652
|
+
// Remove __componentProps from customProps before spreading
|
|
1059
653
|
const { __componentProps, ...customPropsForDOM } = customProps || {};
|
|
1060
654
|
const isComponentRoot = !!(customProps && '__componentProps' in customProps);
|
|
655
|
+
|
|
1061
656
|
const processedProps: Record<string, unknown> = {
|
|
1062
657
|
...props,
|
|
1063
658
|
...customPropsForDOM,
|
|
1064
659
|
key
|
|
1065
|
-
// Note: Data attributes moved to ref callback to avoid rerenders on path changes
|
|
1066
660
|
};
|
|
1067
661
|
|
|
1068
|
-
// Register element
|
|
1069
|
-
// Setting attributes in ref callback avoids React rerenders when paths change during drag/reorder
|
|
662
|
+
// Register element via ref callback
|
|
1070
663
|
processedProps.ref = (el: HTMLElement | null) => {
|
|
1071
664
|
if (el) {
|
|
1072
|
-
// Set data attributes via DOM (not React props) to avoid rerenders on path changes
|
|
1073
665
|
el.setAttribute('data-element-path', pathToString(elementPath));
|
|
1074
666
|
if (cmsItemIndexPath && cmsItemIndexPath.length > 0) {
|
|
1075
667
|
el.setAttribute('data-cms-item-index', cmsItemIndexPath.join('.'));
|
|
@@ -1078,7 +670,7 @@ export class ComponentBuilder {
|
|
|
1078
670
|
if (parentComponentName) el.setAttribute('data-parent-component', parentComponentName);
|
|
1079
671
|
if (componentContext) el.setAttribute('data-component-context', componentContext);
|
|
1080
672
|
|
|
1081
|
-
// Add defineVars data attributes
|
|
673
|
+
// Add defineVars data attributes
|
|
1082
674
|
if (isComponentRoot && componentContext) {
|
|
1083
675
|
const componentDef = this.componentRegistry.get(componentContext);
|
|
1084
676
|
const defineVars = componentDef?.component?.defineVars;
|
|
@@ -1100,8 +692,7 @@ export class ComponentBuilder {
|
|
|
1100
692
|
}
|
|
1101
693
|
}
|
|
1102
694
|
|
|
1103
|
-
// Apply CSS variables
|
|
1104
|
-
// These are resolved per-instance based on component props
|
|
695
|
+
// Apply CSS variables
|
|
1105
696
|
const cssVariables = InteractiveStylesRegistry.getVariables(elementPath);
|
|
1106
697
|
if (cssVariables && Object.keys(cssVariables).length > 0) {
|
|
1107
698
|
for (const [varName, value] of Object.entries(cssVariables)) {
|
|
@@ -1110,14 +701,11 @@ export class ComponentBuilder {
|
|
|
1110
701
|
}
|
|
1111
702
|
}
|
|
1112
703
|
|
|
1113
|
-
// Extract props if this is a component root
|
|
1114
704
|
const componentProps = isComponentRoot && customProps.__componentProps
|
|
1115
705
|
? customProps.__componentProps as Record<string, unknown>
|
|
1116
706
|
: undefined;
|
|
1117
|
-
// Use componentContext as component name for component roots
|
|
1118
707
|
const componentNameForRegistry = isComponentRoot ? componentContext : parentComponentName;
|
|
1119
708
|
|
|
1120
|
-
// Build full metadata for registry
|
|
1121
709
|
const metadata = {
|
|
1122
710
|
parentComponentName: parentComponentName ?? null,
|
|
1123
711
|
componentContext: componentContext ?? null
|
|
@@ -1125,35 +713,25 @@ export class ComponentBuilder {
|
|
|
1125
713
|
|
|
1126
714
|
this.elementRegistry.register(elementPath, el, componentNameForRegistry, isComponentRoot, componentProps, metadata);
|
|
1127
715
|
};
|
|
1128
|
-
|
|
1129
|
-
// Add onClick handler - either custom or selection handler
|
|
1130
|
-
const originalOnClick = props.onClick ? this.createOnClickHandler(props.onClick) : undefined;
|
|
1131
716
|
|
|
1132
|
-
//
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
// Add onClick handler from props if exists
|
|
1136
|
-
if (originalOnClick) {
|
|
1137
|
-
processedProps.onClick = originalOnClick;
|
|
717
|
+
// Add onClick handler
|
|
718
|
+
if (props.onClick) {
|
|
719
|
+
processedProps.onClick = this.createOnClickHandler(props.onClick);
|
|
1138
720
|
}
|
|
1139
721
|
|
|
1140
|
-
//
|
|
1141
|
-
const finalTag = tag || 'div';
|
|
1142
|
-
|
|
1143
|
-
// HTML void elements cannot have children
|
|
722
|
+
// HTML void elements
|
|
1144
723
|
const VOID_ELEMENTS = ['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr'];
|
|
1145
724
|
if (VOID_ELEMENTS.includes(finalTag.toLowerCase())) {
|
|
1146
|
-
|
|
1147
|
-
const { children: _ignoredChildren, ...voidElementProps } = processedProps;
|
|
725
|
+
const { children: _ignored, ...voidElementProps } = processedProps;
|
|
1148
726
|
return h(finalTag, voidElementProps);
|
|
1149
727
|
}
|
|
1150
728
|
|
|
1151
|
-
// Build children
|
|
1152
|
-
const childElements = this.buildChildren(children, {
|
|
729
|
+
// Build children
|
|
730
|
+
const childElements = this.buildChildren(children as any, {
|
|
1153
731
|
elementPath, parentComponentName: effectiveParentComponentName, viewportWidth, componentContext, componentRootPath,
|
|
1154
732
|
locale, i18nConfig, cmsContext, cmsLocale, collectionItemsMap, itemContext, cmsItemIndexPath, templateContext, componentResolvedProps
|
|
1155
733
|
});
|
|
734
|
+
|
|
1156
735
|
return h(finalTag, processedProps, childElements);
|
|
1157
736
|
}
|
|
1158
737
|
}
|
|
1159
|
-
|