@stormlmd/form-builder 0.1.0

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 (50) hide show
  1. package/README.md +73 -0
  2. package/eslint.config.js +23 -0
  3. package/index.html +13 -0
  4. package/package.json +60 -0
  5. package/public/vite.svg +1 -0
  6. package/src/App.css +42 -0
  7. package/src/App.tsx +83 -0
  8. package/src/assets/react.svg +1 -0
  9. package/src/components/FieldRegistry.ts +34 -0
  10. package/src/components/FormContainer.tsx +25 -0
  11. package/src/components/FormRenderer.tsx +121 -0
  12. package/src/components/builder/DraggableTool.tsx +66 -0
  13. package/src/components/builder/DroppableCanvas.tsx +51 -0
  14. package/src/components/builder/EditorWrapper.tsx +87 -0
  15. package/src/components/builder/FormBuilder.tsx +313 -0
  16. package/src/components/builder/FormChildrenRenderer.tsx +68 -0
  17. package/src/components/builder/IntegrationSettings.tsx +110 -0
  18. package/src/components/builder/PropertiesModal.tsx +75 -0
  19. package/src/components/builder/PropertiesPanel.tsx +858 -0
  20. package/src/components/builder/SortableNode.tsx +53 -0
  21. package/src/components/builder/Toolbox.tsx +123 -0
  22. package/src/components/fields/CheckboxField.tsx +41 -0
  23. package/src/components/fields/DateField.tsx +56 -0
  24. package/src/components/fields/FileUploadField.tsx +45 -0
  25. package/src/components/fields/LabelField.tsx +20 -0
  26. package/src/components/fields/NumberField.tsx +39 -0
  27. package/src/components/fields/RichTextField.tsx +39 -0
  28. package/src/components/fields/SelectField.tsx +64 -0
  29. package/src/components/fields/TextField.tsx +44 -0
  30. package/src/components/layout/FormCol.tsx +30 -0
  31. package/src/components/layout/FormDivider.tsx +19 -0
  32. package/src/components/layout/FormPaper.tsx +85 -0
  33. package/src/components/layout/FormRepeater.tsx +130 -0
  34. package/src/components/layout/FormRow.tsx +61 -0
  35. package/src/components/layout/FormTab.tsx +33 -0
  36. package/src/components/layout/FormTable.tsx +47 -0
  37. package/src/components/layout/FormTableCell.tsx +47 -0
  38. package/src/components/layout/FormTabs.tsx +77 -0
  39. package/src/components/layout/LayoutPlaceholder.tsx +85 -0
  40. package/src/components/registerComponents.ts +30 -0
  41. package/src/index.css +75 -0
  42. package/src/index.ts +5 -0
  43. package/src/main.tsx +10 -0
  44. package/src/store/FormStore.ts +811 -0
  45. package/src/utils/apiTransformer.ts +206 -0
  46. package/src/utils/idGenerator.ts +3 -0
  47. package/tsconfig.app.json +28 -0
  48. package/tsconfig.json +7 -0
  49. package/tsconfig.node.json +26 -0
  50. package/vite.config.ts +46 -0
@@ -0,0 +1,53 @@
1
+ import React from 'react';
2
+ import { useSortable } from '@dnd-kit/sortable';
3
+ import { CSS } from '@dnd-kit/utilities';
4
+ import { Box, type SxProps, type Theme } from '@mui/material';
5
+
6
+ import { observer } from 'mobx-react-lite';
7
+ import { formStore } from '../../store/FormStore';
8
+
9
+ interface SortableNodeProps {
10
+ id: string;
11
+ children: React.ReactNode;
12
+ sx?: SxProps<Theme>;
13
+ }
14
+
15
+ export const SortableNode: React.FC<SortableNodeProps> = observer(({ id, children, sx }) => {
16
+ const {
17
+ attributes,
18
+ listeners,
19
+ setNodeRef,
20
+ transform,
21
+ transition,
22
+ isDragging,
23
+ isOver
24
+ } = useSortable({ id });
25
+
26
+ const style = {
27
+ transform: CSS.Transform.toString(transform),
28
+ transition,
29
+ };
30
+
31
+ return (
32
+ <Box
33
+ ref={setNodeRef}
34
+ style={style}
35
+ {...attributes}
36
+ {...listeners}
37
+ sx={{
38
+ display: 'flex',
39
+ flexDirection: 'column',
40
+ opacity: isDragging ? 0.5 : 1,
41
+ position: 'relative',
42
+ zIndex: isDragging ? 999 : 'auto',
43
+ boxSizing: 'border-box',
44
+ minHeight: '20px',
45
+ height: '100%', // Allow stretching
46
+ outline: isOver && !isDragging && !formStore.dropIndicator ? '1px dashed #4caf50' : undefined,
47
+ ...sx
48
+ }}
49
+ >
50
+ {children}
51
+ </Box>
52
+ );
53
+ });
@@ -0,0 +1,123 @@
1
+ import React, { useState } from 'react';
2
+ import { Box, List, ListItem, Tabs, Tab } from '@mui/material';
3
+ import { formStore, type NodeType } from '../../store/FormStore';
4
+ import { generateId } from '../../utils/idGenerator';
5
+ import { DraggableTool } from './DraggableTool';
6
+ import { useTranslation } from 'react-i18next';
7
+ import TextFieldsIcon from '@mui/icons-material/TextFields';
8
+ import NumbersIcon from '@mui/icons-material/Numbers';
9
+ import CheckBoxIcon from '@mui/icons-material/CheckBox';
10
+ import ArrowDropDownCircleIcon from '@mui/icons-material/ArrowDropDownCircle';
11
+ import CalendarTodayIcon from '@mui/icons-material/CalendarToday';
12
+ import CloudUploadIcon from '@mui/icons-material/CloudUpload';
13
+ import FormatQuoteIcon from '@mui/icons-material/FormatQuote';
14
+ import LabelIcon from '@mui/icons-material/Label';
15
+ import HorizontalRuleIcon from '@mui/icons-material/HorizontalRule';
16
+ import ViewColumnIcon from '@mui/icons-material/ViewColumn';
17
+ import TabIcon from '@mui/icons-material/Tab';
18
+ import RepeatIcon from '@mui/icons-material/Repeat';
19
+ import TableChartIcon from '@mui/icons-material/TableChart';
20
+ import LayersIcon from '@mui/icons-material/Layers';
21
+
22
+ interface ToolItem {
23
+ type: NodeType;
24
+ label: string;
25
+ icon: React.ReactNode;
26
+ defaultProps?: Record<string, any>;
27
+ }
28
+
29
+ const TOOLS: ToolItem[] = [
30
+ { type: 'text', label: 'Text Field', icon: <TextFieldsIcon />, defaultProps: { label: 'New Text Field', width: 6 } },
31
+ { type: 'number', label: 'Number Field', icon: <NumbersIcon />, defaultProps: { label: 'New Number Field', width: 6 } },
32
+ { type: 'checkbox', label: 'Checkbox', icon: <CheckBoxIcon />, defaultProps: { label: 'New Checkbox', width: 6 } },
33
+ { type: 'select', label: 'Select', icon: <ArrowDropDownCircleIcon />, defaultProps: { label: 'New Select', width: 6, options: [{ label: 'Option 1', value: '1' }] } },
34
+ { type: 'date', label: 'Date Picker', icon: <CalendarTodayIcon />, defaultProps: { label: 'New Date', width: 6 } },
35
+ { type: 'file', label: 'File Upload', icon: <CloudUploadIcon />, defaultProps: { label: 'New File Upload', width: 6 } },
36
+ { type: 'richtext', label: 'Rich Text', icon: <FormatQuoteIcon />, defaultProps: { label: 'New Rich Text', width: 6 } },
37
+ { type: 'label', label: 'Label', icon: <LabelIcon />, defaultProps: { text: 'Static Tex', variant: 'body1', width: 6 } },
38
+ ];
39
+
40
+ const LAYOUT_TOOLS: ToolItem[] = [
41
+ { type: 'divider', label: 'Divider', icon: <HorizontalRuleIcon />, defaultProps: { text: '' } },
42
+ { type: 'col', label: 'Column', icon: <ViewColumnIcon />, defaultProps: { cols: 6 } },
43
+ { type: 'tabs', label: 'Tabs', icon: <TabIcon />, defaultProps: {} },
44
+ { type: 'repeater', label: 'Repeater', icon: <RepeatIcon />, defaultProps: { label: 'Repeater', addLabel: 'addItem' } },
45
+ { type: 'table', label: 'Table', icon: <TableChartIcon />, defaultProps: { rows: 2, cols: 2, width: 12 } },
46
+ { type: 'paper', label: 'Paper', icon: <LayersIcon />, defaultProps: { label: 'Paper Group', padding: 2, variant: 'elevation', elevation: 1 } },
47
+ ];
48
+
49
+ export const Toolbox: React.FC = () => {
50
+ const { t } = useTranslation();
51
+ const [activeTab, setActiveTab] = useState(0);
52
+
53
+ const handleAdd = (tool: ToolItem) => {
54
+ const parentId = formStore.selectedNodeId || 'root';
55
+ const newNode = {
56
+ id: generateId(tool.type),
57
+ type: tool.type,
58
+ props: { ...tool.defaultProps, name: generateId('field'), label: t(`toolbox.${tool.type}`) },
59
+ children: [] as any[]
60
+ };
61
+
62
+ // Special initialization for Table
63
+ if (tool.type === 'table') {
64
+ const rows = tool.defaultProps?.rows || 2;
65
+ const cols = tool.defaultProps?.cols || 2;
66
+ const cells = [];
67
+ for (let i = 0; i < rows * cols; i++) {
68
+ cells.push({
69
+ id: generateId('cell'),
70
+ type: 'cell',
71
+ children: []
72
+ });
73
+ }
74
+ newNode.children = cells;
75
+ }
76
+
77
+ console.log('Toolbox adding node:', newNode);
78
+ formStore.addNode(parentId, newNode as any);
79
+ };
80
+
81
+ return (
82
+ <Box display="flex" flexDirection="column" height="100%">
83
+ <Box borderBottom={1} borderColor="divider">
84
+ <Tabs value={activeTab} onChange={(_, v) => setActiveTab(v)} variant="fullWidth">
85
+ <Tab label={t("toolbox.fields")} sx={{ textTransform: 'none', minHeight: 48 }} />
86
+ <Tab label={t("toolbox.layout")} sx={{ textTransform: 'none', minHeight: 48 }} />
87
+ </Tabs>
88
+ </Box>
89
+ <Box flexGrow={1} overflow="auto">
90
+ {activeTab === 0 && (
91
+ <List sx={{ pt: 1 }}>
92
+ {TOOLS.map((tool) => (
93
+ <ListItem key={tool.type} disablePadding sx={{ px: 2, py: 0.5 }}>
94
+ <DraggableTool
95
+ type={tool.type}
96
+ label={t(`toolbox.${tool.type}`)}
97
+ icon={tool.icon}
98
+ defaultProps={tool.defaultProps}
99
+ onClick={() => handleAdd(tool)}
100
+ />
101
+ </ListItem>
102
+ ))}
103
+ </List>
104
+ )}
105
+ {activeTab === 1 && (
106
+ <List sx={{ pt: 1 }}>
107
+ {LAYOUT_TOOLS.map((tool) => (
108
+ <ListItem key={tool.type} disablePadding sx={{ px: 2, py: 0.5 }}>
109
+ <DraggableTool
110
+ type={tool.type}
111
+ label={t(`toolbox.${tool.type}`)}
112
+ icon={tool.icon}
113
+ defaultProps={tool.defaultProps}
114
+ onClick={() => handleAdd(tool)}
115
+ />
116
+ </ListItem>
117
+ ))}
118
+ </List>
119
+ )}
120
+ </Box>
121
+ </Box>
122
+ );
123
+ };
@@ -0,0 +1,41 @@
1
+ import React from 'react';
2
+ import { Checkbox, FormControlLabel, FormGroup, Box, Typography } from '@mui/material';
3
+ import { observer } from 'mobx-react-lite';
4
+ import { formStore } from '../../store/FormStore';
5
+ import type { FieldProps } from '../FieldRegistry';
6
+
7
+ export const CheckboxField: React.FC<FieldProps> = observer(({ node, path, isEditing }) => {
8
+ const { name, label } = node.props || {};
9
+ const fullPath = path && name ? `${path}.${name}` : name;
10
+ const value = fullPath ? formStore.getValue(fullPath) : false;
11
+
12
+ return (
13
+ <Box mb={0.5}>
14
+ {isEditing && (
15
+ <Typography variant="caption" color="text.secondary" display="block" mb={0.2} ml={0.5}>
16
+ {label || 'Checkbox'} ({name})
17
+ </Typography>
18
+ )}
19
+ <FormGroup>
20
+ <FormControlLabel
21
+ control={
22
+ <Checkbox
23
+ size="small"
24
+ checked={!!value}
25
+ onChange={(e) => {
26
+ if (fullPath) formStore.updateValue(fullPath, e.target.checked);
27
+ }}
28
+ disabled={isEditing || !!node.calculation?.formula}
29
+ />
30
+ }
31
+ label={
32
+ <Typography variant="body2" color="text.primary">
33
+ {label || name || 'Checkbox'}
34
+ {node.validation?.required && <span style={{ color: 'red' }}> *</span>}
35
+ </Typography>
36
+ }
37
+ />
38
+ </FormGroup>
39
+ </Box>
40
+ );
41
+ });
@@ -0,0 +1,56 @@
1
+ import React from 'react';
2
+ import { DatePicker } from '@mui/x-date-pickers/DatePicker';
3
+ import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
4
+ import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns';
5
+ import { observer } from 'mobx-react-lite';
6
+ import { formStore } from '../../store/FormStore';
7
+ import type { FieldProps } from '../FieldRegistry';
8
+ import { Box, Typography, TextField } from '@mui/material';
9
+
10
+ export const DateField: React.FC<FieldProps> = observer(({ node, path, isEditing }) => {
11
+ const { name, label, placeholder } = node.props || {};
12
+ const fullPath = path && name ? `${path}.${name}` : name;
13
+ const value = fullPath ? formStore.getValue(fullPath) : null;
14
+ const error = fullPath ? formStore.errors[fullPath] : undefined;
15
+
16
+ const handleChange = (newValue: Date | null) => {
17
+ if (fullPath) {
18
+ const isoVal = newValue ? newValue.toISOString() : null;
19
+ formStore.updateField(fullPath, isoVal, node.validation);
20
+ }
21
+ };
22
+
23
+ // Create a Date object from value if string
24
+ const dateValue = value ? new Date(value) : null;
25
+
26
+ const content = (
27
+ <LocalizationProvider dateAdapter={AdapterDateFns}>
28
+ <DatePicker
29
+ value={dateValue}
30
+ onChange={handleChange}
31
+ inputFormat="dd-MM-yyyy"
32
+ renderInput={(params) => (
33
+ <TextField
34
+ {...params}
35
+ fullWidth
36
+ size="small"
37
+ placeholder={placeholder}
38
+ disabled={isEditing || !!node.calculation?.formula}
39
+ error={!!error}
40
+ helperText={error}
41
+ />
42
+ )}
43
+ />
44
+ </LocalizationProvider>
45
+ );
46
+
47
+ return (
48
+ <Box mb={0.5}>
49
+ <Typography variant="caption" color="text.secondary" display="block" mb={0.2} ml={0.5}>
50
+ {label || (isEditing ? 'Date Field' : '')} {isEditing ? `(${name})` : ''}
51
+ {node.validation?.required && <span style={{ color: 'red' }}> *</span>}
52
+ </Typography>
53
+ {content}
54
+ </Box>
55
+ );
56
+ });
@@ -0,0 +1,45 @@
1
+ import React from 'react';
2
+ import { Box, Button, Typography, Input, FormHelperText } from '@mui/material';
3
+ import CloudUploadIcon from '@mui/icons-material/CloudUpload';
4
+ import { observer } from 'mobx-react-lite';
5
+ import { formStore } from '../../store/FormStore';
6
+ import type { FieldProps } from '../FieldRegistry';
7
+
8
+ export const FileUploadField: React.FC<FieldProps> = observer(({ node, path, isEditing }) => {
9
+ const { name, label } = node.props || {};
10
+ const fullPath = path && name ? `${path}.${name}` : name;
11
+ const value = fullPath ? formStore.getValue(fullPath) : null;
12
+ const error = fullPath ? formStore.errors[fullPath] : undefined;
13
+
14
+ const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
15
+ if (fullPath && event.target.files && event.target.files[0]) {
16
+ // Store file name for now (or base64 if needed, but keeping it simple)
17
+ formStore.updateField(fullPath, event.target.files[0].name, node.validation);
18
+ }
19
+ };
20
+
21
+ return (
22
+ <Box mb={1}>
23
+ <Typography variant="caption" color="text.secondary" display="block" mb={0.2} ml={0.5}>
24
+ {label || (isEditing ? 'File Upload' : '')} {isEditing ? `(${name})` : ''}
25
+ {node.validation?.required && <span style={{ color: 'red' }}> *</span>}
26
+ </Typography>
27
+ <Button
28
+ component="label"
29
+ variant="outlined"
30
+ startIcon={<CloudUploadIcon />}
31
+ fullWidth
32
+ disabled={isEditing || !!node.calculation?.formula}
33
+ color={error ? 'error' : 'primary'}
34
+ >
35
+ {value ? `Selected: ${value}` : (label || 'Upload File')}
36
+ <Input
37
+ type="file"
38
+ sx={{ display: 'none' }}
39
+ onChange={handleChange}
40
+ />
41
+ </Button>
42
+ {error && <FormHelperText error>{error}</FormHelperText>}
43
+ </Box>
44
+ );
45
+ });
@@ -0,0 +1,20 @@
1
+ import React from 'react';
2
+ import { Typography, Box } from '@mui/material';
3
+ import { observer } from 'mobx-react-lite';
4
+ import { formStore } from '../../store/FormStore';
5
+ import type { FieldProps } from '../FieldRegistry';
6
+
7
+ export const LabelField: React.FC<FieldProps> = observer(({ node }) => {
8
+ const { name, label = '', align = 'left', variant = 'body1' } = node.props || {};
9
+
10
+ const value = name ? formStore.getValue(name) : undefined;
11
+ const displayText = value !== undefined ? value : label;
12
+
13
+ return (
14
+ <Box width="100%" sx={{ whiteSpace: 'pre-wrap' }}>
15
+ <Typography variant={variant} align={align} color="text.primary">
16
+ {displayText}
17
+ </Typography>
18
+ </Box>
19
+ );
20
+ });
@@ -0,0 +1,39 @@
1
+ import React from 'react';
2
+ import { TextField as MuiTextField, Box, Typography } from '@mui/material';
3
+ import { observer } from 'mobx-react-lite';
4
+ import { formStore } from '../../store/FormStore';
5
+ import type { FieldProps } from '../FieldRegistry';
6
+
7
+ export const NumberField: React.FC<FieldProps> = observer(({ node, path, isEditing }) => {
8
+ const { name, label } = node.props || {};
9
+
10
+ const fullPath = path && name ? `${path}.${name}` : name;
11
+ const value = fullPath ? formStore.getValue(fullPath) : '';
12
+ const error = fullPath ? formStore.errors[fullPath] : undefined;
13
+
14
+ const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
15
+ if (fullPath) {
16
+ const val = e.target.value === '' ? undefined : Number(e.target.value);
17
+ formStore.updateField(fullPath, val, node.validation);
18
+ }
19
+ };
20
+
21
+ return (
22
+ <Box mb={0.5}>
23
+ <Typography variant="caption" color="text.secondary" display="block" mb={0.2} ml={0.5}>
24
+ {label || (isEditing ? 'Number Field' : '')} {isEditing ? `(${name})` : ''}
25
+ {node.validation?.required && <span style={{ color: 'red' }}> *</span>}
26
+ </Typography>
27
+ <MuiTextField
28
+ type="number"
29
+ fullWidth
30
+ value={value ?? ''}
31
+ onChange={handleChange}
32
+ size="small"
33
+ disabled={isEditing || !!node.calculation?.formula}
34
+ error={!!error}
35
+ helperText={error}
36
+ />
37
+ </Box>
38
+ );
39
+ });
@@ -0,0 +1,39 @@
1
+ import React from 'react';
2
+ import { TextField as MuiTextField, Typography, Box } from '@mui/material';
3
+ import { observer } from 'mobx-react-lite';
4
+ import { formStore } from '../../store/FormStore';
5
+ import type { FieldProps } from '../FieldRegistry';
6
+
7
+ export const RichTextField: React.FC<FieldProps> = observer(({ node, path, isEditing }) => {
8
+ const { name, label, placeholder } = node.props || {};
9
+ const fullPath = path && name ? `${path}.${name}` : name;
10
+ const value = fullPath ? formStore.getValue(fullPath) : '';
11
+ const error = fullPath ? formStore.errors[fullPath] : undefined;
12
+
13
+ const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
14
+ if (fullPath) {
15
+ formStore.updateField(fullPath, e.target.value, node.validation);
16
+ }
17
+ };
18
+
19
+ return (
20
+ <Box mb={0.5}>
21
+ <Typography variant="caption" color="text.secondary" display="block" mb={0.2} ml={0.5}>
22
+ {label || (isEditing ? 'Rich Text' : '')} {isEditing ? `(${name})` : ''}
23
+ {node.validation?.required && <span style={{ color: 'red' }}> *</span>}
24
+ </Typography>
25
+ <MuiTextField
26
+ placeholder={placeholder}
27
+ fullWidth
28
+ multiline
29
+ minRows={4}
30
+ value={value}
31
+ onChange={handleChange}
32
+ size="small"
33
+ disabled={isEditing || !!node.calculation?.formula}
34
+ error={!!error}
35
+ helperText={error}
36
+ />
37
+ </Box>
38
+ );
39
+ });
@@ -0,0 +1,64 @@
1
+ import React from 'react';
2
+ import { FormControl, Select, MenuItem, Box, Typography, FormHelperText, Autocomplete, TextField as MuiTextField } from '@mui/material';
3
+ import { observer } from 'mobx-react-lite';
4
+ import { formStore } from '../../store/FormStore';
5
+ import type { FieldProps } from '../FieldRegistry';
6
+
7
+ export const SelectField: React.FC<FieldProps> = observer(({ node, path, isEditing }) => {
8
+ const { name, label, options = [], enableAutocomplete = false, placeholder } = node.props || {};
9
+ const fullPath = path && name ? `${path}.${name}` : name;
10
+ const value = fullPath ? formStore.getValue(fullPath) : '';
11
+ const error = fullPath ? formStore.errors[fullPath] : undefined;
12
+
13
+ const handleChange = (val: any) => {
14
+ if (fullPath) {
15
+ formStore.updateField(fullPath, val, node.validation);
16
+ }
17
+ };
18
+
19
+ const renderSelect = () => (
20
+ <FormControl fullWidth size="small" disabled={isEditing || !!node.calculation?.formula} error={!!error}>
21
+ <Select
22
+ value={value}
23
+ onChange={(e) => handleChange(e.target.value)}
24
+ >
25
+ {options.map((opt: any, index: number) => (
26
+ <MenuItem key={index} value={opt.value}>
27
+ {opt.label}
28
+ </MenuItem>
29
+ ))}
30
+ </Select>
31
+ {error && <FormHelperText>{error}</FormHelperText>}
32
+ </FormControl>
33
+ );
34
+
35
+ const renderAutocomplete = () => (
36
+ <Autocomplete
37
+ options={options}
38
+ getOptionLabel={(option: any) => option.label || ''}
39
+ value={options.find((opt: any) => opt.value === value) || null}
40
+ onChange={(_, newValue: any) => handleChange(newValue ? newValue.value : null)}
41
+ renderInput={(params) => (
42
+ <MuiTextField
43
+ {...params}
44
+ size="small"
45
+ placeholder={placeholder}
46
+ error={!!error}
47
+ helperText={error}
48
+ disabled={isEditing || !!node.calculation?.formula}
49
+ />
50
+ )}
51
+ disabled={isEditing || !!node.calculation?.formula}
52
+ />
53
+ );
54
+
55
+ return (
56
+ <Box mb={0.5}>
57
+ <Typography variant="caption" color="text.secondary" display="block" mb={0.2} ml={0.5}>
58
+ {label || (isEditing ? 'Select Field' : '')} {isEditing ? `(${name})` : ''}
59
+ {node.validation?.required && <span style={{ color: 'red' }}> *</span>}
60
+ </Typography>
61
+ {enableAutocomplete ? renderAutocomplete() : renderSelect()}
62
+ </Box>
63
+ );
64
+ });
@@ -0,0 +1,44 @@
1
+ import React from 'react';
2
+ import { TextField as MuiTextField, Box, Typography } from '@mui/material';
3
+ import { observer } from 'mobx-react-lite';
4
+ import { formStore } from '../../store/FormStore';
5
+ import type { FieldProps } from '../FieldRegistry';
6
+
7
+ export const TextField: React.FC<FieldProps> = observer(({ node, path, isEditing }) => {
8
+ const { name, label, placeholder } = node.props || {};
9
+
10
+ // Resolve full path: path.name or just name
11
+ const fullPath = path && name ? `${path}.${name}` : name;
12
+ const value = fullPath ? (formStore.getValue(fullPath) ?? node.props?.value ?? '') : (node.props?.value ?? '');
13
+ const error = fullPath ? formStore.errors[fullPath] : undefined;
14
+
15
+ const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
16
+ if (fullPath) {
17
+ formStore.updateField(fullPath, e.target.value, node.validation);
18
+ }
19
+ };
20
+
21
+ return (
22
+ <Box mb={0.5}>
23
+ <Typography variant="caption" color="text.secondary" display="block" mb={0.2} ml={0.5}>
24
+ {label || (isEditing ? 'Text Field' : '')} {isEditing ? `(${name})` : ''}
25
+ {node.validation?.required && <span style={{ color: 'red' }}> *</span>}
26
+ </Typography>
27
+ <MuiTextField
28
+ placeholder={placeholder}
29
+ fullWidth
30
+ value={value}
31
+ onChange={handleChange}
32
+ size="small"
33
+ disabled={isEditing || !!node.calculation?.formula}
34
+ error={!!error}
35
+ helperText={error}
36
+ onBlur={() => {
37
+ if (fullPath && node.validation) {
38
+ formStore.validateField(fullPath, value, node.validation);
39
+ }
40
+ }}
41
+ />
42
+ </Box>
43
+ );
44
+ });
@@ -0,0 +1,30 @@
1
+ import React from 'react';
2
+ import { Box } from '@mui/material';
3
+ import { observer } from 'mobx-react-lite';
4
+ import type { FieldProps } from '../FieldRegistry';
5
+ import { FormRenderer } from '../FormRenderer';
6
+ import { SortableContext, rectSortingStrategy } from '@dnd-kit/sortable';
7
+ import { LayoutPlaceholder } from './LayoutPlaceholder';
8
+
9
+ export const FormCol: React.FC<FieldProps> = observer(({ node, path, isEditing }) => {
10
+ const childrenIds = node.children?.map(c => c.id) || [];
11
+
12
+ const content = (
13
+ <Box display="flex" flexDirection="column" gap={0.5} width="100%" sx={{ flexGrow: 1, height: '100%' }}>
14
+ {node.children?.map(child => (
15
+ <FormRenderer key={child.id} node={child} path={path} isEditing={isEditing} />
16
+ ))}
17
+ </Box>
18
+ );
19
+
20
+ if (isEditing) {
21
+ return (
22
+ <LayoutPlaceholder isEditing={isEditing} label="Column" isEmpty={childrenIds.length === 0}>
23
+ <SortableContext items={childrenIds} strategy={rectSortingStrategy}>
24
+ {content}
25
+ </SortableContext>
26
+ </LayoutPlaceholder>
27
+ );
28
+ }
29
+ return content;
30
+ });
@@ -0,0 +1,19 @@
1
+ import React from 'react';
2
+ import { Divider, Chip, Box } from '@mui/material';
3
+ import { observer } from 'mobx-react-lite';
4
+ import type { FieldProps } from '../FieldRegistry';
5
+
6
+ export const FormDivider: React.FC<FieldProps> = observer(({ node }) => {
7
+ const { text, align = 'center' } = node.props || {};
8
+
9
+ // Map align to Divider textAlign if text is present
10
+ const dividerAlign = (align === 'left' ? 'left' : align === 'right' ? 'right' : 'center') as 'left' | 'right' | 'center';
11
+
12
+ return (
13
+ <Box width="100%" py={1}>
14
+ <Divider textAlign={text ? dividerAlign : undefined}>
15
+ {text && <Chip label={text} size="small" />}
16
+ </Divider>
17
+ </Box>
18
+ );
19
+ });
@@ -0,0 +1,85 @@
1
+ import React from 'react';
2
+ import { Paper, Box, Typography } from '@mui/material';
3
+ import type { FieldProps } from '../FieldRegistry';
4
+ import { observer } from 'mobx-react-lite';
5
+ import { FormChildrenRenderer } from '../builder/FormChildrenRenderer';
6
+ import { LayoutPlaceholder } from './LayoutPlaceholder';
7
+
8
+ export const FormPaper: React.FC<FieldProps> = observer(({ node, path, isEditing }) => {
9
+ const { label, elevation: elevationProp, padding = 2, variant: variantProp } = node.props || {};
10
+ const childrenIds = node.children?.map(c => c.id) || [];
11
+
12
+ // In preview mode, default to cleaner look if no props specified
13
+ const elevation = isEditing ? (elevationProp ?? 1) : (elevationProp ?? 0);
14
+ const variant = isEditing ? (variantProp ?? 'elevation') : (variantProp ?? 'transparent');
15
+ const isTransparent = variant === 'transparent';
16
+
17
+ const childrenContent = (
18
+ <FormChildrenRenderer
19
+ parentId={node.id}
20
+ children={node.children}
21
+ isEditing={!!isEditing}
22
+ path={path}
23
+ layout="horizontal"
24
+ />
25
+ );
26
+
27
+ const paperContent = (
28
+ <>
29
+ {label && <Typography variant="h6" mb={1}>{label}</Typography>}
30
+ {isEditing ? (
31
+ <LayoutPlaceholder
32
+ isEditing={isEditing}
33
+ label="Paper Content"
34
+ isEmpty={childrenIds.length === 0}
35
+ sx={{
36
+ flexGrow: 1,
37
+ display: 'flex',
38
+ flexDirection: 'column',
39
+ // Ensure the placeholder itself allows children to span height if possible
40
+ minHeight: childrenIds.length === 0 ? 120 : '100%'
41
+ }}
42
+ >
43
+ {childrenContent}
44
+ </LayoutPlaceholder>
45
+ ) : (
46
+ childrenContent
47
+ )}
48
+ </>
49
+ );
50
+
51
+ if (!isEditing && isTransparent) {
52
+ return (
53
+ <Box sx={{
54
+ p: padding,
55
+ height: '100%',
56
+ display: 'flex',
57
+ flexDirection: 'column',
58
+ mb: 1
59
+ }}>
60
+ {paperContent}
61
+ </Box>
62
+ );
63
+ }
64
+
65
+ return (
66
+ <Paper
67
+ variant={variant === 'outlined' ? 'outlined' : 'elevation'}
68
+ elevation={isTransparent ? 0 : (elevation ?? 1)}
69
+ sx={{
70
+ p: padding,
71
+ height: '100%',
72
+ display: 'flex',
73
+ flexDirection: 'column',
74
+ backgroundColor: isTransparent ? 'transparent' : undefined,
75
+ backgroundImage: isTransparent ? 'none' : undefined,
76
+ boxShadow: isTransparent ? 'none !important' : undefined,
77
+ border: isTransparent ? 'none !important' : undefined,
78
+ outline: isTransparent ? 'none !important' : undefined,
79
+ overflow: 'hidden' // Added to prevent inner margins from leaking out
80
+ }}
81
+ >
82
+ {paperContent}
83
+ </Paper>
84
+ );
85
+ });