pxengine 0.1.3 → 0.1.5

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.
@@ -1,272 +1,295 @@
1
1
  import React from "react";
2
- import { UIComponent, UISchema, UIAtom, UIMolecule } from "../types/schema";
2
+ import { UIComponent, UISchema } from "../types/schema";
3
3
  import * as Atoms from "../atoms";
4
- import * as Molecules from "../molecules/index"; // Explicit index to help resolution
4
+ import * as Molecules from "../molecules/index";
5
5
 
6
- // NOTE: This renderer will be used by the frontend to interpret JSON schemas
7
- // generated by the AI agent via the MCP server.
6
+ // Import all shadcn UI components
7
+ import * as UIComponents from "../components/ui/index";
8
8
 
9
+ /**
10
+ * Components that require specific React Context and cannot be rendered in isolation.
11
+ * These components will show helpful error messages if used directly in schemas.
12
+ */
13
+ const CONTEXT_DEPENDENT_COMPONENTS = new Set([
14
+ // Form components - require FormField + FormItem context
15
+ "FormLabel",
16
+ "FormControl",
17
+ "FormDescription",
18
+ "FormMessage",
19
+ "FormItem",
20
+ "FormField",
21
+ // Select components - require Select parent
22
+ "SelectContent",
23
+ "SelectItem",
24
+ "SelectValue",
25
+ "SelectTrigger",
26
+ "SelectGroup",
27
+ "SelectLabel",
28
+ "SelectSeparator",
29
+ // Accordion components - require Accordion parent
30
+ "AccordionContent",
31
+ "AccordionItem",
32
+ "AccordionTrigger",
33
+ // Tabs components - require Tabs parent
34
+ "TabsContent",
35
+ "TabsList",
36
+ "TabsTrigger",
37
+ // Dialog components - require Dialog parent
38
+ "DialogContent",
39
+ "DialogHeader",
40
+ "DialogFooter",
41
+ "DialogTitle",
42
+ "DialogDescription",
43
+ "DialogClose",
44
+ // Sheet components - require Sheet parent
45
+ "SheetContent",
46
+ "SheetHeader",
47
+ "SheetFooter",
48
+ "SheetTitle",
49
+ "SheetDescription",
50
+ "SheetClose",
51
+ // AlertDialog components - require AlertDialog parent
52
+ "AlertDialogContent",
53
+ "AlertDialogHeader",
54
+ "AlertDialogFooter",
55
+ "AlertDialogTitle",
56
+ "AlertDialogDescription",
57
+ "AlertDialogAction",
58
+ "AlertDialogCancel",
59
+ // Dropdown components - require DropdownMenu parent
60
+ "DropdownMenuContent",
61
+ "DropdownMenuItem",
62
+ "DropdownMenuLabel",
63
+ "DropdownMenuSeparator",
64
+ "DropdownMenuCheckboxItem",
65
+ "DropdownMenuRadioItem",
66
+ "DropdownMenuRadioGroup",
67
+ // Popover components - require Popover parent
68
+ "PopoverContent",
69
+ // Tooltip components - require TooltipProvider parent
70
+ "TooltipContent",
71
+ // Context Menu components
72
+ "ContextMenuContent",
73
+ "ContextMenuItem",
74
+ // Navigation Menu components
75
+ "NavigationMenuContent",
76
+ "NavigationMenuItem",
77
+ ]);
78
+
79
+ /**
80
+ * Mapping of problematic components to their Atom alternatives
81
+ */
82
+ const COMPONENT_SUGGESTIONS: Record<string, string> = {
83
+ FormLabel: "FormInputAtom (with label prop)",
84
+ FormControl: "FormInputAtom",
85
+ FormItem: "FormInputAtom",
86
+ FormField: "FormInputAtom",
87
+ Select: "FormSelectAtom or InputAtom with inputType='select'",
88
+ SelectContent: "FormSelectAtom or InputAtom with inputType='select'",
89
+ SelectItem: "FormSelectAtom or InputAtom with inputType='select'",
90
+ Tabs: "TabsAtom",
91
+ TabsContent: "TabsAtom",
92
+ TabsList: "TabsAtom",
93
+ Dialog: "DialogAtom",
94
+ DialogContent: "DialogAtom",
95
+ Sheet: "SheetAtom",
96
+ SheetContent: "SheetAtom",
97
+ AlertDialog: "AlertDialogAtom",
98
+ AlertDialogContent: "AlertDialogAtom",
99
+ Accordion: "AccordionAtom",
100
+ AccordionItem: "AccordionAtom",
101
+ Input: "InputAtom",
102
+ Textarea: "InputAtom with inputType='textarea'",
103
+ };
104
+
105
+ /**
106
+ * PXEngineRenderer
107
+ *
108
+ * Handles both the full schema { version, root } and individual components.
109
+ * Dynamically resolves components from Atoms/Molecules/UI Components registry.
110
+ * Prevents rendering of context-dependent components to avoid React errors.
111
+ */
9
112
  interface PXEngineRendererProps {
10
113
  schema: UISchema | UIComponent;
11
114
  onAction?: (action: string, payload?: any) => void;
12
115
  }
13
116
 
14
117
  /**
15
- * PXEngineRenderer
16
- *
17
- * The core engine of the @pxengine-ui library.
18
- * Recursively renders Atoms and Molecules based on the provided JSON schema.
118
+ * Renders an error message for context-dependent components
19
119
  */
120
+ const renderContextDependentError = (
121
+ componentName: string,
122
+ normalizedName: string,
123
+ key: string,
124
+ ): React.ReactNode => {
125
+ const suggestion =
126
+ COMPONENT_SUGGESTIONS[normalizedName] ||
127
+ `${componentName}Atom (if available)`;
128
+
129
+ return (
130
+ <div
131
+ key={key}
132
+ className="p-4 border-2 border-amber-500/50 rounded-lg bg-amber-50/80 space-y-2 my-2"
133
+ >
134
+ <div className="flex items-start gap-2">
135
+ <span className="text-amber-600 font-bold text-lg">⚠️</span>
136
+ <div className="flex-1">
137
+ <p className="text-sm font-semibold text-amber-900">
138
+ Invalid Component: {componentName}
139
+ </p>
140
+ <p className="text-xs text-amber-700 mt-1">
141
+ This component requires React Context and cannot be rendered
142
+ directly in schemas.
143
+ </p>
144
+ </div>
145
+ </div>
146
+ <div className="bg-white/60 p-3 rounded border border-amber-200">
147
+ <p className="text-xs font-semibold text-gray-700 mb-1.5">
148
+ ✓ Use instead:
149
+ </p>
150
+ <code className="text-xs text-blue-700 bg-blue-50 px-2 py-1 rounded">
151
+ {suggestion}
152
+ </code>
153
+ </div>
154
+ </div>
155
+ );
156
+ };
157
+
158
+ /**
159
+ * Renders an error message for components not found in any registry
160
+ */
161
+ const renderNotFoundError = (
162
+ componentName: string,
163
+ key: string,
164
+ ): React.ReactNode => {
165
+ return (
166
+ <div
167
+ key={key}
168
+ className="p-3 border border-dashed border-red-500/50 text-red-500 text-xs rounded bg-red-50/30 my-2"
169
+ >
170
+ <span className="font-semibold">❌ Unknown Component:</span>{" "}
171
+ {componentName}
172
+ <p className="text-[10px] text-red-400 mt-1">
173
+ Component not found in Atoms, Molecules, or UI Components registry.
174
+ </p>
175
+ </div>
176
+ );
177
+ };
178
+
20
179
  export const PXEngineRenderer: React.FC<PXEngineRendererProps> = ({
21
180
  schema,
22
181
  onAction,
23
182
  }) => {
24
- // Determine if it's a full schema or just a component
25
- const root = "root" in schema ? schema.root : schema;
183
+ if (!schema) return null;
26
184
 
27
- /**
28
- * Render Atomic components (UI Primitives)
29
- */
30
- const renderAtom = (
31
- atom: UIAtom,
32
- renderComponent: (c: UIComponent) => React.ReactNode,
185
+ // Extract root if it's a full UISchema
186
+ const root = (schema as any).root || (schema as UIComponent);
187
+
188
+ const renderRecursive = (
189
+ component: UIComponent | string | any,
190
+ index?: number,
33
191
  ): React.ReactNode => {
34
- const { type, id } = atom;
192
+ // 1. Handle text nodes (string or number)
193
+ if (typeof component === "string" || typeof component === "number") {
194
+ return component;
195
+ }
35
196
 
36
- switch (type) {
37
- case "layout":
38
- return (
39
- <Atoms.LayoutAtom
40
- key={id}
41
- {...(atom as any)}
42
- renderComponent={renderComponent}
43
- />
44
- );
45
- case "card":
46
- return (
47
- <Atoms.CardAtom
48
- key={id}
49
- {...(atom as any)}
50
- renderComponent={renderComponent}
51
- />
52
- );
53
- case "text":
54
- return <Atoms.TextAtom key={id} {...(atom as any)} />;
55
- case "button":
56
- return (
57
- <Atoms.ButtonAtom key={id} {...(atom as any)} onAction={onAction} />
58
- );
59
- case "input":
60
- return <Atoms.InputAtom key={id} {...(atom as any)} />;
61
- case "badge":
62
- return <Atoms.BadgeAtom key={id} {...(atom as any)} />;
63
- case "avatar":
64
- return <Atoms.AvatarAtom key={id} {...(atom as any)} />;
65
- case "progress":
66
- return <Atoms.ProgressAtom key={id} {...(atom as any)} />;
67
- case "skeleton":
68
- return <Atoms.SkeletonAtom key={id} {...(atom as any)} />;
69
- case "alert":
70
- return <Atoms.AlertAtom key={id} {...(atom as any)} />;
71
- case "separator":
72
- return <Atoms.SeparatorAtom key={id} {...(atom as any)} />;
73
- case "table":
74
- return <Atoms.TableAtom key={id} {...(atom as any)} />;
75
- case "tabs":
76
- return (
77
- <Atoms.TabsAtom
78
- key={id}
79
- {...(atom as any)}
80
- renderComponent={renderComponent}
81
- />
82
- );
83
- case "accordion":
84
- return (
85
- <Atoms.AccordionAtom
86
- key={id}
87
- {...(atom as any)}
88
- renderComponent={renderComponent}
89
- />
90
- );
91
- case "scroll-area":
92
- return (
93
- <Atoms.ScrollAreaAtom
94
- key={id}
95
- {...(atom as any)}
96
- renderComponent={renderComponent}
97
- />
98
- );
99
- case "carousel":
100
- return (
101
- <Atoms.CarouselAtom
102
- key={id}
103
- {...(atom as any)}
104
- renderComponent={renderComponent}
105
- />
106
- );
107
- case "aspect-ratio":
108
- return (
109
- <Atoms.AspectRatioAtom
110
- key={id}
111
- {...(atom as any)}
112
- renderComponent={renderComponent}
113
- />
114
- );
115
- case "collapsible":
116
- return (
117
- <Atoms.CollapsibleAtom
118
- key={id}
119
- {...(atom as any)}
120
- renderComponent={renderComponent}
121
- />
122
- );
123
- case "tooltip":
124
- return (
125
- <Atoms.TooltipAtom
126
- key={id}
127
- {...(atom as any)}
128
- renderComponent={renderComponent}
129
- />
130
- );
131
- case "popover":
132
- return (
133
- <Atoms.PopoverAtom
134
- key={id}
135
- {...(atom as any)}
136
- renderComponent={renderComponent}
137
- />
138
- );
139
- case "dialog":
140
- return (
141
- <Atoms.DialogAtom
142
- key={id}
143
- {...(atom as any)}
144
- renderComponent={renderComponent}
145
- />
146
- );
147
- case "sheet":
148
- return (
149
- <Atoms.SheetAtom
150
- key={id}
151
- {...(atom as any)}
152
- renderComponent={renderComponent}
153
- />
154
- );
155
- case "alert-dialog":
156
- return (
157
- <Atoms.AlertDialogAtom
158
- key={id}
159
- {...(atom as any)}
160
- onAction={onAction}
161
- renderComponent={renderComponent}
162
- />
163
- );
164
- case "breadcrumb":
165
- return <Atoms.BreadcrumbAtom key={id} {...(atom as any)} />;
166
- case "spinner":
167
- return <Atoms.SpinnerAtom key={id} {...(atom as any)} />;
168
- case "calendar":
169
- return <Atoms.CalendarAtom key={id} {...(atom as any)} />;
170
- case "pagination":
171
- return <Atoms.PaginationAtom key={id} {...(atom as any)} />;
172
- case "command":
173
- return <Atoms.CommandAtom key={id} {...(atom as any)} />;
174
- default:
175
- return null;
197
+ // 2. Handle already rendered React elements
198
+ if (React.isValidElement(component)) {
199
+ return component;
176
200
  }
177
- };
178
201
 
179
- /**
180
- * Render Molecule components (Domain-specific / Complex)
181
- */
182
- const renderMolecule = (molecule: UIMolecule): React.ReactNode => {
183
- const { type, id } = molecule;
202
+ if (!component || typeof component !== "object") return null;
184
203
 
185
- switch (type) {
186
- case "campaign-seed":
187
- return (
188
- <Molecules.CampaignSeedCard
189
- key={id}
190
- {...(molecule as any)}
191
- onProceed={() =>
192
- onAction?.((molecule as any).proceedAction || "proceed")
193
- }
194
- />
195
- );
196
- case "search-spec":
197
- return (
198
- <Molecules.SearchSpecCard
199
- key={id}
200
- {...(molecule as any)}
201
- onProceed={() =>
202
- onAction?.((molecule as any).proceedAction || "proceed")
203
- }
204
- />
205
- );
206
- case "mcq":
207
- return (
208
- <Molecules.MCQCard
209
- key={id}
210
- {...(molecule as any)}
211
- onProceed={() =>
212
- onAction?.((molecule as any).proceedAction || "proceed")
213
- }
214
- />
204
+ const { type, name, props = {}, children = [], id } = component;
205
+
206
+ // Determine the component name to search for
207
+ const componentName = name || type;
208
+ if (!componentName || typeof componentName !== "string") return null;
209
+
210
+ // Generate a unique key from id, index, or random
211
+ const uniqueKey =
212
+ id ||
213
+ `${componentName}-${index || Math.random().toString(36).substr(2, 9)}`;
214
+
215
+ // Normalize name to PascalCase
216
+ const normalizedName =
217
+ componentName.charAt(0).toUpperCase() + componentName.slice(1);
218
+ const atomName = normalizedName.endsWith("Atom")
219
+ ? normalizedName
220
+ : `${normalizedName}Atom`;
221
+
222
+ // 3. Resolve Component from registries (PRIORITY ORDER - safest first)
223
+
224
+ // Priority 1: Atoms (schema-safe, self-contained)
225
+ let TargetComponent =
226
+ (Atoms as any)[atomName] ||
227
+ (Atoms as any)[normalizedName] ||
228
+ (Atoms as any)[componentName];
229
+
230
+ // Priority 2: Molecules (schema-safe, composite)
231
+ if (!TargetComponent) {
232
+ TargetComponent =
233
+ (Molecules as any)[normalizedName] || (Molecules as any)[componentName];
234
+ }
235
+
236
+ // Priority 3: UI Components (ONLY if NOT context-dependent)
237
+ if (!TargetComponent && !CONTEXT_DEPENDENT_COMPONENTS.has(normalizedName)) {
238
+ TargetComponent =
239
+ (UIComponents as any)[normalizedName] ||
240
+ (UIComponents as any)[componentName];
241
+ }
242
+
243
+ // 4. Handle component not found or context-dependent
244
+ if (!TargetComponent) {
245
+ if (CONTEXT_DEPENDENT_COMPONENTS.has(normalizedName)) {
246
+ // Show helpful error for context-dependent components
247
+ if (process.env.NODE_ENV === "development") {
248
+ console.error(
249
+ `[PXEngineRenderer] Cannot render context-dependent component: ${componentName}. ` +
250
+ `Use ${COMPONENT_SUGGESTIONS[normalizedName] || `${componentName}Atom`} instead.`,
251
+ );
252
+ }
253
+ return renderContextDependentError(
254
+ componentName,
255
+ normalizedName,
256
+ uniqueKey,
215
257
  );
216
- case "action-button":
217
- return (
218
- <Molecules.ActionButton
219
- key={id}
220
- {...(molecule as any)}
221
- onProceed={() => onAction?.((molecule as any).action || "proceed")}
222
- />
258
+ } else {
259
+ // Show generic "not found" error
260
+ console.warn(
261
+ `[PXEngineRenderer] Component not found: ${componentName}`,
223
262
  );
224
- default:
225
- return null;
263
+ return renderNotFoundError(componentName, uniqueKey);
264
+ }
226
265
  }
227
- };
228
-
229
- const renderComponent = (component: UIComponent): React.ReactNode => {
230
- const { type } = component;
231
266
 
232
- // Route based on defined Atom types
233
- const isAtom = [
234
- "layout",
235
- "card",
236
- "text",
237
- "button",
238
- "input",
239
- "badge",
240
- "avatar",
241
- "progress",
242
- "skeleton",
243
- "alert",
244
- "separator",
245
- "table",
246
- "tabs",
247
- "accordion",
248
- "scroll-area",
249
- "carousel",
250
- "aspect-ratio",
251
- "collapsible",
252
- "tooltip",
253
- "popover",
254
- "dialog",
255
- "sheet",
256
- "alert-dialog",
257
- "breadcrumb",
258
- "spinner",
259
- "calendar",
260
- "pagination",
261
- "command",
262
- ].includes(type);
267
+ // 4. Determine if component expects 'renderComponent' prop (for Atoms with children management)
268
+ const isAtomWithRenderProp = atomName in Atoms;
263
269
 
264
- if (isAtom) {
265
- return renderAtom(component as UIAtom, renderComponent);
270
+ // 5. Render Component
271
+ if (isAtomWithRenderProp) {
272
+ // Atoms handle their own children via renderComponent
273
+ return (
274
+ <TargetComponent
275
+ key={uniqueKey}
276
+ {...props}
277
+ onAction={onAction}
278
+ renderComponent={renderRecursive}
279
+ children={children}
280
+ />
281
+ );
282
+ } else {
283
+ // Standard shadcn components - pass children as React children
284
+ return (
285
+ <TargetComponent key={uniqueKey} {...props}>
286
+ {Array.isArray(children)
287
+ ? children.map((child, idx) => renderRecursive(child, idx))
288
+ : children}
289
+ </TargetComponent>
290
+ );
266
291
  }
267
-
268
- return renderMolecule(component as UIMolecule);
269
292
  };
270
293
 
271
- return <>{renderComponent(root)}</>;
294
+ return <div className="px-engine-root">{renderRecursive(root)}</div>;
272
295
  };
@@ -1,3 +1,7 @@
1
+ import type { FormInputAtomType } from "../atoms/FormInputAtom";
2
+ import type { FormSelectAtomType } from "../atoms/FormSelectAtom";
3
+ import type { FormTextareaAtomType } from "../atoms/FormTextareaAtom";
4
+
1
5
  export type LayoutDirection = "vertical" | "horizontal" | "grid";
2
6
  export type GapSize = "none" | "sm" | "md" | "lg" | "xl";
3
7
  export type TextVariant = "h1" | "h2" | "h3" | "h4" | "p" | "small" | "muted" | "label";
@@ -291,4 +295,7 @@ export type UIAtom =
291
295
  | BreadcrumbAtomType
292
296
  | CalendarAtomType
293
297
  | PaginationAtomType
294
- | CommandAtomType;
298
+ | CommandAtomType
299
+ | FormInputAtomType
300
+ | FormSelectAtomType
301
+ | FormTextareaAtomType;