diff --git a/src/components/admin/ColumnManagerTab.tsx b/src/components/admin/ColumnManagerTab.tsx
index 34612d1..bc2014a 100644
--- a/src/components/admin/ColumnManagerTab.tsx
+++ b/src/components/admin/ColumnManagerTab.tsx
@@ -2,7 +2,7 @@
import { useEffect, useState } from 'react';
import { getComponentTree, getDataTypes, getFeatureById } from '@/utils/featureConfig';
-import ComponentTreeRenderer from '@/utils/ComponentTreeRenderer';
+import ComponentTreeRenderer from '@/utils/componentTreeRenderer';
import ColumnDialog from './ColumnDialog';
type ColumnManagerTabProps = {
diff --git a/src/components/admin/TableManagerTab.tsx b/src/components/admin/TableManagerTab.tsx
index 2266c83..3be86e6 100644
--- a/src/components/admin/TableManagerTab.tsx
+++ b/src/components/admin/TableManagerTab.tsx
@@ -2,7 +2,7 @@
import { useState } from 'react';
import { getComponentTree, getDataTypes, getFeatureById } from '@/utils/featureConfig';
-import ComponentTreeRenderer from '@/utils/ComponentTreeRenderer';
+import ComponentTreeRenderer from '@/utils/componentTreeRenderer';
import CreateTableDialog from './CreateTableDialog';
import DropTableDialog from './DropTableDialog';
diff --git a/src/components/atoms/index.tsx b/src/components/atoms/index.tsx
new file mode 100644
index 0000000..3d690f6
--- /dev/null
+++ b/src/components/atoms/index.tsx
@@ -0,0 +1,253 @@
+/**
+ * Atomic Components Library
+ * Minimal, reusable components with no business logic
+ */
+
+import type { AlertProps, ButtonProps, CardProps, CheckboxProps, ChipProps, CircularProgressProps, IconButtonProps, PaperProps, SelectProps, TextFieldProps, TypographyProps } from '@mui/material';
+import {
+
+ Box,
+
+ CircularProgress,
+
+ FormControlLabel,
+
+ MenuItem,
+ Alert as MuiAlert,
+ Button as MuiButton,
+ Card as MuiCard,
+ Checkbox as MuiCheckbox,
+ Chip as MuiChip,
+ IconButton as MuiIconButton,
+ Paper as MuiPaper,
+ Select as MuiSelect,
+ TextField as MuiTextField,
+ Typography as MuiTypography,
+
+} from '@mui/material';
+import React from 'react';
+
+/**
+ * Atomic Button - Pure presentation, no logic
+ */
+export function Button(props: ButtonProps) {
+ return ;
+}
+
+/**
+ * Atomic TextField - Pure presentation, no logic
+ */
+export function TextField(props: TextFieldProps) {
+ return ;
+}
+
+/**
+ * Atomic Typography - Pure presentation, no logic
+ */
+export function Typography(props: TypographyProps) {
+ return ;
+}
+
+/**
+ * Atomic Select - Pure presentation, no logic
+ */
+export function Select(props: SelectProps & { options?: Array<{ value: string; label: string }> }) {
+ const { options = [], ...selectProps } = props;
+
+ return (
+
+ {options.map(option => (
+
+ ))}
+
+ );
+}
+
+/**
+ * Atomic Checkbox - Pure presentation, no logic
+ */
+export function Checkbox(props: CheckboxProps & { label?: string }) {
+ const { label, ...checkboxProps } = props;
+
+ if (label) {
+ return (
+ }
+ label={label}
+ />
+ );
+ }
+
+ return ;
+}
+
+/**
+ * Atomic IconButton - Pure presentation, no logic
+ */
+export function IconButton(props: IconButtonProps) {
+ return ;
+}
+
+/**
+ * Atomic Paper - Pure presentation, no logic
+ */
+export function Paper(props: PaperProps) {
+ return ;
+}
+
+/**
+ * Atomic Card - Pure presentation, no logic
+ */
+export function Card(props: CardProps) {
+ return ;
+}
+
+/**
+ * Atomic Chip - Pure presentation, no logic
+ */
+export function Chip(props: ChipProps) {
+ return ;
+}
+
+/**
+ * Atomic Alert - Pure presentation, no logic
+ */
+export function Alert(props: AlertProps) {
+ return ;
+}
+
+/**
+ * Atomic Loading Spinner - Pure presentation, no logic
+ */
+export function LoadingSpinner(props: CircularProgressProps) {
+ return (
+
+
+
+ );
+}
+
+/**
+ * Atomic Container - Simple Box wrapper with common styling
+ */
+export function Container({ children, ...props }: React.PropsWithChildren<{ sx?: any }>) {
+ return (
+
+ {children}
+
+ );
+}
+
+/**
+ * Atomic Stack - Vertical or horizontal flex layout
+ */
+export function Stack({
+ children,
+ direction = 'column',
+ spacing = 2,
+ ...props
+}: React.PropsWithChildren<{
+ direction?: 'row' | 'column';
+ spacing?: number;
+ sx?: any;
+}>) {
+ return (
+
+ {children}
+
+ );
+}
+
+/**
+ * Atomic Empty State - Shows when there's no data
+ */
+export function EmptyState({
+ message = 'No data available',
+ icon,
+ action,
+}: {
+ message?: string;
+ icon?: React.ReactNode;
+ action?: React.ReactNode;
+}) {
+ return (
+
+ {icon && {icon}}
+
+ {message}
+
+ {action && {action}}
+
+ );
+}
+
+/**
+ * Atomic Error Display - Shows error messages
+ */
+export function ErrorDisplay({
+ error,
+ onRetry,
+}: {
+ error: string | null;
+ onRetry?: () => void;
+}) {
+ if (!error) {
+ return null;
+ }
+
+ return (
+
+ Retry
+
+ )
+ : undefined
+ }
+ >
+ {error}
+
+ );
+}
+
+/**
+ * Atomic Success Display - Shows success messages
+ */
+export function SuccessDisplay({
+ message,
+ onClose,
+}: {
+ message: string | null;
+ onClose?: () => void;
+}) {
+ if (!message) {
+ return null;
+ }
+
+ return (
+
+ {message}
+
+ );
+}
diff --git a/src/components/molecules/DynamicForm.tsx b/src/components/molecules/DynamicForm.tsx
new file mode 100644
index 0000000..5dff821
--- /dev/null
+++ b/src/components/molecules/DynamicForm.tsx
@@ -0,0 +1,242 @@
+/**
+ * Dynamic Form Renderer
+ * Renders forms based on form schemas from features.json
+ */
+
+import type { FormField } from '@/utils/featureConfig';
+import {
+ Box,
+ Checkbox,
+ FormControl,
+ FormControlLabel,
+ FormHelperText,
+ Grid,
+ InputLabel,
+ MenuItem,
+ Select,
+ TextField,
+} from '@mui/material';
+import React from 'react';
+
+type DynamicFormProps = {
+ fields: FormField[];
+ values: Record;
+ errors: Record;
+ onChange: (fieldName: string, value: any) => void;
+ onBlur?: (fieldName: string) => void;
+ disabled?: boolean;
+};
+
+/**
+ * Render a single form field based on its type
+ */
+function renderField(
+ field: FormField,
+ value: any,
+ error: string | undefined,
+ onChange: (value: any) => void,
+ onBlur?: () => void,
+ disabled?: boolean,
+) {
+ const commonProps = {
+ fullWidth: true,
+ disabled,
+ error: Boolean(error),
+ helperText: error,
+ };
+
+ switch (field.type) {
+ case 'text':
+ case 'email':
+ return (
+ onChange(e.target.value)}
+ onBlur={onBlur}
+ required={field.required}
+ inputProps={{
+ minLength: field.minLength,
+ maxLength: field.maxLength,
+ }}
+ />
+ );
+
+ case 'number':
+ return (
+ onChange(e.target.value ? Number(e.target.value) : null)}
+ onBlur={onBlur}
+ required={field.required}
+ inputProps={{
+ min: field.min,
+ max: field.max,
+ step: field.step,
+ }}
+ InputProps={{
+ startAdornment: field.prefix,
+ endAdornment: field.suffix,
+ }}
+ />
+ );
+
+ case 'textarea':
+ return (
+ onChange(e.target.value)}
+ onBlur={onBlur}
+ required={field.required}
+ inputProps={{
+ maxLength: field.maxLength,
+ }}
+ />
+ );
+
+ case 'select':
+ return (
+
+ {field.label}
+
+ {error && {error}}
+
+ );
+
+ case 'checkbox':
+ return (
+
+ onChange(e.target.checked)}
+ disabled={disabled}
+ />
+ )}
+ label={field.label}
+ />
+ {error && {error}}
+
+ );
+
+ case 'date':
+ return (
+ onChange(e.target.value)}
+ onBlur={onBlur}
+ required={field.required}
+ InputLabelProps={{ shrink: true }}
+ />
+ );
+
+ case 'datetime':
+ return (
+ onChange(e.target.value)}
+ onBlur={onBlur}
+ required={field.required}
+ InputLabelProps={{ shrink: true }}
+ />
+ );
+
+ default:
+ console.warn(`Unknown field type: ${field.type}`);
+ return null;
+ }
+}
+
+/**
+ * Dynamic Form Component
+ * Renders a complete form based on schema from features.json
+ */
+export function DynamicForm({
+ fields,
+ values,
+ errors,
+ onChange,
+ onBlur,
+ disabled,
+}: DynamicFormProps) {
+ return (
+
+ {fields.map((field) => {
+ // Determine grid size based on field requirements
+ const gridSize = field.type === 'textarea' || field.type === 'checkbox' ? 12 : 6;
+
+ return (
+
+ {renderField(
+ field,
+ values[field.name],
+ errors[field.name],
+ value => onChange(field.name, value),
+ onBlur ? () => onBlur(field.name) : undefined,
+ disabled,
+ )}
+
+ );
+ })}
+
+ );
+}
+
+/**
+ * Form Section - Groups related fields with a title
+ */
+export function FormSection({
+ title,
+ description,
+ children,
+}: {
+ title: string;
+ description?: string;
+ children: React.ReactNode;
+}) {
+ return (
+
+
+
+ {title}
+
+ {description && (
+
+ {description}
+
+ )}
+
+ {children}
+
+ );
+}
diff --git a/src/hooks/useFeatureActions.ts b/src/hooks/useFeatureActions.ts
new file mode 100644
index 0000000..05886d8
--- /dev/null
+++ b/src/hooks/useFeatureActions.ts
@@ -0,0 +1,85 @@
+/**
+ * Hook for creating feature actions from features.json configuration
+ */
+
+import { useCallback, useMemo, useState } from 'react';
+import { createResourceActions } from '@/utils/actionWiring';
+
+type ActionState = {
+ loading: boolean;
+ error: string | null;
+ success: string | null;
+};
+
+/**
+ * Hook that creates action handlers based on API endpoints defined in features.json
+ * Provides automatic loading, error, and success state management
+ */
+export function useFeatureActions(resourceName: string) {
+ const [state, setState] = useState({
+ loading: false,
+ error: null,
+ success: null,
+ });
+
+ const clearMessages = useCallback(() => {
+ setState(prev => ({ ...prev, error: null, success: null }));
+ }, []);
+
+ const setLoading = useCallback((loading: boolean) => {
+ setState(prev => ({ ...prev, loading }));
+ }, []);
+
+ const setError = useCallback((error: string | null) => {
+ setState(prev => ({ ...prev, error, loading: false }));
+ }, []);
+
+ const setSuccess = useCallback((success: string | null) => {
+ setState(prev => ({ ...prev, success, loading: false }));
+ }, []);
+
+ // Create action handlers with automatic state management
+ const actions = useMemo(() => {
+ return createResourceActions(resourceName, {
+ onSuccess: (actionName, data) => {
+ setSuccess(data.message || `${actionName} completed successfully`);
+ },
+ onError: (actionName, error) => {
+ setError(error.message || `${actionName} failed`);
+ },
+ });
+ }, [resourceName, setSuccess, setError]);
+
+ // Wrap each action with loading state
+ const wrappedActions = useMemo(() => {
+ const wrapped: Record) => Promise> = {};
+
+ Object.entries(actions).forEach(([actionName, handler]) => {
+ wrapped[actionName] = async (params?: Record) => {
+ setLoading(true);
+ clearMessages();
+ try {
+ const result = await handler(params);
+ return result;
+ } catch (error) {
+ throw error;
+ } finally {
+ setLoading(false);
+ }
+ };
+ });
+
+ return wrapped;
+ }, [actions, setLoading, clearMessages]);
+
+ return {
+ actions: wrappedActions,
+ loading: state.loading,
+ error: state.error,
+ success: state.success,
+ clearMessages,
+ setLoading,
+ setError,
+ setSuccess,
+ };
+}
diff --git a/src/hooks/useFormSchema.ts b/src/hooks/useFormSchema.ts
new file mode 100644
index 0000000..b8c8208
--- /dev/null
+++ b/src/hooks/useFormSchema.ts
@@ -0,0 +1,170 @@
+/**
+ * Hook for managing forms based on form schemas from features.json
+ */
+
+import type { FormField, FormSchema } from '@/utils/featureConfig';
+import { useCallback, useMemo, useState } from 'react';
+import { getFormSchema, getValidationRule } from '@/utils/featureConfig';
+
+type ValidationErrors = Record;
+
+/**
+ * Hook that provides form state management based on schemas from features.json
+ */
+export function useFormSchema(resourceName: string, initialData?: Record) {
+ const schema = getFormSchema(resourceName);
+
+ if (!schema) {
+ console.warn(`No form schema found for resource: ${resourceName}`);
+ }
+
+ const [values, setValues] = useState>(() =>
+ initialData || getDefaultValues(schema),
+ );
+ const [errors, setErrors] = useState({});
+ const [touched, setTouched] = useState>({});
+
+ // Get default values from schema
+ function getDefaultValues(formSchema?: FormSchema): Record {
+ if (!formSchema) {
+ return {};
+ }
+
+ const defaults: Record = {};
+ formSchema.fields.forEach((field) => {
+ if (field.defaultValue !== undefined) {
+ defaults[field.name] = field.defaultValue;
+ }
+ });
+ return defaults;
+ }
+
+ // Validate a single field
+ const validateField = useCallback((field: FormField, value: any): string | null => {
+ // Check required
+ if (field.required && (value === undefined || value === null || value === '')) {
+ return `${field.label} is required`;
+ }
+
+ // Check type-specific validations
+ if (value !== undefined && value !== null && value !== '') {
+ // Min/max length for text fields
+ if (field.type === 'text' || field.type === 'textarea') {
+ const strValue = String(value);
+ if (field.minLength && strValue.length < field.minLength) {
+ return `${field.label} must be at least ${field.minLength} characters`;
+ }
+ if (field.maxLength && strValue.length > field.maxLength) {
+ return `${field.label} must be at most ${field.maxLength} characters`;
+ }
+ }
+
+ // Min/max for number fields
+ if (field.type === 'number') {
+ const numValue = Number(value);
+ if (Number.isNaN(numValue)) {
+ return `${field.label} must be a valid number`;
+ }
+ if (field.min !== undefined && numValue < field.min) {
+ return `${field.label} must be at least ${field.min}`;
+ }
+ if (field.max !== undefined && numValue > field.max) {
+ return `${field.label} must be at most ${field.max}`;
+ }
+ }
+
+ // Custom validation rules from features.json
+ if (field.validation) {
+ const rule = getValidationRule(field.validation);
+ if (rule) {
+ const regex = new RegExp(rule.pattern);
+ if (!regex.test(String(value))) {
+ return rule.message;
+ }
+ }
+ }
+ }
+
+ return null;
+ }, []);
+
+ // Validate all fields
+ const validateForm = useCallback((): boolean => {
+ if (!schema) {
+ return true;
+ }
+
+ const newErrors: ValidationErrors = {};
+ let isValid = true;
+
+ schema.fields.forEach((field) => {
+ const error = validateField(field, values[field.name]);
+ if (error) {
+ newErrors[field.name] = error;
+ isValid = false;
+ }
+ });
+
+ setErrors(newErrors);
+ return isValid;
+ }, [schema, values, validateField]);
+
+ // Handle field change
+ const handleChange = useCallback((fieldName: string, value: any) => {
+ setValues(prev => ({ ...prev, [fieldName]: value }));
+
+ // Clear error for this field
+ setErrors((prev) => {
+ const newErrors = { ...prev };
+ delete newErrors[fieldName];
+ return newErrors;
+ });
+ }, []);
+
+ // Handle field blur
+ const handleBlur = useCallback((fieldName: string) => {
+ setTouched(prev => ({ ...prev, [fieldName]: true }));
+
+ // Validate this field on blur
+ if (schema) {
+ const field = schema.fields.find(f => f.name === fieldName);
+ if (field) {
+ const error = validateField(field, values[fieldName]);
+ if (error) {
+ setErrors(prev => ({ ...prev, [fieldName]: error }));
+ }
+ }
+ }
+ }, [schema, values, validateField]);
+
+ // Reset form
+ const reset = useCallback((newData?: Record) => {
+ setValues(newData || getDefaultValues(schema));
+ setErrors({});
+ setTouched({});
+ }, [schema]);
+
+ // Check if form is valid
+ const isValid = useMemo(() => {
+ return Object.keys(errors).length === 0;
+ }, [errors]);
+
+ // Check if form has been modified
+ const isDirty = useMemo(() => {
+ const defaults = getDefaultValues(schema);
+ return Object.keys(values).some(key => values[key] !== defaults[key]);
+ }, [values, schema]);
+
+ return {
+ schema,
+ values,
+ errors,
+ touched,
+ isValid,
+ isDirty,
+ handleChange,
+ handleBlur,
+ validateForm,
+ reset,
+ };
+}
diff --git a/src/utils/ComponentTreeRenderer.tsx b/src/utils/ComponentTreeRenderer.tsx
deleted file mode 100644
index f072376..0000000
--- a/src/utils/ComponentTreeRenderer.tsx
+++ /dev/null
@@ -1,417 +0,0 @@
-'use client';
-
-import React from 'react';
-import {
- Box,
- Button,
- Typography,
- Paper,
- TextField,
- Select,
- MenuItem,
- Checkbox,
- FormControlLabel,
- IconButton,
- List,
- ListItem,
- ListItemIcon,
- ListItemText,
- Table,
- TableBody,
- TableCell,
- TableContainer,
- TableHead,
- TableRow,
- Dialog,
- DialogTitle,
- DialogContent,
- DialogActions,
- Chip,
- Tooltip,
- FormControl,
- InputLabel,
-} from '@mui/material';
-import AddIcon from '@mui/icons-material/Add';
-import DeleteIcon from '@mui/icons-material/Delete';
-import EditIcon from '@mui/icons-material/Edit';
-import TableChartIcon from '@mui/icons-material/TableChart';
-import SpeedIcon from '@mui/icons-material/Speed';
-import { ComponentNode } from './featureConfig';
-
-// Map of component names to actual components
-const componentMap: Record> = {
- Box,
- Button,
- Typography,
- Paper,
- TextField,
- Select,
- MenuItem,
- Checkbox,
- FormControlLabel,
- IconButton,
- List,
- ListItem,
- ListItemIcon,
- ListItemText,
- Table,
- TableBody,
- TableCell,
- TableContainer,
- TableHead,
- TableRow,
- Dialog,
- DialogTitle,
- DialogContent,
- DialogActions,
- Chip,
- Tooltip,
- FormControl,
- InputLabel,
-};
-
-// Map of icon names to icon components
-const iconMap: Record> = {
- Add: AddIcon,
- Delete: DeleteIcon,
- Edit: EditIcon,
- TableChart: TableChartIcon,
- Speed: SpeedIcon,
-};
-
-type ComponentTreeRendererProps = {
- tree: ComponentNode;
- data?: Record;
- handlers?: Record void>;
-};
-
-/**
- * Safe operator functions for condition evaluation
- */
-const SAFE_OPERATORS: Record boolean> = {
- '===': (a, b) => a === b,
- '!==': (a, b) => a !== b,
- '==': (a, b) => a == b,
- '!=': (a, b) => a != b,
- '>': (a, b) => a > b,
- '<': (a, b) => a < b,
- '>=': (a, b) => a >= b,
- '<=': (a, b) => a <= b,
- '&&': (a, b) => a && b,
- '||': (a, b) => a || b,
-};
-
-/**
- * Safely get nested property value from object using dot notation
- * Only allows alphanumeric and dots - no function calls or arbitrary code
- */
-function safeGetProperty(obj: Record, path: string): any {
- // Validate path contains only safe characters
- if (!/^[a-zA-Z_$][a-zA-Z0-9_$.]*$/.test(path)) {
- console.warn('Invalid property path:', path);
- return undefined;
- }
-
- const parts = path.split('.');
- let current = obj;
-
- for (const part of parts) {
- if (current == null || typeof current !== 'object') {
- return undefined;
- }
- current = current[part];
- }
-
- return current;
-}
-
-/**
- * Evaluate a condition string with the provided data context
- * Uses safe property access and whitelisted operators - NO new Function()
- */
-function evaluateCondition(condition: string, data: Record): boolean {
- try {
- // Simple boolean property check: "isAdmin"
- if (/^[a-zA-Z_$][a-zA-Z0-9_$.]*$/.test(condition.trim())) {
- const value = safeGetProperty(data, condition.trim());
- return Boolean(value);
- }
-
- // Find operator in condition
- let operator: string | null = null;
- let operatorIndex = -1;
-
- // Check for operators in order of precedence
- for (const op of ['===', '!==', '==', '!=', '>=', '<=', '>', '<', '&&', '||']) {
- const idx = condition.indexOf(op);
- if (idx !== -1) {
- operator = op;
- operatorIndex = idx;
- break;
- }
- }
-
- if (!operator || operatorIndex === -1) {
- console.warn('No valid operator found in condition:', condition);
- return false;
- }
-
- // Extract left and right operands
- const left = condition.slice(0, operatorIndex).trim();
- const right = condition.slice(operatorIndex + operator.length).trim();
-
- // Evaluate operands
- let leftValue: any;
- let rightValue: any;
-
- // Left operand - check if it's a property or literal
- if (/^[a-zA-Z_$][a-zA-Z0-9_$.]*$/.test(left)) {
- leftValue = safeGetProperty(data, left);
- } else if (left === 'true') {
- leftValue = true;
- } else if (left === 'false') {
- leftValue = false;
- } else if (left === 'null') {
- leftValue = null;
- } else if (!isNaN(Number(left))) {
- leftValue = Number(left);
- } else if ((left.startsWith('"') && left.endsWith('"')) || (left.startsWith("'") && left.endsWith("'"))) {
- leftValue = left.slice(1, -1);
- } else {
- console.warn('Invalid left operand:', left);
- return false;
- }
-
- // Right operand - same logic
- if (/^[a-zA-Z_$][a-zA-Z0-9_$.]*$/.test(right)) {
- rightValue = safeGetProperty(data, right);
- } else if (right === 'true') {
- rightValue = true;
- } else if (right === 'false') {
- rightValue = false;
- } else if (right === 'null') {
- rightValue = null;
- } else if (!isNaN(Number(right))) {
- rightValue = Number(right);
- } else if ((right.startsWith('"') && right.endsWith('"')) || (right.startsWith("'") && right.endsWith("'"))) {
- rightValue = right.slice(1, -1);
- } else {
- console.warn('Invalid right operand:', right);
- return false;
- }
-
- // Apply operator
- const operatorFunc = SAFE_OPERATORS[operator];
- if (!operatorFunc) {
- console.warn('Unknown operator:', operator);
- return false;
- }
-
- return operatorFunc(leftValue, rightValue);
- } catch (error) {
- console.error('Error evaluating condition:', condition, error);
- return false;
- }
-}
-
-/**
- * Interpolate template strings like {{variable}} with actual values from data
- * Uses safe property access - NO new Function() or eval()
- */
-function interpolateValue(value: any, data: Record): any {
- if (typeof value !== 'string') {
- return value;
- }
-
- // Check if it's a template string
- const templateMatch = value.match(/^\{\{(.+)\}\}$/);
- if (templateMatch && templateMatch[1]) {
- const expression = templateMatch[1].trim();
-
- // Support Math operations for numeric expressions
- if (/^Math\.[a-zA-Z]+\(/.test(expression)) {
- // Allow safe Math operations
- const mathOp = expression.match(/^Math\.([a-zA-Z]+)\((.+)\)$/);
- if (mathOp) {
- const [, operation, argsStr] = mathOp;
- const safeOps = ['abs', 'ceil', 'floor', 'round', 'max', 'min'];
-
- if (operation && argsStr && safeOps.includes(operation)) {
- try {
- // Parse arguments safely
- const args = argsStr.split(',').map(arg => {
- const trimmed = arg.trim();
- const propValue = safeGetProperty(data, trimmed);
- return propValue !== undefined ? propValue : Number(trimmed);
- });
-
- return (Math as any)[operation](...args);
- } catch (error) {
- console.error('Error evaluating Math operation:', expression, error);
- return value;
- }
- }
- }
- }
-
- // Ternary operator: condition ? valueIfTrue : valueIfFalse
- const ternaryMatch = expression.match(/^(.+?)\s*\?\s*(.+?)\s*:\s*(.+)$/);
- if (ternaryMatch) {
- const [, condition, trueValue, falseValue] = ternaryMatch;
- if (condition && trueValue !== undefined && falseValue !== undefined) {
- const conditionResult = evaluateCondition(condition.trim(), data);
- const targetValue = conditionResult ? trueValue.trim() : falseValue.trim();
-
- // Recursively interpolate the result
- return interpolateValue(`{{${targetValue}}}`, data);
- }
- }
-
- // Simple property access
- return safeGetProperty(data, expression);
- }
-
- // Replace inline templates
- return value.replace(/\{\{(.+?)\}\}/g, (_, expression) => {
- const trimmed = expression.trim();
- const result = safeGetProperty(data, trimmed);
- return result !== undefined ? String(result) : '';
- });
-}
-
-/**
- * Interpolate all props in an object
- */
-function interpolateProps(
- props: Record | undefined,
- data: Record,
- handlers: Record void>
-): Record {
- if (!props) return {};
-
- const interpolated: Record = {};
-
- Object.entries(props).forEach(([key, value]) => {
- if (typeof value === 'string' && handlers[value]) {
- // If the value is a handler function name, use the handler
- interpolated[key] = handlers[value];
- } else if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
- // Recursively interpolate nested objects
- interpolated[key] = interpolateProps(value, data, handlers);
- } else {
- // Interpolate the value
- interpolated[key] = interpolateValue(value, data);
- }
- });
-
- return interpolated;
-}
-
-/**
- * Get the singular form of a plural word (simple implementation)
- */
-function getSingular(plural: string): string {
- if (plural.endsWith('ies')) {
- return plural.slice(0, -3) + 'y';
- }
- if (plural.endsWith('es')) {
- return plural.slice(0, -2);
- }
- if (plural.endsWith('s')) {
- return plural.slice(0, -1);
- }
- return plural;
-}
-
-/**
- * Render a single component node
- */
-function renderNode(
- node: ComponentNode,
- data: Record,
- handlers: Record void>,
- key: string | number
-): React.ReactNode {
- // Evaluate condition
- if (node.condition && !evaluateCondition(node.condition, data)) {
- return null;
- }
-
- // Handle forEach loops
- if (node.forEach) {
- const items = data[node.forEach];
- if (!Array.isArray(items)) {
- console.warn(`forEach data "${node.forEach}" is not an array`);
- return null;
- }
-
- const singularName = getSingular(node.forEach);
- return items.map((item, index) => {
- const itemData = { ...data, [singularName]: item, index };
- // Remove forEach from node to avoid infinite loop
- const nodeWithoutForEach = { ...node, forEach: undefined };
- return renderNode(nodeWithoutForEach, itemData, handlers, `${key}-${index}`);
- });
- }
-
- // Get the component
- const componentName = interpolateValue(node.component, data);
- const Component = componentMap[componentName];
-
- if (!Component) {
- console.warn(`Component "${componentName}" not found in componentMap`);
- return null;
- }
-
- // Interpolate props
- const props = interpolateProps(node.props, data, handlers);
-
- // Handle special props
- if (props.startIcon && typeof props.startIcon === 'string') {
- const IconComponent = iconMap[props.startIcon];
- if (IconComponent) {
- props.startIcon = ;
- }
- }
-
- if (props.endIcon && typeof props.endIcon === 'string') {
- const IconComponent = iconMap[props.endIcon];
- if (IconComponent) {
- props.endIcon = ;
- }
- }
-
- // Handle 'text' prop for Typography and Button
- let textContent = null;
- if (props.text !== undefined) {
- textContent = props.text;
- delete props.text;
- }
-
- // Render children
- const children = node.children?.map((child, index) =>
- renderNode(child, data, handlers, `${key}-child-${index}`)
- );
-
- return (
-
- {textContent}
- {children}
-
- );
-}
-
-/**
- * ComponentTreeRenderer - Renders a component tree from JSON configuration
- */
-export default function ComponentTreeRenderer({
- tree,
- data = {},
- handlers = {},
-}: ComponentTreeRendererProps) {
- if (!tree) {
- return null;
- }
-
- return <>{renderNode(tree, data, handlers, 'root')}>;
-}
diff --git a/src/utils/actionWiring.ts b/src/utils/actionWiring.ts
new file mode 100644
index 0000000..76defb7
--- /dev/null
+++ b/src/utils/actionWiring.ts
@@ -0,0 +1,120 @@
+/**
+ * Action Wiring Utilities
+ * Create action handlers dynamically from features.json configuration
+ */
+
+import type { ApiEndpoint } from './featureConfig';
+import { getApiEndpoints } from './featureConfig';
+
+/**
+ * Generic action handler factory
+ * Creates a function that calls an API endpoint with the specified parameters
+ */
+export function createActionHandler(
+ endpoint: ApiEndpoint,
+ onSuccess?: (data: any) => void,
+ onError?: (error: Error) => void,
+) {
+ return async (params?: Record) => {
+ try {
+ const url = interpolatePath(endpoint.path, params || {});
+
+ const options: RequestInit = {
+ method: endpoint.method,
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ };
+
+ // Add body for POST, PUT, PATCH methods
+ if (['POST', 'PUT', 'PATCH'].includes(endpoint.method) && params) {
+ options.body = JSON.stringify(params);
+ }
+
+ const response = await fetch(url, options);
+ const data = await response.json();
+
+ if (!response.ok) {
+ throw new Error(data.error || `Request failed with status ${response.status}`);
+ }
+
+ onSuccess?.(data);
+ return data;
+ } catch (error) {
+ const err = error instanceof Error ? error : new Error('Unknown error');
+ onError?.(err);
+ throw err;
+ }
+ };
+}
+
+/**
+ * Interpolate path parameters like /api/users/:id
+ */
+function interpolatePath(path: string, params: Record): string {
+ return path.replace(/:([a-z_$][\w$]*)/gi, (_, paramName) => {
+ const value = params[paramName];
+ if (value === undefined) {
+ console.warn(`Missing path parameter: ${paramName}`);
+ return `:${paramName}`;
+ }
+ return String(value);
+ });
+}
+
+/**
+ * Create action handlers for a resource from features.json
+ */
+export function createResourceActions(
+ resourceName: string,
+ callbacks?: {
+ onSuccess?: (action: string, data: any) => void;
+ onError?: (action: string, error: Error) => void;
+ },
+): Record) => Promise> {
+ const endpoints = getApiEndpoints(resourceName);
+
+ if (!endpoints) {
+ console.warn(`No API endpoints found for resource: ${resourceName}`);
+ return {};
+ }
+
+ const actions: Record) => Promise> = {};
+
+ Object.entries(endpoints).forEach(([actionName, endpoint]) => {
+ actions[actionName] = createActionHandler(
+ endpoint,
+ data => callbacks?.onSuccess?.(actionName, data),
+ error => callbacks?.onError?.(actionName, error),
+ );
+ });
+
+ return actions;
+}
+
+/**
+ * Batch execute multiple actions
+ */
+export async function batchExecuteActions(
+ actions: Array<{ handler: () => Promise; name: string }>,
+): Promise<{ successes: any[]; errors: Array<{ name: string; error: Error }> }> {
+ const results = await Promise.allSettled(
+ actions.map(({ handler }) => handler()),
+ );
+
+ const successes: any[] = [];
+ const errors: Array<{ name: string; error: Error }> = [];
+
+ results.forEach((result, index) => {
+ if (result.status === 'fulfilled') {
+ successes.push(result.value);
+ } else {
+ errors.push({
+ name: actions[index]?.name || `Action ${index}`,
+ error: result.reason instanceof Error ? result.reason : new Error(String(result.reason)),
+ });
+ }
+ });
+
+ return { successes, errors };
+}
diff --git a/src/utils/componentTreeRenderer.tsx b/src/utils/componentTreeRenderer.tsx
index c2cacd4..b0d27dd 100644
--- a/src/utils/componentTreeRenderer.tsx
+++ b/src/utils/componentTreeRenderer.tsx
@@ -1,8 +1,11 @@
/**
- * Component Tree Renderer
+ * Unified Component Tree Renderer
* Dynamically renders React component trees from JSON configuration
+ * Merges both previous implementations for maximum compatibility
*/
+'use client';
+
import React from 'react';
import type { ComponentNode } from './featureConfig';
@@ -22,6 +25,7 @@ import {
DialogTitle,
Drawer,
FormControl,
+ FormControlLabel,
Grid,
IconButton,
InputLabel,
@@ -43,6 +47,7 @@ import {
Tabs,
TextField,
Toolbar,
+ Tooltip,
Typography,
Chip,
Accordion,
@@ -70,6 +75,7 @@ const componentRegistry: Record> = {
DialogTitle,
Drawer,
FormControl,
+ FormControlLabel,
Grid,
IconButton,
InputLabel,
@@ -91,6 +97,7 @@ const componentRegistry: Record> = {
Tabs,
TextField,
Toolbar,
+ Tooltip,
Typography,
Chip,
Accordion,
@@ -102,6 +109,7 @@ const componentRegistry: Record> = {
type RenderContext = {
data?: Record;
actions?: Record any>;
+ handlers?: Record any>; // Alias for backward compatibility
state?: Record;
};
@@ -171,10 +179,10 @@ function processProps(props: Record = {}, context: RenderContext):
for (const [key, value] of Object.entries(props)) {
// Handle special props
- if (key === 'onClick' || key === 'onChange' || key === 'onClose') {
- // Map to action functions
+ if (key === 'onClick' || key === 'onChange' || key === 'onClose' || key === 'onBlur' || key === 'onFocus') {
+ // Map to action functions - check both actions and handlers for backward compatibility
if (typeof value === 'string') {
- processed[key] = context.actions?.[value];
+ processed[key] = context.actions?.[value] || context.handlers?.[value];
} else {
processed[key] = value;
}
@@ -290,7 +298,7 @@ export function renderComponentNode(
}
/**
- * Main component tree renderer
+ * Main component tree renderer (named export)
*/
export function ComponentTreeRenderer({
tree,
@@ -302,6 +310,27 @@ export function ComponentTreeRenderer({
return renderComponentNode(tree, context, 'root');
}
+/**
+ * Default export for backward compatibility with old imports
+ */
+export default function ComponentTreeRendererDefault({
+ tree,
+ data = {},
+ handlers = {},
+}: {
+ tree: ComponentNode;
+ data?: Record;
+ handlers?: Record void>;
+}): React.ReactElement | null {
+ const context: RenderContext = {
+ data,
+ handlers,
+ actions: handlers, // Map handlers to actions for compatibility
+ };
+
+ return renderComponentNode(tree, context, 'root');
+}
+
/**
* Hook to use component tree with state management
*/