@bravostudioai/react 0.1.28 → 0.1.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +553 -0
- package/package.json +1 -1
- package/src/codegen/generator.ts +74 -0
- package/src/codegen/parser.ts +47 -707
- package/src/codegen/propQualification.ts +284 -0
- package/src/components/EncoreApp.tsx +92 -540
- package/src/components/EncoreContextProviders.tsx +60 -0
- package/src/hooks/useFontLoader.ts +84 -0
- package/src/hooks/usePusherUpdates.ts +14 -23
- package/src/hooks/useRepeatingContainers.ts +147 -0
- package/src/index.ts +4 -1
- package/src/lib/dataPatching.ts +78 -0
- package/src/lib/dynamicModules.ts +8 -9
- package/src/lib/fetcher.ts +2 -9
- package/src/lib/logger.ts +53 -0
- package/src/lib/moduleRegistry.ts +3 -1
- package/src/stores/useEncoreState.ts +62 -2
- package/src/version.ts +1 -1
|
@@ -11,18 +11,15 @@ import React, {
|
|
|
11
11
|
useMemo,
|
|
12
12
|
} from "react";
|
|
13
13
|
import { isLocalMode, setLocalModeOverride } from "../lib/localMode";
|
|
14
|
-
import
|
|
15
|
-
import EncoreComponentIdContext from "../contexts/EncoreComponentIdContext";
|
|
16
|
-
import EncoreActionContext, {
|
|
17
|
-
type EncoreActionPayload,
|
|
18
|
-
} from "../contexts/EncoreActionContext";
|
|
19
|
-
import EncoreRepeatingContainerContext, {
|
|
20
|
-
type RepeatingContainerControl,
|
|
21
|
-
} from "../contexts/EncoreRepeatingContainerContext";
|
|
14
|
+
import type { EncoreActionPayload } from "../contexts/EncoreActionContext";
|
|
22
15
|
import DynamicComponent from "./DynamicComponent";
|
|
23
|
-
// import { Link } from "react-router-dom"; // Removed dependency
|
|
24
16
|
import { useEncoreRouter } from "../contexts/EncoreRouterContext";
|
|
25
17
|
import { usePusherUpdates } from "../hooks/usePusherUpdates";
|
|
18
|
+
import { useFontLoader } from "../hooks/useFontLoader";
|
|
19
|
+
import { useRepeatingContainers } from "../hooks/useRepeatingContainers";
|
|
20
|
+
import { EncoreContextProviders } from "./EncoreContextProviders";
|
|
21
|
+
import { patchPageData } from "../lib/dataPatching";
|
|
22
|
+
import logger from "../lib/logger";
|
|
26
23
|
|
|
27
24
|
// Simple internal Link component that uses our router context
|
|
28
25
|
const Link = ({ to, children, style, ...props }: any) => {
|
|
@@ -42,31 +39,47 @@ const Link = ({ to, children, style, ...props }: any) => {
|
|
|
42
39
|
);
|
|
43
40
|
};
|
|
44
41
|
|
|
45
|
-
|
|
42
|
+
/**
|
|
43
|
+
* Props for the EncoreApp component
|
|
44
|
+
*/
|
|
45
|
+
export type EncoreAppProps = {
|
|
46
|
+
/** Unique identifier for the Encore app */
|
|
46
47
|
appId: string;
|
|
48
|
+
/** Unique identifier for the page to render. If not provided, shows page selector UI */
|
|
47
49
|
pageId?: string;
|
|
50
|
+
/** Optional component identifier for context tracking */
|
|
48
51
|
componentId?: string;
|
|
52
|
+
/** Fallback UI to show during component loading */
|
|
49
53
|
fallback?: React.ReactNode;
|
|
54
|
+
/** Callback fired when the container size changes */
|
|
50
55
|
onSizeChange?: (size: { width: number; height: number }) => void;
|
|
56
|
+
/** Callback fired when the content size changes (including overflow) */
|
|
51
57
|
onContentSizeChange?: (size: { width: number; height: number }) => void;
|
|
58
|
+
/** Callback fired when user interactions trigger actions (button clicks, form submissions, etc.) */
|
|
52
59
|
onAction?: (payload: EncoreActionPayload) => void | Promise<void>;
|
|
60
|
+
/** Data bindings for components with encore:data tags. Maps component IDs to display values */
|
|
53
61
|
data?: Record<string, string | number | any[]>;
|
|
54
|
-
|
|
62
|
+
/** Force component loading from "remote" CDN or "local" filesystem */
|
|
55
63
|
source?: "remote" | "local";
|
|
56
|
-
|
|
64
|
+
/** Control repeating containers (sliders, lists) programmatically by container ID */
|
|
57
65
|
repeatingContainerControls?: Record<
|
|
58
66
|
string,
|
|
59
67
|
{ currentIndex?: number; onIndexChange?: (index: number) => void }
|
|
60
68
|
>;
|
|
61
|
-
|
|
69
|
+
/** Control input groups (radio button-like behavior). Maps group name to active element */
|
|
62
70
|
inputGroups?: Record<string, string>;
|
|
63
|
-
|
|
71
|
+
/** Base URL for the Encore service API */
|
|
64
72
|
baseURL?: string;
|
|
73
|
+
/** Provide app definition directly instead of fetching (for offline/bundled deployments) */
|
|
65
74
|
appDefinition?: any;
|
|
75
|
+
/** Provide page definition directly instead of fetching (for offline/bundled deployments) */
|
|
66
76
|
pageDefinition?: any;
|
|
77
|
+
/** Provide component code directly instead of fetching (for offline/bundled deployments) */
|
|
67
78
|
componentCode?: string;
|
|
68
79
|
};
|
|
69
80
|
|
|
81
|
+
type Props = EncoreAppProps;
|
|
82
|
+
|
|
70
83
|
type EncoreAssetsById = Record<string, { url?: string }>;
|
|
71
84
|
type EncoreState = {
|
|
72
85
|
setApp: (app: unknown) => void;
|
|
@@ -80,6 +93,40 @@ const setAppIdSelector = (state: EncoreState) => state.setAppId;
|
|
|
80
93
|
const setPageIdSelector = (state: EncoreState) => state.setPageId;
|
|
81
94
|
const assetsByIdSelector = (state: EncoreState) => state.assetsById;
|
|
82
95
|
|
|
96
|
+
/**
|
|
97
|
+
* Main Encore runtime component
|
|
98
|
+
*
|
|
99
|
+
* Loads and renders Encore Studio apps dynamically from the Encore service.
|
|
100
|
+
* Handles data fetching, font loading, real-time updates, and component rendering.
|
|
101
|
+
*
|
|
102
|
+
* @example
|
|
103
|
+
* // Basic usage
|
|
104
|
+
* <EncoreApp appId="01ABC123" pageId="01DEF456" />
|
|
105
|
+
*
|
|
106
|
+
* @example
|
|
107
|
+
* // With data binding
|
|
108
|
+
* <EncoreApp
|
|
109
|
+
* appId="01ABC123"
|
|
110
|
+
* pageId="01DEF456"
|
|
111
|
+
* data={{
|
|
112
|
+
* "title-component": { text: "Hello World" }
|
|
113
|
+
* }}
|
|
114
|
+
* />
|
|
115
|
+
*
|
|
116
|
+
* @example
|
|
117
|
+
* // Controlling a slider
|
|
118
|
+
* const [slideIndex, setSlideIndex] = useState(0);
|
|
119
|
+
* <EncoreApp
|
|
120
|
+
* appId="01ABC123"
|
|
121
|
+
* pageId="01DEF456"
|
|
122
|
+
* repeatingContainerControls={{
|
|
123
|
+
* "slider-123": {
|
|
124
|
+
* currentIndex: slideIndex,
|
|
125
|
+
* onIndexChange: setSlideIndex
|
|
126
|
+
* }
|
|
127
|
+
* }}
|
|
128
|
+
* />
|
|
129
|
+
*/
|
|
83
130
|
const EncoreApp = ({
|
|
84
131
|
appId,
|
|
85
132
|
pageId,
|
|
@@ -97,12 +144,7 @@ const EncoreApp = ({
|
|
|
97
144
|
pageDefinition,
|
|
98
145
|
componentCode,
|
|
99
146
|
}: Props) => {
|
|
100
|
-
|
|
101
|
-
console.log("🔥 ENCORE-LIB SOURCE CODE IS ACTIVE 🔥");
|
|
102
|
-
console.log("✨ ENCORE-LIB UPDATED - TEST MESSAGE ✨");
|
|
103
|
-
console.log(
|
|
104
|
-
`[AG_DEBUG] [EncoreApp] Render Start. appId: ${appId}, pageId: ${pageId}`
|
|
105
|
-
);
|
|
147
|
+
logger.debug('EncoreApp render', { appId, pageId });
|
|
106
148
|
|
|
107
149
|
// CRITICAL: Set baseURL BEFORE any hooks that might trigger fetches
|
|
108
150
|
// This must happen synchronously, not in useEffect, because useSWR will fetch immediately
|
|
@@ -117,8 +159,7 @@ const EncoreApp = ({
|
|
|
117
159
|
if (source) {
|
|
118
160
|
setLocalModeOverride(source === "local" ? "local" : "remote");
|
|
119
161
|
}
|
|
120
|
-
|
|
121
|
-
// const noRedirect = searchParams.get("noRedirect");
|
|
162
|
+
|
|
122
163
|
const noRedirect = false;
|
|
123
164
|
|
|
124
165
|
const setApp = useEncoreState(setAppSelector);
|
|
@@ -168,13 +209,6 @@ const EncoreApp = ({
|
|
|
168
209
|
onUpdate: handleUpdate,
|
|
169
210
|
});
|
|
170
211
|
|
|
171
|
-
// usePusherUpdates({
|
|
172
|
-
// appId,
|
|
173
|
-
// pageId: pageId || undefined,
|
|
174
|
-
// enabled: false, // DISABLED FOR DEBUGGING
|
|
175
|
-
// onUpdate: handleUpdate,
|
|
176
|
-
// });
|
|
177
|
-
|
|
178
212
|
const useLocalFlag = source === "local" || isLocalMode();
|
|
179
213
|
// If appDefinition is provided, disable SWR fetch by setting url to null
|
|
180
214
|
const appUrl = appDefinition
|
|
@@ -191,85 +225,8 @@ const EncoreApp = ({
|
|
|
191
225
|
setApp(app.data);
|
|
192
226
|
}, [app.data, setApp]);
|
|
193
227
|
|
|
194
|
-
// Load fonts declared in app.json
|
|
195
|
-
|
|
196
|
-
type EncoreFont = {
|
|
197
|
-
id?: string;
|
|
198
|
-
url?: string;
|
|
199
|
-
broken?: boolean;
|
|
200
|
-
fontName?: { family?: string; postScriptName?: string };
|
|
201
|
-
};
|
|
202
|
-
type AppDataWithFonts = { app?: { fonts?: EncoreFont[] } };
|
|
203
|
-
const fonts: EncoreFont[] =
|
|
204
|
-
(app?.data as AppDataWithFonts | undefined)?.app?.fonts ?? [];
|
|
205
|
-
|
|
206
|
-
console.log(
|
|
207
|
-
`[AG_DEBUG] [EncoreApp] 🔎 Font check initiated. Found ${
|
|
208
|
-
fonts?.length || 0
|
|
209
|
-
} fonts in app definition.`
|
|
210
|
-
);
|
|
211
|
-
if (fonts && fonts.length > 0) {
|
|
212
|
-
console.log(
|
|
213
|
-
"[AG_DEBUG] [EncoreApp] Font list:",
|
|
214
|
-
fonts.map(
|
|
215
|
-
(f) =>
|
|
216
|
-
`${f.fontName?.family} (${f.fontName?.postScriptName}) - Broken: ${f.broken} - URL: ${f.url}`
|
|
217
|
-
)
|
|
218
|
-
);
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
if (!fonts || fonts.length === 0) return;
|
|
222
|
-
if (typeof window === "undefined" || !("FontFace" in window)) return;
|
|
223
|
-
fonts.forEach((f) => {
|
|
224
|
-
try {
|
|
225
|
-
const family = f?.fontName?.family;
|
|
226
|
-
const url = f?.url;
|
|
227
|
-
const postScriptName = f?.fontName?.postScriptName;
|
|
228
|
-
if (!family || !url) return;
|
|
229
|
-
|
|
230
|
-
if (f.broken) {
|
|
231
|
-
console.warn(
|
|
232
|
-
`[EncoreApp] Font "${
|
|
233
|
-
postScriptName || family
|
|
234
|
-
}" is marked as broken in the database. URL: ${url} - SKIPPING DOWNLOAD`
|
|
235
|
-
);
|
|
236
|
-
return;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
const familyName = postScriptName || family;
|
|
240
|
-
const fontFace = new FontFace(familyName, `url(${url})`, {
|
|
241
|
-
weight: "100 900",
|
|
242
|
-
style: "normal",
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
fontFace
|
|
246
|
-
.load()
|
|
247
|
-
.then((ff) => {
|
|
248
|
-
document.fonts.add(ff);
|
|
249
|
-
console.log(
|
|
250
|
-
`[AG_DEBUG] [EncoreApp] ✅ Font loaded successfully: "${familyName}" - Source: ${url}`
|
|
251
|
-
);
|
|
252
|
-
// Check if it's usable - check for any weight since we registered 100-900
|
|
253
|
-
const isCheckPassed = document.fonts.check(
|
|
254
|
-
`400 12px "${familyName}"`
|
|
255
|
-
);
|
|
256
|
-
console.log(
|
|
257
|
-
`[AG_DEBUG] [EncoreApp] 🧪 document.fonts.check result for "${familyName}": ${isCheckPassed}`
|
|
258
|
-
);
|
|
259
|
-
})
|
|
260
|
-
.catch((err) => {
|
|
261
|
-
console.warn(
|
|
262
|
-
`[AG_DEBUG] [EncoreApp] ❌ Failed to load font "${
|
|
263
|
-
postScriptName || family
|
|
264
|
-
}" from ${url}`,
|
|
265
|
-
err
|
|
266
|
-
);
|
|
267
|
-
});
|
|
268
|
-
} catch (err) {
|
|
269
|
-
console.warn(`[EncoreApp] Error processing font:`, err);
|
|
270
|
-
}
|
|
271
|
-
});
|
|
272
|
-
}, [app?.data]);
|
|
228
|
+
// Load fonts declared in app.json
|
|
229
|
+
useFontLoader(app?.data);
|
|
273
230
|
|
|
274
231
|
useEffect(() => {
|
|
275
232
|
setAppId(appId);
|
|
@@ -309,299 +266,29 @@ const EncoreApp = ({
|
|
|
309
266
|
useLocalFlag ? "?useLocal=1" : ""
|
|
310
267
|
}`;
|
|
311
268
|
|
|
312
|
-
|
|
269
|
+
logger.debug('Page data fetch', { pageUrl });
|
|
313
270
|
|
|
314
271
|
const pageSWR = useSWR(pageUrl, fetcher, { suspense: !!pageUrl });
|
|
315
272
|
const pageData = pageDefinition ? { data: pageDefinition } : pageSWR;
|
|
316
273
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
console.log(
|
|
322
|
-
`[AG_DEBUG] [EncoreApp] pageData.data JSON: ${JSON.stringify(
|
|
323
|
-
pageData.data
|
|
324
|
-
).substring(0, 1000)}`
|
|
325
|
-
);
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
// Debug logging commented out to prevent console flooding
|
|
329
|
-
/*
|
|
330
|
-
useEffect(() => {
|
|
331
|
-
if (pageData.data) {
|
|
332
|
-
console.log("=== PAGE DATA STRUCTURE ===");
|
|
333
|
-
console.log("Full pageData.data:", pageData.data);
|
|
334
|
-
console.log("pageData.data keys:", Object.keys(pageData.data));
|
|
335
|
-
console.log("Client data:", pageData.data.clientData);
|
|
336
|
-
console.log("Client data type:", typeof pageData.data.clientData);
|
|
337
|
-
console.log(
|
|
338
|
-
"Client data keys:",
|
|
339
|
-
pageData.data.clientData ? Object.keys(pageData.data.clientData) : "N/A"
|
|
340
|
-
);
|
|
341
|
-
|
|
342
|
-
// Try to find the actual node data structure
|
|
343
|
-
const allKeys = Object.keys(pageData.data);
|
|
344
|
-
console.log("All top-level keys in pageData.data:", allKeys);
|
|
345
|
-
|
|
346
|
-
// Log the full structure more deeply
|
|
347
|
-
console.log(
|
|
348
|
-
"Full pageData.data structure:",
|
|
349
|
-
JSON.stringify(pageData.data, null, 2)
|
|
350
|
-
);
|
|
351
|
-
|
|
352
|
-
const clientData = pageData.data.clientData || pageData.data;
|
|
353
|
-
|
|
354
|
-
// Helper to recursively search for sliders in tree structure
|
|
355
|
-
const findSlidersInTree = (nodeData: any, path = ""): any[] => {
|
|
356
|
-
const sliders: any[] = [];
|
|
357
|
-
if (!nodeData || typeof nodeData !== "object") return sliders;
|
|
358
|
-
|
|
359
|
-
// Check if this node is a slider
|
|
360
|
-
const isSlider =
|
|
361
|
-
nodeData.type === "container:slider" ||
|
|
362
|
-
nodeData.type === "component:slider" ||
|
|
363
|
-
nodeData.name?.toLowerCase().includes("slider") ||
|
|
364
|
-
(Array.isArray(nodeData.tags) &&
|
|
365
|
-
nodeData.tags.some((tag: string) =>
|
|
366
|
-
tag.toLowerCase().includes("slider")
|
|
367
|
-
));
|
|
368
|
-
|
|
369
|
-
if (isSlider) {
|
|
370
|
-
sliders.push({
|
|
371
|
-
path,
|
|
372
|
-
id: nodeData.id,
|
|
373
|
-
name: nodeData.name,
|
|
374
|
-
type: nodeData.type,
|
|
375
|
-
tags: nodeData.tags,
|
|
376
|
-
data: nodeData.data,
|
|
377
|
-
style: nodeData.style,
|
|
378
|
-
fullNodeData: nodeData,
|
|
379
|
-
});
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
// Recursively search children
|
|
383
|
-
if (nodeData.children) {
|
|
384
|
-
const children = Array.isArray(nodeData.children)
|
|
385
|
-
? nodeData.children
|
|
386
|
-
: [nodeData.children];
|
|
387
|
-
children.forEach((child: any, index: number) => {
|
|
388
|
-
if (child && typeof child === "object") {
|
|
389
|
-
sliders.push(...findSlidersInTree(child, `${path}/${index}`));
|
|
390
|
-
}
|
|
391
|
-
});
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
return sliders;
|
|
395
|
-
};
|
|
396
|
-
|
|
397
|
-
// Check if clientData is a flat object keyed by ID (most likely)
|
|
398
|
-
let sliders: any[] = [];
|
|
399
|
-
if (
|
|
400
|
-
clientData &&
|
|
401
|
-
typeof clientData === "object" &&
|
|
402
|
-
!Array.isArray(clientData)
|
|
403
|
-
) {
|
|
404
|
-
// Check if it's a flat structure (keyed by IDs)
|
|
405
|
-
const entries = Object.entries(clientData);
|
|
406
|
-
console.log(`Client data has ${entries.length} entries`);
|
|
407
|
-
|
|
408
|
-
// Search all entries for sliders
|
|
409
|
-
entries.forEach(([key, value]: [string, any]) => {
|
|
410
|
-
if (value && typeof value === "object") {
|
|
411
|
-
// Check if this entry itself is a slider
|
|
412
|
-
const isSlider =
|
|
413
|
-
value.type === "container:slider" ||
|
|
414
|
-
value.type === "component:slider" ||
|
|
415
|
-
value.name?.toLowerCase().includes("slider") ||
|
|
416
|
-
(Array.isArray(value.tags) &&
|
|
417
|
-
value.tags.some((tag: string) =>
|
|
418
|
-
tag.toLowerCase().includes("slider")
|
|
419
|
-
));
|
|
420
|
-
|
|
421
|
-
if (isSlider) {
|
|
422
|
-
sliders.push({
|
|
423
|
-
id: key,
|
|
424
|
-
name: value.name,
|
|
425
|
-
type: value.type,
|
|
426
|
-
tags: value.tags,
|
|
427
|
-
data: value.data,
|
|
428
|
-
style: value.style,
|
|
429
|
-
fullNodeData: value,
|
|
430
|
-
});
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
// Also search recursively in case it's nested
|
|
434
|
-
sliders.push(...findSlidersInTree(value, key));
|
|
435
|
-
}
|
|
436
|
-
});
|
|
437
|
-
|
|
438
|
-
// If no sliders found in flat structure, try treating it as a tree
|
|
439
|
-
if (sliders.length === 0) {
|
|
440
|
-
sliders = findSlidersInTree(clientData, "root");
|
|
441
|
-
}
|
|
442
|
-
} else {
|
|
443
|
-
// Treat as tree structure
|
|
444
|
-
sliders = findSlidersInTree(clientData, "root");
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
console.log("=== SLIDER COMPONENTS FOUND ===");
|
|
448
|
-
console.log(`Found ${sliders.length} slider(s):`, sliders);
|
|
449
|
-
|
|
450
|
-
sliders.forEach((slider, index) => {
|
|
451
|
-
console.log(`\n--- Slider ${index + 1} ---`);
|
|
452
|
-
console.log("ID:", slider.id);
|
|
453
|
-
console.log("Name:", slider.name);
|
|
454
|
-
console.log("Type:", slider.type);
|
|
455
|
-
console.log("Tags:", slider.tags);
|
|
456
|
-
console.log("Data:", slider.data);
|
|
457
|
-
console.log("Full node data:", slider.fullNodeData);
|
|
458
|
-
|
|
459
|
-
// Look for data binding tags
|
|
460
|
-
if (Array.isArray(slider.tags)) {
|
|
461
|
-
const bindingTags = slider.tags.filter(
|
|
462
|
-
(tag: string) =>
|
|
463
|
-
tag.includes("PROP:") ||
|
|
464
|
-
tag.includes("LIST:") ||
|
|
465
|
-
tag.includes("DATA:")
|
|
466
|
-
);
|
|
467
|
-
if (bindingTags.length > 0) {
|
|
468
|
-
console.log("🔍 Data binding tags found:", bindingTags);
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
});
|
|
472
|
-
|
|
473
|
-
// Also log all unique tags found across all components for reference
|
|
474
|
-
const allTags = new Set<string>();
|
|
475
|
-
const allEntries = Object.entries(clientData);
|
|
476
|
-
allEntries.forEach(([_, value]: [string, any]) => {
|
|
477
|
-
if (value?.tags && Array.isArray(value.tags)) {
|
|
478
|
-
value.tags.forEach((tag: string) => allTags.add(tag));
|
|
479
|
-
}
|
|
480
|
-
});
|
|
481
|
-
console.log("\n=== ALL UNIQUE TAGS IN PAGE ===");
|
|
482
|
-
console.log(Array.from(allTags).sort());
|
|
483
|
-
}
|
|
484
|
-
}, [pageData.data]);
|
|
485
|
-
*/
|
|
274
|
+
logger.debug('Page data loaded', {
|
|
275
|
+
hasData: !!pageData?.data,
|
|
276
|
+
dataType: typeof pageData?.data
|
|
277
|
+
});
|
|
486
278
|
|
|
487
279
|
// Memoize the context object to prevent infinite re-renders
|
|
488
280
|
// Only recreate when the actual data changes, not on every render
|
|
489
281
|
const context = useMemo(() => {
|
|
490
282
|
let clientData = pageData.data?.clientData;
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
);
|
|
496
|
-
|
|
497
|
-
// --- GENERIC DATA PATCHING START ---
|
|
498
|
-
// Recursively patch the data to fix layout and slider issues based on heuristics
|
|
499
|
-
const patchPageData = (node: any) => {
|
|
500
|
-
if (!node || typeof node !== "object") return;
|
|
501
|
-
|
|
502
|
-
// Log font usage
|
|
503
|
-
if (node.componentId || node.type === "component:text") {
|
|
504
|
-
console.log(
|
|
505
|
-
`[AG_DEBUG] [EncoreApp] 📦 Node ${node.id} (${node.type}) Style:`,
|
|
506
|
-
JSON.stringify(node.style)
|
|
507
|
-
);
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
// 1. Layout Heuristic: If children widths sum to ~100% or ~375px, force HORIZONTAL layout
|
|
511
|
-
// RELAXED: No longer requires layoutSizingHorizontal: "FIXED" - width data alone is sufficient
|
|
512
|
-
if (
|
|
513
|
-
node.children &&
|
|
514
|
-
Array.isArray(node.children) &&
|
|
515
|
-
node.children.length > 1
|
|
516
|
-
) {
|
|
517
|
-
let totalWidth = 0;
|
|
518
|
-
let childrenWithWidth = 0;
|
|
519
|
-
|
|
520
|
-
node.children.forEach((child: any) => {
|
|
521
|
-
if (child.style?.width) {
|
|
522
|
-
// Width might be a percentage or pixel value.
|
|
523
|
-
// Based on logs, we saw "width: 52" and "width: 48" which sum to 100.
|
|
524
|
-
// If it's percentage, we check if it sums to 100.
|
|
525
|
-
// If it's pixels, we check if it sums to ~375.
|
|
526
|
-
totalWidth += child.style.width;
|
|
527
|
-
childrenWithWidth++;
|
|
528
|
-
}
|
|
529
|
-
});
|
|
530
|
-
|
|
531
|
-
// Check for percentage sum ~100 or pixel sum ~375
|
|
532
|
-
// Only apply if we have at least 2 children with width data
|
|
533
|
-
const isFullWidthRow =
|
|
534
|
-
(Math.abs(totalWidth - 100) < 1 || Math.abs(totalWidth - 375) < 5) &&
|
|
535
|
-
childrenWithWidth >= 2;
|
|
536
|
-
|
|
537
|
-
if (isFullWidthRow) {
|
|
538
|
-
if (!node.style) node.style = {};
|
|
539
|
-
if (!node.style.layout) node.style.layout = {};
|
|
540
|
-
|
|
541
|
-
// Only apply if mode is missing or undefined
|
|
542
|
-
if (!node.style.layout.mode) {
|
|
543
|
-
console.log(
|
|
544
|
-
`[PATCH] Forcing HORIZONTAL layout for node ${node.id} (${childrenWithWidth} children, widths sum: ${totalWidth})`
|
|
545
|
-
);
|
|
546
|
-
node.style.layout.mode = "HORIZONTAL";
|
|
547
|
-
node.style.layout.primaryAxisAlignItems = "flex-start";
|
|
548
|
-
node.style.layout.counterAxisAlignItems = "flex-start";
|
|
549
|
-
}
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
// 2. Slider Heuristic: If slider is taller than wide, force VERTICAL animation
|
|
554
|
-
// DISABLED: This is too aggressive and breaks horizontal sliders that happen to be tall (e.g. 50% width columns)
|
|
555
|
-
/*
|
|
556
|
-
const isSlider =
|
|
557
|
-
node.type === "container:slider" ||
|
|
558
|
-
node.type === "component:slider" ||
|
|
559
|
-
(node.tags &&
|
|
560
|
-
Array.isArray(node.tags) &&
|
|
561
|
-
node.tags.some((t: string) => t.toLowerCase().includes("slider")));
|
|
562
|
-
|
|
563
|
-
if (isSlider) {
|
|
564
|
-
const width = node.style?.width || 0;
|
|
565
|
-
const height = node.style?.height || 0;
|
|
566
|
-
// Check aspect ratio. If height > width, it's likely vertical.
|
|
567
|
-
// Note: width/height might be percentages or pixels.
|
|
568
|
-
// If both are numbers, we can compare.
|
|
569
|
-
// From logs: width: 52, height: 85.27. This is clearly vertical shape.
|
|
570
|
-
if (height > width) {
|
|
571
|
-
if (!node.data) node.data = {};
|
|
572
|
-
if (!node.data.params) node.data.params = {};
|
|
573
|
-
|
|
574
|
-
// Only apply if animation is default or missing
|
|
575
|
-
if (
|
|
576
|
-
!node.data.params.animation ||
|
|
577
|
-
node.data.params.animation === "default" ||
|
|
578
|
-
node.data.params.animation === "horizontal"
|
|
579
|
-
) {
|
|
580
|
-
console.log(
|
|
581
|
-
`[PATCH] Forcing VERTICAL animation for slider ${node.id} (W:${width}, H:${height})`
|
|
582
|
-
);
|
|
583
|
-
node.data.params.animation = "vertical";
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
|
-
*/
|
|
588
|
-
|
|
589
|
-
// Recurse
|
|
590
|
-
if (node.children) {
|
|
591
|
-
if (Array.isArray(node.children)) {
|
|
592
|
-
node.children.forEach(patchPageData);
|
|
593
|
-
} else {
|
|
594
|
-
patchPageData(node.children);
|
|
595
|
-
}
|
|
596
|
-
}
|
|
597
|
-
};
|
|
283
|
+
logger.debug('Building context', {
|
|
284
|
+
hasClientData: !!clientData,
|
|
285
|
+
clientDataKeys: Object.keys(clientData || {}).length
|
|
286
|
+
});
|
|
598
287
|
|
|
288
|
+
// Apply layout heuristics to fix common issues
|
|
599
289
|
if (clientData) {
|
|
600
|
-
// Clone to avoid mutating SWR cache directly if possible, though deep clone might be expensive.
|
|
601
|
-
// For now, patching in place as it's a fix.
|
|
602
290
|
patchPageData(clientData);
|
|
603
291
|
}
|
|
604
|
-
// --- GENERIC DATA PATCHING END ---
|
|
605
292
|
|
|
606
293
|
return {
|
|
607
294
|
nodeData: undefined,
|
|
@@ -615,54 +302,10 @@ const EncoreApp = ({
|
|
|
615
302
|
};
|
|
616
303
|
}, [pageData.data?.clientData, data]);
|
|
617
304
|
|
|
618
|
-
// Manage repeating container controls
|
|
619
|
-
const
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
const [controlPropsMap, setControlPropsMap] = useState<
|
|
623
|
-
Map<
|
|
624
|
-
string,
|
|
625
|
-
{ currentIndex?: number; onIndexChange?: (index: number) => void }
|
|
626
|
-
>
|
|
627
|
-
>(new Map());
|
|
628
|
-
|
|
629
|
-
// Update control props from prop
|
|
630
|
-
useEffect(() => {
|
|
631
|
-
if (repeatingContainerControls) {
|
|
632
|
-
setControlPropsMap((prev) => {
|
|
633
|
-
// Check if content actually changed to avoid unnecessary updates
|
|
634
|
-
let changed = false;
|
|
635
|
-
if (prev.size !== Object.keys(repeatingContainerControls).length) {
|
|
636
|
-
changed = true;
|
|
637
|
-
} else {
|
|
638
|
-
for (const [id, props] of Object.entries(
|
|
639
|
-
repeatingContainerControls
|
|
640
|
-
)) {
|
|
641
|
-
const prevProps = prev.get(id);
|
|
642
|
-
if (!prevProps) {
|
|
643
|
-
changed = true;
|
|
644
|
-
break;
|
|
645
|
-
}
|
|
646
|
-
if (
|
|
647
|
-
prevProps.currentIndex !== props.currentIndex ||
|
|
648
|
-
prevProps.onIndexChange !== props.onIndexChange
|
|
649
|
-
) {
|
|
650
|
-
changed = true;
|
|
651
|
-
break;
|
|
652
|
-
}
|
|
653
|
-
}
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
if (!changed) return prev;
|
|
657
|
-
|
|
658
|
-
const newMap = new Map();
|
|
659
|
-
Object.entries(repeatingContainerControls).forEach(([id, props]) => {
|
|
660
|
-
newMap.set(id, props);
|
|
661
|
-
});
|
|
662
|
-
return newMap;
|
|
663
|
-
});
|
|
664
|
-
}
|
|
665
|
-
}, [repeatingContainerControls]);
|
|
305
|
+
// Manage repeating container controls (sliders, lists)
|
|
306
|
+
const repeatingContainerContextValue = useRepeatingContainers(
|
|
307
|
+
repeatingContainerControls
|
|
308
|
+
);
|
|
666
309
|
|
|
667
310
|
// Sync input groups from props to store
|
|
668
311
|
useEffect(() => {
|
|
@@ -674,92 +317,6 @@ const EncoreApp = ({
|
|
|
674
317
|
}
|
|
675
318
|
}, [inputGroups]);
|
|
676
319
|
|
|
677
|
-
const registerContainer = useCallback(
|
|
678
|
-
(id: string, control: RepeatingContainerControl) => {
|
|
679
|
-
setContainerControls((prev) => {
|
|
680
|
-
const next = new Map(prev);
|
|
681
|
-
next.set(id, control);
|
|
682
|
-
return next;
|
|
683
|
-
});
|
|
684
|
-
},
|
|
685
|
-
[]
|
|
686
|
-
);
|
|
687
|
-
|
|
688
|
-
const unregisterContainer = useCallback((id: string) => {
|
|
689
|
-
setContainerControls((prev) => {
|
|
690
|
-
const next = new Map(prev);
|
|
691
|
-
next.delete(id);
|
|
692
|
-
return next;
|
|
693
|
-
});
|
|
694
|
-
// Do NOT delete from controlPropsMap here.
|
|
695
|
-
// controlPropsMap contains props passed from the parent (repeatingContainerControls).
|
|
696
|
-
// If we delete them, we lose the configuration passed down to us.
|
|
697
|
-
// The props should persist even if the component temporarily unregisters.
|
|
698
|
-
}, []);
|
|
699
|
-
|
|
700
|
-
const getControl = useCallback(
|
|
701
|
-
(id: string) => {
|
|
702
|
-
return containerControls.get(id);
|
|
703
|
-
},
|
|
704
|
-
[containerControls]
|
|
705
|
-
);
|
|
706
|
-
|
|
707
|
-
const setControlProps = useCallback(
|
|
708
|
-
(
|
|
709
|
-
id: string,
|
|
710
|
-
props:
|
|
711
|
-
| { currentIndex?: number; onIndexChange?: (index: number) => void }
|
|
712
|
-
| ((prev: {
|
|
713
|
-
currentIndex?: number;
|
|
714
|
-
onIndexChange?: (index: number) => void;
|
|
715
|
-
}) => {
|
|
716
|
-
currentIndex?: number;
|
|
717
|
-
onIndexChange?: (index: number) => void;
|
|
718
|
-
})
|
|
719
|
-
) => {
|
|
720
|
-
setControlPropsMap((prev) => {
|
|
721
|
-
const next = new Map(prev);
|
|
722
|
-
const current = next.get(id) || {};
|
|
723
|
-
const newProps = typeof props === "function" ? props(current) : props;
|
|
724
|
-
next.set(id, newProps);
|
|
725
|
-
return next;
|
|
726
|
-
});
|
|
727
|
-
},
|
|
728
|
-
[]
|
|
729
|
-
);
|
|
730
|
-
|
|
731
|
-
const getControlProps = useCallback(
|
|
732
|
-
(id: string) => {
|
|
733
|
-
return controlPropsMap.get(id);
|
|
734
|
-
},
|
|
735
|
-
[controlPropsMap]
|
|
736
|
-
);
|
|
737
|
-
|
|
738
|
-
// Control props are automatically passed to registered containers via context
|
|
739
|
-
// Components read props via getControlProps() and update when props change
|
|
740
|
-
|
|
741
|
-
// Create context value - this object changes when controlPropsMap changes,
|
|
742
|
-
// causing all consumers to re-render and get updated props
|
|
743
|
-
const repeatingContainerContextValue = React.useMemo(
|
|
744
|
-
() => ({
|
|
745
|
-
registerContainer,
|
|
746
|
-
unregisterContainer,
|
|
747
|
-
getControl,
|
|
748
|
-
setControlProps,
|
|
749
|
-
getControlProps,
|
|
750
|
-
// Include controlPropsMap size in the value to trigger re-renders when it changes
|
|
751
|
-
_propsVersion: controlPropsMap.size,
|
|
752
|
-
}),
|
|
753
|
-
[
|
|
754
|
-
registerContainer,
|
|
755
|
-
unregisterContainer,
|
|
756
|
-
getControl,
|
|
757
|
-
setControlProps,
|
|
758
|
-
getControlProps,
|
|
759
|
-
controlPropsMap.size,
|
|
760
|
-
]
|
|
761
|
-
);
|
|
762
|
-
|
|
763
320
|
// Observe size changes of the dynamic content container and notify consumer
|
|
764
321
|
useEffect(() => {
|
|
765
322
|
if (!onSizeChange) return;
|
|
@@ -840,24 +397,19 @@ const EncoreApp = ({
|
|
|
840
397
|
}}
|
|
841
398
|
>
|
|
842
399
|
<Suspense fallback={fallback || <div />}>
|
|
843
|
-
<
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
</DynamicComponent>
|
|
857
|
-
</EncoreBindingContext.Provider>
|
|
858
|
-
</EncoreRepeatingContainerContext.Provider>
|
|
859
|
-
</EncoreActionContext.Provider>
|
|
860
|
-
</EncoreComponentIdContext.Provider>
|
|
400
|
+
<EncoreContextProviders
|
|
401
|
+
componentId={componentId}
|
|
402
|
+
onAction={onAction}
|
|
403
|
+
repeatingContainerContextValue={repeatingContainerContextValue}
|
|
404
|
+
bindingContextValue={context}
|
|
405
|
+
>
|
|
406
|
+
<DynamicComponent
|
|
407
|
+
name={`${appId}/draft/components/${pageId}`}
|
|
408
|
+
fallback={fallback}
|
|
409
|
+
reloadKey={reloadKey}
|
|
410
|
+
componentCode={componentCode}
|
|
411
|
+
/>
|
|
412
|
+
</EncoreContextProviders>
|
|
861
413
|
</Suspense>
|
|
862
414
|
</div>
|
|
863
415
|
</div>
|