@bravostudioai/react 0.1.0 → 0.1.1

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 (37) hide show
  1. package/dist/components/EncoreApp.js +145 -131
  2. package/dist/components/EncoreApp.js.map +1 -1
  3. package/dist/contexts/EncoreRouterContext.js +13 -0
  4. package/dist/contexts/EncoreRouterContext.js.map +1 -0
  5. package/dist/hooks/usePusherUpdates.js +4 -2
  6. package/dist/hooks/usePusherUpdates.js.map +1 -1
  7. package/dist/lib/dynamicModules.js +75 -85
  8. package/dist/lib/dynamicModules.js.map +1 -1
  9. package/dist/lib/moduleRegistry.js +20 -0
  10. package/dist/lib/moduleRegistry.js.map +1 -0
  11. package/dist/lib/packages.js +1 -3
  12. package/dist/lib/packages.js.map +1 -1
  13. package/dist/src/codegen/generator.d.ts +10 -0
  14. package/dist/src/codegen/generator.d.ts.map +1 -0
  15. package/dist/src/codegen/parser.d.ts +37 -0
  16. package/dist/src/codegen/parser.d.ts.map +1 -0
  17. package/dist/src/codegen/types.d.ts +53 -0
  18. package/dist/src/codegen/types.d.ts.map +1 -0
  19. package/dist/src/components/EncoreApp.d.ts.map +1 -1
  20. package/dist/src/contexts/EncoreRouterContext.d.ts +10 -0
  21. package/dist/src/contexts/EncoreRouterContext.d.ts.map +1 -0
  22. package/dist/src/hooks/useAuthRedirect.d.ts.map +1 -1
  23. package/dist/src/lib/dynamicModules.d.ts +1 -5
  24. package/dist/src/lib/dynamicModules.d.ts.map +1 -1
  25. package/dist/src/lib/moduleRegistry.d.ts +9 -0
  26. package/dist/src/lib/moduleRegistry.d.ts.map +1 -0
  27. package/dist/src/lib/packages.d.ts.map +1 -1
  28. package/package.json +1 -1
  29. package/src/cli/commands/generate.ts +88 -2723
  30. package/src/codegen/generator.ts +877 -0
  31. package/src/codegen/index.ts +3 -0
  32. package/src/codegen/parser.ts +1614 -0
  33. package/src/codegen/types.ts +58 -0
  34. package/src/components/EncoreApp.tsx +20 -1
  35. package/src/contexts/EncoreRouterContext.ts +28 -0
  36. package/src/hooks/useAuthRedirect.ts +56 -55
  37. 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
+ }