pxengine 0.1.4 → 0.1.6

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.
@@ -3,17 +3,179 @@ import { UIComponent, UISchema } from "../types/schema";
3
3
  import * as Atoms from "../atoms";
4
4
  import * as Molecules from "../molecules/index";
5
5
 
6
+ // Import all shadcn UI components
7
+ import * as UIComponents from "../components/ui/index";
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
+
6
105
  /**
7
106
  * PXEngineRenderer
8
107
  *
9
108
  * Handles both the full schema { version, root } and individual components.
10
- * Dynamically resolves components from the Atoms/Molecules registry.
109
+ * Dynamically resolves components from Atoms/Molecules/UI Components registry.
110
+ * Prevents rendering of context-dependent components to avoid React errors.
11
111
  */
12
112
  interface PXEngineRendererProps {
13
113
  schema: UISchema | UIComponent;
14
114
  onAction?: (action: string, payload?: any) => void;
15
115
  }
16
116
 
117
+ /**
118
+ * Renders an error message for context-dependent components
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
+
17
179
  export const PXEngineRenderer: React.FC<PXEngineRendererProps> = ({
18
180
  schema,
19
181
  onAction,
@@ -25,69 +187,108 @@ export const PXEngineRenderer: React.FC<PXEngineRendererProps> = ({
25
187
 
26
188
  const renderRecursive = (
27
189
  component: UIComponent | string | any,
190
+ index?: number,
28
191
  ): React.ReactNode => {
29
192
  // 1. Handle text nodes (string or number)
30
193
  if (typeof component === "string" || typeof component === "number") {
31
194
  return component;
32
195
  }
33
196
 
34
- if (!component) return null;
197
+ // 2. Handle already rendered React elements
198
+ if (React.isValidElement(component)) {
199
+ return component;
200
+ }
201
+
202
+ if (!component || typeof component !== "object") return null;
35
203
 
36
204
  const { type, name, props = {}, children = [], id } = component;
37
205
 
38
206
  // Determine the component name to search for
39
207
  const componentName = name || type;
40
- if (!componentName) return null;
208
+ if (!componentName || typeof componentName !== "string") return null;
41
209
 
42
- // Normalize name to PascalCase and check for "Atom" suffix
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
43
216
  const normalizedName =
44
217
  componentName.charAt(0).toUpperCase() + componentName.slice(1);
45
218
  const atomName = normalizedName.endsWith("Atom")
46
219
  ? normalizedName
47
220
  : `${normalizedName}Atom`;
48
221
 
49
- // 2. Resolve Component from library registry
50
- const TargetComponent =
222
+ // 3. Resolve Component from registries (PRIORITY ORDER - safest first)
223
+
224
+ // Priority 1: Atoms (schema-safe, self-contained)
225
+ let TargetComponent =
51
226
  (Atoms as any)[atomName] ||
52
227
  (Atoms as any)[normalizedName] ||
53
- (Atoms as any)[componentName] ||
54
- (Molecules as any)[normalizedName] ||
55
- (Molecules as any)[componentName];
228
+ (Atoms as any)[componentName];
56
229
 
230
+ // Priority 2: Molecules (schema-safe, composite)
57
231
  if (!TargetComponent) {
58
- console.warn(`[PXEngineRenderer] Component not found: ${componentName}`);
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,
257
+ );
258
+ } else {
259
+ // Show generic "not found" error
260
+ console.warn(
261
+ `[PXEngineRenderer] Component not found: ${componentName}`,
262
+ );
263
+ return renderNotFoundError(componentName, uniqueKey);
264
+ }
265
+ }
266
+
267
+ // 4. Determine if component expects 'renderComponent' prop (for Atoms with children management)
268
+ const isAtomWithRenderProp = atomName in Atoms;
269
+
270
+ // 5. Render Component
271
+ if (isAtomWithRenderProp) {
272
+ // Atoms handle their own children via renderComponent
59
273
  return (
60
- <div
61
- key={id}
62
- className="p-2 border border-dashed border-red-500/50 text-red-500 text-[10px] rounded"
63
- >
64
- Unknown: {componentName}
65
- </div>
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>
66
290
  );
67
291
  }
68
-
69
- // 3. Render Component
70
- // We pass 'renderComponent' for components that handle their own child rendering (like Layout/Card)
71
- // We also pass children as props for standard React children behavior
72
- return (
73
- <TargetComponent
74
- key={id || Math.random().toString(36).substr(2, 9)}
75
- {...props}
76
- onAction={onAction}
77
- renderComponent={renderRecursive}
78
- children={Array.isArray(children) ? children : undefined}
79
- >
80
- {Array.isArray(children)
81
- ? children.map((child, idx) => (
82
- <React.Fragment key={idx}>
83
- {renderRecursive(child)}
84
- </React.Fragment>
85
- ))
86
- : typeof children === "string"
87
- ? children
88
- : null}
89
- </TargetComponent>
90
- );
91
292
  };
92
293
 
93
294
  return <div className="px-engine-root">{renderRecursive(root)}</div>;
@@ -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;