@bravostudioai/react 0.1.0 → 0.1.2
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/dist/_virtual/main.js +3 -2
- package/dist/cli/commands/generate.js +161 -1438
- package/dist/cli/commands/generate.js.map +1 -1
- package/dist/codegen/generator.js +473 -0
- package/dist/codegen/generator.js.map +1 -0
- package/dist/codegen/parser.js +720 -0
- package/dist/codegen/parser.js.map +1 -0
- package/dist/components/EncoreApp.js +197 -162
- package/dist/components/EncoreApp.js.map +1 -1
- package/dist/contexts/EncoreRouterContext.js +13 -0
- package/dist/contexts/EncoreRouterContext.js.map +1 -0
- package/dist/hooks/usePusherUpdates.js +4 -2
- package/dist/hooks/usePusherUpdates.js.map +1 -1
- package/dist/lib/dynamicModules.js +75 -85
- package/dist/lib/dynamicModules.js.map +1 -1
- package/dist/lib/moduleRegistry.js +20 -0
- package/dist/lib/moduleRegistry.js.map +1 -0
- package/dist/lib/packages.js +1 -3
- package/dist/lib/packages.js.map +1 -1
- package/dist/src/cli/commands/generate.d.ts.map +1 -1
- package/dist/src/codegen/generator.d.ts +10 -0
- package/dist/src/codegen/generator.d.ts.map +1 -0
- package/dist/src/codegen/index.d.ts +4 -0
- package/dist/src/codegen/index.d.ts.map +1 -0
- package/dist/src/codegen/parser.d.ts +37 -0
- package/dist/src/codegen/parser.d.ts.map +1 -0
- package/dist/src/codegen/types.d.ts +53 -0
- package/dist/src/codegen/types.d.ts.map +1 -0
- package/dist/src/components/EncoreApp.d.ts +5 -1
- package/dist/src/components/EncoreApp.d.ts.map +1 -1
- package/dist/src/contexts/EncoreRouterContext.d.ts +10 -0
- package/dist/src/contexts/EncoreRouterContext.d.ts.map +1 -0
- package/dist/src/hooks/useAuthRedirect.d.ts.map +1 -1
- package/dist/src/lib/dynamicModules.d.ts +1 -5
- package/dist/src/lib/dynamicModules.d.ts.map +1 -1
- package/dist/src/lib/moduleRegistry.d.ts +9 -0
- package/dist/src/lib/moduleRegistry.d.ts.map +1 -0
- package/dist/src/lib/packages.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/cli/commands/generate.ts +88 -2723
- package/src/codegen/generator.ts +877 -0
- package/src/codegen/index.ts +3 -0
- package/src/codegen/parser.ts +1614 -0
- package/src/codegen/types.ts +58 -0
- package/src/components/EncoreApp.tsx +75 -22
- package/src/contexts/EncoreRouterContext.ts +28 -0
- package/src/hooks/useAuthRedirect.ts +56 -55
- package/src/lib/packages.ts +8 -15
|
@@ -0,0 +1,877 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ComponentInfo,
|
|
3
|
+
SliderInfo,
|
|
4
|
+
InputGroupInfo,
|
|
5
|
+
FormInfo,
|
|
6
|
+
SelectInputInfo,
|
|
7
|
+
ActionButtonInfo,
|
|
8
|
+
} from "./types";
|
|
9
|
+
import {
|
|
10
|
+
getComponentPropType,
|
|
11
|
+
getComponentPropName,
|
|
12
|
+
sanitizePropName,
|
|
13
|
+
} from "./parser";
|
|
14
|
+
|
|
15
|
+
export interface ComponentMetadata {
|
|
16
|
+
props: string[];
|
|
17
|
+
events: string[];
|
|
18
|
+
jsx: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function generateComponentCode(
|
|
22
|
+
appId: string,
|
|
23
|
+
pageId: string,
|
|
24
|
+
componentName: string,
|
|
25
|
+
sliders: SliderInfo[],
|
|
26
|
+
standaloneComponents: ComponentInfo[],
|
|
27
|
+
inputGroups: InputGroupInfo[],
|
|
28
|
+
forms: FormInfo[],
|
|
29
|
+
selectInputs: SelectInputInfo[],
|
|
30
|
+
actionButtons: ActionButtonInfo[],
|
|
31
|
+
isProduction: boolean = false
|
|
32
|
+
): string {
|
|
33
|
+
// Generate prop types
|
|
34
|
+
const propTypes: string[] = [];
|
|
35
|
+
const controlPropTypes: string[] = [];
|
|
36
|
+
const inputGroupPropTypes: string[] = [];
|
|
37
|
+
const formPropTypes: string[] = [];
|
|
38
|
+
const selectInputPropTypes: string[] = [];
|
|
39
|
+
const actionButtonPropTypes: string[] = [];
|
|
40
|
+
|
|
41
|
+
// Add standalone component props
|
|
42
|
+
standaloneComponents.forEach((comp) => {
|
|
43
|
+
propTypes.push(` ${comp.propName}?: ${comp.propType};`);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Add input group props
|
|
47
|
+
inputGroups.forEach((group) => {
|
|
48
|
+
const propName = sanitizePropName(group.groupName);
|
|
49
|
+
inputGroupPropTypes.push(` ${propName}?: string;`);
|
|
50
|
+
inputGroupPropTypes.push(
|
|
51
|
+
` on${propName[0].toUpperCase()}${propName.slice(
|
|
52
|
+
1
|
|
53
|
+
)}Change?: (value: string) => void;`
|
|
54
|
+
);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Generate form data interfaces and props
|
|
58
|
+
const formDataInterfaces: string[] = [];
|
|
59
|
+
forms.forEach((form) => {
|
|
60
|
+
const formPropName = sanitizePropName(form.formName);
|
|
61
|
+
const formDataTypeName = `${formPropName[0].toUpperCase()}${formPropName.slice(
|
|
62
|
+
1
|
|
63
|
+
)}FormData`;
|
|
64
|
+
|
|
65
|
+
// Generate interface for form data with human-readable property names
|
|
66
|
+
const formDataProps: string[] = [];
|
|
67
|
+
form.inputs.forEach((input) => {
|
|
68
|
+
const inputPropName = input.propName; // Use qualified prop name
|
|
69
|
+
const inputType = getComponentPropType(input.type, input.name);
|
|
70
|
+
formDataProps.push(` ${inputPropName}: ${inputType};`);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
formDataInterfaces.push(`export interface ${formDataTypeName} {
|
|
74
|
+
${formDataProps.join("\n")}
|
|
75
|
+
}`);
|
|
76
|
+
|
|
77
|
+
// Add the callback prop with proper typing
|
|
78
|
+
formPropTypes.push(
|
|
79
|
+
` on${formPropName[0].toUpperCase()}${formPropName.slice(
|
|
80
|
+
1
|
|
81
|
+
)}Submit?: (formData: ${formDataTypeName}) => void;`
|
|
82
|
+
);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// Add standalone select input props (controlled value + onChange + options)
|
|
86
|
+
selectInputs.forEach((input) => {
|
|
87
|
+
const propName = input.propName;
|
|
88
|
+
const capitalizedPropName = propName[0].toUpperCase() + propName.slice(1);
|
|
89
|
+
selectInputPropTypes.push(` ${propName}?: string;`);
|
|
90
|
+
selectInputPropTypes.push(
|
|
91
|
+
` ${propName}Options?: Array<string | { value: string; label: string }>;`
|
|
92
|
+
);
|
|
93
|
+
selectInputPropTypes.push(
|
|
94
|
+
` on${capitalizedPropName}Change?: (value: string) => void;`
|
|
95
|
+
);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// Add action button props (onClick)
|
|
99
|
+
actionButtons.forEach((button) => {
|
|
100
|
+
const propName = button.propName;
|
|
101
|
+
const capitalizedPropName = propName[0].toUpperCase() + propName.slice(1);
|
|
102
|
+
actionButtonPropTypes.push(` on${capitalizedPropName}Click?: () => void;`);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
sliders.forEach((slider) => {
|
|
106
|
+
if (slider.arrayContainer && slider.arrayContainer.components.length > 0) {
|
|
107
|
+
const container = slider.arrayContainer;
|
|
108
|
+
const itemTypeName = `${container.propName[0].toUpperCase()}${container.propName.slice(
|
|
109
|
+
1
|
|
110
|
+
)}Item`;
|
|
111
|
+
propTypes.push(` ${container.propName}: ${itemTypeName}[];`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Add control props for each slider/repeating container
|
|
115
|
+
const sliderPropName = sanitizePropName(slider.name || "container");
|
|
116
|
+
controlPropTypes.push(` ${sliderPropName}CurrentIndex?: number;`);
|
|
117
|
+
controlPropTypes.push(
|
|
118
|
+
` on${sliderPropName[0].toUpperCase()}${sliderPropName.slice(
|
|
119
|
+
1
|
|
120
|
+
)}IndexChange?: (index: number) => void;`
|
|
121
|
+
);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const allPropTypes = [
|
|
125
|
+
...propTypes,
|
|
126
|
+
...controlPropTypes,
|
|
127
|
+
...inputGroupPropTypes,
|
|
128
|
+
...formPropTypes,
|
|
129
|
+
...selectInputPropTypes,
|
|
130
|
+
...actionButtonPropTypes,
|
|
131
|
+
];
|
|
132
|
+
const hasProps = allPropTypes.length > 0;
|
|
133
|
+
const propsInterface = hasProps
|
|
134
|
+
? `export interface ${componentName}Props {
|
|
135
|
+
${allPropTypes.join("\n")}
|
|
136
|
+
}`
|
|
137
|
+
: "";
|
|
138
|
+
|
|
139
|
+
const itemTypes = sliders
|
|
140
|
+
.filter((s) => s.arrayContainer && s.arrayContainer.components.length > 0)
|
|
141
|
+
.map((slider) => {
|
|
142
|
+
const container = slider.arrayContainer!;
|
|
143
|
+
const itemTypeName = `${container.propName[0].toUpperCase()}${container.propName.slice(
|
|
144
|
+
1
|
|
145
|
+
)}Item`;
|
|
146
|
+
const itemProps = container.components
|
|
147
|
+
.map((comp) => {
|
|
148
|
+
return ` ${comp.propName}: ${comp.propType};`;
|
|
149
|
+
})
|
|
150
|
+
.join("\n");
|
|
151
|
+
return `export interface ${itemTypeName} {
|
|
152
|
+
${itemProps}
|
|
153
|
+
}`;
|
|
154
|
+
})
|
|
155
|
+
.join("\n\n");
|
|
156
|
+
|
|
157
|
+
const formDataTypes = formDataInterfaces.join("\n\n");
|
|
158
|
+
|
|
159
|
+
// Generate data mapping
|
|
160
|
+
const dataMapping: string[] = [];
|
|
161
|
+
const controlMapping: string[] = [];
|
|
162
|
+
|
|
163
|
+
// Add standalone component mappings
|
|
164
|
+
standaloneComponents.forEach((comp) => {
|
|
165
|
+
const propKey = getComponentPropName(comp.type);
|
|
166
|
+
dataMapping.push(` // ${comp.name}
|
|
167
|
+
...(props.${comp.propName} !== undefined && { "${comp.id}": { ${propKey}: props.${comp.propName} } as any }),`);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// Add select input mappings (for controlled values and options)
|
|
171
|
+
selectInputs.forEach((input) => {
|
|
172
|
+
dataMapping.push(` // ${input.name}
|
|
173
|
+
...((props.${input.propName} !== undefined || props.${input.propName}Options !== undefined) && {
|
|
174
|
+
"${input.id}": {
|
|
175
|
+
...(props.${input.propName} !== undefined && { value: props.${input.propName} }),
|
|
176
|
+
...(props.${input.propName}Options !== undefined && { options: props.${input.propName}Options }),
|
|
177
|
+
} as any
|
|
178
|
+
}),`);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
sliders.forEach((slider) => {
|
|
182
|
+
if (slider.arrayContainer && slider.arrayContainer.components.length > 0) {
|
|
183
|
+
const container = slider.arrayContainer;
|
|
184
|
+
const itemMapping = container.components
|
|
185
|
+
.map((comp) => {
|
|
186
|
+
const propKey = getComponentPropName(comp.type);
|
|
187
|
+
return ` // ${comp.name}
|
|
188
|
+
"${comp.id}": {
|
|
189
|
+
${propKey}: item.${comp.propName},
|
|
190
|
+
}`;
|
|
191
|
+
})
|
|
192
|
+
.join(",\n");
|
|
193
|
+
|
|
194
|
+
dataMapping.push(` // ${container.name}
|
|
195
|
+
"${container.id}": props.${container.propName}.map((item) => ({
|
|
196
|
+
${itemMapping}
|
|
197
|
+
})),`);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Generate control mapping for each slider
|
|
201
|
+
const sliderPropName = sanitizePropName(slider.name || "container");
|
|
202
|
+
const controlPropName = `${sliderPropName[0].toUpperCase()}${sliderPropName.slice(
|
|
203
|
+
1
|
|
204
|
+
)}IndexChange`;
|
|
205
|
+
const controlEntry: string[] = [];
|
|
206
|
+
if (slider.id) {
|
|
207
|
+
controlEntry.push(` // ${slider.name}`);
|
|
208
|
+
controlEntry.push(` "${slider.id}": {`);
|
|
209
|
+
controlEntry.push(
|
|
210
|
+
` currentIndex: props.${sliderPropName}CurrentIndex,`
|
|
211
|
+
);
|
|
212
|
+
controlEntry.push(` onIndexChange: props.on${controlPropName},`);
|
|
213
|
+
controlEntry.push(` }`);
|
|
214
|
+
}
|
|
215
|
+
if (controlEntry.length > 0) {
|
|
216
|
+
controlMapping.push(controlEntry.join("\n"));
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
const repeatingContainerControlsCode =
|
|
221
|
+
controlMapping.length > 0
|
|
222
|
+
? `\n repeatingContainerControls={{
|
|
223
|
+
${controlMapping.join(",\n")}
|
|
224
|
+
}}`
|
|
225
|
+
: "";
|
|
226
|
+
|
|
227
|
+
// Generate input group mappings
|
|
228
|
+
const inputGroupMapping: string[] = [];
|
|
229
|
+
const inputGroupHandlers: string[] = [];
|
|
230
|
+
|
|
231
|
+
// Start action handler if we have input groups, forms, select inputs, or action buttons
|
|
232
|
+
if (
|
|
233
|
+
inputGroups.length > 0 ||
|
|
234
|
+
forms.length > 0 ||
|
|
235
|
+
selectInputs.length > 0 ||
|
|
236
|
+
actionButtons.length > 0
|
|
237
|
+
) {
|
|
238
|
+
inputGroupHandlers.push(` const handleAction = (payload: any) => {`);
|
|
239
|
+
inputGroupHandlers.push(` const { action } = payload?.bravo || {};`);
|
|
240
|
+
inputGroupHandlers.push(``);
|
|
241
|
+
|
|
242
|
+
// Add select input handling
|
|
243
|
+
if (selectInputs.length > 0) {
|
|
244
|
+
inputGroupHandlers.push(` // Handle select input changes`);
|
|
245
|
+
inputGroupHandlers.push(
|
|
246
|
+
` if (action?.action === "input-change" || action?.action === "select-change") {`
|
|
247
|
+
);
|
|
248
|
+
inputGroupHandlers.push(` const nodeId = action?.nodeId;`);
|
|
249
|
+
inputGroupHandlers.push(` const value = action?.params?.value;`);
|
|
250
|
+
inputGroupHandlers.push(``);
|
|
251
|
+
|
|
252
|
+
selectInputs.forEach((input) => {
|
|
253
|
+
const propName = input.propName;
|
|
254
|
+
const capitalizedPropName =
|
|
255
|
+
propName[0].toUpperCase() + propName.slice(1);
|
|
256
|
+
const handlerPropName = `on${capitalizedPropName}Change`;
|
|
257
|
+
inputGroupHandlers.push(` // ${input.name}`);
|
|
258
|
+
inputGroupHandlers.push(
|
|
259
|
+
` if (nodeId === "${input.id}" && props.${handlerPropName}) {`
|
|
260
|
+
);
|
|
261
|
+
inputGroupHandlers.push(` props.${handlerPropName}(value);`);
|
|
262
|
+
inputGroupHandlers.push(` return;`);
|
|
263
|
+
inputGroupHandlers.push(` }`);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
inputGroupHandlers.push(` }`);
|
|
267
|
+
inputGroupHandlers.push(``);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Add action button handling
|
|
271
|
+
if (actionButtons.length > 0) {
|
|
272
|
+
inputGroupHandlers.push(` // Handle button clicks`);
|
|
273
|
+
inputGroupHandlers.push(
|
|
274
|
+
` if (action?.action === "remote" || action?.action === "tap" || action?.action === "link") {`
|
|
275
|
+
);
|
|
276
|
+
inputGroupHandlers.push(` const nodeId = action?.nodeId;`);
|
|
277
|
+
inputGroupHandlers.push(``);
|
|
278
|
+
|
|
279
|
+
actionButtons.forEach((button) => {
|
|
280
|
+
const propName = button.propName;
|
|
281
|
+
const capitalizedPropName =
|
|
282
|
+
propName[0].toUpperCase() + propName.slice(1);
|
|
283
|
+
const handlerPropName = `on${capitalizedPropName}Click`;
|
|
284
|
+
inputGroupHandlers.push(` // ${button.name}`);
|
|
285
|
+
inputGroupHandlers.push(
|
|
286
|
+
` if (nodeId === "${button.id}" && props.${handlerPropName}) {`
|
|
287
|
+
);
|
|
288
|
+
inputGroupHandlers.push(` props.${handlerPropName}();`);
|
|
289
|
+
inputGroupHandlers.push(` return;`);
|
|
290
|
+
inputGroupHandlers.push(` }`);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
inputGroupHandlers.push(` }`);
|
|
294
|
+
inputGroupHandlers.push(``);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Add input group handling
|
|
298
|
+
if (inputGroups.length > 0) {
|
|
299
|
+
inputGroupHandlers.push(
|
|
300
|
+
` if (action?.action === "input-group-change") {`
|
|
301
|
+
);
|
|
302
|
+
inputGroupHandlers.push(
|
|
303
|
+
` const { groupName, value } = action.params;`
|
|
304
|
+
);
|
|
305
|
+
|
|
306
|
+
inputGroups.forEach((group) => {
|
|
307
|
+
const propName = sanitizePropName(group.groupName);
|
|
308
|
+
const handlerPropName = `on${propName[0].toUpperCase()}${propName.slice(
|
|
309
|
+
1
|
|
310
|
+
)}Change`;
|
|
311
|
+
inputGroupHandlers.push(
|
|
312
|
+
` if (groupName === "${group.groupName}" && props.${handlerPropName}) {`
|
|
313
|
+
);
|
|
314
|
+
inputGroupHandlers.push(` props.${handlerPropName}(value);`);
|
|
315
|
+
inputGroupHandlers.push(` return;`);
|
|
316
|
+
inputGroupHandlers.push(` }`);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
inputGroupHandlers.push(` }`);
|
|
320
|
+
inputGroupHandlers.push(``);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Add form submission handling
|
|
324
|
+
if (forms.length > 0) {
|
|
325
|
+
inputGroupHandlers.push(` if (action?.action === "submit") {`);
|
|
326
|
+
inputGroupHandlers.push(` // Get form inputs from Encore state`);
|
|
327
|
+
inputGroupHandlers.push(
|
|
328
|
+
` const formInputs = useEncoreState.getState().formInputs["${pageId}"] || {};`
|
|
329
|
+
);
|
|
330
|
+
inputGroupHandlers.push(` const submitNodeId = action?.nodeId;`);
|
|
331
|
+
inputGroupHandlers.push(``);
|
|
332
|
+
|
|
333
|
+
forms.forEach((form, index) => {
|
|
334
|
+
const formPropName = sanitizePropName(form.formName);
|
|
335
|
+
const handlerPropName = `on${formPropName[0].toUpperCase()}${formPropName.slice(
|
|
336
|
+
1
|
|
337
|
+
)}Submit`;
|
|
338
|
+
|
|
339
|
+
if (index > 0) {
|
|
340
|
+
inputGroupHandlers.push(``);
|
|
341
|
+
}
|
|
342
|
+
inputGroupHandlers.push(
|
|
343
|
+
` // Form: ${form.formName} (${form.formId})`
|
|
344
|
+
);
|
|
345
|
+
// Check if this form's submit button was clicked (if submitButtonId is available)
|
|
346
|
+
if (form.submitButtonId) {
|
|
347
|
+
inputGroupHandlers.push(
|
|
348
|
+
` if (submitNodeId === "${form.submitButtonId}" && props.${handlerPropName}) {`
|
|
349
|
+
);
|
|
350
|
+
} else if (forms.length === 1) {
|
|
351
|
+
// If only one form, don't check submit button ID
|
|
352
|
+
inputGroupHandlers.push(` if (props.${handlerPropName}) {`);
|
|
353
|
+
} else {
|
|
354
|
+
// Multiple forms but no submit button ID - check all
|
|
355
|
+
inputGroupHandlers.push(` if (props.${handlerPropName}) {`);
|
|
356
|
+
}
|
|
357
|
+
inputGroupHandlers.push(` // Extract form inputs for this form`);
|
|
358
|
+
const formDataTypeName = `${formPropName[0].toUpperCase()}${formPropName.slice(
|
|
359
|
+
1
|
|
360
|
+
)}FormData`;
|
|
361
|
+
inputGroupHandlers.push(
|
|
362
|
+
` const formData: ${formDataTypeName} = {`
|
|
363
|
+
);
|
|
364
|
+
const formDataEntries: string[] = [];
|
|
365
|
+
form.inputs.forEach((input) => {
|
|
366
|
+
const inputPropName = input.propName; // Use qualified prop name
|
|
367
|
+
formDataEntries.push(
|
|
368
|
+
` ${inputPropName}: formInputs["${input.id}"]`
|
|
369
|
+
);
|
|
370
|
+
});
|
|
371
|
+
inputGroupHandlers.push(formDataEntries.join(",\n"));
|
|
372
|
+
inputGroupHandlers.push(` };`);
|
|
373
|
+
inputGroupHandlers.push(` props.${handlerPropName}(formData);`);
|
|
374
|
+
inputGroupHandlers.push(
|
|
375
|
+
` // Note: Default form submission will still proceed after callback`
|
|
376
|
+
);
|
|
377
|
+
inputGroupHandlers.push(` }`);
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
inputGroupHandlers.push(` }`);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
inputGroupHandlers.push(` };`);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Generate input groups code
|
|
387
|
+
inputGroups.forEach((group) => {
|
|
388
|
+
const propName = sanitizePropName(group.groupName);
|
|
389
|
+
inputGroupMapping.push(
|
|
390
|
+
` ...(props.${propName} !== undefined && { ${propName}: props.${propName} }),`
|
|
391
|
+
);
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
const inputGroupsCode =
|
|
395
|
+
inputGroupMapping.length > 0
|
|
396
|
+
? `\n inputGroups={{
|
|
397
|
+
${inputGroupMapping.join("\n")}
|
|
398
|
+
}}`
|
|
399
|
+
: "";
|
|
400
|
+
|
|
401
|
+
const onActionCode =
|
|
402
|
+
inputGroupHandlers.length > 0 ? `\n onAction={handleAction}` : "";
|
|
403
|
+
|
|
404
|
+
const propsParameter = hasProps ? `props: ${componentName}Props` : "";
|
|
405
|
+
const propsInterfaceSection = propsInterface ? `${propsInterface}\n\n` : "";
|
|
406
|
+
|
|
407
|
+
return `/**
|
|
408
|
+
* ${componentName}
|
|
409
|
+
*
|
|
410
|
+
* Wrapper component for Encore Studio app.
|
|
411
|
+
* See README.md for detailed documentation.
|
|
412
|
+
*/
|
|
413
|
+
|
|
414
|
+
import { EncoreApp${
|
|
415
|
+
forms.length > 0 ? ", useEncoreState" : ""
|
|
416
|
+
} } from "@bravostudioai/react";
|
|
417
|
+
${isProduction ? `import productionData from "./data.json";` : ""}
|
|
418
|
+
|
|
419
|
+
${itemTypes ? `${itemTypes}\n\n` : ""}${
|
|
420
|
+
formDataTypes ? `${formDataTypes}\n\n` : ""
|
|
421
|
+
}${propsInterfaceSection}export function ${componentName}(${propsParameter}) {
|
|
422
|
+
${inputGroupHandlers.length > 0 ? inputGroupHandlers.join("\n") : ""}
|
|
423
|
+
return (
|
|
424
|
+
<EncoreApp
|
|
425
|
+
appId="${appId}"
|
|
426
|
+
pageId="${pageId}"
|
|
427
|
+
${
|
|
428
|
+
isProduction
|
|
429
|
+
? `appDefinition={productionData.app}
|
|
430
|
+
pageDefinition={productionData.page}
|
|
431
|
+
componentCode={productionData.componentCode}`
|
|
432
|
+
: ""
|
|
433
|
+
}
|
|
434
|
+
data={{
|
|
435
|
+
${dataMapping.join("\n")}
|
|
436
|
+
}}${repeatingContainerControlsCode}${inputGroupsCode}${onActionCode}
|
|
437
|
+
/>
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
export default ${componentName};
|
|
442
|
+
`;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
export function generateReadme(
|
|
446
|
+
appId: string,
|
|
447
|
+
pageId: string,
|
|
448
|
+
appName: string,
|
|
449
|
+
pageName: string,
|
|
450
|
+
componentName: string,
|
|
451
|
+
sliders: SliderInfo[],
|
|
452
|
+
standaloneComponents: ComponentInfo[],
|
|
453
|
+
inputGroups: InputGroupInfo[],
|
|
454
|
+
forms: FormInfo[],
|
|
455
|
+
selectInputs: SelectInputInfo[],
|
|
456
|
+
actionButtons: ActionButtonInfo[]
|
|
457
|
+
): string {
|
|
458
|
+
const componentDocs: string[] = [];
|
|
459
|
+
const controlDocs: string[] = [];
|
|
460
|
+
const selectInputDocs: string[] = [];
|
|
461
|
+
const actionButtonDocs: string[] = [];
|
|
462
|
+
|
|
463
|
+
// Add standalone component documentation
|
|
464
|
+
standaloneComponents.forEach((comp) => {
|
|
465
|
+
componentDocs.push(`### \`${comp.propName}\` (${comp.propType}, optional)
|
|
466
|
+
|
|
467
|
+
${comp.name} (${comp.type}) - Component ID: ${comp.id}`);
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
sliders.forEach((slider) => {
|
|
471
|
+
if (slider.arrayContainer && slider.arrayContainer.components.length > 0) {
|
|
472
|
+
const container = slider.arrayContainer;
|
|
473
|
+
|
|
474
|
+
// Generate component documentation
|
|
475
|
+
const compDocs = container.components
|
|
476
|
+
.map((comp) => {
|
|
477
|
+
return `- \`${comp.propName}\` (${comp.propType}): ${comp.name} (${comp.type}) - Component ID: ${comp.id}`;
|
|
478
|
+
})
|
|
479
|
+
.join("\n");
|
|
480
|
+
|
|
481
|
+
componentDocs.push(`### \`${
|
|
482
|
+
container.propName
|
|
483
|
+
}\` (${container.propName[0].toUpperCase()}${container.propName.slice(
|
|
484
|
+
1
|
|
485
|
+
)}Item[])
|
|
486
|
+
|
|
487
|
+
Array of items for "${container.name}" container (ID: ${container.id})
|
|
488
|
+
|
|
489
|
+
**Properties:**
|
|
490
|
+
|
|
491
|
+
${compDocs}`);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// Generate control documentation for each slider/repeating container
|
|
495
|
+
const sliderPropName = sanitizePropName(slider.name || "container");
|
|
496
|
+
const controlPropName = `${sliderPropName[0].toUpperCase()}${sliderPropName.slice(
|
|
497
|
+
1
|
|
498
|
+
)}IndexChange`;
|
|
499
|
+
controlDocs.push(`### \`${sliderPropName}CurrentIndex\` (number, optional)
|
|
500
|
+
|
|
501
|
+
Controls the currently visible slide/index for the "${slider.name}" container (ID: ${slider.id}).
|
|
502
|
+
|
|
503
|
+
When provided, the slider operates in controlled mode - the parent component controls which slide is displayed. When not provided, the slider manages its own state.
|
|
504
|
+
|
|
505
|
+
### \`on${controlPropName}\` ((index: number) => void, optional)
|
|
506
|
+
|
|
507
|
+
Callback fired when the user navigates to a different slide. Called with the new slide index (0-based).
|
|
508
|
+
|
|
509
|
+
This event fires whenever the slide changes, whether by user interaction, automatic advancement, or programmatic control.`);
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
const dataPropsSection =
|
|
513
|
+
componentDocs.length > 0
|
|
514
|
+
? componentDocs.join("\n\n")
|
|
515
|
+
: "This component currently has no data-bound props.";
|
|
516
|
+
|
|
517
|
+
const controlPropsSection =
|
|
518
|
+
controlDocs.length > 0
|
|
519
|
+
? `## Control Props
|
|
520
|
+
|
|
521
|
+
These props allow you to imperatively control repeating containers (sliders, lists, etc.) and receive notifications when the current index changes.
|
|
522
|
+
|
|
523
|
+
${controlDocs.join("\n\n")}`
|
|
524
|
+
: "";
|
|
525
|
+
|
|
526
|
+
// Generate input group documentation
|
|
527
|
+
const inputGroupDocs: string[] = [];
|
|
528
|
+
inputGroups.forEach((group) => {
|
|
529
|
+
const propName = sanitizePropName(group.groupName);
|
|
530
|
+
const handlerPropName = `on${propName[0].toUpperCase()}${propName.slice(
|
|
531
|
+
1
|
|
532
|
+
)}Change`;
|
|
533
|
+
const elementsList = group.elements
|
|
534
|
+
.map((el) => `- "${el.name}"`)
|
|
535
|
+
.join("\n");
|
|
536
|
+
|
|
537
|
+
inputGroupDocs.push(`### \`${propName}\` (string, optional)
|
|
538
|
+
|
|
539
|
+
Sets which element is active in the "${group.groupName}" input group (type: ${group.groupType}).
|
|
540
|
+
|
|
541
|
+
**Available elements:**
|
|
542
|
+
${elementsList}
|
|
543
|
+
|
|
544
|
+
### \`${handlerPropName}\` ((value: string) => void, optional)
|
|
545
|
+
|
|
546
|
+
Callback fired when the user selects a different element in the "${group.groupName}" input group. Called with the name of the selected element.`);
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
const inputGroupPropsSection =
|
|
550
|
+
inputGroupDocs.length > 0
|
|
551
|
+
? `## Input Group Props
|
|
552
|
+
|
|
553
|
+
These props allow you to control input groups (radio button-like behavior) and receive notifications when the selection changes.
|
|
554
|
+
|
|
555
|
+
${inputGroupDocs.join("\n\n")}`
|
|
556
|
+
: "";
|
|
557
|
+
|
|
558
|
+
// Generate form documentation
|
|
559
|
+
const formDocs: string[] = [];
|
|
560
|
+
forms.forEach((form) => {
|
|
561
|
+
const formPropName = sanitizePropName(form.formName);
|
|
562
|
+
const handlerPropName = `on${formPropName[0].toUpperCase()}${formPropName.slice(
|
|
563
|
+
1
|
|
564
|
+
)}Submit`;
|
|
565
|
+
const formDataTypeName = `${formPropName[0].toUpperCase()}${formPropName.slice(
|
|
566
|
+
1
|
|
567
|
+
)}FormData`;
|
|
568
|
+
const inputsList = form.inputs
|
|
569
|
+
.map((input) => {
|
|
570
|
+
const inputPropName = input.propName; // Use qualified prop name
|
|
571
|
+
const inputType = getComponentPropType(input.type, input.name);
|
|
572
|
+
return `- \`${inputPropName}\` (${inputType}) - ${input.name}`;
|
|
573
|
+
})
|
|
574
|
+
.join("\n");
|
|
575
|
+
|
|
576
|
+
formDocs.push(`### \`${handlerPropName}\` ((formData: ${formDataTypeName}) => void, optional)
|
|
577
|
+
|
|
578
|
+
Callback fired when the "${
|
|
579
|
+
form.formName
|
|
580
|
+
}" form is submitted. Called with a typed object containing all form input values with human-readable property names.
|
|
581
|
+
|
|
582
|
+
**Form data shape:**
|
|
583
|
+
\`\`\`typescript
|
|
584
|
+
interface ${formDataTypeName} {
|
|
585
|
+
${form.inputs
|
|
586
|
+
.map((input) => {
|
|
587
|
+
const inputPropName = input.propName; // Use qualified prop name
|
|
588
|
+
const inputType = getComponentPropType(input.type, input.name);
|
|
589
|
+
return ` ${inputPropName}: ${inputType};`;
|
|
590
|
+
})
|
|
591
|
+
.join("\n")}
|
|
592
|
+
}
|
|
593
|
+
\`\`\`
|
|
594
|
+
|
|
595
|
+
**Form inputs:**
|
|
596
|
+
${inputsList}`);
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
const formPropsSection =
|
|
600
|
+
formDocs.length > 0
|
|
601
|
+
? `## Form Submission Props
|
|
602
|
+
|
|
603
|
+
These props allow you to handle form submissions and access form input values.
|
|
604
|
+
|
|
605
|
+
${formDocs.join("\n\n")}`
|
|
606
|
+
: "";
|
|
607
|
+
|
|
608
|
+
// Generate select input documentation
|
|
609
|
+
selectInputs.forEach((input) => {
|
|
610
|
+
const propName = input.propName;
|
|
611
|
+
const capitalizedPropName = propName[0].toUpperCase() + propName.slice(1);
|
|
612
|
+
const handlerPropName = `on${capitalizedPropName}Change`;
|
|
613
|
+
const optionsPropName = `${propName}Options`;
|
|
614
|
+
|
|
615
|
+
selectInputDocs.push(`### \`${propName}\` (string, optional)
|
|
616
|
+
|
|
617
|
+
Controls the selected value of the "${input.name}" dropdown (Component ID: ${input.id}).
|
|
618
|
+
|
|
619
|
+
When provided, the select input operates in controlled mode - the parent component controls the current value.
|
|
620
|
+
|
|
621
|
+
### \`${optionsPropName}\` (Array<string | { value: string; label: string }>, optional)
|
|
622
|
+
|
|
623
|
+
Sets the available options for the "${input.name}" dropdown. Can be an array of strings (used as both value and label) or an array of objects with \`value\` and \`label\` properties.
|
|
624
|
+
|
|
625
|
+
**Example:**
|
|
626
|
+
\`\`\`tsx
|
|
627
|
+
// Simple string array
|
|
628
|
+
${optionsPropName}={["Option 1", "Option 2", "Option 3"]}
|
|
629
|
+
|
|
630
|
+
// Object array with separate values and labels
|
|
631
|
+
${optionsPropName}={[
|
|
632
|
+
{ value: "opt1", label: "Option 1" },
|
|
633
|
+
{ value: "opt2", label: "Option 2" },
|
|
634
|
+
]}
|
|
635
|
+
\`\`\`
|
|
636
|
+
|
|
637
|
+
### \`${handlerPropName}\` ((value: string) => void, optional)
|
|
638
|
+
|
|
639
|
+
Callback fired when the user selects a different option in the "${input.name}" dropdown. Called with the selected value.`);
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
const selectInputPropsSection =
|
|
643
|
+
selectInputDocs.length > 0
|
|
644
|
+
? `## Select Input Props
|
|
645
|
+
|
|
646
|
+
These props allow you to control select/dropdown inputs and respond to value changes.
|
|
647
|
+
|
|
648
|
+
${selectInputDocs.join("\n\n")}`
|
|
649
|
+
: "";
|
|
650
|
+
|
|
651
|
+
// Generate action button documentation
|
|
652
|
+
actionButtons.forEach((button) => {
|
|
653
|
+
const propName = button.propName;
|
|
654
|
+
const capitalizedPropName = propName[0].toUpperCase() + propName.slice(1);
|
|
655
|
+
const handlerPropName = `on${capitalizedPropName}Click`;
|
|
656
|
+
|
|
657
|
+
actionButtonDocs.push(`### \`${handlerPropName}\` (() => void, optional)
|
|
658
|
+
|
|
659
|
+
Callback fired when the "${button.name}" button is clicked (Component ID: ${button.id}).
|
|
660
|
+
|
|
661
|
+
Action type: \`${button.actionType}\``);
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
const actionButtonPropsSection =
|
|
665
|
+
actionButtonDocs.length > 0
|
|
666
|
+
? `## Action Button Props
|
|
667
|
+
|
|
668
|
+
These props allow you to respond to button clicks and other user interactions.
|
|
669
|
+
|
|
670
|
+
${actionButtonDocs.join("\n\n")}`
|
|
671
|
+
: "";
|
|
672
|
+
|
|
673
|
+
const propsSection = `## Props
|
|
674
|
+
|
|
675
|
+
### Data Props
|
|
676
|
+
|
|
677
|
+
${dataPropsSection}
|
|
678
|
+
|
|
679
|
+
${controlPropsSection}
|
|
680
|
+
|
|
681
|
+
${inputGroupPropsSection}
|
|
682
|
+
|
|
683
|
+
${formPropsSection}
|
|
684
|
+
|
|
685
|
+
${selectInputPropsSection}
|
|
686
|
+
|
|
687
|
+
${actionButtonPropsSection}`;
|
|
688
|
+
|
|
689
|
+
const usageExample =
|
|
690
|
+
sliders.length > 0 && sliders[0].arrayContainer
|
|
691
|
+
? `<${componentName}
|
|
692
|
+
${sliders
|
|
693
|
+
.map((s) =>
|
|
694
|
+
s.arrayContainer
|
|
695
|
+
? `${s.arrayContainer.propName}={[
|
|
696
|
+
{
|
|
697
|
+
${s.arrayContainer?.components
|
|
698
|
+
.map(
|
|
699
|
+
(c) =>
|
|
700
|
+
`${c.propName}: "${
|
|
701
|
+
c.type === "component:image"
|
|
702
|
+
? "https://example.com/image.jpg"
|
|
703
|
+
: "Example value"
|
|
704
|
+
}"`
|
|
705
|
+
)
|
|
706
|
+
.join(",\n ")}
|
|
707
|
+
}
|
|
708
|
+
]}`
|
|
709
|
+
: ""
|
|
710
|
+
)
|
|
711
|
+
.filter(Boolean)
|
|
712
|
+
.join("\n ")}
|
|
713
|
+
/>`
|
|
714
|
+
: `<${componentName} />`;
|
|
715
|
+
|
|
716
|
+
// Generate control example
|
|
717
|
+
let controlExample = "";
|
|
718
|
+
if (sliders.length > 0 && sliders[0]) {
|
|
719
|
+
const firstSlider = sliders[0];
|
|
720
|
+
const sliderPropName = sanitizePropName(firstSlider.name || "container");
|
|
721
|
+
const controlPropName = `${sliderPropName[0].toUpperCase()}${sliderPropName.slice(
|
|
722
|
+
1
|
|
723
|
+
)}IndexChange`;
|
|
724
|
+
|
|
725
|
+
controlExample = `## Controlling Slides
|
|
726
|
+
|
|
727
|
+
You can imperatively control which slide is displayed and listen for slide changes:
|
|
728
|
+
|
|
729
|
+
\`\`\`tsx
|
|
730
|
+
import { useState } from "react";
|
|
731
|
+
import { ${componentName} } from "./${componentName}";
|
|
732
|
+
|
|
733
|
+
function MyComponent() {
|
|
734
|
+
const [currentSlide, setCurrentSlide] = useState(0);
|
|
735
|
+
|
|
736
|
+
return (
|
|
737
|
+
<>
|
|
738
|
+
<button onClick={() => setCurrentSlide((prev) => Math.max(0, prev - 1))}>
|
|
739
|
+
Previous
|
|
740
|
+
</button>
|
|
741
|
+
<button onClick={() => setCurrentSlide((prev) => prev + 1)}>
|
|
742
|
+
Next
|
|
743
|
+
</button>
|
|
744
|
+
<${componentName}
|
|
745
|
+
${sliders
|
|
746
|
+
.map((s) =>
|
|
747
|
+
s.arrayContainer
|
|
748
|
+
? `${s.arrayContainer.propName}={[/* array of items */]}`
|
|
749
|
+
: ""
|
|
750
|
+
)
|
|
751
|
+
.filter(Boolean)
|
|
752
|
+
.join("\n ")}
|
|
753
|
+
${sliderPropName}CurrentIndex={currentSlide}
|
|
754
|
+
on${controlPropName}={(index) => setCurrentSlide(index)}
|
|
755
|
+
/>
|
|
756
|
+
</>
|
|
757
|
+
);
|
|
758
|
+
}
|
|
759
|
+
\`\`\``;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
return `# ${componentName}
|
|
763
|
+
|
|
764
|
+
Encore App Wrapper Component
|
|
765
|
+
|
|
766
|
+
This component wraps the Encore Studio app **"${appName}"** (App ID: \`${appId}\`) for the page **"${pageName}"** (Page ID: \`${pageId}\`).
|
|
767
|
+
|
|
768
|
+
The component automatically maps props to data-bound components within the app. Components marked with \`encore:data\` tags are exposed as props, allowing you to dynamically populate content.
|
|
769
|
+
|
|
770
|
+
${propsSection}
|
|
771
|
+
|
|
772
|
+
## Usage
|
|
773
|
+
|
|
774
|
+
\`\`\`tsx
|
|
775
|
+
import { ${componentName} } from "./${componentName}";
|
|
776
|
+
|
|
777
|
+
function MyComponent() {
|
|
778
|
+
return (
|
|
779
|
+
<${componentName}
|
|
780
|
+
${sliders
|
|
781
|
+
.map((s) =>
|
|
782
|
+
s.arrayContainer
|
|
783
|
+
? `${s.arrayContainer.propName}={[/* array of items */]}`
|
|
784
|
+
: ""
|
|
785
|
+
)
|
|
786
|
+
.filter(Boolean)
|
|
787
|
+
.join("\n ")}
|
|
788
|
+
/>
|
|
789
|
+
);
|
|
790
|
+
}
|
|
791
|
+
\`\`\`
|
|
792
|
+
|
|
793
|
+
## Example
|
|
794
|
+
|
|
795
|
+
\`\`\`tsx
|
|
796
|
+
${usageExample}
|
|
797
|
+
\`\`\`
|
|
798
|
+
|
|
799
|
+
${controlExample}
|
|
800
|
+
`;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
export function generateComponentMetadata(
|
|
804
|
+
_appName: string,
|
|
805
|
+
pageName: string,
|
|
806
|
+
sliders: SliderInfo[],
|
|
807
|
+
standaloneComponents: ComponentInfo[],
|
|
808
|
+
inputGroups: InputGroupInfo[],
|
|
809
|
+
forms: FormInfo[],
|
|
810
|
+
selectInputs: SelectInputInfo[],
|
|
811
|
+
actionButtons: ActionButtonInfo[]
|
|
812
|
+
): ComponentMetadata {
|
|
813
|
+
const props: string[] = [];
|
|
814
|
+
const events: string[] = [];
|
|
815
|
+
const componentName = sanitizePropName(pageName);
|
|
816
|
+
|
|
817
|
+
// Add standalone component props
|
|
818
|
+
standaloneComponents.forEach((comp) => {
|
|
819
|
+
props.push(comp.propName);
|
|
820
|
+
});
|
|
821
|
+
|
|
822
|
+
// Add input group props
|
|
823
|
+
inputGroups.forEach((group) => {
|
|
824
|
+
const propName = sanitizePropName(group.groupName);
|
|
825
|
+
props.push(propName);
|
|
826
|
+
events.push(`on${propName[0].toUpperCase()}${propName.slice(1)}Change`);
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
// Add form props
|
|
830
|
+
forms.forEach((form) => {
|
|
831
|
+
const formPropName = sanitizePropName(form.formName);
|
|
832
|
+
events.push(
|
|
833
|
+
`on${formPropName[0].toUpperCase()}${formPropName.slice(1)}Submit`
|
|
834
|
+
);
|
|
835
|
+
});
|
|
836
|
+
|
|
837
|
+
// Add select input props
|
|
838
|
+
selectInputs.forEach((input) => {
|
|
839
|
+
const propName = input.propName;
|
|
840
|
+
const capitalizedPropName = propName[0].toUpperCase() + propName.slice(1);
|
|
841
|
+
props.push(propName);
|
|
842
|
+
props.push(`${propName}Options`);
|
|
843
|
+
events.push(`on${capitalizedPropName}Change`);
|
|
844
|
+
});
|
|
845
|
+
|
|
846
|
+
// Add action button events
|
|
847
|
+
actionButtons.forEach((button) => {
|
|
848
|
+
const propName = button.propName;
|
|
849
|
+
const capitalizedPropName = propName[0].toUpperCase() + propName.slice(1);
|
|
850
|
+
events.push(`on${capitalizedPropName}Click`);
|
|
851
|
+
});
|
|
852
|
+
|
|
853
|
+
// Add slider props/events
|
|
854
|
+
sliders.forEach((slider) => {
|
|
855
|
+
if (slider.arrayContainer && slider.arrayContainer.components.length > 0) {
|
|
856
|
+
props.push(slider.arrayContainer.propName);
|
|
857
|
+
}
|
|
858
|
+
const sliderPropName = sanitizePropName(slider.name || "container");
|
|
859
|
+
const controlPropName = `${sliderPropName[0].toUpperCase()}${sliderPropName.slice(
|
|
860
|
+
1
|
|
861
|
+
)}IndexChange`;
|
|
862
|
+
props.push(`${sliderPropName}CurrentIndex`);
|
|
863
|
+
events.push(`on${controlPropName}`);
|
|
864
|
+
});
|
|
865
|
+
|
|
866
|
+
// Generate generic JSX usage
|
|
867
|
+
const jsxUsage = `<${componentName}
|
|
868
|
+
${props.map((p) => `${p}={${p}}`).join("\n ")}
|
|
869
|
+
${events.map((e) => `${e}={${e}}`).join("\n ")}
|
|
870
|
+
/>`;
|
|
871
|
+
|
|
872
|
+
return {
|
|
873
|
+
props,
|
|
874
|
+
events,
|
|
875
|
+
jsx: jsxUsage,
|
|
876
|
+
};
|
|
877
|
+
}
|