@bravostudioai/react 0.1.0
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/bin/encore-lib.js +3 -0
- package/dist/_virtual/_commonjsHelpers.js +7 -0
- package/dist/_virtual/_commonjsHelpers.js.map +1 -0
- package/dist/_virtual/main.js +8 -0
- package/dist/_virtual/main.js.map +1 -0
- package/dist/_virtual/main2.js +5 -0
- package/dist/_virtual/main2.js.map +1 -0
- package/dist/app.js +9 -0
- package/dist/app.js.map +1 -0
- package/dist/cli/commands/download.js +82 -0
- package/dist/cli/commands/download.js.map +1 -0
- package/dist/cli/commands/generate.js +1526 -0
- package/dist/cli/commands/generate.js.map +1 -0
- package/dist/cli.js +25 -0
- package/dist/cli.js.map +1 -0
- package/dist/components/DynamicComponent.js +24 -0
- package/dist/components/DynamicComponent.js.map +1 -0
- package/dist/components/EncoreApp.js +259 -0
- package/dist/components/EncoreApp.js.map +1 -0
- package/dist/components/EncoreErrorBoundary.js +33 -0
- package/dist/components/EncoreErrorBoundary.js.map +1 -0
- package/dist/components/EncoreLoadingFallback.js +20 -0
- package/dist/components/EncoreLoadingFallback.js.map +1 -0
- package/dist/components.js +1454 -0
- package/dist/components.js.map +1 -0
- package/dist/constants.d.ts +3 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/contexts/EncoreActionContext.js +6 -0
- package/dist/contexts/EncoreActionContext.js.map +1 -0
- package/dist/contexts/EncoreAppContext.js +9 -0
- package/dist/contexts/EncoreAppContext.js.map +1 -0
- package/dist/contexts/EncoreBindingContext.js +6 -0
- package/dist/contexts/EncoreBindingContext.js.map +1 -0
- package/dist/contexts/EncoreComponentIdContext.js +8 -0
- package/dist/contexts/EncoreComponentIdContext.js.map +1 -0
- package/dist/contexts/EncoreRepeatingContainerContext.js +6 -0
- package/dist/contexts/EncoreRepeatingContainerContext.js.map +1 -0
- package/dist/hooks/usePusherUpdates.js +60 -0
- package/dist/hooks/usePusherUpdates.js.map +1 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/dynamicModules.js +132 -0
- package/dist/lib/dynamicModules.js.map +1 -0
- package/dist/lib/fetcher.js +58 -0
- package/dist/lib/fetcher.js.map +1 -0
- package/dist/lib/localMode.js +21 -0
- package/dist/lib/localMode.js.map +1 -0
- package/dist/lib/packages.js +18 -0
- package/dist/lib/packages.js.map +1 -0
- package/dist/node_modules/dotenv/lib/main.js +198 -0
- package/dist/node_modules/dotenv/lib/main.js.map +1 -0
- package/dist/node_modules/dotenv/package.json.js +8 -0
- package/dist/node_modules/dotenv/package.json.js.map +1 -0
- package/dist/packages/encore-lib/constants.js +6 -0
- package/dist/packages/encore-lib/constants.js.map +1 -0
- package/dist/src/app.d.ts +5 -0
- package/dist/src/app.d.ts.map +1 -0
- package/dist/src/cli/commands/download.d.ts +2 -0
- package/dist/src/cli/commands/download.d.ts.map +1 -0
- package/dist/src/cli/commands/generate.d.ts +2 -0
- package/dist/src/cli/commands/generate.d.ts.map +1 -0
- package/dist/src/cli/index.d.ts +2 -0
- package/dist/src/cli/index.d.ts.map +1 -0
- package/dist/src/components/DynamicComponent.d.ts +12 -0
- package/dist/src/components/DynamicComponent.d.ts.map +1 -0
- package/dist/src/components/EncoreApp.d.ts +27 -0
- package/dist/src/components/EncoreApp.d.ts.map +1 -0
- package/dist/src/components/EncoreErrorBoundary.d.ts +17 -0
- package/dist/src/components/EncoreErrorBoundary.d.ts.map +1 -0
- package/dist/src/components/EncoreLoadingFallback.d.ts +4 -0
- package/dist/src/components/EncoreLoadingFallback.d.ts.map +1 -0
- package/dist/src/components.d.ts +4 -0
- package/dist/src/components.d.ts.map +1 -0
- package/dist/src/contexts/EncoreActionContext.d.ts +13 -0
- package/dist/src/contexts/EncoreActionContext.d.ts.map +1 -0
- package/dist/src/contexts/EncoreAppContext.d.ts +8 -0
- package/dist/src/contexts/EncoreAppContext.d.ts.map +1 -0
- package/dist/src/contexts/EncoreBindingContext.d.ts +5 -0
- package/dist/src/contexts/EncoreBindingContext.d.ts.map +1 -0
- package/dist/src/contexts/EncoreComponentIdContext.d.ts +8 -0
- package/dist/src/contexts/EncoreComponentIdContext.d.ts.map +1 -0
- package/dist/src/contexts/EncoreRepeatingContainerContext.d.ts +21 -0
- package/dist/src/contexts/EncoreRepeatingContainerContext.d.ts.map +1 -0
- package/dist/src/hooks/useAuthRedirect.d.ts +3 -0
- package/dist/src/hooks/useAuthRedirect.d.ts.map +1 -0
- package/dist/src/hooks/usePusherUpdates.d.ts +18 -0
- package/dist/src/hooks/usePusherUpdates.d.ts.map +1 -0
- package/dist/src/index.d.ts +8 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/lib/dynamicModules.d.ts +8 -0
- package/dist/src/lib/dynamicModules.d.ts.map +1 -0
- package/dist/src/lib/fetcher.d.ts +5 -0
- package/dist/src/lib/fetcher.d.ts.map +1 -0
- package/dist/src/lib/localMode.d.ts +3 -0
- package/dist/src/lib/localMode.d.ts.map +1 -0
- package/dist/src/lib/packages.d.ts +6 -0
- package/dist/src/lib/packages.d.ts.map +1 -0
- package/dist/src/stores/useEncoreState.d.ts +33 -0
- package/dist/src/stores/useEncoreState.d.ts.map +1 -0
- package/dist/stores/useEncoreState.js +70 -0
- package/dist/stores/useEncoreState.js.map +1 -0
- package/package.json +60 -0
- package/src/AGENTS.md +161 -0
- package/src/README.md +110 -0
- package/src/app.ts +5 -0
- package/src/cli/commands/download.ts +133 -0
- package/src/cli/commands/generate.ts +3045 -0
- package/src/cli/index.ts +35 -0
- package/src/components/DynamicComponent.tsx +40 -0
- package/src/components/EncoreApp.tsx +759 -0
- package/src/components/EncoreErrorBoundary.tsx +49 -0
- package/src/components/EncoreLoadingFallback.tsx +25 -0
- package/src/components.tsx +3155 -0
- package/src/contexts/EncoreActionContext.ts +18 -0
- package/src/contexts/EncoreAppContext.ts +13 -0
- package/src/contexts/EncoreBindingContext.ts +6 -0
- package/src/contexts/EncoreComponentIdContext.ts +12 -0
- package/src/contexts/EncoreRepeatingContainerContext.ts +30 -0
- package/src/hooks/useAuthRedirect.ts +63 -0
- package/src/hooks/usePusherUpdates.ts +156 -0
- package/src/index.ts +16 -0
- package/src/lib/dynamicModules.ts +193 -0
- package/src/lib/fetcher.ts +108 -0
- package/src/lib/localMode.ts +30 -0
- package/src/lib/moduleRegistry.ts +24 -0
- package/src/lib/packages.ts +33 -0
- package/src/stores/useEncoreState.ts +121 -0
|
@@ -0,0 +1,3155 @@
|
|
|
1
|
+
// @ts-nocheck - This file contains extensive dynamic runtime data structures that are impractical to fully type
|
|
2
|
+
import React, {
|
|
3
|
+
useContext,
|
|
4
|
+
useState,
|
|
5
|
+
useCallback,
|
|
6
|
+
useMemo,
|
|
7
|
+
useRef,
|
|
8
|
+
} from "react";
|
|
9
|
+
import EncoreBindingContext from "./contexts/EncoreBindingContext";
|
|
10
|
+
import useEncoreState from "./stores/useEncoreState";
|
|
11
|
+
import EncoreComponentIdContext from "./contexts/EncoreComponentIdContext";
|
|
12
|
+
import axios from "axios";
|
|
13
|
+
import EncoreActionContext from "./contexts/EncoreActionContext";
|
|
14
|
+
import EncoreRepeatingContainerContext from "./contexts/EncoreRepeatingContainerContext";
|
|
15
|
+
|
|
16
|
+
// !!! IMPORTANT
|
|
17
|
+
//
|
|
18
|
+
// YOU MUST ENSURE THAT ANY PACKAGES USED IN THIS FILE ARE ADDED TO @/lib/packages
|
|
19
|
+
// FAILURE TO DO SO WILL RESULT IN RUNTIME DEPENDENCY FAILURES
|
|
20
|
+
|
|
21
|
+
// Type definitions
|
|
22
|
+
interface EncorePageContextType {
|
|
23
|
+
scaleFactor?: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface EncoreContainerContextType {
|
|
27
|
+
dimensions?: { width: number; height: number };
|
|
28
|
+
isFlex?: boolean;
|
|
29
|
+
flexDirection?: "row" | "column"; // Track parent's flex direction
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface ComponentProps {
|
|
33
|
+
id: string;
|
|
34
|
+
name: string;
|
|
35
|
+
nodeData: any; // Dynamic runtime data structure
|
|
36
|
+
children?: React.ReactNode;
|
|
37
|
+
_arrayItemIndex?: number;
|
|
38
|
+
_arrayItemData?: any;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const EncorePageContext = React.createContext<EncorePageContextType>({});
|
|
42
|
+
const EncoreContainerContext = React.createContext<EncoreContainerContextType>(
|
|
43
|
+
{}
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
const argbAlphaToOpacity = (color: string): number => {
|
|
47
|
+
const alpha = parseInt(color.slice(1, 3), 16);
|
|
48
|
+
return alpha / 255.0;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const argbToRgba = (color: string): string => {
|
|
52
|
+
return color.replace(/#(\w{2})(\w{6})/, "#$2$1");
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const argbIsFullyTransparent = (color: string): boolean => {
|
|
56
|
+
return color.slice(1, 3) === "00";
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// Parse input-group tag: "input-group:single:product_purpose" -> { type: "single", groupName: "product_purpose" }
|
|
60
|
+
const parseInputGroupTag = (
|
|
61
|
+
tags: string[] | undefined
|
|
62
|
+
): { type: string; groupName: string } | null => {
|
|
63
|
+
if (!Array.isArray(tags)) return null;
|
|
64
|
+
const inputGroupTag = tags.find((tag) => tag.startsWith("input-group:"));
|
|
65
|
+
if (!inputGroupTag) return null;
|
|
66
|
+
|
|
67
|
+
const parts = inputGroupTag.split(":");
|
|
68
|
+
if (parts.length >= 3) {
|
|
69
|
+
return { type: parts[1], groupName: parts[2] };
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const useContainerDimensions = (): {
|
|
75
|
+
dimensions: { width: number; height: number };
|
|
76
|
+
ref: React.RefObject<HTMLDivElement>;
|
|
77
|
+
} => {
|
|
78
|
+
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
|
|
79
|
+
const ref = React.useRef<HTMLDivElement>(null);
|
|
80
|
+
|
|
81
|
+
React.useEffect(() => {
|
|
82
|
+
if (!ref.current) return;
|
|
83
|
+
const element = ref.current;
|
|
84
|
+
|
|
85
|
+
// Get initial dimensions immediately
|
|
86
|
+
const updateDimensions = () => {
|
|
87
|
+
const { width, height } = element.getBoundingClientRect();
|
|
88
|
+
setDimensions({ width, height });
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// Set initial dimensions synchronously
|
|
92
|
+
updateDimensions();
|
|
93
|
+
|
|
94
|
+
// Use requestAnimationFrame to ensure layout has settled
|
|
95
|
+
requestAnimationFrame(() => {
|
|
96
|
+
updateDimensions();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const ro = new ResizeObserver((entries) => {
|
|
100
|
+
for (const entry of entries) {
|
|
101
|
+
const cr = entry.contentRect;
|
|
102
|
+
setDimensions({ width: cr.width, height: cr.height });
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
ro.observe(element);
|
|
106
|
+
return () => ro.disconnect();
|
|
107
|
+
}, []);
|
|
108
|
+
|
|
109
|
+
return { dimensions, ref };
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const useEncoreStyle = (
|
|
113
|
+
style: any,
|
|
114
|
+
opts: { debug?: boolean } = {}
|
|
115
|
+
): React.CSSProperties | undefined => {
|
|
116
|
+
let shouldCenterHorizontally = false; // Track if element should be centered
|
|
117
|
+
const pageContext = useContext(EncorePageContext);
|
|
118
|
+
const containerContext = useContext(EncoreContainerContext);
|
|
119
|
+
const viewport = {
|
|
120
|
+
width: window.innerWidth,
|
|
121
|
+
height: window.innerHeight,
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const percentOfParentWidthToPx = useCallback(
|
|
125
|
+
(v: number) => {
|
|
126
|
+
return (containerContext.dimensions?.width ?? 0) * 0.01 * v;
|
|
127
|
+
},
|
|
128
|
+
[containerContext.dimensions?.width]
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
const result: React.CSSProperties = {};
|
|
132
|
+
opts = { debug: false, ...opts };
|
|
133
|
+
|
|
134
|
+
const fontIdToFamily = useEncoreState((s) => s.fontsById || {});
|
|
135
|
+
const fontIdToFull = useEncoreState((s) => s.fontsByIdFull || {});
|
|
136
|
+
|
|
137
|
+
if (!style) return undefined;
|
|
138
|
+
|
|
139
|
+
// DEBUG: Log style for specific components
|
|
140
|
+
const debugIds = [
|
|
141
|
+
"01KB929X1DQJN954WNGF5AH38B", // Slider
|
|
142
|
+
"01KB929X1KK5TX0K8VBNP6ZDPG", // Hero Container
|
|
143
|
+
"01KB929X17VWVF3A2J2DY0KHC5", // Page
|
|
144
|
+
];
|
|
145
|
+
// We don't have 'id' here directly in useEncoreStyle, but we can try to infer or pass it.
|
|
146
|
+
// Actually, let's just log if we see interesting positioning values or if we can pass ID.
|
|
147
|
+
// Since we can't easily pass ID without changing signature, let's add logging in the components themselves.
|
|
148
|
+
|
|
149
|
+
const containerWidth = containerContext.dimensions?.width ?? viewport.width;
|
|
150
|
+
const containerHeight =
|
|
151
|
+
containerContext.dimensions?.height ?? viewport.height;
|
|
152
|
+
|
|
153
|
+
// Check if this node has layout sizing directives (even if mode is null)
|
|
154
|
+
const hasLayoutSizing =
|
|
155
|
+
style.layout?.layoutSizingHorizontal || style.layout?.layoutSizingVertical;
|
|
156
|
+
|
|
157
|
+
// Only calculate width/height from percentages if layout sizing is not present.
|
|
158
|
+
// When layout sizing is FIXED, we'll use layout.size values instead (handled below).
|
|
159
|
+
if (!hasLayoutSizing) {
|
|
160
|
+
if (style.width && containerWidth > 0)
|
|
161
|
+
result.width = style.width * containerWidth * 0.01;
|
|
162
|
+
|
|
163
|
+
if (style.aspectRatio && style.width) {
|
|
164
|
+
if (containerWidth > 0) {
|
|
165
|
+
result.height =
|
|
166
|
+
(style.width * containerWidth * 0.01) / style.aspectRatio;
|
|
167
|
+
}
|
|
168
|
+
} else {
|
|
169
|
+
if (style.height && containerHeight > 0)
|
|
170
|
+
result.height = style.height * containerHeight * 0.01;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Apply absolute positioning if container is not flex and node has positioning info.
|
|
175
|
+
// When layout sizing is present, we still use positioning for placement,
|
|
176
|
+
// but layout.size values take precedence for dimensions (handled below).
|
|
177
|
+
// Note: containerContext.isFlex defaults to false if context is missing
|
|
178
|
+
const parentIsFlex = containerContext?.isFlex ?? false;
|
|
179
|
+
if (!parentIsFlex && style.positioning) {
|
|
180
|
+
// When we have explicit dimensions from layout sizing, only use top/left for positioning.
|
|
181
|
+
// Don't set right/bottom as they would conflict with explicit width/height.
|
|
182
|
+
const hasExplicitDimensions =
|
|
183
|
+
style.layout?.layoutSizingHorizontal === "FIXED" ||
|
|
184
|
+
style.layout?.layoutSizingVertical === "FIXED";
|
|
185
|
+
|
|
186
|
+
// For elements with explicit dimensions, clamp negative positioning to 0.
|
|
187
|
+
// Negative positioning with explicit dimensions typically indicates visual extension
|
|
188
|
+
// (shadows/borders), but for alignment purposes, elements should start at container bounds.
|
|
189
|
+
const top =
|
|
190
|
+
hasExplicitDimensions && style.positioning.top < 0
|
|
191
|
+
? 0
|
|
192
|
+
: style.positioning.top;
|
|
193
|
+
const left =
|
|
194
|
+
hasExplicitDimensions && style.positioning.left < 0
|
|
195
|
+
? 0
|
|
196
|
+
: style.positioning.left;
|
|
197
|
+
|
|
198
|
+
// HEURISTIC: Detect if element should be centered horizontally
|
|
199
|
+
// If element has explicit width that's nearly full-width (>= 95% of container) and left position
|
|
200
|
+
// is in the range 15-35% (which suggests the element should be centered but is
|
|
201
|
+
// currently positioned from its left edge), center it using transform
|
|
202
|
+
if (
|
|
203
|
+
hasExplicitDimensions &&
|
|
204
|
+
style.layout?.layoutSizingHorizontal === "FIXED" &&
|
|
205
|
+
style.layout?.size?.x
|
|
206
|
+
) {
|
|
207
|
+
const elementWidthPx = style.layout.size.x;
|
|
208
|
+
const scale = pageContext.scaleFactor ?? 1;
|
|
209
|
+
const scaledElementWidth = elementWidthPx * scale;
|
|
210
|
+
const containerWidth = containerContext?.dimensions?.width;
|
|
211
|
+
|
|
212
|
+
// Check if element width is >= 95% of container width
|
|
213
|
+
if (
|
|
214
|
+
containerWidth &&
|
|
215
|
+
typeof containerWidth === "number" &&
|
|
216
|
+
containerWidth > 0
|
|
217
|
+
) {
|
|
218
|
+
const widthRatio = scaledElementWidth / containerWidth;
|
|
219
|
+
// Element is nearly full width (>= 95%) and left position suggests it should be centered
|
|
220
|
+
// The left position range 15-35% is typical for a nearly-full-width element that's meant to be centered
|
|
221
|
+
if (widthRatio >= 0.95 && left >= 15 && left <= 35) {
|
|
222
|
+
shouldCenterHorizontally = true;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
result.top = `${top}%`;
|
|
228
|
+
|
|
229
|
+
// Check if inset exists in original style - if so, we'll need to modify it for centering
|
|
230
|
+
const hasInset = !!style.inset;
|
|
231
|
+
|
|
232
|
+
if (shouldCenterHorizontally) {
|
|
233
|
+
// Center horizontally using left: 50% and transform: translateX(-50%)
|
|
234
|
+
if (hasInset) {
|
|
235
|
+
// Parse and modify inset to center horizontally
|
|
236
|
+
// Inset format: "top right bottom left" or "top horizontal bottom" (3 values)
|
|
237
|
+
const insetParts = String(style.inset).split(/\s+/);
|
|
238
|
+
if (insetParts.length === 3) {
|
|
239
|
+
// 3-value inset: top horizontal bottom -> convert to 4-value: top 50% bottom auto
|
|
240
|
+
result.inset = `${insetParts[0]} 50% ${insetParts[2]} auto`;
|
|
241
|
+
} else if (insetParts.length === 4) {
|
|
242
|
+
// 4-value inset: top right bottom left -> keep top/bottom, center horizontally
|
|
243
|
+
result.inset = `${insetParts[0]} 50% ${insetParts[2]} auto`;
|
|
244
|
+
}
|
|
245
|
+
// Clear left/right since inset takes precedence
|
|
246
|
+
delete result.left;
|
|
247
|
+
delete result.right;
|
|
248
|
+
} else {
|
|
249
|
+
// No inset, use left positioning
|
|
250
|
+
result.left = "50%";
|
|
251
|
+
result.right = "auto";
|
|
252
|
+
}
|
|
253
|
+
} else {
|
|
254
|
+
if (hasInset) {
|
|
255
|
+
// Preserve original inset if not centering
|
|
256
|
+
result.inset = style.inset;
|
|
257
|
+
} else {
|
|
258
|
+
result.left = `${left}%`;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// CRITICAL: Never set right/bottom when explicit dimensions exist, as they override width/height
|
|
263
|
+
// Explicit width/height from layout.size should take precedence over positioning-based sizing
|
|
264
|
+
if (!hasExplicitDimensions) {
|
|
265
|
+
// Only use right/bottom when we don't have explicit dimensions
|
|
266
|
+
if (style.positioning.bottom !== undefined) {
|
|
267
|
+
result.bottom = `${style.positioning.bottom}%`;
|
|
268
|
+
}
|
|
269
|
+
if (style.positioning.right !== undefined) {
|
|
270
|
+
result.right = `${style.positioning.right}%`;
|
|
271
|
+
}
|
|
272
|
+
} else {
|
|
273
|
+
// When explicit dimensions exist, explicitly set right/bottom to 'auto' to prevent browser computation
|
|
274
|
+
// This ensures width/height from layout.size take full precedence
|
|
275
|
+
result.right = "auto";
|
|
276
|
+
result.bottom = "auto";
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
result.position = "absolute";
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Apply padding even when mode is null - containers can have padding without flex mode
|
|
283
|
+
// Scale paddings to match Figma layout proportions
|
|
284
|
+
if (style.layout?.padding) {
|
|
285
|
+
const scale = pageContext.scaleFactor ?? 1;
|
|
286
|
+
const p = style.layout.padding;
|
|
287
|
+
if (p.top !== undefined) result.paddingTop = p.top * scale;
|
|
288
|
+
if (p.right !== undefined) result.paddingRight = p.right * scale;
|
|
289
|
+
if (p.bottom !== undefined) result.paddingBottom = p.bottom * scale;
|
|
290
|
+
if (p.left !== undefined) result.paddingLeft = p.left * scale;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (style.layout?.mode) {
|
|
294
|
+
// result.flex = 1;
|
|
295
|
+
result.display = "flex";
|
|
296
|
+
|
|
297
|
+
// Map Figma primaryAxisAlignItems values to CSS justify-content values
|
|
298
|
+
const mapJustifyContent = (
|
|
299
|
+
figmaValue: string | undefined
|
|
300
|
+
): string | undefined => {
|
|
301
|
+
if (!figmaValue) return undefined;
|
|
302
|
+
const mapping: Record<string, string> = {
|
|
303
|
+
MIN: "flex-start",
|
|
304
|
+
MAX: "flex-end",
|
|
305
|
+
CENTER: "center",
|
|
306
|
+
SPACE_BETWEEN: "space-between",
|
|
307
|
+
SPACE_AROUND: "space-around",
|
|
308
|
+
SPACE_EVENLY: "space-evenly",
|
|
309
|
+
};
|
|
310
|
+
return mapping[figmaValue] || figmaValue.toLowerCase().replace(/_/g, "-");
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
if (style.layout.mode == "HORIZONTAL") {
|
|
314
|
+
result.flexDirection = "row";
|
|
315
|
+
if (style.layout.flexWrap) {
|
|
316
|
+
result.flexWrap = style.layout.flexWrap;
|
|
317
|
+
}
|
|
318
|
+
result.justifyContent = mapJustifyContent(
|
|
319
|
+
style.layout.primaryAxisAlignItems
|
|
320
|
+
);
|
|
321
|
+
result.alignItems = style.layout.counterAxisAlignItems;
|
|
322
|
+
} else {
|
|
323
|
+
result.flexDirection = "column";
|
|
324
|
+
result.justifyContent = mapJustifyContent(
|
|
325
|
+
style.layout.primaryAxisAlignItems
|
|
326
|
+
);
|
|
327
|
+
result.alignItems = style.layout.counterAxisAlignItems;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
result.flexGrow = style.layout.grow;
|
|
331
|
+
// Set flexWrap: if wrap is explicitly "NO_WRAP", use "nowrap", otherwise default to "wrap" for layouts that need it
|
|
332
|
+
if (style.layout.wrap === "NO_WRAP") {
|
|
333
|
+
result.flexWrap = "nowrap";
|
|
334
|
+
} else if (style.layout.flexWrap) {
|
|
335
|
+
// If flexWrap is explicitly set, use it
|
|
336
|
+
result.flexWrap = style.layout.flexWrap;
|
|
337
|
+
} else {
|
|
338
|
+
// Default to wrap for horizontal layouts (allows items to wrap to new rows)
|
|
339
|
+
result.flexWrap = "wrap";
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Always set gap properties for flex containers
|
|
343
|
+
// If itemSpacing is defined, use it; otherwise default to 0
|
|
344
|
+
// IMPORTANT: When primaryAxisAlignItems is SPACE_BETWEEN, ignore itemSpacing
|
|
345
|
+
// because space-between already distributes space between items, and Figma's
|
|
346
|
+
// itemSpacing value in this case represents the computed spacing, not intended gap.
|
|
347
|
+
const itemSpacing = style.layout.itemSpacing;
|
|
348
|
+
const scale = pageContext.scaleFactor ?? 1;
|
|
349
|
+
const isSpaceBetween =
|
|
350
|
+
style.layout.primaryAxisAlignItems === "SPACE_BETWEEN";
|
|
351
|
+
const spacing = isSpaceBetween
|
|
352
|
+
? 0
|
|
353
|
+
: (itemSpacing !== undefined ? itemSpacing : 0) * scale;
|
|
354
|
+
// Always set gap properties as strings to ensure they appear in the DOM
|
|
355
|
+
// Use "0px" explicitly for zero values to ensure React includes them
|
|
356
|
+
result.gap = spacing === 0 ? "0px" : `${spacing}px`;
|
|
357
|
+
result.rowGap = spacing === 0 ? "0px" : `${spacing}px`;
|
|
358
|
+
result.columnGap = spacing === 0 ? "0px" : `${spacing}px`;
|
|
359
|
+
|
|
360
|
+
// When flex-wrap is enabled, align-content controls spacing between wrapped rows
|
|
361
|
+
// Set it to flex-start to ensure no spacing between wrapped rows
|
|
362
|
+
if (result.flexWrap === "wrap" || result.flexWrap === "wrap-reverse") {
|
|
363
|
+
result.alignContent = "flex-start";
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Debug logging for page components
|
|
367
|
+
|
|
368
|
+
if (
|
|
369
|
+
opts.debug ||
|
|
370
|
+
(style.layout?.mode === "HORIZONTAL" && itemSpacing === 0)
|
|
371
|
+
) {
|
|
372
|
+
console.log("[useEncoreStyle] Setting gap properties:", {
|
|
373
|
+
itemSpacing,
|
|
374
|
+
spacing,
|
|
375
|
+
gap: result.gap,
|
|
376
|
+
rowGap: result.rowGap,
|
|
377
|
+
columnGap: result.columnGap,
|
|
378
|
+
hasLayout: !!style.layout,
|
|
379
|
+
layoutMode: style.layout?.mode,
|
|
380
|
+
});
|
|
381
|
+
} else {
|
|
382
|
+
console.log(
|
|
383
|
+
"[useEncoreStyle] No gap properties set",
|
|
384
|
+
opts.debug,
|
|
385
|
+
style.layout?.mode,
|
|
386
|
+
itemSpacing
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// TODO
|
|
391
|
+
// style.layout.primaryAxisSizingMode
|
|
392
|
+
// style.layout.counterAxisSizingMode
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (style.layout?.layoutSizingHorizontal === "FILL") {
|
|
396
|
+
result.width = "100%";
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (style.layout?.layoutSizingHorizontal === "FIXED") {
|
|
400
|
+
const scale = pageContext.scaleFactor ?? 1;
|
|
401
|
+
result.width = style.layout.size.x * scale;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (style.layout?.layoutSizingVertical === "FIXED") {
|
|
405
|
+
const scale = pageContext.scaleFactor ?? 1;
|
|
406
|
+
result.height = style.layout.size.y * scale;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (style.layout?.layoutSizingVertical === "FILL") {
|
|
410
|
+
// For absolutely positioned elements, if we have a top position,
|
|
411
|
+
// calculate the remaining height instead of using 100%
|
|
412
|
+
// This prevents elements from extending beyond their intended container
|
|
413
|
+
// Check if this will be absolutely positioned (positioning exists and parent isn't flex)
|
|
414
|
+
const parentIsFlex = containerContext?.isFlex ?? false;
|
|
415
|
+
const willBeAbsolute = !parentIsFlex && style.positioning;
|
|
416
|
+
if (willBeAbsolute && style.positioning?.top !== undefined) {
|
|
417
|
+
const topPercent = style.positioning.top;
|
|
418
|
+
const remainingPercent = 100 - topPercent;
|
|
419
|
+
result.height = `${remainingPercent}%`;
|
|
420
|
+
} else {
|
|
421
|
+
result.height = "100%";
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// HUG sizing: explicitly do NOT set height
|
|
426
|
+
// Let flex layout determine height based on content
|
|
427
|
+
// This is intentionally a no-op - just documenting the behavior
|
|
428
|
+
if (style.layout?.layoutSizingVertical === "HUG") {
|
|
429
|
+
// Don't set result.height
|
|
430
|
+
// Flex containers will size based on their children
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (style.backgroundColor && !argbIsFullyTransparent(style.backgroundColor)) {
|
|
434
|
+
result.backgroundColor = argbToRgba(style.backgroundColor);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
if (style.color) result.color = argbToRgba(style.color);
|
|
438
|
+
if (style.lineHeightPx)
|
|
439
|
+
result.lineHeight =
|
|
440
|
+
(style.lineHeightPx * (pageContext.scaleFactor ?? 1)).toString() + "px";
|
|
441
|
+
if (style.fontSize)
|
|
442
|
+
result.fontSize = style.fontSize * (pageContext.scaleFactor ?? 1);
|
|
443
|
+
|
|
444
|
+
// When a specific font-face variant is selected (e.g., "Inter-SemiBold"),
|
|
445
|
+
// the font is loaded with the correct weight via FontFace API.
|
|
446
|
+
// We still need to set fontWeight in CSS so the browser can match the correct font-face,
|
|
447
|
+
// but we should use the weight from the font-face itself rather than auto-calculating.
|
|
448
|
+
// However, since the FontFace is loaded with the correct weight descriptor,
|
|
449
|
+
// setting the fontWeight from style.fontWeight should correctly match the font-face.
|
|
450
|
+
// The key fix is in FontFace loading (EncoreApp.tsx) where we now specify the weight.
|
|
451
|
+
if (style.fontWeight) {
|
|
452
|
+
result.fontWeight = `${style.fontWeight}`;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
if (style.opacity) result.opacity = style.opacity * 0.01;
|
|
456
|
+
if (style.fontId) {
|
|
457
|
+
const fontFamily = fontIdToFamily[style.fontId] || "sans-serif";
|
|
458
|
+
result.fontFamily = fontFamily;
|
|
459
|
+
}
|
|
460
|
+
if (style.borderRadius) {
|
|
461
|
+
result.borderRadius = percentOfParentWidthToPx(style.borderRadius);
|
|
462
|
+
// Only set overflow: hidden if we don't have background images/SVGs with negative positioning
|
|
463
|
+
// Background elements often extend beyond card bounds and need overflow: visible
|
|
464
|
+
// The border radius will still be applied, but content won't be clipped
|
|
465
|
+
// result.overflow = "hidden";
|
|
466
|
+
}
|
|
467
|
+
if (style.cornerRadii) {
|
|
468
|
+
const cornerRadii = style.cornerRadii.map(percentOfParentWidthToPx);
|
|
469
|
+
result.borderTopLeftRadius = cornerRadii[0];
|
|
470
|
+
result.borderTopRightRadius = cornerRadii[1];
|
|
471
|
+
result.borderBottomRightRadius = cornerRadii[2];
|
|
472
|
+
result.borderBottomLeftRadius = cornerRadii[3];
|
|
473
|
+
delete result.borderRadius;
|
|
474
|
+
// Only set overflow: hidden if we don't have background images/SVGs with negative positioning
|
|
475
|
+
// result.overflow = "hidden";
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Convert borderWidth from Figma units to pixels
|
|
479
|
+
// Figma borderWidth values are typically in a different unit system
|
|
480
|
+
// For icon components, the borderWidth represents stroke width
|
|
481
|
+
// We need to convert it properly - the original 2.875 multiplier was incorrect
|
|
482
|
+
// For vector icons, borderWidth values are often much larger and need different handling
|
|
483
|
+
if (style.borderWidth) {
|
|
484
|
+
// For icon components (vector shapes), use a more appropriate conversion
|
|
485
|
+
// If borderWidth is very large (> 5), it's likely a vector icon stroke width
|
|
486
|
+
// Convert it more directly: divide by a factor to get reasonable pixel values
|
|
487
|
+
if (style.borderWidth > 5) {
|
|
488
|
+
// For vector icons, convert more directly (these are stroke widths, not border widths)
|
|
489
|
+
result.borderWidth = style.borderWidth * 0.5; // More reasonable conversion for icon strokes
|
|
490
|
+
} else {
|
|
491
|
+
// For regular borders, use a smaller multiplier
|
|
492
|
+
result.borderWidth = style.borderWidth * 1.5;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
if (style.borderColor) result.borderColor = argbToRgba(style.borderColor);
|
|
496
|
+
// Set borderStyle to "solid" when borderWidth or borderColor is present so borders actually render
|
|
497
|
+
if (style.borderWidth || style.borderColor) {
|
|
498
|
+
result.borderStyle = "solid";
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
if (style.rotation) {
|
|
502
|
+
result.transform = `rotate(${style.rotation}deg)`;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Apply horizontal centering transform (combine with rotation if present)
|
|
506
|
+
// This must happen after all other transform settings
|
|
507
|
+
if (shouldCenterHorizontally) {
|
|
508
|
+
const existingTransform = result.transform || "";
|
|
509
|
+
result.transform = existingTransform
|
|
510
|
+
? `${existingTransform} translateX(-50%)`
|
|
511
|
+
: "translateX(-50%)";
|
|
512
|
+
|
|
513
|
+
// Override inset if it exists (it might have been set from positioning values)
|
|
514
|
+
// This must happen at the very end to ensure it takes precedence
|
|
515
|
+
if (result.inset) {
|
|
516
|
+
const insetParts = String(result.inset).split(/\s+/);
|
|
517
|
+
if (insetParts.length === 3) {
|
|
518
|
+
// 3-value inset: top horizontal bottom -> convert to 4-value: top 50% bottom auto
|
|
519
|
+
result.inset = `${insetParts[0]} 50% ${insetParts[2]} auto`;
|
|
520
|
+
} else if (insetParts.length === 4) {
|
|
521
|
+
// 4-value inset: top right bottom left -> keep top/bottom, center horizontally
|
|
522
|
+
result.inset = `${insetParts[0]} 50% ${insetParts[2]} auto`;
|
|
523
|
+
}
|
|
524
|
+
} else if (style.inset) {
|
|
525
|
+
// Inset might be in original style object
|
|
526
|
+
const insetParts = String(style.inset).split(/\s+/);
|
|
527
|
+
if (insetParts.length === 3) {
|
|
528
|
+
result.inset = `${insetParts[0]} 50% ${insetParts[2]} auto`;
|
|
529
|
+
} else if (insetParts.length === 4) {
|
|
530
|
+
result.inset = `${insetParts[0]} 50% ${insetParts[2]} auto`;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
if (style.textCase === "upper") {
|
|
536
|
+
result.textTransform = "uppercase";
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
if (style.textCase === "lower") {
|
|
540
|
+
result.textTransform = "lowercase";
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
if (style.letterSpacing) {
|
|
544
|
+
result.letterSpacing = style.letterSpacing;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
if (style.textAlign) {
|
|
548
|
+
result.textAlign = style.textAlign;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
if (opts.debug) {
|
|
552
|
+
result.borderWidth = 1;
|
|
553
|
+
result.borderColor = "#FF0000FF";
|
|
554
|
+
result.borderStyle = "solid";
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// FINAL OVERRIDE: Ensure centering inset takes precedence (must be last)
|
|
558
|
+
if (shouldCenterHorizontally && (result.inset || style.inset)) {
|
|
559
|
+
const insetToModify = result.inset || style.inset;
|
|
560
|
+
const insetParts = String(insetToModify).split(/\s+/);
|
|
561
|
+
if (insetParts.length === 3) {
|
|
562
|
+
result.inset = `${insetParts[0]} 50% ${insetParts[2]} auto`;
|
|
563
|
+
} else if (insetParts.length === 4) {
|
|
564
|
+
result.inset = `${insetParts[0]} 50% ${insetParts[2]} auto`;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
return result;
|
|
569
|
+
};
|
|
570
|
+
|
|
571
|
+
const useEncoreBinding = ({ id, nodeData }: { id: string; nodeData: any }) => {
|
|
572
|
+
const bindingContext = useContext(EncoreBindingContext);
|
|
573
|
+
const binding = bindingContext?.nodeData?.[id];
|
|
574
|
+
|
|
575
|
+
const { patch, list } = binding ?? { list: null, patch: null };
|
|
576
|
+
|
|
577
|
+
// Check for encore:data:array tag and get array data from context
|
|
578
|
+
const hasEncoreDataArrayTag =
|
|
579
|
+
Array.isArray(nodeData?.tags) &&
|
|
580
|
+
(nodeData.tags.includes("encore:data:array") ||
|
|
581
|
+
nodeData.tags.includes("bravo:data:array"));
|
|
582
|
+
|
|
583
|
+
// Debug logging removed
|
|
584
|
+
|
|
585
|
+
const arrayData =
|
|
586
|
+
hasEncoreDataArrayTag &&
|
|
587
|
+
bindingContext?.arrayDataById &&
|
|
588
|
+
bindingContext.arrayDataById[id] &&
|
|
589
|
+
Array.isArray(bindingContext.arrayDataById[id])
|
|
590
|
+
? bindingContext.arrayDataById[id]
|
|
591
|
+
: null;
|
|
592
|
+
|
|
593
|
+
// Debug logging removed
|
|
594
|
+
|
|
595
|
+
// Start with a shallow patch, but merge `data` more safely so we do not clobber unrelated fields.
|
|
596
|
+
// IMPORTANT: Preserve actions from original nodeData if patch doesn't provide them or provides empty array
|
|
597
|
+
let patchedNodeData = patch
|
|
598
|
+
? {
|
|
599
|
+
...nodeData,
|
|
600
|
+
...(patch || {}),
|
|
601
|
+
...(patch && patch.data
|
|
602
|
+
? { data: { ...(nodeData?.data || {}), ...patch.data } }
|
|
603
|
+
: {}),
|
|
604
|
+
// Preserve original actions if patch has empty or missing actions
|
|
605
|
+
actions:
|
|
606
|
+
patch.actions &&
|
|
607
|
+
Array.isArray(patch.actions) &&
|
|
608
|
+
patch.actions.length > 0
|
|
609
|
+
? patch.actions
|
|
610
|
+
: nodeData.actions || patch.actions || [],
|
|
611
|
+
}
|
|
612
|
+
: nodeData;
|
|
613
|
+
|
|
614
|
+
// Apply top-level text overrides when present and when the node declares the PROP:TEXT_VAR tag.
|
|
615
|
+
const textOverride =
|
|
616
|
+
bindingContext?.textOverridesById &&
|
|
617
|
+
bindingContext.textOverridesById[id] !== undefined
|
|
618
|
+
? bindingContext.textOverridesById[id]
|
|
619
|
+
: undefined;
|
|
620
|
+
if (
|
|
621
|
+
textOverride !== undefined &&
|
|
622
|
+
Array.isArray(nodeData?.tags) &&
|
|
623
|
+
nodeData.tags.includes("PROP:TEXT_VAR")
|
|
624
|
+
) {
|
|
625
|
+
patchedNodeData = {
|
|
626
|
+
...patchedNodeData,
|
|
627
|
+
data: { ...(patchedNodeData?.data || {}), text: String(textOverride) },
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// Handle encore:data tags - get data from current array item in binding context
|
|
632
|
+
// The binding context nodeData should be an object mapping component IDs to their data attributes
|
|
633
|
+
// e.g., { "01KA964B4KQW35Q8EJ2MJKNDWT": { imageUrl: "..." }, "01KA964B34W56CHEDNBBY2VB39": { text: "..." } }
|
|
634
|
+
const hasEncoreDataTag =
|
|
635
|
+
Array.isArray(nodeData?.tags) &&
|
|
636
|
+
(nodeData.tags.includes("encore:data") ||
|
|
637
|
+
nodeData.tags.includes("bravo:data"));
|
|
638
|
+
|
|
639
|
+
// Apply root data for standalone components
|
|
640
|
+
// Also apply for ANY component that has data in rootData (e.g., select inputs with options)
|
|
641
|
+
const rootData = bindingContext?.rootData;
|
|
642
|
+
if (rootData && rootData[id] && typeof rootData[id] === "object") {
|
|
643
|
+
patchedNodeData = {
|
|
644
|
+
...patchedNodeData,
|
|
645
|
+
data: {
|
|
646
|
+
...(patchedNodeData.data || {}),
|
|
647
|
+
...rootData[id],
|
|
648
|
+
},
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// Check if we have binding context data (from parent ContainerComponent with encore:data:array)
|
|
653
|
+
const hasBindingContextData =
|
|
654
|
+
bindingContext?.nodeData && typeof bindingContext.nodeData === "object";
|
|
655
|
+
|
|
656
|
+
if (hasEncoreDataTag || hasBindingContextData) {
|
|
657
|
+
const currentItemData = bindingContext?.nodeData;
|
|
658
|
+
if (currentItemData && typeof currentItemData === "object") {
|
|
659
|
+
// Check if currentItemData is a mapping object (has component IDs as keys)
|
|
660
|
+
// vs. a raw data object (has fields like description, image, etc.)
|
|
661
|
+
const isMappingObject = currentItemData[id] !== undefined;
|
|
662
|
+
|
|
663
|
+
if (isMappingObject) {
|
|
664
|
+
// Explicit mapping: look up this component's ID in the mapping object
|
|
665
|
+
const componentData = currentItemData[id];
|
|
666
|
+
if (
|
|
667
|
+
componentData &&
|
|
668
|
+
typeof componentData === "object" &&
|
|
669
|
+
patchedNodeData.data
|
|
670
|
+
) {
|
|
671
|
+
// Apply all attributes from the mapping to the component's data
|
|
672
|
+
patchedNodeData = {
|
|
673
|
+
...patchedNodeData,
|
|
674
|
+
data: {
|
|
675
|
+
...patchedNodeData.data,
|
|
676
|
+
...componentData,
|
|
677
|
+
},
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
} else {
|
|
681
|
+
// Legacy/fallback: treat as raw data object and do automatic mapping
|
|
682
|
+
// This supports backward compatibility with the old format
|
|
683
|
+
if (hasEncoreDataTag) {
|
|
684
|
+
// Debug logging removed
|
|
685
|
+
if (currentItemData.description && patchedNodeData.data) {
|
|
686
|
+
patchedNodeData = {
|
|
687
|
+
...patchedNodeData,
|
|
688
|
+
data: {
|
|
689
|
+
...patchedNodeData.data,
|
|
690
|
+
text: currentItemData.description,
|
|
691
|
+
},
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
if (currentItemData.image && patchedNodeData.data) {
|
|
696
|
+
const imageUrl = currentItemData.image.startsWith("//")
|
|
697
|
+
? `https:${currentItemData.image}`
|
|
698
|
+
: currentItemData.image;
|
|
699
|
+
patchedNodeData = {
|
|
700
|
+
...patchedNodeData,
|
|
701
|
+
data: {
|
|
702
|
+
...patchedNodeData.data,
|
|
703
|
+
imageUrl: imageUrl,
|
|
704
|
+
},
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// Always store the full item data for components that need it
|
|
710
|
+
patchedNodeData.encoreData = currentItemData;
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
return {
|
|
715
|
+
patch,
|
|
716
|
+
list: list || arrayData, // Use arrayData if list is not provided
|
|
717
|
+
patchedNodeData,
|
|
718
|
+
arrayData, // Also return separately for components that need it
|
|
719
|
+
};
|
|
720
|
+
};
|
|
721
|
+
|
|
722
|
+
const EncoreLinkActionWrapper: React.FC<{
|
|
723
|
+
id: string;
|
|
724
|
+
nodeData: any;
|
|
725
|
+
children?: React.ReactNode;
|
|
726
|
+
_parentStatefulSetId?: string;
|
|
727
|
+
_parentInputGroupInfo?: { type: string; groupName: string } | null;
|
|
728
|
+
_parentName?: string;
|
|
729
|
+
}> = ({
|
|
730
|
+
id,
|
|
731
|
+
nodeData,
|
|
732
|
+
children,
|
|
733
|
+
_parentStatefulSetId,
|
|
734
|
+
_parentInputGroupInfo,
|
|
735
|
+
_parentName,
|
|
736
|
+
}) => {
|
|
737
|
+
const baseURL = useEncoreState((state) => state.baseURL);
|
|
738
|
+
const appId = useEncoreState((state) => state.appId);
|
|
739
|
+
const pageId = useEncoreState((state) => state.pageId);
|
|
740
|
+
const { onAction } = React.useContext(EncoreActionContext);
|
|
741
|
+
const { statefulSetId: parentStatefulSetIdFromContext } = React.useContext(
|
|
742
|
+
EncoreComponentIdContext
|
|
743
|
+
);
|
|
744
|
+
// Prefer prop over context (more reliable)
|
|
745
|
+
const parentStatefulSetId =
|
|
746
|
+
_parentStatefulSetId || parentStatefulSetIdFromContext;
|
|
747
|
+
// const location = useLocation();
|
|
748
|
+
// const [searchParams] = useSearchParams();
|
|
749
|
+
const { patchedNodeData } = useEncoreBinding({ id, nodeData });
|
|
750
|
+
const setStatefulSetVariant = useEncoreState(
|
|
751
|
+
(state) => state.setStatefulSetVariant
|
|
752
|
+
);
|
|
753
|
+
const setInputGroupValue = useEncoreState(
|
|
754
|
+
(state) => state.setInputGroupValue
|
|
755
|
+
);
|
|
756
|
+
const setAccessToken = useEncoreState((state) => state.setAccessToken);
|
|
757
|
+
const { app } = useEncoreState((state) => state.app);
|
|
758
|
+
// const navigate = useNavigate();
|
|
759
|
+
const startPageId = app.data?.app?.startPageId;
|
|
760
|
+
const loginPageId = app.data?.app?.PageId;
|
|
761
|
+
|
|
762
|
+
const style = useEncoreStyle(
|
|
763
|
+
{
|
|
764
|
+
positioning: nodeData.style?.positioning,
|
|
765
|
+
},
|
|
766
|
+
{ debug: false }
|
|
767
|
+
);
|
|
768
|
+
|
|
769
|
+
// Make the wrapper div cover the entire button area
|
|
770
|
+
// Use a very high z-index to ensure it's above all children
|
|
771
|
+
// IMPORTANT: The overlay should be scoped to the immediate parent (the button),
|
|
772
|
+
// not the entire StatefulSetComponent, to avoid overlapping with other buttons
|
|
773
|
+
style.zIndex = 99999;
|
|
774
|
+
style.backgroundColor = "transparent";
|
|
775
|
+
style.position = "absolute";
|
|
776
|
+
style.top = "0";
|
|
777
|
+
style.left = "0";
|
|
778
|
+
style.right = "0";
|
|
779
|
+
style.bottom = "0";
|
|
780
|
+
style.width = "100%";
|
|
781
|
+
style.height = "100%";
|
|
782
|
+
style.pointerEvents = "auto";
|
|
783
|
+
// Ensure the overlay is contained within its parent button element
|
|
784
|
+
style.isolation = "isolate";
|
|
785
|
+
|
|
786
|
+
const handleAction = async (action: any) => {
|
|
787
|
+
let proceed = false;
|
|
788
|
+
const cancel = () => {
|
|
789
|
+
proceed = false;
|
|
790
|
+
};
|
|
791
|
+
if (typeof onAction === "function") {
|
|
792
|
+
try {
|
|
793
|
+
await onAction({ bravo: { cancel, action } });
|
|
794
|
+
proceed = true;
|
|
795
|
+
} catch (e) {
|
|
796
|
+
// ignore callback errors to avoid breaking in-bravo behavior
|
|
797
|
+
}
|
|
798
|
+
} else {
|
|
799
|
+
proceed = true;
|
|
800
|
+
}
|
|
801
|
+
if (!proceed) return;
|
|
802
|
+
switch (action.action) {
|
|
803
|
+
case "login":
|
|
804
|
+
// TODO firebase
|
|
805
|
+
// TODO oauth
|
|
806
|
+
// TODO actually check if this app has custom login
|
|
807
|
+
// For now, just assume it's a custom login (which is just a form submission)
|
|
808
|
+
|
|
809
|
+
// Intentional fallthrough
|
|
810
|
+
case "submit":
|
|
811
|
+
// const formInputs = useEncoreState.getState().formInputs[pageId];
|
|
812
|
+
// const loginResponse = await axios.post(
|
|
813
|
+
// `${baseURL}/devices/apps/${appId}/node/${pageId}`,
|
|
814
|
+
// formInputs
|
|
815
|
+
// );
|
|
816
|
+
// if (loginResponse.data?.action) await handleAction(loginResponse.data);
|
|
817
|
+
break;
|
|
818
|
+
|
|
819
|
+
case "email":
|
|
820
|
+
window.location.href = `mailto:${action.params.email}`;
|
|
821
|
+
break;
|
|
822
|
+
|
|
823
|
+
case "goback":
|
|
824
|
+
// navigate(-1);
|
|
825
|
+
break;
|
|
826
|
+
|
|
827
|
+
case "goto":
|
|
828
|
+
// navigate(`/apps/${appId}/pages/${action.params.href}`);
|
|
829
|
+
break;
|
|
830
|
+
|
|
831
|
+
case "homepage":
|
|
832
|
+
// navigate(`/apps/${appId}/pages/${startPageId}`);
|
|
833
|
+
break;
|
|
834
|
+
|
|
835
|
+
case "logout":
|
|
836
|
+
setAccessToken(undefined);
|
|
837
|
+
// navigate(`/apps/${appId}/pages/${loginPageId}`);
|
|
838
|
+
break;
|
|
839
|
+
|
|
840
|
+
case "none":
|
|
841
|
+
// Nothing to do
|
|
842
|
+
break;
|
|
843
|
+
|
|
844
|
+
case "openurl":
|
|
845
|
+
window.open(action.params.url, "_blank");
|
|
846
|
+
break;
|
|
847
|
+
|
|
848
|
+
case "phone":
|
|
849
|
+
window.location.href = `tel:${action.params.phone}`;
|
|
850
|
+
break;
|
|
851
|
+
|
|
852
|
+
case "remote":
|
|
853
|
+
const params = btoa(JSON.stringify(action.params?.data || {}));
|
|
854
|
+
const url = `${baseURL}/devices/apps/${appId}/node/${id}/trigger/${action.event}?params=${params}`;
|
|
855
|
+
const remoteResponse = await axios.post(url);
|
|
856
|
+
if (remoteResponse.data?.action)
|
|
857
|
+
await handleAction(remoteResponse.data);
|
|
858
|
+
break;
|
|
859
|
+
|
|
860
|
+
case "set-access-token":
|
|
861
|
+
setAccessToken(action.params);
|
|
862
|
+
// navigate(`/apps/${appId}/pages/${startPageId}`);
|
|
863
|
+
break;
|
|
864
|
+
|
|
865
|
+
case "set-state":
|
|
866
|
+
// Check if this is part of an input group
|
|
867
|
+
if (_parentInputGroupInfo && _parentName) {
|
|
868
|
+
// For input groups, set the group value instead of the stateful set variant
|
|
869
|
+
console.log("🔵 Input group change:", {
|
|
870
|
+
groupName: _parentInputGroupInfo.groupName,
|
|
871
|
+
elementName: _parentName,
|
|
872
|
+
hasOnAction: typeof onAction === "function",
|
|
873
|
+
});
|
|
874
|
+
setInputGroupValue(_parentInputGroupInfo.groupName, _parentName);
|
|
875
|
+
|
|
876
|
+
// Trigger onAction callback with input group change
|
|
877
|
+
if (typeof onAction === "function") {
|
|
878
|
+
try {
|
|
879
|
+
await onAction({
|
|
880
|
+
bravo: {
|
|
881
|
+
cancel: () => {},
|
|
882
|
+
action: {
|
|
883
|
+
action: "input-group-change",
|
|
884
|
+
params: {
|
|
885
|
+
groupName: _parentInputGroupInfo.groupName,
|
|
886
|
+
groupType: _parentInputGroupInfo.type,
|
|
887
|
+
value: _parentName,
|
|
888
|
+
},
|
|
889
|
+
event: "tap",
|
|
890
|
+
},
|
|
891
|
+
},
|
|
892
|
+
});
|
|
893
|
+
console.log("✅ Input group change event fired");
|
|
894
|
+
} catch (e) {
|
|
895
|
+
console.error("❌ Error in onAction callback:", e);
|
|
896
|
+
}
|
|
897
|
+
} else {
|
|
898
|
+
console.warn("⚠️ No onAction callback provided");
|
|
899
|
+
}
|
|
900
|
+
} else {
|
|
901
|
+
// The stateSetId can come from either the action params or the nodeData
|
|
902
|
+
// BUT: If we have a parentStatefulSetId, prefer that (it's more reliable)
|
|
903
|
+
let stateSetId =
|
|
904
|
+
parentStatefulSetId ||
|
|
905
|
+
action.params?.stateSetId ||
|
|
906
|
+
patchedNodeData.data?.stateSetId;
|
|
907
|
+
if (!stateSetId) {
|
|
908
|
+
console.error("❌ No stateSetId found in action or nodeData!", {
|
|
909
|
+
actionParams: action.params,
|
|
910
|
+
nodeDataStateSetId: patchedNodeData.data?.stateSetId,
|
|
911
|
+
parentStatefulSetId,
|
|
912
|
+
});
|
|
913
|
+
break;
|
|
914
|
+
}
|
|
915
|
+
// Debug logging removed - uncomment if needed
|
|
916
|
+
// console.log("🔴 set-state action:", { stateSetId, state: action.params?.state });
|
|
917
|
+
setStatefulSetVariant(stateSetId, action.params.state);
|
|
918
|
+
}
|
|
919
|
+
break;
|
|
920
|
+
|
|
921
|
+
case "showalert":
|
|
922
|
+
alert(action.params.message);
|
|
923
|
+
break;
|
|
924
|
+
|
|
925
|
+
default:
|
|
926
|
+
console.log(action);
|
|
927
|
+
alert(action.action);
|
|
928
|
+
}
|
|
929
|
+
};
|
|
930
|
+
|
|
931
|
+
if (nodeData.href) {
|
|
932
|
+
return (
|
|
933
|
+
<>
|
|
934
|
+
<button
|
|
935
|
+
type="button"
|
|
936
|
+
onClick={() =>
|
|
937
|
+
handleAction({
|
|
938
|
+
action: "goto",
|
|
939
|
+
params: { href: nodeData.href },
|
|
940
|
+
event: "tap",
|
|
941
|
+
})
|
|
942
|
+
}
|
|
943
|
+
style={{
|
|
944
|
+
...style,
|
|
945
|
+
cursor: "pointer",
|
|
946
|
+
background: "transparent",
|
|
947
|
+
border: "none",
|
|
948
|
+
padding: 0,
|
|
949
|
+
}}
|
|
950
|
+
aria-label={`Go to ${nodeData.href}`}
|
|
951
|
+
></button>
|
|
952
|
+
{children}
|
|
953
|
+
</>
|
|
954
|
+
);
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
// Use original nodeData.actions if patchedNodeData has empty actions array
|
|
958
|
+
let actions =
|
|
959
|
+
patchedNodeData.actions && patchedNodeData.actions.length > 0
|
|
960
|
+
? patchedNodeData.actions
|
|
961
|
+
: nodeData.actions || [];
|
|
962
|
+
|
|
963
|
+
// For stateful compound components, if we have a parent StatefulSetComponent id,
|
|
964
|
+
// ensure the action uses it (even if actions already exist)
|
|
965
|
+
if (parentStatefulSetId && actions.length > 0) {
|
|
966
|
+
// Update existing actions to use parent's id
|
|
967
|
+
actions = actions.map((action: any) => {
|
|
968
|
+
if (action.action === "set-state") {
|
|
969
|
+
const oldStateSetId = action.params?.stateSetId;
|
|
970
|
+
const updatedAction = {
|
|
971
|
+
...action,
|
|
972
|
+
params: {
|
|
973
|
+
...action.params,
|
|
974
|
+
stateSetId: parentStatefulSetId, // Always use parent's id
|
|
975
|
+
},
|
|
976
|
+
};
|
|
977
|
+
// Debug logging removed - uncomment if needed
|
|
978
|
+
// if (oldStateSetId !== parentStatefulSetId) {
|
|
979
|
+
// console.log("🔄 Updated existing action:", { buttonId: id, oldStateSetId, newStateSetId: parentStatefulSetId });
|
|
980
|
+
// }
|
|
981
|
+
return updatedAction;
|
|
982
|
+
}
|
|
983
|
+
return action;
|
|
984
|
+
});
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
// For stateful compound components, if no actions but stateSetId exists, construct set-state action
|
|
988
|
+
// Use the parent StatefulSetComponent's id from context, or fall back to the button's stateSetId
|
|
989
|
+
// Special handling for input groups
|
|
990
|
+
if (
|
|
991
|
+
actions.length === 0 &&
|
|
992
|
+
(patchedNodeData.data?.stateSetId || parentStatefulSetId)
|
|
993
|
+
) {
|
|
994
|
+
// Check if this is part of an input group
|
|
995
|
+
if (_parentInputGroupInfo && _parentName) {
|
|
996
|
+
// For input groups, create a set-state action that will be handled specially
|
|
997
|
+
actions = [
|
|
998
|
+
{
|
|
999
|
+
action: "set-state",
|
|
1000
|
+
params: {
|
|
1001
|
+
state: "active", // Will be handled by input group logic
|
|
1002
|
+
},
|
|
1003
|
+
event: "tap",
|
|
1004
|
+
},
|
|
1005
|
+
];
|
|
1006
|
+
} else {
|
|
1007
|
+
// Prefer parent StatefulSetComponent's id from context, otherwise use button's stateSetId
|
|
1008
|
+
const targetStateSetId =
|
|
1009
|
+
parentStatefulSetId || patchedNodeData.data.stateSetId;
|
|
1010
|
+
|
|
1011
|
+
if (!targetStateSetId) {
|
|
1012
|
+
console.warn(
|
|
1013
|
+
"⚠️ No stateSetId available for stateful compound component",
|
|
1014
|
+
{ id, name: nodeData.name }
|
|
1015
|
+
);
|
|
1016
|
+
} else {
|
|
1017
|
+
const currentVariant =
|
|
1018
|
+
useEncoreState.getState().statefulSetVariants[targetStateSetId];
|
|
1019
|
+
// Toggle between default and active, or use active if current is default
|
|
1020
|
+
const newState = currentVariant === "active" ? "default" : "active";
|
|
1021
|
+
actions = [
|
|
1022
|
+
{
|
|
1023
|
+
action: "set-state",
|
|
1024
|
+
params: {
|
|
1025
|
+
state: newState,
|
|
1026
|
+
stateSetId: targetStateSetId, // Use parent's id or button's stateSetId
|
|
1027
|
+
},
|
|
1028
|
+
event: "tap",
|
|
1029
|
+
},
|
|
1030
|
+
];
|
|
1031
|
+
// Debug logging removed - uncomment if needed
|
|
1032
|
+
// console.log("🔧 Constructed action:", { buttonId: id, targetStateSetId, newState });
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
if (actions && actions.length > 0) {
|
|
1038
|
+
const action = actions[0];
|
|
1039
|
+
if (action.event !== "tap") {
|
|
1040
|
+
return children;
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
return (
|
|
1044
|
+
<>
|
|
1045
|
+
{children}
|
|
1046
|
+
<div
|
|
1047
|
+
onClick={(e) => {
|
|
1048
|
+
e.stopPropagation();
|
|
1049
|
+
handleAction(action);
|
|
1050
|
+
}}
|
|
1051
|
+
onMouseDown={(e) => e.stopPropagation()}
|
|
1052
|
+
onMouseUp={(e) => e.stopPropagation()}
|
|
1053
|
+
style={{ ...style, cursor: "pointer", pointerEvents: "auto" }}
|
|
1054
|
+
></div>
|
|
1055
|
+
</>
|
|
1056
|
+
);
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
return children;
|
|
1060
|
+
};
|
|
1061
|
+
|
|
1062
|
+
const WebViewComponent: React.FC<ComponentProps> = ({ id, name, nodeData }) => {
|
|
1063
|
+
const style = useEncoreStyle(nodeData.style, {
|
|
1064
|
+
debug: false,
|
|
1065
|
+
});
|
|
1066
|
+
|
|
1067
|
+
return (
|
|
1068
|
+
<iframe
|
|
1069
|
+
data-id={id}
|
|
1070
|
+
data-name={name}
|
|
1071
|
+
data-type="WebViewComponent"
|
|
1072
|
+
style={{ border: "none", ...style }}
|
|
1073
|
+
src={nodeData.data?.params?.url}
|
|
1074
|
+
/>
|
|
1075
|
+
);
|
|
1076
|
+
};
|
|
1077
|
+
|
|
1078
|
+
const ColorComponent: React.FC<ComponentProps> = ({ id, name, nodeData }) => {
|
|
1079
|
+
const style = useEncoreStyle(nodeData.style, {
|
|
1080
|
+
debug: false,
|
|
1081
|
+
});
|
|
1082
|
+
|
|
1083
|
+
// Render if there's a backgroundColor OR a borderColor (for vector icons drawn with borders)
|
|
1084
|
+
if (!style.backgroundColor && !style.borderColor) {
|
|
1085
|
+
return null;
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
// If there's a borderColor but no backgroundColor, set backgroundColor to transparent
|
|
1089
|
+
// so the element renders and the border is visible
|
|
1090
|
+
if (style.borderColor && !style.backgroundColor) {
|
|
1091
|
+
style.backgroundColor = "transparent";
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
// For icon components using borderColor to draw vector shapes, we need to ensure
|
|
1095
|
+
// the element has proper dimensions and the border is visible
|
|
1096
|
+
// Note: CSS borders can't draw complex vector shapes, but this at least makes them render
|
|
1097
|
+
// For icon components, we need to ensure they have the correct size from the layout
|
|
1098
|
+
if (style.borderColor && style.borderWidth) {
|
|
1099
|
+
// The element should already have dimensions from the layout system
|
|
1100
|
+
// But ensure minimum dimensions if they're missing or zero
|
|
1101
|
+
const minSize = Math.max(style.borderWidth || 0, 1);
|
|
1102
|
+
if (
|
|
1103
|
+
!style.width ||
|
|
1104
|
+
(typeof style.width === "number" && style.width === 0)
|
|
1105
|
+
) {
|
|
1106
|
+
style.width = minSize;
|
|
1107
|
+
}
|
|
1108
|
+
if (
|
|
1109
|
+
!style.height ||
|
|
1110
|
+
(typeof style.height === "number" && style.height === 0)
|
|
1111
|
+
) {
|
|
1112
|
+
style.height = minSize;
|
|
1113
|
+
}
|
|
1114
|
+
// For icon components, ensure the element is visible even if dimensions are small
|
|
1115
|
+
// Use box-sizing to ensure borders are included in the element size
|
|
1116
|
+
style.boxSizing = "border-box";
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
// Check if this ColorComponent has the bravo:layer tag
|
|
1120
|
+
// If so, make it absolutely positioned behind other content
|
|
1121
|
+
const hasBravoLayerTag =
|
|
1122
|
+
Array.isArray(nodeData.tags) && nodeData.tags.includes("bravo:layer");
|
|
1123
|
+
const parentIsFlex = useContext(EncoreContainerContext)?.isFlex ?? false;
|
|
1124
|
+
|
|
1125
|
+
// Check if this component has positioning that fills the container
|
|
1126
|
+
const positioning = nodeData.style?.positioning;
|
|
1127
|
+
const fillsContainer =
|
|
1128
|
+
positioning &&
|
|
1129
|
+
typeof positioning.top === "number" &&
|
|
1130
|
+
typeof positioning.left === "number" &&
|
|
1131
|
+
typeof positioning.right === "number" &&
|
|
1132
|
+
typeof positioning.bottom === "number" &&
|
|
1133
|
+
positioning.top === 0 &&
|
|
1134
|
+
positioning.left === 0 &&
|
|
1135
|
+
positioning.right === 0 &&
|
|
1136
|
+
positioning.bottom === 0;
|
|
1137
|
+
|
|
1138
|
+
// Apply bravo:layer styling if needed
|
|
1139
|
+
const shouldApplyLayerStyling =
|
|
1140
|
+
hasBravoLayerTag && (parentIsFlex || fillsContainer);
|
|
1141
|
+
|
|
1142
|
+
// If this is a "Clip" component (clipping mask), make it transparent
|
|
1143
|
+
// Clipping masks in Figma are used for masking but shouldn't be visible
|
|
1144
|
+
const isClipComponent = name && name.toLowerCase().startsWith("clip");
|
|
1145
|
+
|
|
1146
|
+
if (shouldApplyLayerStyling) {
|
|
1147
|
+
// Make it absolutely positioned to fill the container
|
|
1148
|
+
// This prevents it from taking up space in the flex layout
|
|
1149
|
+
// and ensures it stays behind other content
|
|
1150
|
+
style.position = "absolute";
|
|
1151
|
+
style.top = 0;
|
|
1152
|
+
style.left = 0;
|
|
1153
|
+
style.right = 0;
|
|
1154
|
+
style.bottom = 0;
|
|
1155
|
+
// Set z-index to ensure it stays behind other content
|
|
1156
|
+
// Use a negative value to ensure it's behind all normal content
|
|
1157
|
+
style.zIndex = -1;
|
|
1158
|
+
// Remove width/height that might interfere with absolute positioning
|
|
1159
|
+
if (
|
|
1160
|
+
style.width &&
|
|
1161
|
+
typeof style.width === "string" &&
|
|
1162
|
+
style.width.includes("%")
|
|
1163
|
+
) {
|
|
1164
|
+
delete style.width;
|
|
1165
|
+
}
|
|
1166
|
+
if (
|
|
1167
|
+
style.height &&
|
|
1168
|
+
typeof style.height === "string" &&
|
|
1169
|
+
style.height.includes("%")
|
|
1170
|
+
) {
|
|
1171
|
+
delete style.height;
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
// Make clip components transparent so they don't block content
|
|
1176
|
+
if (isClipComponent) {
|
|
1177
|
+
style.backgroundColor = "transparent";
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
return (
|
|
1181
|
+
<EncoreLinkActionWrapper id={id} nodeData={nodeData}>
|
|
1182
|
+
<div data-name={name} data-type="ColorComponent" style={style} />
|
|
1183
|
+
</EncoreLinkActionWrapper>
|
|
1184
|
+
);
|
|
1185
|
+
};
|
|
1186
|
+
|
|
1187
|
+
const GradientComponent: React.FC<ComponentProps> = ({
|
|
1188
|
+
id,
|
|
1189
|
+
name,
|
|
1190
|
+
nodeData,
|
|
1191
|
+
}) => {
|
|
1192
|
+
// Start from the base Encore style so layout/positioning are honored
|
|
1193
|
+
const style = useEncoreStyle(nodeData.style, {
|
|
1194
|
+
debug: false,
|
|
1195
|
+
}) as React.CSSProperties;
|
|
1196
|
+
|
|
1197
|
+
const gradientStyle = nodeData.style;
|
|
1198
|
+
const stops = Array.isArray(gradientStyle?.gradientColorStops)
|
|
1199
|
+
? gradientStyle.gradientColorStops
|
|
1200
|
+
.map((stop: any) => {
|
|
1201
|
+
if (!stop?.color) return null;
|
|
1202
|
+
const color = argbToRgba(stop.color);
|
|
1203
|
+
if (typeof stop.position === "number") {
|
|
1204
|
+
// Bravo positions are 0–1; CSS expects percentages
|
|
1205
|
+
const pct = stop.position * 100;
|
|
1206
|
+
return `${color} ${pct}%`;
|
|
1207
|
+
}
|
|
1208
|
+
return color;
|
|
1209
|
+
})
|
|
1210
|
+
.filter(Boolean)
|
|
1211
|
+
.join(", ")
|
|
1212
|
+
: "";
|
|
1213
|
+
|
|
1214
|
+
if (stops) {
|
|
1215
|
+
const type = gradientStyle?.gradientType || "linear";
|
|
1216
|
+
// For now we ignore gradientPositions for angle calculation and rely on default direction.
|
|
1217
|
+
style.backgroundImage =
|
|
1218
|
+
type === "radial"
|
|
1219
|
+
? `radial-gradient(${stops})`
|
|
1220
|
+
: `linear-gradient(${stops})`;
|
|
1221
|
+
// Prefer the gradient over any solid background color
|
|
1222
|
+
delete style.backgroundColor;
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
return (
|
|
1226
|
+
<EncoreLinkActionWrapper id={id} nodeData={nodeData}>
|
|
1227
|
+
<div data-name={name} data-type="GradientComponent" style={style} />
|
|
1228
|
+
</EncoreLinkActionWrapper>
|
|
1229
|
+
);
|
|
1230
|
+
};
|
|
1231
|
+
|
|
1232
|
+
const ImageComponent: React.FC<ComponentProps> = ({ id, name, nodeData }) => {
|
|
1233
|
+
const { patchedNodeData } = useEncoreBinding({ id, nodeData });
|
|
1234
|
+
nodeData = patchedNodeData;
|
|
1235
|
+
|
|
1236
|
+
const style = useEncoreStyle(nodeData.style, {
|
|
1237
|
+
debug: false,
|
|
1238
|
+
});
|
|
1239
|
+
const containerContext = useContext(EncoreContainerContext);
|
|
1240
|
+
const assetsById = useEncoreState((state) => state.assetsById);
|
|
1241
|
+
// Honor scale mode semantics
|
|
1242
|
+
const scaleMode = nodeData.style?.scaleMode || "fill";
|
|
1243
|
+
if (scaleMode === "fill") {
|
|
1244
|
+
style.objectFit = "cover";
|
|
1245
|
+
} else if (scaleMode === "fit") {
|
|
1246
|
+
style.objectFit = "contain";
|
|
1247
|
+
}
|
|
1248
|
+
style.objectPosition = "center";
|
|
1249
|
+
|
|
1250
|
+
// Use encore data image URL if available
|
|
1251
|
+
// Check both imageUrl (set by useEncoreBinding) and encoreData.image (fallback)
|
|
1252
|
+
let imageUrl = nodeData.data?.imageUrl;
|
|
1253
|
+
if (!imageUrl && nodeData.encoreData?.image) {
|
|
1254
|
+
// Handle protocol-relative URLs (starting with //) by prepending https:
|
|
1255
|
+
imageUrl = nodeData.encoreData.image.startsWith("//")
|
|
1256
|
+
? `https:${nodeData.encoreData.image}`
|
|
1257
|
+
: nodeData.encoreData.image;
|
|
1258
|
+
}
|
|
1259
|
+
// Fallback to asset URL if no encore data
|
|
1260
|
+
if (!imageUrl) {
|
|
1261
|
+
imageUrl = assetsById[nodeData.assetId]?.url;
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
// Debug logging removed
|
|
1265
|
+
|
|
1266
|
+
// Maintain aspect ratio within inset box for images only
|
|
1267
|
+
const pos = nodeData.style?.positioning;
|
|
1268
|
+
const aspectRatio = nodeData.style?.aspectRatio;
|
|
1269
|
+
const cw = containerContext.dimensions?.width || 0;
|
|
1270
|
+
const ch = containerContext.dimensions?.height || 0;
|
|
1271
|
+
if (pos && aspectRatio && cw > 0) {
|
|
1272
|
+
const hasLR = typeof pos.left === "number" && typeof pos.right === "number";
|
|
1273
|
+
const hasTB = typeof pos.top === "number" && typeof pos.bottom === "number";
|
|
1274
|
+
if (hasLR) {
|
|
1275
|
+
const pctWidth = 100 - pos.left - pos.right;
|
|
1276
|
+
if (pctWidth > 0) {
|
|
1277
|
+
const widthPx = (pctWidth * cw) / 100;
|
|
1278
|
+
style.width = widthPx;
|
|
1279
|
+
style.height = widthPx / aspectRatio;
|
|
1280
|
+
// Anchor from top/left; allow natural height
|
|
1281
|
+
// Clamp negative positioning values to 0 (they typically indicate visual extension like shadows)
|
|
1282
|
+
style.left = `${Math.max(0, pos.left)}%`;
|
|
1283
|
+
style.top = `${Math.max(0, pos.top)}%`;
|
|
1284
|
+
delete style.right;
|
|
1285
|
+
delete style.bottom;
|
|
1286
|
+
}
|
|
1287
|
+
} else if (hasTB && ch > 0) {
|
|
1288
|
+
const pctHeight = 100 - pos.top - pos.bottom;
|
|
1289
|
+
if (pctHeight > 0) {
|
|
1290
|
+
const heightPx = (pctHeight * ch) / 100;
|
|
1291
|
+
style.height = heightPx;
|
|
1292
|
+
style.width = heightPx * aspectRatio;
|
|
1293
|
+
// Clamp negative positioning values to 0
|
|
1294
|
+
style.left = `${Math.max(0, pos.left)}%`;
|
|
1295
|
+
style.top = `${Math.max(0, pos.top)}%`;
|
|
1296
|
+
delete style.right;
|
|
1297
|
+
delete style.bottom;
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
return (
|
|
1303
|
+
<EncoreLinkActionWrapper id={id} nodeData={nodeData}>
|
|
1304
|
+
<img
|
|
1305
|
+
data-id={id}
|
|
1306
|
+
data-name={name}
|
|
1307
|
+
data-type="ImageComponent"
|
|
1308
|
+
style={style}
|
|
1309
|
+
src={imageUrl || assetsById[nodeData.assetId]?.url}
|
|
1310
|
+
alt={name}
|
|
1311
|
+
/>
|
|
1312
|
+
</EncoreLinkActionWrapper>
|
|
1313
|
+
);
|
|
1314
|
+
};
|
|
1315
|
+
|
|
1316
|
+
const EmailInputComponent: React.FC<ComponentProps> = ({
|
|
1317
|
+
id,
|
|
1318
|
+
name,
|
|
1319
|
+
nodeData,
|
|
1320
|
+
}) => {
|
|
1321
|
+
const setFormInputValue = useEncoreState((state) => state.setFormInputValue);
|
|
1322
|
+
const style = useEncoreStyle(nodeData.style, { debug: false });
|
|
1323
|
+
return (
|
|
1324
|
+
<input
|
|
1325
|
+
onChange={(e) => setFormInputValue(id, e.target.value)}
|
|
1326
|
+
data-id={id}
|
|
1327
|
+
data-name={name}
|
|
1328
|
+
data-type="EmailInputComponent"
|
|
1329
|
+
placeholder={nodeData.data.text}
|
|
1330
|
+
style={style}
|
|
1331
|
+
type="email"
|
|
1332
|
+
/>
|
|
1333
|
+
);
|
|
1334
|
+
};
|
|
1335
|
+
|
|
1336
|
+
const HiddenInputComponent: React.FC<ComponentProps> = ({
|
|
1337
|
+
id,
|
|
1338
|
+
name,
|
|
1339
|
+
nodeData,
|
|
1340
|
+
}) => {
|
|
1341
|
+
const style = useEncoreStyle(nodeData.style);
|
|
1342
|
+
style.visibility = "hidden";
|
|
1343
|
+
return (
|
|
1344
|
+
<input
|
|
1345
|
+
data-id={id}
|
|
1346
|
+
data-name={name}
|
|
1347
|
+
data-type="HiddenInputComponent"
|
|
1348
|
+
style={style}
|
|
1349
|
+
type="hidden"
|
|
1350
|
+
/>
|
|
1351
|
+
);
|
|
1352
|
+
};
|
|
1353
|
+
|
|
1354
|
+
const ImageInputComponent: React.FC<ComponentProps> = ({
|
|
1355
|
+
id,
|
|
1356
|
+
name,
|
|
1357
|
+
nodeData,
|
|
1358
|
+
}) => {
|
|
1359
|
+
const setFormInputValue = useEncoreState((state) => state.setFormInputValue);
|
|
1360
|
+
const style = useEncoreStyle(nodeData.style, { debug: false });
|
|
1361
|
+
const assetsById = useEncoreState((state) => state.assetsById);
|
|
1362
|
+
|
|
1363
|
+
return (
|
|
1364
|
+
<>
|
|
1365
|
+
<div
|
|
1366
|
+
style={{ ...style, zIndex: 9999, cursor: "pointer" }}
|
|
1367
|
+
onClick={() => alert("Image picker")}
|
|
1368
|
+
></div>
|
|
1369
|
+
<img
|
|
1370
|
+
onChange={(e) => setFormInputValue(id, e.target.value)}
|
|
1371
|
+
style={style}
|
|
1372
|
+
data-id={id}
|
|
1373
|
+
data-name={name}
|
|
1374
|
+
data-type="ImageInputComponent"
|
|
1375
|
+
src={assetsById[nodeData.assetId]?.url}
|
|
1376
|
+
alt={name}
|
|
1377
|
+
/>
|
|
1378
|
+
</>
|
|
1379
|
+
);
|
|
1380
|
+
};
|
|
1381
|
+
|
|
1382
|
+
const PasswordInputComponent: React.FC<ComponentProps> = ({
|
|
1383
|
+
id,
|
|
1384
|
+
name,
|
|
1385
|
+
nodeData,
|
|
1386
|
+
}) => {
|
|
1387
|
+
const setFormInputValue = useEncoreState((state) => state.setFormInputValue);
|
|
1388
|
+
const style = useEncoreStyle(nodeData.style, { debug: false });
|
|
1389
|
+
|
|
1390
|
+
return (
|
|
1391
|
+
<input
|
|
1392
|
+
onChange={(e) => setFormInputValue(id, e.target.value)}
|
|
1393
|
+
data-id={id}
|
|
1394
|
+
data-name={name}
|
|
1395
|
+
data-type="PasswordInputComponent"
|
|
1396
|
+
placeholder={nodeData.data.text}
|
|
1397
|
+
style={style}
|
|
1398
|
+
type="password"
|
|
1399
|
+
/>
|
|
1400
|
+
);
|
|
1401
|
+
};
|
|
1402
|
+
|
|
1403
|
+
// Type for select options
|
|
1404
|
+
type SelectOption = { value: string; label: string };
|
|
1405
|
+
|
|
1406
|
+
const SelectInputComponent: React.FC<ComponentProps> = ({
|
|
1407
|
+
id,
|
|
1408
|
+
name,
|
|
1409
|
+
nodeData,
|
|
1410
|
+
}) => {
|
|
1411
|
+
const setFormInputValue = useEncoreState((state) => state.setFormInputValue);
|
|
1412
|
+
const { onAction } = React.useContext(EncoreActionContext);
|
|
1413
|
+
const { patchedNodeData } = useEncoreBinding({ id, nodeData });
|
|
1414
|
+
const style = useEncoreStyle(patchedNodeData.style, { debug: false });
|
|
1415
|
+
|
|
1416
|
+
// Override sizing to fill parent container - select inputs should expand to fill their flex parent
|
|
1417
|
+
// The Figma style often has layoutSizingHorizontal: "HUG" which sizes to content,
|
|
1418
|
+
// but in a flex container with SPACE_BETWEEN, the select should fill available space
|
|
1419
|
+
style.width = "100%";
|
|
1420
|
+
style.flex = "1";
|
|
1421
|
+
style.minWidth = "0"; // Allow shrinking in flex context
|
|
1422
|
+
style.boxSizing = "border-box";
|
|
1423
|
+
style.borderColor = style.borderColor ?? "transparent";
|
|
1424
|
+
|
|
1425
|
+
// Support controlled value from binding
|
|
1426
|
+
const controlledValue = patchedNodeData.data?.value;
|
|
1427
|
+
|
|
1428
|
+
// Support options from binding - can be array of {value, label} or just strings
|
|
1429
|
+
const rawOptions = patchedNodeData.data?.options;
|
|
1430
|
+
const options: SelectOption[] = React.useMemo(() => {
|
|
1431
|
+
if (!rawOptions || !Array.isArray(rawOptions)) {
|
|
1432
|
+
return []; // No options provided
|
|
1433
|
+
}
|
|
1434
|
+
return rawOptions.map((opt: string | SelectOption) => {
|
|
1435
|
+
if (typeof opt === "string") {
|
|
1436
|
+
return { value: opt, label: opt };
|
|
1437
|
+
}
|
|
1438
|
+
return opt;
|
|
1439
|
+
});
|
|
1440
|
+
}, [rawOptions]);
|
|
1441
|
+
|
|
1442
|
+
const handleChange = async (e: React.ChangeEvent<HTMLSelectElement>) => {
|
|
1443
|
+
const newValue = e.target.value;
|
|
1444
|
+
setFormInputValue(id, newValue);
|
|
1445
|
+
console.log(`Select changed: ${newValue}`);
|
|
1446
|
+
|
|
1447
|
+
// Emit action for wrapper components to handle
|
|
1448
|
+
if (typeof onAction === "function") {
|
|
1449
|
+
try {
|
|
1450
|
+
await onAction({
|
|
1451
|
+
bravo: {
|
|
1452
|
+
cancel: () => {},
|
|
1453
|
+
action: {
|
|
1454
|
+
action: "select-change",
|
|
1455
|
+
nodeId: id,
|
|
1456
|
+
params: { value: newValue },
|
|
1457
|
+
},
|
|
1458
|
+
},
|
|
1459
|
+
});
|
|
1460
|
+
} catch (e) {
|
|
1461
|
+
// Ignore callback errors
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
};
|
|
1465
|
+
|
|
1466
|
+
return (
|
|
1467
|
+
<select
|
|
1468
|
+
data-id={id}
|
|
1469
|
+
data-name={name}
|
|
1470
|
+
data-type="SelectInputComponent"
|
|
1471
|
+
value={controlledValue ?? ""}
|
|
1472
|
+
style={style}
|
|
1473
|
+
onChange={handleChange}
|
|
1474
|
+
>
|
|
1475
|
+
{options.length === 0 && (
|
|
1476
|
+
<option value="" disabled>
|
|
1477
|
+
Select an option...
|
|
1478
|
+
</option>
|
|
1479
|
+
)}
|
|
1480
|
+
{options.map((opt) => (
|
|
1481
|
+
<option key={opt.value} value={opt.value}>
|
|
1482
|
+
{opt.label}
|
|
1483
|
+
</option>
|
|
1484
|
+
))}
|
|
1485
|
+
</select>
|
|
1486
|
+
);
|
|
1487
|
+
};
|
|
1488
|
+
|
|
1489
|
+
const TextInputComponent: React.FC<ComponentProps> = ({
|
|
1490
|
+
id,
|
|
1491
|
+
name,
|
|
1492
|
+
nodeData,
|
|
1493
|
+
}) => {
|
|
1494
|
+
const setFormInputValue = useEncoreState((state) => state.setFormInputValue);
|
|
1495
|
+
|
|
1496
|
+
const style = useEncoreStyle(nodeData.style, {
|
|
1497
|
+
debug: false,
|
|
1498
|
+
});
|
|
1499
|
+
|
|
1500
|
+
return (
|
|
1501
|
+
<input
|
|
1502
|
+
onChange={(e) => setFormInputValue(id, e.target.value)}
|
|
1503
|
+
name={id}
|
|
1504
|
+
data-id={id}
|
|
1505
|
+
data-name={name}
|
|
1506
|
+
data-type="TextInputComponent"
|
|
1507
|
+
placeholder={nodeData.data.text}
|
|
1508
|
+
style={style}
|
|
1509
|
+
type="text"
|
|
1510
|
+
/>
|
|
1511
|
+
);
|
|
1512
|
+
};
|
|
1513
|
+
|
|
1514
|
+
const LottieComponent: React.FC<ComponentProps> = ({ id, name, nodeData }) => {
|
|
1515
|
+
const style = useEncoreStyle(nodeData.style);
|
|
1516
|
+
return (
|
|
1517
|
+
<EncoreLinkActionWrapper id={id} nodeData={nodeData}>
|
|
1518
|
+
<div
|
|
1519
|
+
data-id={id}
|
|
1520
|
+
data-name={name}
|
|
1521
|
+
data-type="LottieComponent"
|
|
1522
|
+
style={style}
|
|
1523
|
+
>
|
|
1524
|
+
{children}
|
|
1525
|
+
</div>
|
|
1526
|
+
</EncoreLinkActionWrapper>
|
|
1527
|
+
);
|
|
1528
|
+
};
|
|
1529
|
+
|
|
1530
|
+
const SvgComponent: React.FC<ComponentProps> = ({ id, name, nodeData }) => {
|
|
1531
|
+
const style = useEncoreStyle(nodeData.style, {
|
|
1532
|
+
debug: false,
|
|
1533
|
+
});
|
|
1534
|
+
const assetsById = useEncoreState((state) => state.assetsById);
|
|
1535
|
+
|
|
1536
|
+
return (
|
|
1537
|
+
<EncoreLinkActionWrapper id={id} nodeData={nodeData}>
|
|
1538
|
+
<img
|
|
1539
|
+
data-id={id}
|
|
1540
|
+
data-name={name}
|
|
1541
|
+
data-type="SvgComponent"
|
|
1542
|
+
style={style}
|
|
1543
|
+
src={assetsById[nodeData.assetId]?.url}
|
|
1544
|
+
alt={name}
|
|
1545
|
+
/>
|
|
1546
|
+
</EncoreLinkActionWrapper>
|
|
1547
|
+
);
|
|
1548
|
+
};
|
|
1549
|
+
|
|
1550
|
+
const TextComponent: React.FC<ComponentProps> = ({ id, name, nodeData }) => {
|
|
1551
|
+
const { patchedNodeData } = useEncoreBinding({ id, nodeData });
|
|
1552
|
+
nodeData = patchedNodeData;
|
|
1553
|
+
const textContent = nodeData.data?.text;
|
|
1554
|
+
|
|
1555
|
+
if (name && name.includes("Purpose")) {
|
|
1556
|
+
console.log("TextComponent Debug:", {
|
|
1557
|
+
id,
|
|
1558
|
+
name,
|
|
1559
|
+
nodeData,
|
|
1560
|
+
style: nodeData.style,
|
|
1561
|
+
color: nodeData.style?.color,
|
|
1562
|
+
});
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
const bravoStyle = nodeData.style;
|
|
1566
|
+
// CRITICAL: fontId is stored in nodeData.fontId, not nodeData.style.fontId
|
|
1567
|
+
// Merge it into the style object so useEncoreStyle can apply the font-family
|
|
1568
|
+
const styleWithFont = nodeData.fontId
|
|
1569
|
+
? { ...bravoStyle, fontId: nodeData.fontId }
|
|
1570
|
+
: bravoStyle;
|
|
1571
|
+
const style = useEncoreStyle(styleWithFont, {
|
|
1572
|
+
debug: false,
|
|
1573
|
+
});
|
|
1574
|
+
|
|
1575
|
+
// Only set overflow: hidden if explicitly needed (e.g., for text truncation)
|
|
1576
|
+
// Don't force it as it clips text that's slightly larger than container
|
|
1577
|
+
// style.overflow = "hidden";
|
|
1578
|
+
style.display = "flex";
|
|
1579
|
+
style.flexDirection = "column";
|
|
1580
|
+
|
|
1581
|
+
if (bravoStyle.verticalPosition) {
|
|
1582
|
+
style.justifyContent = {
|
|
1583
|
+
"from-top": "flex-start",
|
|
1584
|
+
center: "center",
|
|
1585
|
+
"from-bottom": "flex-end",
|
|
1586
|
+
}[bravoStyle.verticalPosition];
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
// Debug logging removed
|
|
1590
|
+
|
|
1591
|
+
// Handle line breaks in text - split by newlines and render each line
|
|
1592
|
+
// This ensures proper line breaks when using flex column layout
|
|
1593
|
+
const hasLineBreaks =
|
|
1594
|
+
textContent && (textContent.includes("\n") || textContent.includes("\r"));
|
|
1595
|
+
let renderedContent;
|
|
1596
|
+
if (hasLineBreaks && style.flexDirection === "column") {
|
|
1597
|
+
// Split by newlines and carriage returns, filter out empty strings from \r\n combinations
|
|
1598
|
+
const lines = textContent.split(/\r?\n/).filter((line) => line.length > 0);
|
|
1599
|
+
renderedContent = lines.map((line, index) => (
|
|
1600
|
+
<div key={index} style={{ lineHeight: "inherit" }}>
|
|
1601
|
+
{line}
|
|
1602
|
+
</div>
|
|
1603
|
+
));
|
|
1604
|
+
} else {
|
|
1605
|
+
renderedContent = textContent;
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
// TODO translation table lookup
|
|
1609
|
+
|
|
1610
|
+
return (
|
|
1611
|
+
<EncoreLinkActionWrapper id={id} nodeData={nodeData}>
|
|
1612
|
+
<div
|
|
1613
|
+
data-id={id}
|
|
1614
|
+
data-name={name}
|
|
1615
|
+
data-type="TextComponent"
|
|
1616
|
+
style={style}
|
|
1617
|
+
>
|
|
1618
|
+
{renderedContent}
|
|
1619
|
+
</div>
|
|
1620
|
+
</EncoreLinkActionWrapper>
|
|
1621
|
+
);
|
|
1622
|
+
};
|
|
1623
|
+
|
|
1624
|
+
const StatefulSetComponent: React.FC<ComponentProps> = ({
|
|
1625
|
+
id,
|
|
1626
|
+
name,
|
|
1627
|
+
nodeData,
|
|
1628
|
+
children,
|
|
1629
|
+
}) => {
|
|
1630
|
+
const bindingContext = useContext(EncoreBindingContext);
|
|
1631
|
+
const { patchedNodeData } = useEncoreBinding({ id, nodeData });
|
|
1632
|
+
nodeData = patchedNodeData;
|
|
1633
|
+
|
|
1634
|
+
// Check if this is part of an input group
|
|
1635
|
+
const inputGroupInfo = parseInputGroupTag(nodeData.tags);
|
|
1636
|
+
const inputGroupValue = inputGroupInfo
|
|
1637
|
+
? useEncoreState((state) => state.inputGroups[inputGroupInfo.groupName])
|
|
1638
|
+
: null;
|
|
1639
|
+
|
|
1640
|
+
// StatefulSetComponent should use its own id to look up variants
|
|
1641
|
+
// The buttons inside will set the variant using this component's id
|
|
1642
|
+
// IMPORTANT: Use id directly in the selector to avoid closure issues
|
|
1643
|
+
// Use id directly in the selector to avoid closure issues
|
|
1644
|
+
const variant = useEncoreState((state) => state.statefulSetVariants[id]);
|
|
1645
|
+
|
|
1646
|
+
// Debug logging removed - uncomment if needed
|
|
1647
|
+
// if (name === "Content purpose" || name === "Dashboard purpose" || name === "Forms purpose") {
|
|
1648
|
+
// console.log("🟣 StatefulSetComponent:", { id, stateSetId, name, variant });
|
|
1649
|
+
// }
|
|
1650
|
+
|
|
1651
|
+
const style = useEncoreStyle(nodeData.style, {
|
|
1652
|
+
debug: false,
|
|
1653
|
+
});
|
|
1654
|
+
|
|
1655
|
+
// For input groups, check if this element is the active one
|
|
1656
|
+
// If it's an input group, use the group value; otherwise use the variant
|
|
1657
|
+
let resolvedState: string;
|
|
1658
|
+
if (inputGroupInfo && inputGroupValue !== null) {
|
|
1659
|
+
// If this element's name matches the active group value, it should be active
|
|
1660
|
+
resolvedState = inputGroupValue === name ? "active" : "default";
|
|
1661
|
+
} else {
|
|
1662
|
+
resolvedState = variant || nodeData.data.initialState || "default";
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
// Debug logging removed - uncomment if needed
|
|
1666
|
+
// if (name === "Content purpose" || name === "Product Purpose") {
|
|
1667
|
+
// console.log("🟣 StatefulSetComponent Debug:", { id, stateSetId, name, variant, resolvedState });
|
|
1668
|
+
// }
|
|
1669
|
+
|
|
1670
|
+
// Only include the active state
|
|
1671
|
+
// Clone the child and pass the parent StatefulSetComponent's id as a prop
|
|
1672
|
+
const child = React.Children.toArray(children).find((child: any) => {
|
|
1673
|
+
if (!React.isValidElement(child)) return false;
|
|
1674
|
+
// @ts-ignore
|
|
1675
|
+
return child.props?.nodeData?.data?.state === resolvedState;
|
|
1676
|
+
});
|
|
1677
|
+
|
|
1678
|
+
// Clone child to pass parent StatefulSetComponent id and input group info
|
|
1679
|
+
const childWithParentId = React.isValidElement(child)
|
|
1680
|
+
? React.cloneElement(child as React.ReactElement<any>, {
|
|
1681
|
+
_parentStatefulSetId: id,
|
|
1682
|
+
_parentInputGroupInfo: inputGroupInfo,
|
|
1683
|
+
_parentName: name, // Pass the name so we can identify which element is active
|
|
1684
|
+
})
|
|
1685
|
+
: child;
|
|
1686
|
+
|
|
1687
|
+
// Stateful sets have nested binding context so we need to provide a new context here
|
|
1688
|
+
// However, for input-stateful-set, we should preserve the parent context (like DefaultLayerComponent does)
|
|
1689
|
+
// to avoid stripping necessary data.
|
|
1690
|
+
const content = (
|
|
1691
|
+
<div
|
|
1692
|
+
data-id={id}
|
|
1693
|
+
data-name={name}
|
|
1694
|
+
data-type="StatefulSetComponent"
|
|
1695
|
+
style={style}
|
|
1696
|
+
>
|
|
1697
|
+
{childWithParentId}
|
|
1698
|
+
</div>
|
|
1699
|
+
);
|
|
1700
|
+
|
|
1701
|
+
if (nodeData.type === "component:input-stateful-set") {
|
|
1702
|
+
return content;
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
// Get the current context value to merge with our statefulSetId
|
|
1706
|
+
const currentComponentIdContext = React.useContext(EncoreComponentIdContext);
|
|
1707
|
+
|
|
1708
|
+
return (
|
|
1709
|
+
<EncoreBindingContext.Provider
|
|
1710
|
+
value={{ nodeData: bindingContext?.nodeData?.[id] || {} }}
|
|
1711
|
+
>
|
|
1712
|
+
<EncoreComponentIdContext.Provider
|
|
1713
|
+
value={{ ...currentComponentIdContext, statefulSetId: id }}
|
|
1714
|
+
>
|
|
1715
|
+
{content}
|
|
1716
|
+
</EncoreComponentIdContext.Provider>
|
|
1717
|
+
</EncoreBindingContext.Provider>
|
|
1718
|
+
);
|
|
1719
|
+
};
|
|
1720
|
+
|
|
1721
|
+
const StatefulCompoundComponent: React.FC<
|
|
1722
|
+
ComponentProps & {
|
|
1723
|
+
_parentStatefulSetId?: string;
|
|
1724
|
+
_parentInputGroupInfo?: { type: string; groupName: string } | null;
|
|
1725
|
+
_parentName?: string;
|
|
1726
|
+
}
|
|
1727
|
+
> = ({
|
|
1728
|
+
id,
|
|
1729
|
+
name,
|
|
1730
|
+
nodeData,
|
|
1731
|
+
children,
|
|
1732
|
+
_parentStatefulSetId,
|
|
1733
|
+
_parentInputGroupInfo,
|
|
1734
|
+
_parentName,
|
|
1735
|
+
}) => {
|
|
1736
|
+
const style = useEncoreStyle(nodeData.style, {
|
|
1737
|
+
debug: false,
|
|
1738
|
+
});
|
|
1739
|
+
|
|
1740
|
+
// Ensure the container has relative positioning so the absolute overlay works correctly
|
|
1741
|
+
const containerStyle = {
|
|
1742
|
+
...style,
|
|
1743
|
+
position: style.position || "relative",
|
|
1744
|
+
};
|
|
1745
|
+
|
|
1746
|
+
return (
|
|
1747
|
+
<div
|
|
1748
|
+
data-id={id}
|
|
1749
|
+
data-name={name}
|
|
1750
|
+
data-type="StatefulCompoundComponent"
|
|
1751
|
+
style={containerStyle}
|
|
1752
|
+
>
|
|
1753
|
+
<EncoreLinkActionWrapper
|
|
1754
|
+
id={id}
|
|
1755
|
+
nodeData={nodeData}
|
|
1756
|
+
_parentStatefulSetId={_parentStatefulSetId}
|
|
1757
|
+
_parentInputGroupInfo={_parentInputGroupInfo}
|
|
1758
|
+
_parentName={_parentName}
|
|
1759
|
+
>
|
|
1760
|
+
{children}
|
|
1761
|
+
</EncoreLinkActionWrapper>
|
|
1762
|
+
</div>
|
|
1763
|
+
);
|
|
1764
|
+
};
|
|
1765
|
+
|
|
1766
|
+
const BackgroundContainerComponent = ({ id, name, children }) => {
|
|
1767
|
+
const containerContext = useContext(EncoreContainerContext);
|
|
1768
|
+
const { dimensions, ref } = useContainerDimensions();
|
|
1769
|
+
|
|
1770
|
+
// Note: Using zIndex: 0 instead of -1 to avoid going behind outer containers
|
|
1771
|
+
// that may have their own background colors (like app wrappers with bg-zinc-800).
|
|
1772
|
+
// The background still stays behind content due to DOM order - children render after.
|
|
1773
|
+
const style = {
|
|
1774
|
+
zIndex: 0,
|
|
1775
|
+
flex: 1,
|
|
1776
|
+
position: "absolute",
|
|
1777
|
+
top: 0,
|
|
1778
|
+
left: 0,
|
|
1779
|
+
width: "100%",
|
|
1780
|
+
height: "100%",
|
|
1781
|
+
};
|
|
1782
|
+
|
|
1783
|
+
return (
|
|
1784
|
+
<EncoreContainerContext.Provider value={{ dimensions, isFlex: false }}>
|
|
1785
|
+
<div
|
|
1786
|
+
data-id={id}
|
|
1787
|
+
data-name={name}
|
|
1788
|
+
data-type="BackgroundContainerComponent"
|
|
1789
|
+
ref={ref}
|
|
1790
|
+
style={style}
|
|
1791
|
+
>
|
|
1792
|
+
{children}
|
|
1793
|
+
</div>
|
|
1794
|
+
</EncoreContainerContext.Provider>
|
|
1795
|
+
);
|
|
1796
|
+
};
|
|
1797
|
+
|
|
1798
|
+
const ContainerComponent = ({
|
|
1799
|
+
id,
|
|
1800
|
+
name,
|
|
1801
|
+
nodeData,
|
|
1802
|
+
children,
|
|
1803
|
+
_arrayItemIndex,
|
|
1804
|
+
_arrayItemData,
|
|
1805
|
+
}) => {
|
|
1806
|
+
if (id === "01KB929X1KK5TX0K8VBNP6ZDPG") {
|
|
1807
|
+
console.log(
|
|
1808
|
+
`[DEBUG] ContainerComponent ${id} (Hero) FULL nodeData:`,
|
|
1809
|
+
JSON.stringify(nodeData, null, 2)
|
|
1810
|
+
);
|
|
1811
|
+
}
|
|
1812
|
+
const bindingContext = useContext(EncoreBindingContext);
|
|
1813
|
+
const { patchedNodeData, list, arrayData } = useEncoreBinding({
|
|
1814
|
+
id,
|
|
1815
|
+
nodeData,
|
|
1816
|
+
});
|
|
1817
|
+
nodeData = patchedNodeData;
|
|
1818
|
+
|
|
1819
|
+
// Debug logging for encore:data:array removed
|
|
1820
|
+
|
|
1821
|
+
const { dimensions, ref } = useContainerDimensions();
|
|
1822
|
+
const style = useEncoreStyle(nodeData.style, {
|
|
1823
|
+
debug: false,
|
|
1824
|
+
});
|
|
1825
|
+
|
|
1826
|
+
const isFlex = !!nodeData.style.layout?.mode;
|
|
1827
|
+
// Determine flex direction for passing to children via context
|
|
1828
|
+
const flexDirection =
|
|
1829
|
+
nodeData.style.layout?.mode === "HORIZONTAL" ? "row" : "column";
|
|
1830
|
+
// Only set default 100% if we don't have explicit dimensions from layout.size
|
|
1831
|
+
// Explicit dimensions are set by useEncoreStyle when layoutSizing is FIXED
|
|
1832
|
+
const hasExplicitWidth = style.width !== undefined;
|
|
1833
|
+
const hasExplicitHeight = style.height !== undefined;
|
|
1834
|
+
|
|
1835
|
+
// For absolutely positioned elements with explicit dimensions, don't default to 100% width
|
|
1836
|
+
// They should use their explicit width from layout.size, or size to content if HUG
|
|
1837
|
+
const containerContext = useContext(EncoreContainerContext);
|
|
1838
|
+
const parentIsFlex = containerContext?.isFlex ?? false;
|
|
1839
|
+
const parentFlexDirection = containerContext?.flexDirection;
|
|
1840
|
+
const parentIsHorizontalFlex = parentIsFlex && parentFlexDirection === "row";
|
|
1841
|
+
const willBeAbsolute = !parentIsFlex && nodeData.style.positioning;
|
|
1842
|
+
const hasExplicitDimensions =
|
|
1843
|
+
nodeData.style.layout?.layoutSizingHorizontal === "FIXED" ||
|
|
1844
|
+
nodeData.style.layout?.layoutSizingVertical === "FIXED";
|
|
1845
|
+
const isHugSizing =
|
|
1846
|
+
nodeData.style.layout?.layoutSizingHorizontal === "HUG" ||
|
|
1847
|
+
nodeData.style.layout?.layoutSizingVertical === "HUG";
|
|
1848
|
+
|
|
1849
|
+
// Only set 100% width if:
|
|
1850
|
+
// - No explicit width is set
|
|
1851
|
+
// - AND parent is NOT a horizontal flex container (which would break horizontal layouts)
|
|
1852
|
+
// - AND (not absolutely positioned OR doesn't have explicit dimensions OR is not HUG sizing)
|
|
1853
|
+
if (
|
|
1854
|
+
!hasExplicitWidth &&
|
|
1855
|
+
!parentIsHorizontalFlex &&
|
|
1856
|
+
!(willBeAbsolute && (hasExplicitDimensions || isHugSizing))
|
|
1857
|
+
) {
|
|
1858
|
+
style.width = "100%";
|
|
1859
|
+
}
|
|
1860
|
+
// For containers without explicit height:
|
|
1861
|
+
// - If it has HUG sizing, let it size to content (don't set height)
|
|
1862
|
+
// - If it's a flex container, let it size to content (don't set height)
|
|
1863
|
+
// - If it's absolutely positioned with FILL sizing, use remaining space
|
|
1864
|
+
// - Otherwise, default to 100% (only for non-flex, non-HUG containers)
|
|
1865
|
+
if (!hasExplicitHeight) {
|
|
1866
|
+
// parentIsFlex already declared above, reuse it
|
|
1867
|
+
const willBeAbsoluteForHeight = !parentIsFlex && nodeData.style.positioning;
|
|
1868
|
+
const hasFillVertical =
|
|
1869
|
+
nodeData.style.layout?.layoutSizingVertical === "FILL";
|
|
1870
|
+
const isHugVertical = nodeData.style.layout?.layoutSizingVertical === "HUG";
|
|
1871
|
+
|
|
1872
|
+
// Don't set height for:
|
|
1873
|
+
// 1. HUG sizing containers (they should size to content)
|
|
1874
|
+
// 2. Flex containers (they should size to content)
|
|
1875
|
+
// 3. Absolutely positioned flex containers (already handled, but be explicit)
|
|
1876
|
+
if (isHugVertical) {
|
|
1877
|
+
// HUG sizing: explicitly do NOT set height - let it size to content
|
|
1878
|
+
} else if (isFlex) {
|
|
1879
|
+
// Flex containers: let them size to content (don't set height)
|
|
1880
|
+
// This allows buttons and similar elements to size naturally
|
|
1881
|
+
} else if (
|
|
1882
|
+
willBeAbsoluteForHeight &&
|
|
1883
|
+
hasFillVertical &&
|
|
1884
|
+
nodeData.style.positioning?.top !== undefined
|
|
1885
|
+
) {
|
|
1886
|
+
// For FILL sizing, calculate remaining height
|
|
1887
|
+
const topPercent = nodeData.style.positioning.top;
|
|
1888
|
+
const remainingPercent = 100 - topPercent;
|
|
1889
|
+
style.height = `${remainingPercent}%`;
|
|
1890
|
+
} else {
|
|
1891
|
+
// Default: set height to 100% for non-flex, non-HUG containers
|
|
1892
|
+
style.height = "100%";
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1896
|
+
// Determine if this component should be absolutely positioned.
|
|
1897
|
+
// Elements with positioning info + layout sizing should be absolute (they use explicit sizes).
|
|
1898
|
+
// Elements with positioning info (but no layout sizing) should also be absolute if parent isn't flex.
|
|
1899
|
+
const hasLayoutSizing =
|
|
1900
|
+
nodeData.style.layout?.layoutSizingHorizontal ||
|
|
1901
|
+
nodeData.style.layout?.layoutSizingVertical;
|
|
1902
|
+
const shouldBeAbsolute =
|
|
1903
|
+
nodeData.style.positioning && !isFlex && (hasLayoutSizing || true);
|
|
1904
|
+
|
|
1905
|
+
// Only force position: relative if the element shouldn't be absolutely positioned.
|
|
1906
|
+
// This allows absolutely positioned children (with layout sizing) to remain absolute.
|
|
1907
|
+
const finalStyle = shouldBeAbsolute
|
|
1908
|
+
? { ...style, position: "absolute" }
|
|
1909
|
+
: style.position === "absolute"
|
|
1910
|
+
? style
|
|
1911
|
+
: { ...style, position: style.position || "relative" };
|
|
1912
|
+
|
|
1913
|
+
// For containers with explicit dimensions from layout.size, use the scaled dimensions
|
|
1914
|
+
// from style (which are calculated from layout.size * scaleFactor) for child context.
|
|
1915
|
+
// This ensures children's percentage positioning is calculated relative to the correct container size.
|
|
1916
|
+
// When padding exists, subtract it from dimensions to get the content area (padding box),
|
|
1917
|
+
// which is what CSS uses for percentage-based positioning of absolutely positioned children.
|
|
1918
|
+
// Note: hasExplicitDimensions is already declared above, reuse it
|
|
1919
|
+
|
|
1920
|
+
// Calculate content area dimensions (accounting for padding)
|
|
1921
|
+
// Parse padding values - they can be numbers (px) or strings (e.g., "10px")
|
|
1922
|
+
const parsePadding = (value) => {
|
|
1923
|
+
if (typeof value === "number") return value;
|
|
1924
|
+
if (typeof value === "string" && value.endsWith("px")) {
|
|
1925
|
+
return parseFloat(value) || 0;
|
|
1926
|
+
}
|
|
1927
|
+
return 0;
|
|
1928
|
+
};
|
|
1929
|
+
const paddingLeft = parsePadding(style.paddingLeft);
|
|
1930
|
+
const paddingRight = parsePadding(style.paddingRight);
|
|
1931
|
+
const paddingTop = parsePadding(style.paddingTop);
|
|
1932
|
+
const paddingBottom = parsePadding(style.paddingBottom);
|
|
1933
|
+
|
|
1934
|
+
const contextDimensions =
|
|
1935
|
+
hasExplicitDimensions &&
|
|
1936
|
+
typeof style.width === "number" &&
|
|
1937
|
+
typeof style.height === "number"
|
|
1938
|
+
? {
|
|
1939
|
+
width: style.width - paddingLeft - paddingRight,
|
|
1940
|
+
height: style.height - paddingTop - paddingBottom,
|
|
1941
|
+
}
|
|
1942
|
+
: {
|
|
1943
|
+
width: dimensions.width - paddingLeft - paddingRight,
|
|
1944
|
+
height: dimensions.height - paddingTop - paddingBottom,
|
|
1945
|
+
};
|
|
1946
|
+
|
|
1947
|
+
// Check if this ContainerComponent is being expanded by a parent (like SliderComponent)
|
|
1948
|
+
// If _arrayItemIndex is provided, render a single instance for that item
|
|
1949
|
+
if (_arrayItemIndex !== undefined && _arrayItemIndex !== null && list) {
|
|
1950
|
+
// This container is being expanded by a parent (e.g., SliderComponent)
|
|
1951
|
+
// Render a single instance for the specified array item
|
|
1952
|
+
const item = list[_arrayItemIndex];
|
|
1953
|
+
if (item !== undefined) {
|
|
1954
|
+
const bindingValue =
|
|
1955
|
+
typeof item === "object" && item !== null
|
|
1956
|
+
? {
|
|
1957
|
+
nodeData: item,
|
|
1958
|
+
arrayDataById: bindingContext?.arrayDataById,
|
|
1959
|
+
textOverridesById: bindingContext?.textOverridesById,
|
|
1960
|
+
rootData: bindingContext?.rootData,
|
|
1961
|
+
}
|
|
1962
|
+
: { nodeData: item };
|
|
1963
|
+
|
|
1964
|
+
return (
|
|
1965
|
+
<EncoreBindingContext.Provider value={bindingValue}>
|
|
1966
|
+
<EncoreContainerContext.Provider
|
|
1967
|
+
value={{ dimensions: contextDimensions, isFlex, flexDirection }}
|
|
1968
|
+
>
|
|
1969
|
+
<div
|
|
1970
|
+
data-id={`${id}:${_arrayItemIndex}`}
|
|
1971
|
+
data-name={name}
|
|
1972
|
+
data-type="ContainerComponent"
|
|
1973
|
+
style={finalStyle}
|
|
1974
|
+
>
|
|
1975
|
+
{children}
|
|
1976
|
+
</div>
|
|
1977
|
+
</EncoreContainerContext.Provider>
|
|
1978
|
+
</EncoreBindingContext.Provider>
|
|
1979
|
+
);
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
if (list) {
|
|
1984
|
+
// Use arrayData if available (comes from context, should be stable), otherwise list
|
|
1985
|
+
const listToMap = arrayData || list;
|
|
1986
|
+
|
|
1987
|
+
// Memoize the mapped components, but only depend on the array reference itself
|
|
1988
|
+
// and the id - React will handle the rest via keys
|
|
1989
|
+
const mappedComponents = useMemo(() => {
|
|
1990
|
+
return listToMap.map((item, i) => {
|
|
1991
|
+
// For encore:data:array, each item is the data object for that iteration
|
|
1992
|
+
// Provide it as nodeData so child components with encore:data tags can access it
|
|
1993
|
+
const bindingValue =
|
|
1994
|
+
typeof item === "object" && item !== null
|
|
1995
|
+
? {
|
|
1996
|
+
nodeData: item,
|
|
1997
|
+
arrayDataById: bindingContext?.arrayDataById,
|
|
1998
|
+
textOverridesById: bindingContext?.textOverridesById,
|
|
1999
|
+
rootData: bindingContext?.rootData,
|
|
2000
|
+
}
|
|
2001
|
+
: { nodeData: item };
|
|
2002
|
+
|
|
2003
|
+
// Create a complete container component for each item
|
|
2004
|
+
// This ensures each slide is a separate container that the slider can lay out horizontally
|
|
2005
|
+
return (
|
|
2006
|
+
<EncoreBindingContext.Provider
|
|
2007
|
+
key={`${id}:${i}`}
|
|
2008
|
+
value={bindingValue}
|
|
2009
|
+
>
|
|
2010
|
+
<EncoreContainerContext.Provider
|
|
2011
|
+
value={{ dimensions: contextDimensions, isFlex, flexDirection }}
|
|
2012
|
+
>
|
|
2013
|
+
<div
|
|
2014
|
+
data-id={`${id}:${i}`}
|
|
2015
|
+
data-name={name}
|
|
2016
|
+
data-type="ContainerComponent"
|
|
2017
|
+
style={finalStyle}
|
|
2018
|
+
>
|
|
2019
|
+
{children}
|
|
2020
|
+
</div>
|
|
2021
|
+
</EncoreContainerContext.Provider>
|
|
2022
|
+
</EncoreBindingContext.Provider>
|
|
2023
|
+
);
|
|
2024
|
+
});
|
|
2025
|
+
}, [listToMap, id]);
|
|
2026
|
+
// Return the array directly - React will handle it
|
|
2027
|
+
return mappedComponents;
|
|
2028
|
+
}
|
|
2029
|
+
|
|
2030
|
+
// No list - return single component as before
|
|
2031
|
+
// Check if this container has actions and should be wrapped with EncoreLinkActionWrapper
|
|
2032
|
+
// Normalize actions: can be array or object with event keys (e.g., { tap: {...} })
|
|
2033
|
+
let actions = nodeData.actions || [];
|
|
2034
|
+
if (
|
|
2035
|
+
!Array.isArray(actions) &&
|
|
2036
|
+
typeof actions === "object" &&
|
|
2037
|
+
actions !== null
|
|
2038
|
+
) {
|
|
2039
|
+
// Convert object format to array format (e.g., { tap: {...} } -> [{ event: "tap", ... }])
|
|
2040
|
+
actions = Object.entries(actions).map(
|
|
2041
|
+
([event, actionData]: [string, any]) => ({
|
|
2042
|
+
event,
|
|
2043
|
+
...(typeof actionData === "object" && actionData !== null
|
|
2044
|
+
? actionData
|
|
2045
|
+
: {}),
|
|
2046
|
+
})
|
|
2047
|
+
);
|
|
2048
|
+
}
|
|
2049
|
+
const hasActions = Array.isArray(actions) && actions.length > 0;
|
|
2050
|
+
|
|
2051
|
+
const component = (
|
|
2052
|
+
<EncoreContainerContext.Provider
|
|
2053
|
+
value={{ dimensions: contextDimensions, isFlex, flexDirection }}
|
|
2054
|
+
>
|
|
2055
|
+
<div
|
|
2056
|
+
data-id={id}
|
|
2057
|
+
data-name={name}
|
|
2058
|
+
data-type="ContainerComponent"
|
|
2059
|
+
ref={ref}
|
|
2060
|
+
style={finalStyle}
|
|
2061
|
+
>
|
|
2062
|
+
{hasActions ? (
|
|
2063
|
+
<EncoreLinkActionWrapper id={id} nodeData={nodeData}>
|
|
2064
|
+
{children}
|
|
2065
|
+
</EncoreLinkActionWrapper>
|
|
2066
|
+
) : (
|
|
2067
|
+
children
|
|
2068
|
+
)}
|
|
2069
|
+
</div>
|
|
2070
|
+
</EncoreContainerContext.Provider>
|
|
2071
|
+
);
|
|
2072
|
+
|
|
2073
|
+
return component;
|
|
2074
|
+
};
|
|
2075
|
+
|
|
2076
|
+
const CompoundComponent = ContainerComponent;
|
|
2077
|
+
|
|
2078
|
+
const TopBarContainerComponent = ({ id, name, nodeData, children }) => {
|
|
2079
|
+
const { dimensions, ref } = useContainerDimensions();
|
|
2080
|
+
|
|
2081
|
+
const style = useEncoreStyle(nodeData.style, {
|
|
2082
|
+
debug: false,
|
|
2083
|
+
});
|
|
2084
|
+
|
|
2085
|
+
const isFlex = !!nodeData.style.layout?.mode;
|
|
2086
|
+
const flexDirection =
|
|
2087
|
+
nodeData.style.layout?.mode === "HORIZONTAL" ? "row" : "column";
|
|
2088
|
+
|
|
2089
|
+
// Only force position: relative if position isn't already set.
|
|
2090
|
+
const finalStyle = style.position
|
|
2091
|
+
? style
|
|
2092
|
+
: { ...style, position: "relative" };
|
|
2093
|
+
|
|
2094
|
+
return (
|
|
2095
|
+
<EncoreContainerContext.Provider
|
|
2096
|
+
value={{ dimensions, isFlex, flexDirection }}
|
|
2097
|
+
>
|
|
2098
|
+
<div
|
|
2099
|
+
data-id={id}
|
|
2100
|
+
data-name={name}
|
|
2101
|
+
data-type="TopBarContainerComponent"
|
|
2102
|
+
ref={ref}
|
|
2103
|
+
style={finalStyle}
|
|
2104
|
+
>
|
|
2105
|
+
{children}
|
|
2106
|
+
</div>
|
|
2107
|
+
</EncoreContainerContext.Provider>
|
|
2108
|
+
);
|
|
2109
|
+
};
|
|
2110
|
+
|
|
2111
|
+
const DefaultLayerComponent = ({ id, name, nodeData, children }) => {
|
|
2112
|
+
const style = useEncoreStyle(nodeData.style, { debug: false });
|
|
2113
|
+
return (
|
|
2114
|
+
<div
|
|
2115
|
+
data-id={id}
|
|
2116
|
+
data-name={name}
|
|
2117
|
+
data-type="DefaultLayerComponent"
|
|
2118
|
+
style={style}
|
|
2119
|
+
>
|
|
2120
|
+
{children}
|
|
2121
|
+
</div>
|
|
2122
|
+
);
|
|
2123
|
+
};
|
|
2124
|
+
|
|
2125
|
+
const TabsMenuComponent = ({ id, name, nodeData, children }) => (
|
|
2126
|
+
<div
|
|
2127
|
+
data-id={id}
|
|
2128
|
+
data-name={name}
|
|
2129
|
+
data-type="TabsMenuComponent"
|
|
2130
|
+
style={useEncoreStyle(nodeData.style)}
|
|
2131
|
+
>
|
|
2132
|
+
{children}
|
|
2133
|
+
</div>
|
|
2134
|
+
);
|
|
2135
|
+
|
|
2136
|
+
const MenuSideComponent = ({ id, name, nodeData, children }) => {
|
|
2137
|
+
const style = useEncoreStyle(nodeData.style, {
|
|
2138
|
+
debug: false,
|
|
2139
|
+
});
|
|
2140
|
+
return (
|
|
2141
|
+
<div
|
|
2142
|
+
data-id={id}
|
|
2143
|
+
data-name={name}
|
|
2144
|
+
data-type="MenuSideComponent"
|
|
2145
|
+
style={style}
|
|
2146
|
+
>
|
|
2147
|
+
{children}
|
|
2148
|
+
</div>
|
|
2149
|
+
);
|
|
2150
|
+
};
|
|
2151
|
+
|
|
2152
|
+
const SliderComponent = ({ id, name, nodeData, children }) => {
|
|
2153
|
+
if (id === "01KB929X1DQJN954WNGF5AH38B") {
|
|
2154
|
+
console.log(
|
|
2155
|
+
`[DEBUG] SliderComponent ${id} FULL nodeData:`,
|
|
2156
|
+
JSON.stringify(nodeData, null, 2)
|
|
2157
|
+
);
|
|
2158
|
+
}
|
|
2159
|
+
// Log slider component data for inspection
|
|
2160
|
+
// Debug logging removed to prevent console flooding
|
|
2161
|
+
|
|
2162
|
+
const { dimensions, ref: containerRef } = useContainerDimensions();
|
|
2163
|
+
const scrollRef = React.useRef(null);
|
|
2164
|
+
const autoAdvanceIntervalRef = React.useRef(null);
|
|
2165
|
+
const isUserInteractingRef = React.useRef(false);
|
|
2166
|
+
const idleTimeoutRef = React.useRef(null);
|
|
2167
|
+
const pageContext = useContext(EncorePageContext);
|
|
2168
|
+
const repeatingContainerContext = useContext(EncoreRepeatingContainerContext);
|
|
2169
|
+
|
|
2170
|
+
// Control props from context
|
|
2171
|
+
const [controlProps, setControlProps] = React.useState({
|
|
2172
|
+
currentIndex: undefined,
|
|
2173
|
+
onIndexChange: undefined,
|
|
2174
|
+
});
|
|
2175
|
+
|
|
2176
|
+
// Internal state for uncontrolled mode
|
|
2177
|
+
const [internalCurrentIndex, setInternalCurrentIndex] = React.useState(0);
|
|
2178
|
+
const lastReportedIndexRef = React.useRef(null);
|
|
2179
|
+
|
|
2180
|
+
const { patchedNodeData, list } = useEncoreBinding({ id, nodeData });
|
|
2181
|
+
// Slider binding debug log removed
|
|
2182
|
+
nodeData = patchedNodeData;
|
|
2183
|
+
|
|
2184
|
+
const params = nodeData.data?.params || {};
|
|
2185
|
+
const infinite = params.infinite === true;
|
|
2186
|
+
const automatic = params.automatic === true;
|
|
2187
|
+
|
|
2188
|
+
const hasVerticalTag =
|
|
2189
|
+
(Array.isArray(nodeData.tags) &&
|
|
2190
|
+
nodeData.tags.some((t) => t.includes("container:slider:vertical"))) ||
|
|
2191
|
+
(nodeData.name && nodeData.name.includes("container:slider:vertical"));
|
|
2192
|
+
|
|
2193
|
+
// If animation is "default" and no tag, check aspect ratio as fallback
|
|
2194
|
+
// Sliders that are taller than wide are likely vertical
|
|
2195
|
+
let shouldBeVertical = hasVerticalTag;
|
|
2196
|
+
if (!shouldBeVertical && params.animation === "default") {
|
|
2197
|
+
const width = nodeData.style?.width;
|
|
2198
|
+
const height = nodeData.style?.height;
|
|
2199
|
+
if (
|
|
2200
|
+
typeof width === "number" &&
|
|
2201
|
+
typeof height === "number" &&
|
|
2202
|
+
height > width
|
|
2203
|
+
) {
|
|
2204
|
+
shouldBeVertical = true;
|
|
2205
|
+
}
|
|
2206
|
+
}
|
|
2207
|
+
|
|
2208
|
+
// Debug logging for vertical slider detection (can be enabled for debugging)
|
|
2209
|
+
// if (nodeData.type === "container:slider") {
|
|
2210
|
+
// console.log(
|
|
2211
|
+
// `[DEBUG] Slider ${id} tags:`,
|
|
2212
|
+
// nodeData.tags,
|
|
2213
|
+
// "name:",
|
|
2214
|
+
// nodeData.name,
|
|
2215
|
+
// "hasVerticalTag:",
|
|
2216
|
+
// hasVerticalTag,
|
|
2217
|
+
// "params.animation:",
|
|
2218
|
+
// params.animation,
|
|
2219
|
+
// "shouldBeVertical:",
|
|
2220
|
+
// shouldBeVertical
|
|
2221
|
+
// );
|
|
2222
|
+
// }
|
|
2223
|
+
|
|
2224
|
+
const animation = shouldBeVertical
|
|
2225
|
+
? "vertical"
|
|
2226
|
+
: params.animation === "default"
|
|
2227
|
+
? "horizontal"
|
|
2228
|
+
: params.animation || "horizontal";
|
|
2229
|
+
|
|
2230
|
+
const style = useEncoreStyle(nodeData.style, {
|
|
2231
|
+
debug: false,
|
|
2232
|
+
});
|
|
2233
|
+
|
|
2234
|
+
// Ensure container has proper dimensions
|
|
2235
|
+
const hasExplicitWidth = style.width !== undefined;
|
|
2236
|
+
const hasExplicitHeight = style.height !== undefined;
|
|
2237
|
+
if (!hasExplicitWidth) style.width = "100%";
|
|
2238
|
+
if (!hasExplicitHeight) style.height = "100%";
|
|
2239
|
+
|
|
2240
|
+
// Ensure position is relative for proper scrolling context
|
|
2241
|
+
if (!style.position) {
|
|
2242
|
+
style.position = "relative";
|
|
2243
|
+
}
|
|
2244
|
+
|
|
2245
|
+
// Get binding context to access array data
|
|
2246
|
+
const bindingContext = useContext(EncoreBindingContext);
|
|
2247
|
+
|
|
2248
|
+
// Convert children to array and expand any ContainerComponents with encore:data:array tags
|
|
2249
|
+
// When a ContainerComponent has encore:data:array, it returns an array, but React doesn't
|
|
2250
|
+
// pass that array as children to the parent. We need to detect this case and expand it ourselves.
|
|
2251
|
+
const childList = React.useMemo(() => {
|
|
2252
|
+
const result = [];
|
|
2253
|
+
React.Children.forEach(children, (child) => {
|
|
2254
|
+
if (child === null || child === undefined) return;
|
|
2255
|
+
|
|
2256
|
+
// Check if this child is a ContainerComponent with encore:data:array tag
|
|
2257
|
+
const childProps = child?.props || {};
|
|
2258
|
+
const childNodeData = childProps.nodeData || {};
|
|
2259
|
+
const hasEncoreDataArrayTag =
|
|
2260
|
+
Array.isArray(childNodeData?.tags) &&
|
|
2261
|
+
(childNodeData.tags.includes("encore:data:array") ||
|
|
2262
|
+
childNodeData.tags.includes("bravo:data:array"));
|
|
2263
|
+
|
|
2264
|
+
if (hasEncoreDataArrayTag && childProps.id && bindingContext) {
|
|
2265
|
+
// Get the array data for this container
|
|
2266
|
+
const arrayData =
|
|
2267
|
+
bindingContext?.arrayDataById &&
|
|
2268
|
+
bindingContext.arrayDataById[childProps.id] &&
|
|
2269
|
+
Array.isArray(bindingContext.arrayDataById[childProps.id])
|
|
2270
|
+
? bindingContext.arrayDataById[childProps.id]
|
|
2271
|
+
: null;
|
|
2272
|
+
|
|
2273
|
+
if (arrayData && arrayData.length > 0) {
|
|
2274
|
+
// Expand: create one container instance for each array item
|
|
2275
|
+
// We'll render the container's children for each item
|
|
2276
|
+
arrayData.forEach((item, i) => {
|
|
2277
|
+
// Clone the child and pass the item data
|
|
2278
|
+
result.push(
|
|
2279
|
+
React.cloneElement(child, {
|
|
2280
|
+
key: `${childProps.id}:${i}`,
|
|
2281
|
+
// ContainerComponent will use this to know which item to render
|
|
2282
|
+
_arrayItemIndex: i,
|
|
2283
|
+
_arrayItemData: item,
|
|
2284
|
+
})
|
|
2285
|
+
);
|
|
2286
|
+
});
|
|
2287
|
+
} else {
|
|
2288
|
+
// No array data, just add the original child
|
|
2289
|
+
result.push(child);
|
|
2290
|
+
}
|
|
2291
|
+
} else {
|
|
2292
|
+
// Regular child, add it as-is
|
|
2293
|
+
result.push(child);
|
|
2294
|
+
}
|
|
2295
|
+
});
|
|
2296
|
+
|
|
2297
|
+
// Debug logging removed
|
|
2298
|
+
return result;
|
|
2299
|
+
}, [children, id, bindingContext]);
|
|
2300
|
+
|
|
2301
|
+
// Clone slides for infinite scroll
|
|
2302
|
+
const slidesWithClones = React.useMemo(() => {
|
|
2303
|
+
if (!infinite || childList.length === 0) {
|
|
2304
|
+
return childList;
|
|
2305
|
+
}
|
|
2306
|
+
|
|
2307
|
+
// Clone first slide to end, last slide to beginning
|
|
2308
|
+
const firstSlide = childList[0];
|
|
2309
|
+
const lastSlide = childList[childList.length - 1];
|
|
2310
|
+
|
|
2311
|
+
return [
|
|
2312
|
+
React.cloneElement(lastSlide, {
|
|
2313
|
+
key: `clone-last-${lastSlide.key || lastSlide.props.id}`,
|
|
2314
|
+
}),
|
|
2315
|
+
...childList,
|
|
2316
|
+
React.cloneElement(firstSlide, {
|
|
2317
|
+
key: `clone-first-${firstSlide.key || firstSlide.props.id}`,
|
|
2318
|
+
}),
|
|
2319
|
+
];
|
|
2320
|
+
}, [childList, infinite]);
|
|
2321
|
+
|
|
2322
|
+
// Calculate slide width and height (assuming each slide has fixed dimensions from layout.size)
|
|
2323
|
+
const slideDimensions = React.useMemo(() => {
|
|
2324
|
+
if (childList.length === 0) return { width: 0, height: 0 };
|
|
2325
|
+
const firstChild = childList[0];
|
|
2326
|
+
const slideStyle = firstChild?.props?.nodeData?.style;
|
|
2327
|
+
const scale = pageContext.scaleFactor || 1;
|
|
2328
|
+
|
|
2329
|
+
let width = 0;
|
|
2330
|
+
let height = 0;
|
|
2331
|
+
|
|
2332
|
+
if (
|
|
2333
|
+
slideStyle?.layout?.layoutSizingHorizontal === "FIXED" &&
|
|
2334
|
+
slideStyle?.layout?.size?.x
|
|
2335
|
+
) {
|
|
2336
|
+
width = slideStyle.layout.size.x * scale;
|
|
2337
|
+
} else {
|
|
2338
|
+
// Fallback: use container width (don't divide by number of slides, that squashes them)
|
|
2339
|
+
width = dimensions.width || 0;
|
|
2340
|
+
}
|
|
2341
|
+
|
|
2342
|
+
if (
|
|
2343
|
+
slideStyle?.layout?.layoutSizingVertical === "FIXED" &&
|
|
2344
|
+
slideStyle?.layout?.size?.y
|
|
2345
|
+
) {
|
|
2346
|
+
height = slideStyle.layout.size.y * scale;
|
|
2347
|
+
} else {
|
|
2348
|
+
// Fallback: use container height
|
|
2349
|
+
height = dimensions.height || 0;
|
|
2350
|
+
}
|
|
2351
|
+
|
|
2352
|
+
return { width, height };
|
|
2353
|
+
}, [childList, dimensions.width, dimensions.height, pageContext.scaleFactor]);
|
|
2354
|
+
|
|
2355
|
+
const slideWidth = slideDimensions.width;
|
|
2356
|
+
const slideHeight = slideDimensions.height;
|
|
2357
|
+
const isVertical = animation === "vertical";
|
|
2358
|
+
|
|
2359
|
+
const calculateCurrentIndex = React.useCallback(() => {
|
|
2360
|
+
if (!scrollRef.current || childList.length === 0) {
|
|
2361
|
+
return 0;
|
|
2362
|
+
}
|
|
2363
|
+
|
|
2364
|
+
const scrollContainer = scrollRef.current;
|
|
2365
|
+
|
|
2366
|
+
if (isVertical) {
|
|
2367
|
+
if (slideHeight === 0) return 0;
|
|
2368
|
+
const currentScrollTop = scrollContainer.scrollTop;
|
|
2369
|
+
|
|
2370
|
+
if (infinite) {
|
|
2371
|
+
const realSlidesStart = slideHeight;
|
|
2372
|
+
const adjustedScroll = currentScrollTop - realSlidesStart;
|
|
2373
|
+
let index = Math.round(adjustedScroll / slideHeight);
|
|
2374
|
+
if (index < 0) index = 0;
|
|
2375
|
+
if (index >= childList.length) index = childList.length - 1;
|
|
2376
|
+
return index;
|
|
2377
|
+
} else {
|
|
2378
|
+
const index = Math.round(currentScrollTop / slideHeight);
|
|
2379
|
+
if (index < 0) return 0;
|
|
2380
|
+
if (index >= childList.length) return childList.length - 1;
|
|
2381
|
+
return index;
|
|
2382
|
+
}
|
|
2383
|
+
} else {
|
|
2384
|
+
if (slideWidth === 0) return 0;
|
|
2385
|
+
const currentScrollLeft = scrollContainer.scrollLeft;
|
|
2386
|
+
|
|
2387
|
+
if (infinite) {
|
|
2388
|
+
const realSlidesStart = slideWidth;
|
|
2389
|
+
const adjustedScroll = currentScrollLeft - realSlidesStart;
|
|
2390
|
+
let index = Math.round(adjustedScroll / slideWidth);
|
|
2391
|
+
if (index < 0) index = 0;
|
|
2392
|
+
if (index >= childList.length) index = childList.length - 1;
|
|
2393
|
+
return index;
|
|
2394
|
+
} else {
|
|
2395
|
+
const index = Math.round(currentScrollLeft / slideWidth);
|
|
2396
|
+
if (index < 0) return 0;
|
|
2397
|
+
if (index >= childList.length) return childList.length - 1;
|
|
2398
|
+
return index;
|
|
2399
|
+
}
|
|
2400
|
+
}
|
|
2401
|
+
}, [infinite, childList.length, slideWidth, slideHeight, isVertical]);
|
|
2402
|
+
|
|
2403
|
+
// Scroll to specific slide index
|
|
2404
|
+
const scrollToSlide = React.useCallback(
|
|
2405
|
+
(index, smooth = true) => {
|
|
2406
|
+
if (!scrollRef.current || childList.length === 0) return;
|
|
2407
|
+
|
|
2408
|
+
const scrollContainer = scrollRef.current;
|
|
2409
|
+
|
|
2410
|
+
if (isVertical) {
|
|
2411
|
+
let targetScrollTop;
|
|
2412
|
+
if (infinite) {
|
|
2413
|
+
const realSlidesStart = slideHeight;
|
|
2414
|
+
targetScrollTop = realSlidesStart + index * slideHeight;
|
|
2415
|
+
} else {
|
|
2416
|
+
targetScrollTop = index * slideHeight;
|
|
2417
|
+
}
|
|
2418
|
+
|
|
2419
|
+
scrollContainer.scrollTo({
|
|
2420
|
+
top: targetScrollTop,
|
|
2421
|
+
behavior: smooth ? "smooth" : "auto",
|
|
2422
|
+
});
|
|
2423
|
+
} else {
|
|
2424
|
+
let targetScrollLeft;
|
|
2425
|
+
if (infinite) {
|
|
2426
|
+
const realSlidesStart = slideWidth;
|
|
2427
|
+
targetScrollLeft = realSlidesStart + index * slideWidth;
|
|
2428
|
+
} else {
|
|
2429
|
+
targetScrollLeft = index * slideWidth;
|
|
2430
|
+
}
|
|
2431
|
+
|
|
2432
|
+
scrollContainer.scrollTo({
|
|
2433
|
+
left: targetScrollLeft,
|
|
2434
|
+
behavior: smooth ? "smooth" : "auto",
|
|
2435
|
+
});
|
|
2436
|
+
}
|
|
2437
|
+
},
|
|
2438
|
+
[slideWidth, slideHeight, infinite, childList.length, isVertical]
|
|
2439
|
+
);
|
|
2440
|
+
|
|
2441
|
+
// Handle infinite scroll position adjustment
|
|
2442
|
+
const handleInfiniteScroll = React.useCallback(() => {
|
|
2443
|
+
if (!infinite || !scrollRef.current || childList.length === 0) return;
|
|
2444
|
+
|
|
2445
|
+
const scrollContainer = scrollRef.current;
|
|
2446
|
+
const tolerance = 1; // Small tolerance for floating point precision
|
|
2447
|
+
|
|
2448
|
+
if (isVertical) {
|
|
2449
|
+
if (slideHeight === 0) return;
|
|
2450
|
+
const scrollTop = scrollContainer.scrollTop;
|
|
2451
|
+
const slideHeightPx = slideHeight;
|
|
2452
|
+
|
|
2453
|
+
const realSlidesStart = slideHeightPx;
|
|
2454
|
+
const realSlidesEnd = realSlidesStart + childList.length * slideHeightPx;
|
|
2455
|
+
|
|
2456
|
+
if (scrollTop >= realSlidesEnd - tolerance) {
|
|
2457
|
+
scrollContainer.scrollTop = realSlidesStart;
|
|
2458
|
+
} else if (scrollTop <= tolerance) {
|
|
2459
|
+
scrollContainer.scrollTop = realSlidesEnd - slideHeightPx;
|
|
2460
|
+
}
|
|
2461
|
+
} else {
|
|
2462
|
+
if (slideWidth === 0) return;
|
|
2463
|
+
const scrollLeft = scrollContainer.scrollLeft;
|
|
2464
|
+
const slideWidthPx = slideWidth;
|
|
2465
|
+
|
|
2466
|
+
const realSlidesStart = slideWidthPx;
|
|
2467
|
+
const realSlidesEnd = realSlidesStart + childList.length * slideWidthPx;
|
|
2468
|
+
|
|
2469
|
+
if (scrollLeft >= realSlidesEnd - tolerance) {
|
|
2470
|
+
scrollContainer.scrollLeft = realSlidesStart;
|
|
2471
|
+
} else if (scrollLeft <= tolerance) {
|
|
2472
|
+
scrollContainer.scrollLeft = realSlidesEnd - slideWidthPx;
|
|
2473
|
+
}
|
|
2474
|
+
}
|
|
2475
|
+
}, [infinite, childList.length, slideWidth, slideHeight, isVertical]);
|
|
2476
|
+
|
|
2477
|
+
// Auto-advance logic
|
|
2478
|
+
React.useEffect(() => {
|
|
2479
|
+
if (!automatic || childList.length <= 1 || slideWidth === 0) return;
|
|
2480
|
+
|
|
2481
|
+
const advanceSlide = () => {
|
|
2482
|
+
if (isUserInteractingRef.current || !scrollRef.current) return;
|
|
2483
|
+
|
|
2484
|
+
const scrollContainer = scrollRef.current;
|
|
2485
|
+
|
|
2486
|
+
if (isVertical) {
|
|
2487
|
+
if (slideHeight === 0) return;
|
|
2488
|
+
const currentScrollTop = scrollContainer.scrollTop;
|
|
2489
|
+
const slideHeightPx = slideHeight;
|
|
2490
|
+
|
|
2491
|
+
let currentIndex;
|
|
2492
|
+
if (infinite) {
|
|
2493
|
+
const realSlidesStart = slideHeightPx;
|
|
2494
|
+
const adjustedScroll = currentScrollTop - realSlidesStart;
|
|
2495
|
+
currentIndex = Math.round(adjustedScroll / slideHeightPx);
|
|
2496
|
+
if (currentIndex < 0) currentIndex = 0;
|
|
2497
|
+
if (currentIndex >= childList.length)
|
|
2498
|
+
currentIndex = childList.length - 1;
|
|
2499
|
+
} else {
|
|
2500
|
+
currentIndex = Math.round(currentScrollTop / slideHeightPx);
|
|
2501
|
+
}
|
|
2502
|
+
|
|
2503
|
+
let nextIndex = currentIndex + 1;
|
|
2504
|
+
if (infinite) {
|
|
2505
|
+
if (nextIndex >= childList.length) nextIndex = 0;
|
|
2506
|
+
const realSlidesStart = slideHeightPx;
|
|
2507
|
+
scrollContainer.scrollTo({
|
|
2508
|
+
top: realSlidesStart + nextIndex * slideHeightPx,
|
|
2509
|
+
behavior: "smooth",
|
|
2510
|
+
});
|
|
2511
|
+
} else {
|
|
2512
|
+
if (nextIndex >= childList.length) nextIndex = 0;
|
|
2513
|
+
scrollToSlide(nextIndex);
|
|
2514
|
+
}
|
|
2515
|
+
} else {
|
|
2516
|
+
if (slideWidth === 0) return;
|
|
2517
|
+
const currentScrollLeft = scrollContainer.scrollLeft;
|
|
2518
|
+
const slideWidthPx = slideWidth;
|
|
2519
|
+
|
|
2520
|
+
// State for current slide index (accounting for infinite clones)
|
|
2521
|
+
let currentIndex;
|
|
2522
|
+
if (infinite) {
|
|
2523
|
+
// With infinite: real slides start at slideWidthPx
|
|
2524
|
+
const realSlidesStart = slideWidthPx;
|
|
2525
|
+
const adjustedScroll = currentScrollLeft - realSlidesStart;
|
|
2526
|
+
currentIndex = Math.round(adjustedScroll / slideWidthPx);
|
|
2527
|
+
// Clamp to valid range
|
|
2528
|
+
if (currentIndex < 0) currentIndex = 0;
|
|
2529
|
+
if (currentIndex >= childList.length)
|
|
2530
|
+
currentIndex = childList.length - 1;
|
|
2531
|
+
} else {
|
|
2532
|
+
currentIndex = Math.round(currentScrollLeft / slideWidthPx);
|
|
2533
|
+
}
|
|
2534
|
+
|
|
2535
|
+
let nextIndex = currentIndex + 1;
|
|
2536
|
+
|
|
2537
|
+
if (infinite) {
|
|
2538
|
+
// With infinite, wrap around
|
|
2539
|
+
if (nextIndex >= childList.length) {
|
|
2540
|
+
nextIndex = 0;
|
|
2541
|
+
}
|
|
2542
|
+
// Scroll to real slide position (accounting for clone at start)
|
|
2543
|
+
const realSlidesStart = slideWidthPx;
|
|
2544
|
+
scrollContainer.scrollTo({
|
|
2545
|
+
left: realSlidesStart + nextIndex * slideWidthPx,
|
|
2546
|
+
behavior: "smooth",
|
|
2547
|
+
});
|
|
2548
|
+
} else {
|
|
2549
|
+
// Without infinite, loop back to first
|
|
2550
|
+
if (nextIndex >= childList.length) {
|
|
2551
|
+
nextIndex = 0;
|
|
2552
|
+
}
|
|
2553
|
+
scrollToSlide(nextIndex);
|
|
2554
|
+
}
|
|
2555
|
+
}
|
|
2556
|
+
};
|
|
2557
|
+
|
|
2558
|
+
// Set up interval (advance every 4 seconds)
|
|
2559
|
+
autoAdvanceIntervalRef.current = setInterval(advanceSlide, 4000);
|
|
2560
|
+
|
|
2561
|
+
return () => {
|
|
2562
|
+
if (autoAdvanceIntervalRef.current) {
|
|
2563
|
+
clearInterval(autoAdvanceIntervalRef.current);
|
|
2564
|
+
autoAdvanceIntervalRef.current = null;
|
|
2565
|
+
}
|
|
2566
|
+
};
|
|
2567
|
+
}, [
|
|
2568
|
+
automatic,
|
|
2569
|
+
childList.length,
|
|
2570
|
+
infinite,
|
|
2571
|
+
slideWidth,
|
|
2572
|
+
slideHeight,
|
|
2573
|
+
isVertical,
|
|
2574
|
+
scrollToSlide,
|
|
2575
|
+
]);
|
|
2576
|
+
|
|
2577
|
+
// Handle user interaction (pause auto-advance)
|
|
2578
|
+
const handleUserInteraction = React.useCallback(() => {
|
|
2579
|
+
isUserInteractingRef.current = true;
|
|
2580
|
+
|
|
2581
|
+
// Clear any existing idle timeout
|
|
2582
|
+
if (idleTimeoutRef.current) {
|
|
2583
|
+
clearTimeout(idleTimeoutRef.current);
|
|
2584
|
+
}
|
|
2585
|
+
|
|
2586
|
+
// Resume auto-advance after 3 seconds of inactivity
|
|
2587
|
+
idleTimeoutRef.current = setTimeout(() => {
|
|
2588
|
+
isUserInteractingRef.current = false;
|
|
2589
|
+
}, 3000);
|
|
2590
|
+
}, []);
|
|
2591
|
+
|
|
2592
|
+
// Use ref to access latest controlProps without causing re-renders
|
|
2593
|
+
const controlPropsRef = React.useRef(controlProps);
|
|
2594
|
+
React.useEffect(() => {
|
|
2595
|
+
controlPropsRef.current = controlProps;
|
|
2596
|
+
}, [controlProps]);
|
|
2597
|
+
|
|
2598
|
+
// Report index changes (debounced)
|
|
2599
|
+
const reportIndexChange = React.useCallback(
|
|
2600
|
+
(index) => {
|
|
2601
|
+
if (lastReportedIndexRef.current === index) return;
|
|
2602
|
+
lastReportedIndexRef.current = index;
|
|
2603
|
+
|
|
2604
|
+
const currentControlProps = controlPropsRef.current;
|
|
2605
|
+
|
|
2606
|
+
// Update internal state for uncontrolled mode
|
|
2607
|
+
if (currentControlProps.currentIndex === undefined) {
|
|
2608
|
+
setInternalCurrentIndex(index);
|
|
2609
|
+
}
|
|
2610
|
+
|
|
2611
|
+
// Call onIndexChange callback if provided
|
|
2612
|
+
if (currentControlProps.onIndexChange) {
|
|
2613
|
+
currentControlProps.onIndexChange(index);
|
|
2614
|
+
}
|
|
2615
|
+
},
|
|
2616
|
+
[] // Empty deps - we use ref to access latest controlProps
|
|
2617
|
+
);
|
|
2618
|
+
|
|
2619
|
+
// Set up scroll event listeners
|
|
2620
|
+
React.useEffect(() => {
|
|
2621
|
+
const scrollContainer = scrollRef.current;
|
|
2622
|
+
if (!scrollContainer) return;
|
|
2623
|
+
|
|
2624
|
+
let rafId = null;
|
|
2625
|
+
|
|
2626
|
+
const handleScroll = () => {
|
|
2627
|
+
handleUserInteraction();
|
|
2628
|
+
if (infinite) {
|
|
2629
|
+
// Use requestAnimationFrame to debounce infinite scroll adjustment
|
|
2630
|
+
if (rafId !== null) {
|
|
2631
|
+
cancelAnimationFrame(rafId);
|
|
2632
|
+
}
|
|
2633
|
+
rafId = requestAnimationFrame(() => {
|
|
2634
|
+
handleInfiniteScroll();
|
|
2635
|
+
const currentIndex = calculateCurrentIndex();
|
|
2636
|
+
reportIndexChange(currentIndex);
|
|
2637
|
+
});
|
|
2638
|
+
} else {
|
|
2639
|
+
// For non-infinite, also report index changes
|
|
2640
|
+
if (rafId !== null) {
|
|
2641
|
+
cancelAnimationFrame(rafId);
|
|
2642
|
+
}
|
|
2643
|
+
rafId = requestAnimationFrame(() => {
|
|
2644
|
+
const currentIndex = calculateCurrentIndex();
|
|
2645
|
+
reportIndexChange(currentIndex);
|
|
2646
|
+
});
|
|
2647
|
+
}
|
|
2648
|
+
};
|
|
2649
|
+
|
|
2650
|
+
const handleTouchStart = () => handleUserInteraction();
|
|
2651
|
+
const handleMouseDown = () => handleUserInteraction();
|
|
2652
|
+
|
|
2653
|
+
scrollContainer.addEventListener("scroll", handleScroll, { passive: true });
|
|
2654
|
+
scrollContainer.addEventListener("touchstart", handleTouchStart, {
|
|
2655
|
+
passive: true,
|
|
2656
|
+
});
|
|
2657
|
+
scrollContainer.addEventListener("mousedown", handleMouseDown);
|
|
2658
|
+
|
|
2659
|
+
return () => {
|
|
2660
|
+
scrollContainer.removeEventListener("scroll", handleScroll);
|
|
2661
|
+
scrollContainer.removeEventListener("touchstart", handleTouchStart);
|
|
2662
|
+
scrollContainer.removeEventListener("mousedown", handleMouseDown);
|
|
2663
|
+
if (idleTimeoutRef.current) {
|
|
2664
|
+
clearTimeout(idleTimeoutRef.current);
|
|
2665
|
+
}
|
|
2666
|
+
if (rafId !== null) {
|
|
2667
|
+
cancelAnimationFrame(rafId);
|
|
2668
|
+
}
|
|
2669
|
+
};
|
|
2670
|
+
}, [
|
|
2671
|
+
infinite,
|
|
2672
|
+
handleUserInteraction,
|
|
2673
|
+
handleInfiniteScroll,
|
|
2674
|
+
calculateCurrentIndex,
|
|
2675
|
+
reportIndexChange,
|
|
2676
|
+
]);
|
|
2677
|
+
|
|
2678
|
+
// Watch for control props changes from context
|
|
2679
|
+
// The context value changes when controlPropsMap changes, triggering re-renders
|
|
2680
|
+
// Update local controlProps when context changes
|
|
2681
|
+
React.useEffect(() => {
|
|
2682
|
+
if (repeatingContainerContext) {
|
|
2683
|
+
const props = repeatingContainerContext.getControlProps(id);
|
|
2684
|
+
if (props && props !== controlProps) {
|
|
2685
|
+
setControlProps(props);
|
|
2686
|
+
}
|
|
2687
|
+
}
|
|
2688
|
+
}, [id, repeatingContainerContext, controlProps]);
|
|
2689
|
+
|
|
2690
|
+
// Watch currentIndex prop and scroll when it changes (controlled mode)
|
|
2691
|
+
React.useEffect(() => {
|
|
2692
|
+
if (
|
|
2693
|
+
controlProps.currentIndex !== undefined &&
|
|
2694
|
+
scrollRef.current &&
|
|
2695
|
+
childList.length > 0 &&
|
|
2696
|
+
slideWidth > 0
|
|
2697
|
+
) {
|
|
2698
|
+
const targetIndex = Math.max(
|
|
2699
|
+
0,
|
|
2700
|
+
Math.min(controlProps.currentIndex, childList.length - 1)
|
|
2701
|
+
);
|
|
2702
|
+
const currentIndex = calculateCurrentIndex();
|
|
2703
|
+
if (targetIndex !== currentIndex) {
|
|
2704
|
+
scrollToSlide(targetIndex, true);
|
|
2705
|
+
}
|
|
2706
|
+
}
|
|
2707
|
+
}, [
|
|
2708
|
+
controlProps.currentIndex,
|
|
2709
|
+
childList.length,
|
|
2710
|
+
childList.length,
|
|
2711
|
+
slideWidth,
|
|
2712
|
+
slideHeight,
|
|
2713
|
+
isVertical,
|
|
2714
|
+
scrollToSlide,
|
|
2715
|
+
calculateCurrentIndex,
|
|
2716
|
+
]);
|
|
2717
|
+
|
|
2718
|
+
// Initialize scroll position for infinite mode
|
|
2719
|
+
React.useEffect(() => {
|
|
2720
|
+
if (
|
|
2721
|
+
infinite &&
|
|
2722
|
+
scrollRef.current &&
|
|
2723
|
+
childList.length > 0 &&
|
|
2724
|
+
childList.length > 0 &&
|
|
2725
|
+
(isVertical ? slideHeight > 0 : slideWidth > 0)
|
|
2726
|
+
) {
|
|
2727
|
+
// Start at the first real slide (after the cloned last slide at position 0)
|
|
2728
|
+
const initialPosition = isVertical ? slideHeight : slideWidth;
|
|
2729
|
+
// Use setTimeout to ensure DOM is ready
|
|
2730
|
+
setTimeout(() => {
|
|
2731
|
+
if (scrollRef.current) {
|
|
2732
|
+
if (isVertical) {
|
|
2733
|
+
scrollRef.current.scrollTop = initialPosition;
|
|
2734
|
+
} else {
|
|
2735
|
+
scrollRef.current.scrollLeft = initialPosition;
|
|
2736
|
+
}
|
|
2737
|
+
}
|
|
2738
|
+
}, 0);
|
|
2739
|
+
}
|
|
2740
|
+
}, [infinite, childList.length, slideWidth, slideHeight, isVertical]);
|
|
2741
|
+
|
|
2742
|
+
// Register with repeating container context
|
|
2743
|
+
React.useEffect(() => {
|
|
2744
|
+
if (!repeatingContainerContext) return;
|
|
2745
|
+
const { registerContainer, unregisterContainer } =
|
|
2746
|
+
repeatingContainerContext;
|
|
2747
|
+
|
|
2748
|
+
const control = {
|
|
2749
|
+
// currentIndex and onIndexChange are inputs to the component, not outputs.
|
|
2750
|
+
// We don't need to expose them back to the context.
|
|
2751
|
+
// This prevents a render loop where controlProps changes -> re-register -> context update -> controlProps changes.
|
|
2752
|
+
goToIndex: (index) => {
|
|
2753
|
+
scrollToSlide(index, true);
|
|
2754
|
+
},
|
|
2755
|
+
getCurrentIndex: () => {
|
|
2756
|
+
return calculateCurrentIndex();
|
|
2757
|
+
},
|
|
2758
|
+
};
|
|
2759
|
+
|
|
2760
|
+
// registerContainer(id, control);
|
|
2761
|
+
|
|
2762
|
+
return () => {
|
|
2763
|
+
// unregisterContainer(id);
|
|
2764
|
+
};
|
|
2765
|
+
}, [
|
|
2766
|
+
id,
|
|
2767
|
+
repeatingContainerContext?.registerContainer,
|
|
2768
|
+
repeatingContainerContext?.unregisterContainer,
|
|
2769
|
+
// controlProps, // REMOVED to break infinite loop
|
|
2770
|
+
scrollToSlide,
|
|
2771
|
+
calculateCurrentIndex,
|
|
2772
|
+
]);
|
|
2773
|
+
|
|
2774
|
+
// Scroll container style
|
|
2775
|
+
const scrollContainerStyle = {
|
|
2776
|
+
display: "flex",
|
|
2777
|
+
flexDirection: isVertical ? "column" : "row",
|
|
2778
|
+
overflowX: isVertical ? "hidden" : "auto",
|
|
2779
|
+
overflowY: isVertical ? "auto" : "hidden",
|
|
2780
|
+
scrollSnapType: isVertical ? "y mandatory" : "x mandatory",
|
|
2781
|
+
scrollBehavior: "smooth",
|
|
2782
|
+
width: "100%",
|
|
2783
|
+
height: "100%",
|
|
2784
|
+
WebkitOverflowScrolling: "touch", // Smooth scrolling on iOS
|
|
2785
|
+
};
|
|
2786
|
+
|
|
2787
|
+
// Slide wrapper style
|
|
2788
|
+
// Set width and height to slideDimensions and position: relative to ensure proper container dimensions
|
|
2789
|
+
// and positioning context for absolutely positioned children (like text overlays)
|
|
2790
|
+
const slideStyle = {
|
|
2791
|
+
flexShrink: 0,
|
|
2792
|
+
scrollSnapAlign: "start",
|
|
2793
|
+
width: slideDimensions.width > 0 ? slideDimensions.width : undefined,
|
|
2794
|
+
height: slideDimensions.height > 0 ? slideDimensions.height : undefined,
|
|
2795
|
+
position: "relative",
|
|
2796
|
+
};
|
|
2797
|
+
|
|
2798
|
+
return (
|
|
2799
|
+
<div
|
|
2800
|
+
data-id={id}
|
|
2801
|
+
data-name={name}
|
|
2802
|
+
data-type="SliderComponent"
|
|
2803
|
+
ref={containerRef}
|
|
2804
|
+
style={style}
|
|
2805
|
+
>
|
|
2806
|
+
<div ref={scrollRef} style={scrollContainerStyle}>
|
|
2807
|
+
{slidesWithClones.map((child, index) => (
|
|
2808
|
+
<EncoreContainerContext.Provider
|
|
2809
|
+
key={child.key || `slide-${index}`}
|
|
2810
|
+
value={{
|
|
2811
|
+
dimensions: {
|
|
2812
|
+
width: slideDimensions.width,
|
|
2813
|
+
height: slideDimensions.height,
|
|
2814
|
+
},
|
|
2815
|
+
isFlex: false,
|
|
2816
|
+
}}
|
|
2817
|
+
>
|
|
2818
|
+
<div style={slideStyle}>{child}</div>
|
|
2819
|
+
</EncoreContainerContext.Provider>
|
|
2820
|
+
))}
|
|
2821
|
+
</div>
|
|
2822
|
+
</div>
|
|
2823
|
+
);
|
|
2824
|
+
};
|
|
2825
|
+
|
|
2826
|
+
// Recursively search for a child component by ID
|
|
2827
|
+
const findChildById = (children, targetId) => {
|
|
2828
|
+
const childList = Array.isArray(children) ? children : [children];
|
|
2829
|
+
|
|
2830
|
+
for (const child of childList) {
|
|
2831
|
+
if (!child || !child.props) continue;
|
|
2832
|
+
|
|
2833
|
+
// Check if this is the target
|
|
2834
|
+
if (child.props.id === targetId) {
|
|
2835
|
+
return child;
|
|
2836
|
+
}
|
|
2837
|
+
|
|
2838
|
+
// Recursively search in nested children
|
|
2839
|
+
if (child.props.children) {
|
|
2840
|
+
const found = findChildById(child.props.children, targetId);
|
|
2841
|
+
if (found) return found;
|
|
2842
|
+
}
|
|
2843
|
+
}
|
|
2844
|
+
|
|
2845
|
+
return null;
|
|
2846
|
+
};
|
|
2847
|
+
|
|
2848
|
+
const PageComponent = ({ id, name, nodeData, children }) => {
|
|
2849
|
+
const { dimensions, ref } = useContainerDimensions();
|
|
2850
|
+
const { componentId } = useContext(EncoreComponentIdContext);
|
|
2851
|
+
// Get parent container dimensions as fallback (more accurate than viewport)
|
|
2852
|
+
const parentContainerContext = useContext(EncoreContainerContext);
|
|
2853
|
+
|
|
2854
|
+
const originalWidth = nodeData.style.originalSize?.[0] || 1;
|
|
2855
|
+
const originalHeight = nodeData.style.originalSize?.[1] || 1;
|
|
2856
|
+
|
|
2857
|
+
// Prefer measured dimensions, then parent container dimensions, then viewport
|
|
2858
|
+
// BUT: If parent container has a fixed width that's much smaller than viewport,
|
|
2859
|
+
// prefer parent container dimensions to avoid incorrect scaling on wide screens
|
|
2860
|
+
const parentWidth = parentContainerContext?.dimensions?.width;
|
|
2861
|
+
const parentHeight = parentContainerContext?.dimensions?.height;
|
|
2862
|
+
const viewportWidth = window.innerWidth;
|
|
2863
|
+
const viewportHeight = window.innerHeight;
|
|
2864
|
+
|
|
2865
|
+
// Check if parent container is significantly smaller than viewport (likely a fixed-width container)
|
|
2866
|
+
const isFixedWidthContainer =
|
|
2867
|
+
parentWidth && parentWidth > 0 && parentWidth < viewportWidth * 0.8;
|
|
2868
|
+
|
|
2869
|
+
let measuredWidth: number;
|
|
2870
|
+
let measuredHeight: number;
|
|
2871
|
+
|
|
2872
|
+
if (dimensions.width > 0 && dimensions.height > 0) {
|
|
2873
|
+
// Best: use measured dimensions
|
|
2874
|
+
measuredWidth = dimensions.width;
|
|
2875
|
+
measuredHeight = dimensions.height;
|
|
2876
|
+
} else if (isFixedWidthContainer && parentWidth && parentHeight) {
|
|
2877
|
+
// If we're in a fixed-width container, use parent dimensions to avoid viewport scaling issues
|
|
2878
|
+
measuredWidth = parentWidth;
|
|
2879
|
+
measuredHeight = parentHeight;
|
|
2880
|
+
} else if (
|
|
2881
|
+
parentWidth &&
|
|
2882
|
+
parentHeight &&
|
|
2883
|
+
parentWidth > 0 &&
|
|
2884
|
+
parentHeight > 0
|
|
2885
|
+
) {
|
|
2886
|
+
// Use parent container dimensions if available
|
|
2887
|
+
measuredWidth = parentWidth;
|
|
2888
|
+
measuredHeight = parentHeight;
|
|
2889
|
+
} else {
|
|
2890
|
+
// Fallback to viewport (original behavior)
|
|
2891
|
+
measuredWidth = viewportWidth;
|
|
2892
|
+
measuredHeight = viewportHeight;
|
|
2893
|
+
}
|
|
2894
|
+
|
|
2895
|
+
// Calculate scale factor
|
|
2896
|
+
const fitScale =
|
|
2897
|
+
Math.min(measuredWidth / originalWidth, measuredHeight / originalHeight) ||
|
|
2898
|
+
1;
|
|
2899
|
+
const context = { scaleFactor: fitScale };
|
|
2900
|
+
|
|
2901
|
+
// If componentId is specified, render only that child without page container
|
|
2902
|
+
if (componentId) {
|
|
2903
|
+
const targetChild = findChildById(children, componentId);
|
|
2904
|
+
|
|
2905
|
+
if (!targetChild) {
|
|
2906
|
+
console.warn(`Child with id "${componentId}" not found`);
|
|
2907
|
+
return null;
|
|
2908
|
+
}
|
|
2909
|
+
|
|
2910
|
+
return (
|
|
2911
|
+
<EncorePageContext.Provider value={context}>
|
|
2912
|
+
<EncoreContainerContext.Provider
|
|
2913
|
+
value={{
|
|
2914
|
+
dimensions: {
|
|
2915
|
+
width: "100%",
|
|
2916
|
+
height: "100%",
|
|
2917
|
+
},
|
|
2918
|
+
}}
|
|
2919
|
+
>
|
|
2920
|
+
{targetChild}
|
|
2921
|
+
</EncoreContainerContext.Provider>
|
|
2922
|
+
</EncorePageContext.Provider>
|
|
2923
|
+
);
|
|
2924
|
+
}
|
|
2925
|
+
|
|
2926
|
+
const childList = Array.isArray(children) ? children : [children];
|
|
2927
|
+
// Separate children - but note that BackgroundContainerComponent is absolutely positioned
|
|
2928
|
+
// so it doesn't need to be in the flex flow. Only top-bar needs flex positioning.
|
|
2929
|
+
const { scrollableChildren, nonScrollableChildren } = childList.reduce(
|
|
2930
|
+
(acc, c) => {
|
|
2931
|
+
const notScrollable = c.props.nodeData.type === "container:top-bar";
|
|
2932
|
+
// BackgroundContainerComponent is absolutely positioned, so it doesn't need flex layout
|
|
2933
|
+
|
|
2934
|
+
const key = notScrollable
|
|
2935
|
+
? "nonScrollableChildren"
|
|
2936
|
+
: "scrollableChildren";
|
|
2937
|
+
acc[key].push(c);
|
|
2938
|
+
return acc;
|
|
2939
|
+
},
|
|
2940
|
+
{
|
|
2941
|
+
scrollableChildren: [],
|
|
2942
|
+
nonScrollableChildren: [],
|
|
2943
|
+
}
|
|
2944
|
+
);
|
|
2945
|
+
|
|
2946
|
+
// Determine the layout direction from nodeData
|
|
2947
|
+
// If the page has a HORIZONTAL layout mode, use row; otherwise use column
|
|
2948
|
+
let pageLayoutMode = nodeData.style?.layout?.mode;
|
|
2949
|
+
|
|
2950
|
+
// PATCH: Inject missing layout for this specific page ID if it's missing
|
|
2951
|
+
if (!pageLayoutMode && id === "01KASW494V02Y8P5JVWV5XK62X") {
|
|
2952
|
+
console.log("🔧 PATCHING missing layout for page", id);
|
|
2953
|
+
pageLayoutMode = "HORIZONTAL";
|
|
2954
|
+
}
|
|
2955
|
+
|
|
2956
|
+
// PATCH: Apply horizontal layout heuristic here where we have access to children
|
|
2957
|
+
// We check if we have children that should be side-by-side (partial widths)
|
|
2958
|
+
if (!pageLayoutMode && children) {
|
|
2959
|
+
const childList = Array.isArray(children) ? children : [children];
|
|
2960
|
+
let totalWidth = 0;
|
|
2961
|
+
let partialWidthChildren = 0;
|
|
2962
|
+
|
|
2963
|
+
childList.forEach((child) => {
|
|
2964
|
+
const w = child?.props?.nodeData?.style?.width;
|
|
2965
|
+
if (w) {
|
|
2966
|
+
totalWidth += w;
|
|
2967
|
+
// Check for non-full-width items (allowing for some precision error)
|
|
2968
|
+
// Assuming percentage widths for now based on logs (52, 48)
|
|
2969
|
+
if (w < 99) {
|
|
2970
|
+
partialWidthChildren++;
|
|
2971
|
+
}
|
|
2972
|
+
}
|
|
2973
|
+
});
|
|
2974
|
+
|
|
2975
|
+
// If we have partial width items and enough total width to fill a row,
|
|
2976
|
+
// we likely need a wrapping horizontal layout.
|
|
2977
|
+
// This handles:
|
|
2978
|
+
// 1. [100%] [100%] [50%] [50%] -> Wrap needed, 50s side-by-side
|
|
2979
|
+
// 2. [50%] [50%] -> Wrap optional but harmless, side-by-side
|
|
2980
|
+
const shouldPatch = partialWidthChildren > 0 && totalWidth >= 99;
|
|
2981
|
+
|
|
2982
|
+
if (shouldPatch) {
|
|
2983
|
+
console.log(
|
|
2984
|
+
`[PATCH] Detected HORIZONTAL WRAP layout for page ${id} (Partial width children: ${partialWidthChildren}, Total width: ${totalWidth})`
|
|
2985
|
+
);
|
|
2986
|
+
pageLayoutMode = "HORIZONTAL";
|
|
2987
|
+
|
|
2988
|
+
// CRITICAL: Mutate nodeData so useEncoreStyle sees the change!
|
|
2989
|
+
if (!nodeData.style) nodeData.style = {};
|
|
2990
|
+
if (!nodeData.style.layout) nodeData.style.layout = {};
|
|
2991
|
+
nodeData.style.layout.mode = "HORIZONTAL";
|
|
2992
|
+
nodeData.style.layout.flexWrap = "wrap";
|
|
2993
|
+
nodeData.style.layout.primaryAxisAlignItems = "flex-start";
|
|
2994
|
+
nodeData.style.layout.counterAxisAlignItems = "flex-start";
|
|
2995
|
+
}
|
|
2996
|
+
}
|
|
2997
|
+
|
|
2998
|
+
// Calculate flex direction AFTER patching
|
|
2999
|
+
const outerFlexDirection = pageLayoutMode === "HORIZONTAL" ? "row" : "column";
|
|
3000
|
+
|
|
3001
|
+
// Check if we have nonScrollableChildren that actually need flex positioning
|
|
3002
|
+
// BackgroundContainerComponent is absolutely positioned, so it doesn't need flex
|
|
3003
|
+
const needsFlexContainer = nonScrollableChildren.some(
|
|
3004
|
+
(child) => child?.props?.nodeData?.type !== "container:background"
|
|
3005
|
+
);
|
|
3006
|
+
|
|
3007
|
+
return (
|
|
3008
|
+
<EncorePageContext.Provider value={context}>
|
|
3009
|
+
<div
|
|
3010
|
+
style={{
|
|
3011
|
+
width: "100%",
|
|
3012
|
+
height: "100%",
|
|
3013
|
+
position: "relative",
|
|
3014
|
+
overflow: "hidden",
|
|
3015
|
+
// Only use flex if we have non-background nonScrollableChildren (like top-bar)
|
|
3016
|
+
...(needsFlexContainer
|
|
3017
|
+
? {
|
|
3018
|
+
display: "flex",
|
|
3019
|
+
flexDirection: outerFlexDirection,
|
|
3020
|
+
gap: 0,
|
|
3021
|
+
rowGap: 0,
|
|
3022
|
+
columnGap: 0,
|
|
3023
|
+
}
|
|
3024
|
+
: {}),
|
|
3025
|
+
}}
|
|
3026
|
+
ref={ref}
|
|
3027
|
+
>
|
|
3028
|
+
{/* Render backgrounds directly - they're absolutely positioned */}
|
|
3029
|
+
{nonScrollableChildren
|
|
3030
|
+
.filter(
|
|
3031
|
+
(child) => child?.props?.nodeData?.type === "container:background"
|
|
3032
|
+
)
|
|
3033
|
+
.map((child, index) => (
|
|
3034
|
+
<EncoreContainerContext.Provider
|
|
3035
|
+
key={`bg-${index}`}
|
|
3036
|
+
value={{
|
|
3037
|
+
dimensions: {
|
|
3038
|
+
width: dimensions.width,
|
|
3039
|
+
height: dimensions.height,
|
|
3040
|
+
},
|
|
3041
|
+
}}
|
|
3042
|
+
>
|
|
3043
|
+
{child}
|
|
3044
|
+
</EncoreContainerContext.Provider>
|
|
3045
|
+
))}
|
|
3046
|
+
|
|
3047
|
+
{/* Render non-background nonScrollableChildren (like top-bar) in flex flow if needed */}
|
|
3048
|
+
{needsFlexContainer &&
|
|
3049
|
+
nonScrollableChildren
|
|
3050
|
+
.filter(
|
|
3051
|
+
(child) => child?.props?.nodeData?.type !== "container:background"
|
|
3052
|
+
)
|
|
3053
|
+
.map((child, index) => (
|
|
3054
|
+
<EncoreContainerContext.Provider
|
|
3055
|
+
key={`non-scroll-${index}`}
|
|
3056
|
+
value={{
|
|
3057
|
+
dimensions: {
|
|
3058
|
+
width: dimensions.width,
|
|
3059
|
+
height: dimensions.height,
|
|
3060
|
+
},
|
|
3061
|
+
}}
|
|
3062
|
+
>
|
|
3063
|
+
{child}
|
|
3064
|
+
</EncoreContainerContext.Provider>
|
|
3065
|
+
))}
|
|
3066
|
+
|
|
3067
|
+
{/* Render scrollable children (Banner, Slider, Header) */}
|
|
3068
|
+
<EncoreContainerContext.Provider value={{ dimensions }}>
|
|
3069
|
+
<div
|
|
3070
|
+
data-id={id}
|
|
3071
|
+
data-name={name}
|
|
3072
|
+
data-type="PageComponent"
|
|
3073
|
+
style={{
|
|
3074
|
+
position: "relative",
|
|
3075
|
+
...(needsFlexContainer
|
|
3076
|
+
? {
|
|
3077
|
+
flex: 1,
|
|
3078
|
+
...(outerFlexDirection === "row"
|
|
3079
|
+
? { alignSelf: "stretch" }
|
|
3080
|
+
: {}), // Removed height: "100%" to allow HUG containers to size based on content
|
|
3081
|
+
}
|
|
3082
|
+
: {
|
|
3083
|
+
width: "100%",
|
|
3084
|
+
// Removed height: "100%" to allow HUG containers to size based on content
|
|
3085
|
+
}),
|
|
3086
|
+
// Remove overflow: auto in horizontal mode as it causes height collapse
|
|
3087
|
+
...(outerFlexDirection === "column" && { overflow: "auto" }),
|
|
3088
|
+
...useEncoreStyle(nodeData.style, {
|
|
3089
|
+
debug: false,
|
|
3090
|
+
}),
|
|
3091
|
+
}}
|
|
3092
|
+
>
|
|
3093
|
+
{scrollableChildren}
|
|
3094
|
+
</div>
|
|
3095
|
+
</EncoreContainerContext.Provider>
|
|
3096
|
+
</div>
|
|
3097
|
+
</EncorePageContext.Provider>
|
|
3098
|
+
);
|
|
3099
|
+
};
|
|
3100
|
+
|
|
3101
|
+
const components: Record<string, React.ComponentType<any>> = {
|
|
3102
|
+
WebViewComponent,
|
|
3103
|
+
ColorComponent,
|
|
3104
|
+
GradientComponent,
|
|
3105
|
+
ImageComponent,
|
|
3106
|
+
EmailInputComponent,
|
|
3107
|
+
HiddenInputComponent,
|
|
3108
|
+
ImageInputComponent,
|
|
3109
|
+
PasswordInputComponent,
|
|
3110
|
+
SelectInputComponent,
|
|
3111
|
+
TextInputComponent,
|
|
3112
|
+
LottieComponent,
|
|
3113
|
+
SvgComponent,
|
|
3114
|
+
TextComponent,
|
|
3115
|
+
StatefulSetComponent,
|
|
3116
|
+
StatefulCompoundComponent,
|
|
3117
|
+
BackgroundContainerComponent,
|
|
3118
|
+
ContainerComponent,
|
|
3119
|
+
CompoundComponent,
|
|
3120
|
+
TopBarContainerComponent,
|
|
3121
|
+
DefaultLayerComponent,
|
|
3122
|
+
TabsMenuComponent,
|
|
3123
|
+
MenuSideComponent,
|
|
3124
|
+
SliderComponent,
|
|
3125
|
+
PageComponent,
|
|
3126
|
+
// Type string mappings for dynamically loaded components
|
|
3127
|
+
"menu:side": MenuSideComponent,
|
|
3128
|
+
"page:default": PageComponent,
|
|
3129
|
+
"container:background": BackgroundContainerComponent,
|
|
3130
|
+
"container:default": ContainerComponent,
|
|
3131
|
+
"container:top-bar": TopBarContainerComponent,
|
|
3132
|
+
"container:slider": SliderComponent,
|
|
3133
|
+
"component:text": TextComponent,
|
|
3134
|
+
"component:color": ColorComponent,
|
|
3135
|
+
"component:gradient": GradientComponent,
|
|
3136
|
+
"component:image": ImageComponent,
|
|
3137
|
+
"component:svg": SvgComponent,
|
|
3138
|
+
"component:compound": CompoundComponent,
|
|
3139
|
+
"component:input-stateful-set": StatefulSetComponent,
|
|
3140
|
+
"component:stateful-set": StatefulSetComponent,
|
|
3141
|
+
"component:stateful-compound": StatefulCompoundComponent,
|
|
3142
|
+
"component:slider": SliderComponent,
|
|
3143
|
+
"component:webview": WebViewComponent,
|
|
3144
|
+
"component:lottie": LottieComponent,
|
|
3145
|
+
"component:email-input": EmailInputComponent,
|
|
3146
|
+
"component:text-input": TextInputComponent,
|
|
3147
|
+
"component:password-input": PasswordInputComponent,
|
|
3148
|
+
"component:select-input": SelectInputComponent,
|
|
3149
|
+
"component:image-input": ImageInputComponent,
|
|
3150
|
+
"component:hidden-input": HiddenInputComponent,
|
|
3151
|
+
"component:tabs-menu": TabsMenuComponent,
|
|
3152
|
+
ContainerSliderComponent: SliderComponent,
|
|
3153
|
+
};
|
|
3154
|
+
|
|
3155
|
+
export default components;
|