@bravostudioai/react 0.1.28 → 0.1.30

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.
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Context provider composition component
3
+ *
4
+ * Wraps children with all necessary Encore contexts in the correct order.
5
+ * This reduces nesting in the main EncoreApp component.
6
+ */
7
+
8
+ import React from "react";
9
+ import EncoreComponentIdContext from "../contexts/EncoreComponentIdContext";
10
+ import EncoreActionContext, {
11
+ type EncoreActionPayload,
12
+ } from "../contexts/EncoreActionContext";
13
+ import EncoreRepeatingContainerContext from "../contexts/EncoreRepeatingContainerContext";
14
+ import EncoreBindingContext from "../contexts/EncoreBindingContext";
15
+
16
+ interface EncoreContextProvidersProps {
17
+ componentId?: string;
18
+ onAction?: (payload: EncoreActionPayload) => void | Promise<void>;
19
+ repeatingContainerContextValue: any;
20
+ bindingContextValue: any;
21
+ children: React.ReactNode;
22
+ }
23
+
24
+ /**
25
+ * Composes all Encore context providers
26
+ *
27
+ * Provides a cleaner way to wrap content with multiple contexts
28
+ * instead of deeply nested Provider components.
29
+ *
30
+ * @example
31
+ * <EncoreContextProviders
32
+ * componentId={componentId}
33
+ * onAction={onAction}
34
+ * repeatingContainerContextValue={containerContextValue}
35
+ * bindingContextValue={bindingContext}
36
+ * >
37
+ * <DynamicComponent />
38
+ * </EncoreContextProviders>
39
+ */
40
+ export function EncoreContextProviders({
41
+ componentId,
42
+ onAction,
43
+ repeatingContainerContextValue,
44
+ bindingContextValue,
45
+ children,
46
+ }: EncoreContextProvidersProps) {
47
+ return (
48
+ <EncoreComponentIdContext.Provider value={{ componentId }}>
49
+ <EncoreActionContext.Provider value={{ onAction }}>
50
+ <EncoreRepeatingContainerContext.Provider
51
+ value={repeatingContainerContextValue}
52
+ >
53
+ <EncoreBindingContext.Provider value={bindingContextValue}>
54
+ {children}
55
+ </EncoreBindingContext.Provider>
56
+ </EncoreRepeatingContainerContext.Provider>
57
+ </EncoreActionContext.Provider>
58
+ </EncoreComponentIdContext.Provider>
59
+ );
60
+ }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Custom hook for loading fonts from app definition
3
+ *
4
+ * Handles FontFace API loading of custom fonts declared in the app JSON.
5
+ * Skips fonts marked as broken and provides debug logging in development.
6
+ */
7
+
8
+ import { useEffect } from "react";
9
+ import logger from "../lib/logger";
10
+
11
+ interface EncoreFont {
12
+ id?: string;
13
+ url?: string;
14
+ broken?: boolean;
15
+ fontName?: { family?: string; postScriptName?: string };
16
+ }
17
+
18
+ interface AppDataWithFonts {
19
+ app?: { fonts?: EncoreFont[] };
20
+ }
21
+
22
+ /**
23
+ * Loads fonts declared in app definition using the FontFace API
24
+ *
25
+ * @param appData - App definition containing fonts array
26
+ *
27
+ * @example
28
+ * const { data: appData } = useSWR(appUrl, fetcher);
29
+ * useFontLoader(appData);
30
+ */
31
+ export function useFontLoader(appData: unknown) {
32
+ useEffect(() => {
33
+ const fonts: EncoreFont[] =
34
+ (appData as AppDataWithFonts | undefined)?.app?.fonts ?? [];
35
+
36
+ logger.debug('Font loading check', { fontCount: fonts?.length || 0 });
37
+
38
+ if (!fonts || fonts.length === 0) return;
39
+ if (typeof window === "undefined" || !("FontFace" in window)) return;
40
+
41
+ fonts.forEach((f) => {
42
+ try {
43
+ const family = f?.fontName?.family;
44
+ const url = f?.url;
45
+ const postScriptName = f?.fontName?.postScriptName;
46
+
47
+ if (!family || !url) return;
48
+
49
+ if (f.broken) {
50
+ logger.warn('Skipping broken font', {
51
+ font: postScriptName || family,
52
+ url
53
+ });
54
+ return;
55
+ }
56
+
57
+ const familyName = postScriptName || family;
58
+ const fontFace = new FontFace(familyName, `url(${url})`, {
59
+ weight: "100 900",
60
+ style: "normal",
61
+ });
62
+
63
+ fontFace
64
+ .load()
65
+ .then((ff) => {
66
+ document.fonts.add(ff);
67
+ logger.debug('Font loaded', { familyName, url });
68
+
69
+ const isCheckPassed = document.fonts.check(`400 12px "${familyName}"`);
70
+ logger.debug('Font check result', { familyName, isCheckPassed });
71
+ })
72
+ .catch((err) => {
73
+ logger.warn('Failed to load font', {
74
+ font: postScriptName || family,
75
+ url,
76
+ error: err.message
77
+ });
78
+ });
79
+ } catch (err) {
80
+ logger.warn('Error processing font', err);
81
+ }
82
+ });
83
+ }, [appData]);
84
+ }
@@ -4,6 +4,7 @@ import { useEffect, useRef } from "react";
4
4
  import Pusher from "pusher-js";
5
5
  import { mutate } from "swr";
6
6
  import { clearModuleCache } from "../lib/moduleRegistry";
7
+ import logger from "../lib/logger";
7
8
 
8
9
  type PusherConfig = {
9
10
  key?: string;
@@ -51,7 +52,7 @@ export function usePusherUpdates({
51
52
 
52
53
  // Skip if Pusher key is not configured
53
54
  if (!config.key) {
54
- console.warn("[Pusher] not configured. Pusher updates disabled.");
55
+ logger.warn('Pusher not configured - real-time updates disabled');
55
56
  return;
56
57
  }
57
58
 
@@ -63,9 +64,9 @@ export function usePusherUpdates({
63
64
 
64
65
  pusherRef.current = pusher;
65
66
 
66
- // Channel name format: appId-pageId (e.g., "01KA23JMNBQ2V9NR7K0VXKT5TF-01KA23JMPBZSG2YRJ6M5X87SKJ")
67
+ // Channel name format: appId (e.g., "01KA23JMNBQ2V9NR7K0VXKT5TF")
67
68
  const channelName = `${appId}`;
68
- console.log(`[Pusher] Subscribing to channel: ${channelName}`);
69
+ logger.debug('Subscribing to Pusher channel', { channelName });
69
70
 
70
71
  // Subscribe to the channel
71
72
  const channel = pusher.subscribe(channelName);
@@ -73,24 +74,18 @@ export function usePusherUpdates({
73
74
 
74
75
  // Handle subscription success
75
76
  channel.bind("pusher:subscription_succeeded", () => {
76
- console.log(
77
- `[Pusher] Successfully subscribed to channel: ${channelName}`
78
- );
77
+ logger.debug('Pusher subscription succeeded', { channelName });
79
78
  });
80
79
 
81
80
  // Handle subscription error
82
81
  channel.bind("pusher:subscription_error", (status: number) => {
83
- console.error(
84
- `[Pusher] Subscription error for channel ${channelName}:`,
85
- status
86
- );
82
+ logger.error('Pusher subscription error', { channelName, status });
87
83
  });
88
84
 
89
85
  // Listen for update events
90
- // You can customize the event name based on your backend's Pusher event names
91
- // Common event names: 'update', 'component-updated', 'app-updated', etc.
86
+ // Common event names: 'update', 'component-updated', 'app-updated'
92
87
  const handleUpdate = (data: unknown) => {
93
- console.log(`[Pusher] Update received for ${channelName}:`, data);
88
+ logger.debug('Pusher update received', { channelName, data });
94
89
 
95
90
  // Clear the module cache for the component to force reload
96
91
  const componentName = `${appId}/draft/components/${pageId}`;
@@ -102,19 +97,15 @@ export function usePusherUpdates({
102
97
 
103
98
  // Mutate both URLs to trigger refetch
104
99
  mutate(appUrl).catch((err) => {
105
- console.error("[Pusher] Error invalidating app cache:", err);
100
+ logger.error('Error invalidating app cache', err);
106
101
  });
107
102
  mutate(pageUrl).catch((err) => {
108
- console.error("[Pusher] Error invalidating page cache:", err);
103
+ logger.error('Error invalidating page cache', err);
109
104
  });
110
105
 
111
- // Also invalidate URLs with useLocal query param if they exist
112
- mutate(`${appUrl}?useLocal=1`).catch(() => {
113
- // Ignore errors for optional URLs
114
- });
115
- mutate(`${pageUrl}?useLocal=1`).catch(() => {
116
- // Ignore errors for optional URLs
117
- });
106
+ // Also invalidate URLs with useLocal query param
107
+ mutate(`${appUrl}?useLocal=1`).catch(() => {});
108
+ mutate(`${pageUrl}?useLocal=1`).catch(() => {});
118
109
 
119
110
  // Call the onUpdate callback if provided
120
111
  onUpdate?.();
@@ -128,7 +119,7 @@ export function usePusherUpdates({
128
119
 
129
120
  // Cleanup function
130
121
  return () => {
131
- console.log(`[Pusher] Unsubscribing from channel: ${channelName}`);
122
+ logger.debug('Unsubscribing from Pusher channel', { channelName });
132
123
 
133
124
  // Unbind all event handlers
134
125
  if (channelRef.current) {
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Custom hook for managing repeating container controls
3
+ *
4
+ * Handles slider/list index management and synchronization between
5
+ * parent-provided controls and internal component state.
6
+ */
7
+
8
+ import { useState, useCallback, useMemo, useEffect } from "react";
9
+ import React from "react";
10
+ import type { RepeatingContainerControl } from "../contexts/EncoreRepeatingContainerContext";
11
+
12
+ interface ContainerControlProps {
13
+ currentIndex?: number;
14
+ onIndexChange?: (index: number) => void;
15
+ }
16
+
17
+ /**
18
+ * Manages repeating container (slider/list) controls
19
+ *
20
+ * @param externalControls - Optional controls passed from parent component
21
+ * @returns Context value for EncoreRepeatingContainerContext
22
+ *
23
+ * @example
24
+ * const containerContextValue = useRepeatingContainers(repeatingContainerControls);
25
+ * <EncoreRepeatingContainerContext.Provider value={containerContextValue}>
26
+ */
27
+ export function useRepeatingContainers(
28
+ externalControls?: Record<string, ContainerControlProps>
29
+ ) {
30
+ const [containerControls, setContainerControls] = useState<
31
+ Map<string, RepeatingContainerControl>
32
+ >(new Map());
33
+
34
+ const [controlPropsMap, setControlPropsMap] = useState<
35
+ Map<string, ContainerControlProps>
36
+ >(new Map());
37
+
38
+ // Update control props from external prop
39
+ useEffect(() => {
40
+ if (externalControls) {
41
+ setControlPropsMap((prev) => {
42
+ // Check if content actually changed to avoid unnecessary updates
43
+ let changed = false;
44
+ if (prev.size !== Object.keys(externalControls).length) {
45
+ changed = true;
46
+ } else {
47
+ for (const [id, props] of Object.entries(externalControls)) {
48
+ const prevProps = prev.get(id);
49
+ if (!prevProps) {
50
+ changed = true;
51
+ break;
52
+ }
53
+ if (
54
+ prevProps.currentIndex !== props.currentIndex ||
55
+ prevProps.onIndexChange !== props.onIndexChange
56
+ ) {
57
+ changed = true;
58
+ break;
59
+ }
60
+ }
61
+ }
62
+
63
+ if (!changed) return prev;
64
+
65
+ const newMap = new Map();
66
+ Object.entries(externalControls).forEach(([id, props]) => {
67
+ newMap.set(id, props);
68
+ });
69
+ return newMap;
70
+ });
71
+ }
72
+ }, [externalControls]);
73
+
74
+ const registerContainer = useCallback(
75
+ (id: string, control: RepeatingContainerControl) => {
76
+ setContainerControls((prev) => {
77
+ const next = new Map(prev);
78
+ next.set(id, control);
79
+ return next;
80
+ });
81
+ },
82
+ []
83
+ );
84
+
85
+ const unregisterContainer = useCallback((id: string) => {
86
+ setContainerControls((prev) => {
87
+ const next = new Map(prev);
88
+ next.delete(id);
89
+ return next;
90
+ });
91
+ }, []);
92
+
93
+ const getControl = useCallback(
94
+ (id: string) => {
95
+ return containerControls.get(id);
96
+ },
97
+ [containerControls]
98
+ );
99
+
100
+ const setControlProps = useCallback(
101
+ (
102
+ id: string,
103
+ props:
104
+ | ContainerControlProps
105
+ | ((prev: ContainerControlProps) => ContainerControlProps)
106
+ ) => {
107
+ setControlPropsMap((prev) => {
108
+ const next = new Map(prev);
109
+ const current = next.get(id) || {};
110
+ const newProps = typeof props === "function" ? props(current) : props;
111
+ next.set(id, newProps);
112
+ return next;
113
+ });
114
+ },
115
+ []
116
+ );
117
+
118
+ const getControlProps = useCallback(
119
+ (id: string) => {
120
+ return controlPropsMap.get(id);
121
+ },
122
+ [controlPropsMap]
123
+ );
124
+
125
+ // Create context value - changes when controlPropsMap changes
126
+ const contextValue = useMemo(
127
+ () => ({
128
+ registerContainer,
129
+ unregisterContainer,
130
+ getControl,
131
+ setControlProps,
132
+ getControlProps,
133
+ // Include size to trigger re-renders when it changes
134
+ _propsVersion: controlPropsMap.size,
135
+ }),
136
+ [
137
+ registerContainer,
138
+ unregisterContainer,
139
+ getControl,
140
+ setControlProps,
141
+ getControlProps,
142
+ controlPropsMap.size,
143
+ ]
144
+ );
145
+
146
+ return contextValue;
147
+ }
package/src/index.ts CHANGED
@@ -1,4 +1,3 @@
1
- console.log("Encore Lib Loaded");
2
1
  import EncoreApp from "./components/EncoreApp";
3
2
  import EncoreErrorBoundary from "./components/EncoreErrorBoundary";
4
3
  import EncoreLoadingFallback from "./components/EncoreLoadingFallback";
@@ -17,4 +16,8 @@ export {
17
16
  PACKAGE_VERSION,
18
17
  };
19
18
 
19
+ // Export types for TypeScript consumers
20
+ export type { EncoreAppProps } from "./components/EncoreApp";
21
+ export type { EncoreActionPayload } from "./contexts/EncoreActionContext";
22
+
20
23
  export * from "./codegen";
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Data patching utilities for fixing layout and component issues
3
+ *
4
+ * Applies heuristic-based fixes to page data to correct common layout problems
5
+ * that occur during the Figma-to-Encore conversion process.
6
+ */
7
+
8
+ import logger from "./logger";
9
+
10
+ /**
11
+ * Recursively patches page data to fix layout issues
12
+ *
13
+ * Current heuristics:
14
+ * 1. Horizontal layout detection - If children widths sum to ~100% or ~375px,
15
+ * force HORIZONTAL layout mode to prevent vertical stacking
16
+ *
17
+ * @param clientData - The client data object from page definition
18
+ * @returns Patched client data (mutates in place)
19
+ */
20
+ export function patchPageData(clientData: any): any {
21
+ if (!clientData) return clientData;
22
+
23
+ const patchNode = (node: any) => {
24
+ if (!node || typeof node !== "object") return;
25
+
26
+ // Heuristic: If children widths sum to ~100% or ~375px, force HORIZONTAL layout
27
+ if (
28
+ node.children &&
29
+ Array.isArray(node.children) &&
30
+ node.children.length > 1
31
+ ) {
32
+ let totalWidth = 0;
33
+ let childrenWithWidth = 0;
34
+
35
+ node.children.forEach((child: any) => {
36
+ if (child.style?.width) {
37
+ // Width might be percentage (sums to 100) or pixels (sums to 375)
38
+ totalWidth += child.style.width;
39
+ childrenWithWidth++;
40
+ }
41
+ });
42
+
43
+ // Check if widths sum to full width (100% or 375px)
44
+ const isFullWidthRow =
45
+ (Math.abs(totalWidth - 100) < 1 || Math.abs(totalWidth - 375) < 5) &&
46
+ childrenWithWidth >= 2;
47
+
48
+ if (isFullWidthRow) {
49
+ if (!node.style) node.style = {};
50
+ if (!node.style.layout) node.style.layout = {};
51
+
52
+ // Only apply if mode is missing or undefined
53
+ if (!node.style.layout.mode) {
54
+ logger.debug('Layout patch applied', {
55
+ nodeId: node.id,
56
+ childrenCount: childrenWithWidth,
57
+ totalWidth
58
+ });
59
+ node.style.layout.mode = "HORIZONTAL";
60
+ node.style.layout.primaryAxisAlignItems = "flex-start";
61
+ node.style.layout.counterAxisAlignItems = "flex-start";
62
+ }
63
+ }
64
+ }
65
+
66
+ // Recurse through children
67
+ if (node.children) {
68
+ if (Array.isArray(node.children)) {
69
+ node.children.forEach(patchNode);
70
+ } else {
71
+ patchNode(node.children);
72
+ }
73
+ }
74
+ };
75
+
76
+ patchNode(clientData);
77
+ return clientData;
78
+ }
@@ -2,6 +2,7 @@ import packages from "./packages";
2
2
  import { isLocalMode } from "./localMode";
3
3
  import { CONST_COMPONENTS_CDN_URL } from "../../constants";
4
4
  import { registerModule, getModuleExports, haveModule } from "./moduleRegistry";
5
+ import logger from "./logger";
5
6
 
6
7
  // Initialize registry with default packages
7
8
  Object.keys(packages).forEach((p) => {
@@ -85,7 +86,7 @@ export function loadAMDModule(name: string, code: string) {
85
86
  try {
86
87
  ${patchedCode}\n//# sourceURL=dynamic-module://${name}.js
87
88
  } catch (error) {
88
- console.error('[Module Evaluation Error]', {
89
+ console.error('[encore-lib] Module evaluation error', {
89
90
  moduleName: '${name}',
90
91
  error: error.message,
91
92
  stack: error.stack,
@@ -97,18 +98,16 @@ export function loadAMDModule(name: string, code: string) {
97
98
  `
98
99
  );
99
100
 
100
- console.debug(`[Module Loading] Attempting to load module: ${name}`);
101
+ logger.debug('Loading AMD module', { name });
101
102
  wrapper(define);
102
- console.debug(`[Module Loading] Successfully loaded module: ${name}`);
103
+ logger.debug('Module loaded successfully', { name });
103
104
  } catch (e: any) {
104
- // Enhanced error logging
105
- console.error("[Module Loading Failed]", {
105
+ logger.error('Module loading failed', {
106
106
  moduleName: name,
107
- error: e,
108
107
  errorType: e.constructor.name,
109
108
  message: e.message,
110
109
  stack: e.stack,
111
- code: code.slice(0, 500) + (code.length > 500 ? "..." : ""), // Show first 500 chars of code
110
+ codePreview: code.slice(0, 200) + (code.length > 200 ? "..." : "")
112
111
  });
113
112
  reject(e);
114
113
  }
@@ -117,7 +116,7 @@ export function loadAMDModule(name: string, code: string) {
117
116
 
118
117
  export async function fetchDep(name: string) {
119
118
  // Local mode: map Encore component name to local JSX/JS under /flex-layout
120
- console.log(`🔍 fetchDep called for ${name}. isLocalMode: ${isLocalMode()}`);
119
+ logger.debug('Fetching dependency', { name, isLocal: isLocalMode() });
121
120
  if (isLocalMode()) {
122
121
  // Expecting `${appId}/draft/components/${pageId}`
123
122
  const m = name.match(/^([^/]+)\/draft\/components\/([^/]+)$/);
@@ -160,7 +159,7 @@ export async function fetchDep(name: string) {
160
159
  // Remote mode: use AMD loader
161
160
  const cacheBuster = Math.round(Date.now() / 1000);
162
161
  const url = `${CONST_COMPONENTS_CDN_URL}/${name}.js?cacheBuster=${cacheBuster}`;
163
- console.log(`[Module Loading] Fetching remote component from: ${url}`);
162
+ logger.debug('Fetching remote component', { url });
164
163
  const text = await fetch(url).then(async (a) => {
165
164
  if (!a.ok) {
166
165
  if (a.status === 403) {
@@ -1,6 +1,7 @@
1
1
  import axios from "axios";
2
2
  import { isLocalMode } from "./localMode";
3
3
  import useEncoreState from "../stores/useEncoreState";
4
+ import logger from "./logger";
4
5
 
5
6
  // Get baseURL from store at runtime instead of build time
6
7
  const getAppsServiceUrl = () => {
@@ -86,19 +87,11 @@ const fetcher = (url: string) => {
86
87
  // Get baseURL at runtime from store
87
88
  const appsServiceUrl = getAppsServiceUrl();
88
89
 
89
- console.log(
90
- "[Fetcher] Requesting:",
91
- url,
92
- "BaseURL:",
93
- appsServiceUrl,
94
- "Headers:",
95
- { "x-app-clientrendered": "disabled" }
96
- );
90
+ logger.debug('Fetching from Encore service', { url, baseURL: appsServiceUrl });
97
91
 
98
92
  return axios({
99
93
  baseURL: appsServiceUrl,
100
94
  url,
101
- // headers: { "x-app-clientrendered": "true" },
102
95
  }).then((res) => res.data);
103
96
  };
104
97
 
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Centralized logging utility for encore-lib
3
+ *
4
+ * Provides conditional logging based on environment.
5
+ * Debug logs only appear in development mode.
6
+ */
7
+
8
+ const isDev = typeof import.meta !== 'undefined'
9
+ ? import.meta.env?.DEV || import.meta.env?.MODE === 'development'
10
+ : process.env.NODE_ENV === 'development';
11
+
12
+ /**
13
+ * Logger instance with environment-aware methods
14
+ */
15
+ export const logger = {
16
+ /**
17
+ * Debug-level logging (only in development)
18
+ * Use for detailed diagnostic information
19
+ */
20
+ debug: (...args: any[]) => {
21
+ if (isDev) {
22
+ console.debug('[encore-lib]', ...args);
23
+ }
24
+ },
25
+
26
+ /**
27
+ * Info-level logging (only in development)
28
+ * Use for general informational messages
29
+ */
30
+ info: (...args: any[]) => {
31
+ if (isDev) {
32
+ console.info('[encore-lib]', ...args);
33
+ }
34
+ },
35
+
36
+ /**
37
+ * Warning-level logging (always shown)
38
+ * Use for recoverable issues that need attention
39
+ */
40
+ warn: (...args: any[]) => {
41
+ console.warn('[encore-lib]', ...args);
42
+ },
43
+
44
+ /**
45
+ * Error-level logging (always shown)
46
+ * Use for errors and exceptions
47
+ */
48
+ error: (...args: any[]) => {
49
+ console.error('[encore-lib]', ...args);
50
+ },
51
+ };
52
+
53
+ export default logger;
@@ -1,3 +1,5 @@
1
+ import logger from "./logger";
2
+
1
3
  const registry: Record<string, any> = {};
2
4
 
3
5
  export function registerModule(name: string, module: any) {
@@ -19,6 +21,6 @@ export function haveModule(name: string): boolean {
19
21
  export function clearModuleCache(name: string): void {
20
22
  if (name in registry) {
21
23
  delete registry[name];
22
- console.log(`[Module Cache] Cleared module: ${name}`);
24
+ logger.debug('Module cache cleared', { name });
23
25
  }
24
26
  }