@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.
Files changed (127) hide show
  1. package/bin/encore-lib.js +3 -0
  2. package/dist/_virtual/_commonjsHelpers.js +7 -0
  3. package/dist/_virtual/_commonjsHelpers.js.map +1 -0
  4. package/dist/_virtual/main.js +8 -0
  5. package/dist/_virtual/main.js.map +1 -0
  6. package/dist/_virtual/main2.js +5 -0
  7. package/dist/_virtual/main2.js.map +1 -0
  8. package/dist/app.js +9 -0
  9. package/dist/app.js.map +1 -0
  10. package/dist/cli/commands/download.js +82 -0
  11. package/dist/cli/commands/download.js.map +1 -0
  12. package/dist/cli/commands/generate.js +1526 -0
  13. package/dist/cli/commands/generate.js.map +1 -0
  14. package/dist/cli.js +25 -0
  15. package/dist/cli.js.map +1 -0
  16. package/dist/components/DynamicComponent.js +24 -0
  17. package/dist/components/DynamicComponent.js.map +1 -0
  18. package/dist/components/EncoreApp.js +259 -0
  19. package/dist/components/EncoreApp.js.map +1 -0
  20. package/dist/components/EncoreErrorBoundary.js +33 -0
  21. package/dist/components/EncoreErrorBoundary.js.map +1 -0
  22. package/dist/components/EncoreLoadingFallback.js +20 -0
  23. package/dist/components/EncoreLoadingFallback.js.map +1 -0
  24. package/dist/components.js +1454 -0
  25. package/dist/components.js.map +1 -0
  26. package/dist/constants.d.ts +3 -0
  27. package/dist/constants.d.ts.map +1 -0
  28. package/dist/contexts/EncoreActionContext.js +6 -0
  29. package/dist/contexts/EncoreActionContext.js.map +1 -0
  30. package/dist/contexts/EncoreAppContext.js +9 -0
  31. package/dist/contexts/EncoreAppContext.js.map +1 -0
  32. package/dist/contexts/EncoreBindingContext.js +6 -0
  33. package/dist/contexts/EncoreBindingContext.js.map +1 -0
  34. package/dist/contexts/EncoreComponentIdContext.js +8 -0
  35. package/dist/contexts/EncoreComponentIdContext.js.map +1 -0
  36. package/dist/contexts/EncoreRepeatingContainerContext.js +6 -0
  37. package/dist/contexts/EncoreRepeatingContainerContext.js.map +1 -0
  38. package/dist/hooks/usePusherUpdates.js +60 -0
  39. package/dist/hooks/usePusherUpdates.js.map +1 -0
  40. package/dist/index.js +16 -0
  41. package/dist/index.js.map +1 -0
  42. package/dist/lib/dynamicModules.js +132 -0
  43. package/dist/lib/dynamicModules.js.map +1 -0
  44. package/dist/lib/fetcher.js +58 -0
  45. package/dist/lib/fetcher.js.map +1 -0
  46. package/dist/lib/localMode.js +21 -0
  47. package/dist/lib/localMode.js.map +1 -0
  48. package/dist/lib/packages.js +18 -0
  49. package/dist/lib/packages.js.map +1 -0
  50. package/dist/node_modules/dotenv/lib/main.js +198 -0
  51. package/dist/node_modules/dotenv/lib/main.js.map +1 -0
  52. package/dist/node_modules/dotenv/package.json.js +8 -0
  53. package/dist/node_modules/dotenv/package.json.js.map +1 -0
  54. package/dist/packages/encore-lib/constants.js +6 -0
  55. package/dist/packages/encore-lib/constants.js.map +1 -0
  56. package/dist/src/app.d.ts +5 -0
  57. package/dist/src/app.d.ts.map +1 -0
  58. package/dist/src/cli/commands/download.d.ts +2 -0
  59. package/dist/src/cli/commands/download.d.ts.map +1 -0
  60. package/dist/src/cli/commands/generate.d.ts +2 -0
  61. package/dist/src/cli/commands/generate.d.ts.map +1 -0
  62. package/dist/src/cli/index.d.ts +2 -0
  63. package/dist/src/cli/index.d.ts.map +1 -0
  64. package/dist/src/components/DynamicComponent.d.ts +12 -0
  65. package/dist/src/components/DynamicComponent.d.ts.map +1 -0
  66. package/dist/src/components/EncoreApp.d.ts +27 -0
  67. package/dist/src/components/EncoreApp.d.ts.map +1 -0
  68. package/dist/src/components/EncoreErrorBoundary.d.ts +17 -0
  69. package/dist/src/components/EncoreErrorBoundary.d.ts.map +1 -0
  70. package/dist/src/components/EncoreLoadingFallback.d.ts +4 -0
  71. package/dist/src/components/EncoreLoadingFallback.d.ts.map +1 -0
  72. package/dist/src/components.d.ts +4 -0
  73. package/dist/src/components.d.ts.map +1 -0
  74. package/dist/src/contexts/EncoreActionContext.d.ts +13 -0
  75. package/dist/src/contexts/EncoreActionContext.d.ts.map +1 -0
  76. package/dist/src/contexts/EncoreAppContext.d.ts +8 -0
  77. package/dist/src/contexts/EncoreAppContext.d.ts.map +1 -0
  78. package/dist/src/contexts/EncoreBindingContext.d.ts +5 -0
  79. package/dist/src/contexts/EncoreBindingContext.d.ts.map +1 -0
  80. package/dist/src/contexts/EncoreComponentIdContext.d.ts +8 -0
  81. package/dist/src/contexts/EncoreComponentIdContext.d.ts.map +1 -0
  82. package/dist/src/contexts/EncoreRepeatingContainerContext.d.ts +21 -0
  83. package/dist/src/contexts/EncoreRepeatingContainerContext.d.ts.map +1 -0
  84. package/dist/src/hooks/useAuthRedirect.d.ts +3 -0
  85. package/dist/src/hooks/useAuthRedirect.d.ts.map +1 -0
  86. package/dist/src/hooks/usePusherUpdates.d.ts +18 -0
  87. package/dist/src/hooks/usePusherUpdates.d.ts.map +1 -0
  88. package/dist/src/index.d.ts +8 -0
  89. package/dist/src/index.d.ts.map +1 -0
  90. package/dist/src/lib/dynamicModules.d.ts +8 -0
  91. package/dist/src/lib/dynamicModules.d.ts.map +1 -0
  92. package/dist/src/lib/fetcher.d.ts +5 -0
  93. package/dist/src/lib/fetcher.d.ts.map +1 -0
  94. package/dist/src/lib/localMode.d.ts +3 -0
  95. package/dist/src/lib/localMode.d.ts.map +1 -0
  96. package/dist/src/lib/packages.d.ts +6 -0
  97. package/dist/src/lib/packages.d.ts.map +1 -0
  98. package/dist/src/stores/useEncoreState.d.ts +33 -0
  99. package/dist/src/stores/useEncoreState.d.ts.map +1 -0
  100. package/dist/stores/useEncoreState.js +70 -0
  101. package/dist/stores/useEncoreState.js.map +1 -0
  102. package/package.json +60 -0
  103. package/src/AGENTS.md +161 -0
  104. package/src/README.md +110 -0
  105. package/src/app.ts +5 -0
  106. package/src/cli/commands/download.ts +133 -0
  107. package/src/cli/commands/generate.ts +3045 -0
  108. package/src/cli/index.ts +35 -0
  109. package/src/components/DynamicComponent.tsx +40 -0
  110. package/src/components/EncoreApp.tsx +759 -0
  111. package/src/components/EncoreErrorBoundary.tsx +49 -0
  112. package/src/components/EncoreLoadingFallback.tsx +25 -0
  113. package/src/components.tsx +3155 -0
  114. package/src/contexts/EncoreActionContext.ts +18 -0
  115. package/src/contexts/EncoreAppContext.ts +13 -0
  116. package/src/contexts/EncoreBindingContext.ts +6 -0
  117. package/src/contexts/EncoreComponentIdContext.ts +12 -0
  118. package/src/contexts/EncoreRepeatingContainerContext.ts +30 -0
  119. package/src/hooks/useAuthRedirect.ts +63 -0
  120. package/src/hooks/usePusherUpdates.ts +156 -0
  121. package/src/index.ts +16 -0
  122. package/src/lib/dynamicModules.ts +193 -0
  123. package/src/lib/fetcher.ts +108 -0
  124. package/src/lib/localMode.ts +30 -0
  125. package/src/lib/moduleRegistry.ts +24 -0
  126. package/src/lib/packages.ts +33 -0
  127. 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;