@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.
- package/README.md +73 -0
- package/eslint.config.js +23 -0
- package/index.html +13 -0
- package/package.json +60 -0
- package/public/vite.svg +1 -0
- package/src/App.css +42 -0
- package/src/App.tsx +83 -0
- package/src/assets/react.svg +1 -0
- package/src/components/FieldRegistry.ts +34 -0
- package/src/components/FormContainer.tsx +25 -0
- package/src/components/FormRenderer.tsx +121 -0
- package/src/components/builder/DraggableTool.tsx +66 -0
- package/src/components/builder/DroppableCanvas.tsx +51 -0
- package/src/components/builder/EditorWrapper.tsx +87 -0
- package/src/components/builder/FormBuilder.tsx +313 -0
- package/src/components/builder/FormChildrenRenderer.tsx +68 -0
- package/src/components/builder/IntegrationSettings.tsx +110 -0
- package/src/components/builder/PropertiesModal.tsx +75 -0
- package/src/components/builder/PropertiesPanel.tsx +858 -0
- package/src/components/builder/SortableNode.tsx +53 -0
- package/src/components/builder/Toolbox.tsx +123 -0
- package/src/components/fields/CheckboxField.tsx +41 -0
- package/src/components/fields/DateField.tsx +56 -0
- package/src/components/fields/FileUploadField.tsx +45 -0
- package/src/components/fields/LabelField.tsx +20 -0
- package/src/components/fields/NumberField.tsx +39 -0
- package/src/components/fields/RichTextField.tsx +39 -0
- package/src/components/fields/SelectField.tsx +64 -0
- package/src/components/fields/TextField.tsx +44 -0
- package/src/components/layout/FormCol.tsx +30 -0
- package/src/components/layout/FormDivider.tsx +19 -0
- package/src/components/layout/FormPaper.tsx +85 -0
- package/src/components/layout/FormRepeater.tsx +130 -0
- package/src/components/layout/FormRow.tsx +61 -0
- package/src/components/layout/FormTab.tsx +33 -0
- package/src/components/layout/FormTable.tsx +47 -0
- package/src/components/layout/FormTableCell.tsx +47 -0
- package/src/components/layout/FormTabs.tsx +77 -0
- package/src/components/layout/LayoutPlaceholder.tsx +85 -0
- package/src/components/registerComponents.ts +30 -0
- package/src/index.css +75 -0
- package/src/index.ts +5 -0
- package/src/main.tsx +10 -0
- package/src/store/FormStore.ts +811 -0
- package/src/utils/apiTransformer.ts +206 -0
- package/src/utils/idGenerator.ts +3 -0
- package/tsconfig.app.json +28 -0
- package/tsconfig.json +7 -0
- package/tsconfig.node.json +26 -0
- package/vite.config.ts +46 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Box, Button, Typography, Paper, IconButton } from '@mui/material';
|
|
3
|
+
import DeleteIcon from '@mui/icons-material/Delete';
|
|
4
|
+
import AddIcon from '@mui/icons-material/Add';
|
|
5
|
+
import { observer } from 'mobx-react-lite';
|
|
6
|
+
import { formStore } from '../../store/FormStore';
|
|
7
|
+
import type { FieldProps } from '../FieldRegistry';
|
|
8
|
+
import { LayoutPlaceholder } from './LayoutPlaceholder';
|
|
9
|
+
import { FormChildrenRenderer } from '../builder/FormChildrenRenderer';
|
|
10
|
+
|
|
11
|
+
export const FormRepeater: React.FC<FieldProps> = observer(({ node, path, isEditing }) => {
|
|
12
|
+
const { name, label, addLabel = 'Add Item' } = node.props || {};
|
|
13
|
+
const fullPath = path && name ? `${path}.${name}` : name;
|
|
14
|
+
const items: any[] = (fullPath ? formStore.getValue(fullPath) : []) || [];
|
|
15
|
+
const childrenIds = node.children?.map(c => c.id) || [];
|
|
16
|
+
|
|
17
|
+
const handleAdd = () => {
|
|
18
|
+
if (!fullPath) return;
|
|
19
|
+
const newItems = [...items, {}];
|
|
20
|
+
formStore.updateValue(fullPath, newItems);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const handleRemove = (index: number) => {
|
|
24
|
+
if (!fullPath) return;
|
|
25
|
+
const newItems = [...items];
|
|
26
|
+
newItems.splice(index, 1);
|
|
27
|
+
formStore.updateValue(fullPath, newItems);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// Note: FormRepeater structure is complex. It renders a list of *values*.
|
|
31
|
+
// And for each value, it renders a set of *fields* (children of repeater node).
|
|
32
|
+
// The DRAG & DROP here typically refers to reordering the *fields* in the schema (the template),
|
|
33
|
+
// OR reordering the *items* in the data array?
|
|
34
|
+
// In "Builder" mode, we are editing the template. So we want to reorder the *fields*.
|
|
35
|
+
|
|
36
|
+
// So inside each item Paper, we render the children fields.
|
|
37
|
+
// If we wrap THAT in SortableContext, we can reorder fields.
|
|
38
|
+
// However, since we render fields Multiple times (once per item),
|
|
39
|
+
// dnd-kit might get confused if IDs are not unique across the entire DOM.
|
|
40
|
+
// But SortableNode uses schema node ID. If we render multiple SortableNodes with SAME ID (one per repeater item),
|
|
41
|
+
// dnd-kit WILL break.
|
|
42
|
+
|
|
43
|
+
// In EDITING mode, typically we don't need to see ALL items.
|
|
44
|
+
// Or we should only allow editing schema in a specific way.
|
|
45
|
+
// For now, let's just wrap the inner children in SortableContext.
|
|
46
|
+
// BUT we must be careful about ID duplication if we render multiple items.
|
|
47
|
+
// Actually, in Builder mode, maybe we should only render ONE item as a preview?
|
|
48
|
+
// Or just render the children directly without mapping over data?
|
|
49
|
+
|
|
50
|
+
// Let's stick to the current implementation but if isEditing is true,
|
|
51
|
+
// maybe we should ensure we only have one "template" instance to avoid ID clash?
|
|
52
|
+
// OR, we just don't support dragging INSIDE a repeater instance for now.
|
|
53
|
+
// Support dragging the repeater ITSELF (which is handled by parent).
|
|
54
|
+
|
|
55
|
+
// Let's add SortableContext for the children of the repeater template.
|
|
56
|
+
// To avoid ID clash, we should only enable SortableNode in the FIRST item if there are multiple.
|
|
57
|
+
// Or, simpler: In Builder mode, force Repeater to show at least 1 item, and maybe ONLY 1 dummy item?
|
|
58
|
+
|
|
59
|
+
// Let's try to just wrap it, but we might hit the ID issue.
|
|
60
|
+
// If we have 2 items, we have 2 rendered "Title" fields with ID "node_123". Dnd-kit will duplicate ids.
|
|
61
|
+
|
|
62
|
+
// PROPOSAL: In isEditing mode, FormRepeater only renders ONE dummy item.
|
|
63
|
+
|
|
64
|
+
// Force only 1 item in Builder mode to prevent ID collisions since we render generic "template" fields
|
|
65
|
+
const displayItems = isEditing ? [{}] : items;
|
|
66
|
+
// If items is empty in preview, show nothing? Or 0 items.
|
|
67
|
+
// But in builder, if items is empty, we MUST show 1 dummy item to allow dropping.
|
|
68
|
+
// The current logic `isEditing ? [{}] : items` handles this well.
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<Box mb={1}>
|
|
72
|
+
{label && <Typography variant="h6" mb={0.5}>{label}</Typography>}
|
|
73
|
+
|
|
74
|
+
{displayItems.length === 0 && isEditing && (
|
|
75
|
+
<Box p={1} bgcolor="#f5f5f5" color="text.secondary" sx={{ border: '1px dashed #ccc', borderRadius: 1, textAlign: 'center' }}>
|
|
76
|
+
Repeater Template
|
|
77
|
+
</Box>
|
|
78
|
+
)}
|
|
79
|
+
|
|
80
|
+
{displayItems.map((_item, index) => (
|
|
81
|
+
<Paper key={index} variant="outlined" sx={{ p: 1, mb: 0.5, position: 'relative' }}>
|
|
82
|
+
<Box position="absolute" right={4} top={4} zIndex={5}>
|
|
83
|
+
<IconButton size="small" onClick={() => handleRemove(index)} color="error" disabled={isEditing}>
|
|
84
|
+
<DeleteIcon fontSize="inherit" />
|
|
85
|
+
</IconButton>
|
|
86
|
+
</Box>
|
|
87
|
+
|
|
88
|
+
<Box sx={{ display: 'flex', flexWrap: 'wrap' }}>
|
|
89
|
+
{isEditing ? (
|
|
90
|
+
<LayoutPlaceholder
|
|
91
|
+
isEditing={isEditing}
|
|
92
|
+
label="Item Template"
|
|
93
|
+
isEmpty={childrenIds.length === 0}
|
|
94
|
+
sx={{ p: 0.5, width: '100%' }}
|
|
95
|
+
>
|
|
96
|
+
<FormChildrenRenderer
|
|
97
|
+
parentId={node.id}
|
|
98
|
+
children={node.children}
|
|
99
|
+
isEditing={true}
|
|
100
|
+
layout="horizontal"
|
|
101
|
+
path={`${fullPath}[${index}]`}
|
|
102
|
+
/>
|
|
103
|
+
</LayoutPlaceholder>
|
|
104
|
+
) : (
|
|
105
|
+
<FormChildrenRenderer
|
|
106
|
+
parentId={node.id}
|
|
107
|
+
children={node.children}
|
|
108
|
+
isEditing={false}
|
|
109
|
+
layout="horizontal"
|
|
110
|
+
path={`${fullPath}[${index}]`}
|
|
111
|
+
/>
|
|
112
|
+
)}
|
|
113
|
+
</Box>
|
|
114
|
+
</Paper>
|
|
115
|
+
))}
|
|
116
|
+
|
|
117
|
+
{!isEditing && (
|
|
118
|
+
<Button
|
|
119
|
+
startIcon={<AddIcon />}
|
|
120
|
+
variant="outlined"
|
|
121
|
+
size="small"
|
|
122
|
+
onClick={handleAdd}
|
|
123
|
+
sx={{ mt: 0.5 }}
|
|
124
|
+
>
|
|
125
|
+
{addLabel}
|
|
126
|
+
</Button>
|
|
127
|
+
)}
|
|
128
|
+
</Box>
|
|
129
|
+
);
|
|
130
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { observer } from 'mobx-react-lite';
|
|
3
|
+
import type { FieldProps } from '../FieldRegistry';
|
|
4
|
+
import { LayoutPlaceholder } from './LayoutPlaceholder';
|
|
5
|
+
import { FormChildrenRenderer } from '../builder/FormChildrenRenderer';
|
|
6
|
+
|
|
7
|
+
export const FormRow: React.FC<FieldProps> = observer(({ node, path, isEditing }) => {
|
|
8
|
+
const childrenIds = node.children?.map(c => c.id) || [];
|
|
9
|
+
|
|
10
|
+
if (isEditing) {
|
|
11
|
+
return (
|
|
12
|
+
<LayoutPlaceholder isEditing={isEditing} label="Row" isEmpty={childrenIds.length === 0}>
|
|
13
|
+
<FormChildrenRenderer
|
|
14
|
+
parentId={node.id}
|
|
15
|
+
children={node.children}
|
|
16
|
+
isEditing={true}
|
|
17
|
+
layout="horizontal"
|
|
18
|
+
path={path}
|
|
19
|
+
/>
|
|
20
|
+
</LayoutPlaceholder>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<FormChildrenRenderer
|
|
26
|
+
parentId={node.id}
|
|
27
|
+
children={node.children}
|
|
28
|
+
isEditing={false}
|
|
29
|
+
layout="horizontal"
|
|
30
|
+
path={path}
|
|
31
|
+
/>
|
|
32
|
+
);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
export const FormCol: React.FC<FieldProps> = observer(({ node, path, isEditing }) => {
|
|
36
|
+
const childrenIds = node.children?.map(c => c.id) || [];
|
|
37
|
+
|
|
38
|
+
if (isEditing) {
|
|
39
|
+
return (
|
|
40
|
+
<LayoutPlaceholder isEditing={isEditing} label="Column" isEmpty={childrenIds.length === 0}>
|
|
41
|
+
<FormChildrenRenderer
|
|
42
|
+
parentId={node.id}
|
|
43
|
+
children={node.children}
|
|
44
|
+
isEditing={true}
|
|
45
|
+
layout="vertical"
|
|
46
|
+
path={path}
|
|
47
|
+
/>
|
|
48
|
+
</LayoutPlaceholder>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<FormChildrenRenderer
|
|
54
|
+
parentId={node.id}
|
|
55
|
+
children={node.children}
|
|
56
|
+
isEditing={false}
|
|
57
|
+
layout="vertical"
|
|
58
|
+
path={path}
|
|
59
|
+
/>
|
|
60
|
+
);
|
|
61
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { observer } from 'mobx-react-lite';
|
|
3
|
+
import type { FieldProps } from '../FieldRegistry';
|
|
4
|
+
import { FormChildrenRenderer } from '../builder/FormChildrenRenderer';
|
|
5
|
+
import { LayoutPlaceholder } from './LayoutPlaceholder';
|
|
6
|
+
|
|
7
|
+
export const FormTab: React.FC<FieldProps> = observer(({ node, path, isEditing }) => {
|
|
8
|
+
const childrenIds = node.children?.map(c => c.id) || [];
|
|
9
|
+
|
|
10
|
+
const content = (
|
|
11
|
+
<FormChildrenRenderer
|
|
12
|
+
parentId={node.id}
|
|
13
|
+
children={node.children}
|
|
14
|
+
isEditing={!!isEditing}
|
|
15
|
+
path={path}
|
|
16
|
+
layout="vertical"
|
|
17
|
+
/>
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
if (isEditing) {
|
|
21
|
+
return (
|
|
22
|
+
<LayoutPlaceholder isEditing={isEditing} label={node.props?.label || "Tab Content"} isEmpty={childrenIds.length === 0} sx={{ p: 1 }}>
|
|
23
|
+
{content}
|
|
24
|
+
</LayoutPlaceholder>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<div style={{ padding: 0 }}>
|
|
30
|
+
{content}
|
|
31
|
+
</div>
|
|
32
|
+
);
|
|
33
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
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 { LayoutPlaceholder } from './LayoutPlaceholder';
|
|
7
|
+
|
|
8
|
+
export const FormTable: React.FC<FieldProps> = observer(({ node, path, isEditing }) => {
|
|
9
|
+
const { rows = 2, cols = 2 } = node.props || {};
|
|
10
|
+
|
|
11
|
+
console.log(`FormTable Render: ${node.id}`, {
|
|
12
|
+
childrenCount: node.children?.length,
|
|
13
|
+
rows,
|
|
14
|
+
cols,
|
|
15
|
+
childTypes: node.children?.map(c => c.type)
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const gridStyle = {
|
|
19
|
+
display: 'grid',
|
|
20
|
+
gridTemplateColumns: `repeat(${cols}, 1fr)`,
|
|
21
|
+
gridTemplateRows: `repeat(${rows}, minmax(32px, auto))`,
|
|
22
|
+
gap: '4px',
|
|
23
|
+
width: '100%',
|
|
24
|
+
backgroundColor: isEditing ? 'rgba(0, 0, 0, 0.02)' : 'transparent', // Subtle background in edit mode
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const tableContent = (
|
|
28
|
+
<Box sx={gridStyle}>
|
|
29
|
+
{node.children?.map((child, index) => (
|
|
30
|
+
<Box key={child.id} sx={{ width: '100%', height: '100%' }}>
|
|
31
|
+
<FormRenderer node={child} path={`${path}[${index}]`} isEditing={isEditing} />
|
|
32
|
+
</Box>
|
|
33
|
+
))}
|
|
34
|
+
</Box>
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
if (isEditing) {
|
|
38
|
+
// Import LayoutPlaceholder at the top if not already
|
|
39
|
+
return (
|
|
40
|
+
<LayoutPlaceholder isEditing={isEditing} label="Table" isEmpty={false}>
|
|
41
|
+
{tableContent}
|
|
42
|
+
</LayoutPlaceholder>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return tableContent;
|
|
47
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { observer } from 'mobx-react-lite';
|
|
3
|
+
import type { FieldProps } from '../FieldRegistry';
|
|
4
|
+
import { LayoutPlaceholder } from './LayoutPlaceholder';
|
|
5
|
+
import { FormChildrenRenderer } from '../builder/FormChildrenRenderer';
|
|
6
|
+
|
|
7
|
+
export const FormTableCell: React.FC<FieldProps> = observer(({ node, path, isEditing }) => {
|
|
8
|
+
const childrenIds = node.children?.map(c => c.id) || [];
|
|
9
|
+
const isEmpty = childrenIds.length === 0;
|
|
10
|
+
|
|
11
|
+
if (isEditing) {
|
|
12
|
+
return (
|
|
13
|
+
<LayoutPlaceholder
|
|
14
|
+
isEditing={isEditing}
|
|
15
|
+
label="Cell"
|
|
16
|
+
isEmpty={isEmpty}
|
|
17
|
+
sx={{
|
|
18
|
+
height: '100%',
|
|
19
|
+
border: '1px dashed #bbb', // Darker border
|
|
20
|
+
backgroundColor: isEmpty ? '#f9f9f9' : 'transparent', // Light background if empty
|
|
21
|
+
'&:hover': {
|
|
22
|
+
borderColor: '#2196f3',
|
|
23
|
+
backgroundColor: '#f0f7ff'
|
|
24
|
+
}
|
|
25
|
+
}}
|
|
26
|
+
>
|
|
27
|
+
<FormChildrenRenderer
|
|
28
|
+
parentId={node.id}
|
|
29
|
+
children={node.children}
|
|
30
|
+
isEditing={true}
|
|
31
|
+
layout="vertical"
|
|
32
|
+
path={path}
|
|
33
|
+
/>
|
|
34
|
+
</LayoutPlaceholder>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<FormChildrenRenderer
|
|
40
|
+
parentId={node.id}
|
|
41
|
+
children={node.children}
|
|
42
|
+
isEditing={false}
|
|
43
|
+
layout="vertical"
|
|
44
|
+
path={path}
|
|
45
|
+
/>
|
|
46
|
+
);
|
|
47
|
+
});
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Box, Tabs, Tab, Paper } from '@mui/material';
|
|
3
|
+
import { observer } from 'mobx-react-lite';
|
|
4
|
+
import type { FieldProps } from '../FieldRegistry';
|
|
5
|
+
import { FormRenderer } from '../FormRenderer';
|
|
6
|
+
import { formStore } from '../../store/FormStore';
|
|
7
|
+
import { LayoutPlaceholder } from './LayoutPlaceholder';
|
|
8
|
+
|
|
9
|
+
export const FormTabs: React.FC<FieldProps> = observer(({ node, path, isEditing }) => {
|
|
10
|
+
const [activeTab, setActiveTab] = React.useState(0);
|
|
11
|
+
const allTabs = node.children || [];
|
|
12
|
+
const tabs = isEditing
|
|
13
|
+
? allTabs
|
|
14
|
+
: allTabs.filter(tabNode => {
|
|
15
|
+
if (!tabNode.condition) return true;
|
|
16
|
+
return formStore.evaluateCondition(tabNode.condition);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const { elevation: elevationProp = 1, variant: variantProp } = node.props || {};
|
|
20
|
+
const variant = isEditing ? (variantProp ?? 'outlined') : (variantProp ?? 'transparent');
|
|
21
|
+
const elevation = variant === 'elevation' ? elevationProp : 0;
|
|
22
|
+
|
|
23
|
+
const handleTabChange = (_: React.SyntheticEvent, newValue: number) => {
|
|
24
|
+
setActiveTab(newValue);
|
|
25
|
+
if (tabs[newValue]) {
|
|
26
|
+
formStore.selectNode(tabs[newValue].id);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
if (!isEditing && tabs.length === 0) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (activeTab >= tabs.length && tabs.length > 0) {
|
|
35
|
+
setActiveTab(0);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const currentTabNode = tabs[activeTab];
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<Paper
|
|
42
|
+
variant={variant}
|
|
43
|
+
elevation={variant === 'elevation' ? elevation : 0}
|
|
44
|
+
sx={{
|
|
45
|
+
mb: 2,
|
|
46
|
+
display: 'flex',
|
|
47
|
+
flexDirection: 'column',
|
|
48
|
+
backgroundColor: variant === 'transparent' ? 'transparent' : undefined,
|
|
49
|
+
backgroundImage: variant === 'transparent' ? 'none' : undefined,
|
|
50
|
+
boxShadow: variant === 'transparent' ? 'none !important' : undefined,
|
|
51
|
+
border: variant === 'transparent' ? 'none !important' : undefined,
|
|
52
|
+
overflow: 'hidden'
|
|
53
|
+
}}
|
|
54
|
+
>
|
|
55
|
+
<Tabs
|
|
56
|
+
value={tabs.length > 0 ? activeTab : false}
|
|
57
|
+
onChange={handleTabChange}
|
|
58
|
+
sx={{ borderBottom: 1, borderColor: 'divider', minHeight: 48 }}
|
|
59
|
+
variant="scrollable"
|
|
60
|
+
scrollButtons="auto"
|
|
61
|
+
>
|
|
62
|
+
{tabs.map((tabNode, index) => (
|
|
63
|
+
<Tab key={tabNode.id} label={tabNode.props?.label || `Tab ${index + 1}`} />
|
|
64
|
+
))}
|
|
65
|
+
</Tabs>
|
|
66
|
+
<Box p={0} flexGrow={1}>
|
|
67
|
+
{tabs.length === 0 && isEditing ? (
|
|
68
|
+
<LayoutPlaceholder isEditing={isEditing} label="Tabs" isEmpty={true} sx={{ minHeight: 120 }}>
|
|
69
|
+
<Box />
|
|
70
|
+
</LayoutPlaceholder>
|
|
71
|
+
) : (
|
|
72
|
+
currentTabNode && <FormRenderer key={currentTabNode.id} node={currentTabNode} path={path} isEditing={isEditing} />
|
|
73
|
+
)}
|
|
74
|
+
</Box>
|
|
75
|
+
</Paper>
|
|
76
|
+
);
|
|
77
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Box, Typography, Chip } from '@mui/material';
|
|
3
|
+
|
|
4
|
+
interface LayoutPlaceholderProps {
|
|
5
|
+
isEditing?: boolean;
|
|
6
|
+
label: string;
|
|
7
|
+
children: React.ReactNode;
|
|
8
|
+
sx?: any;
|
|
9
|
+
isEmpty?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
import AddCircleOutlineIcon from '@mui/icons-material/AddCircleOutline';
|
|
13
|
+
|
|
14
|
+
export const LayoutPlaceholder: React.FC<LayoutPlaceholderProps> = ({
|
|
15
|
+
isEditing,
|
|
16
|
+
label,
|
|
17
|
+
children,
|
|
18
|
+
sx = {},
|
|
19
|
+
isEmpty
|
|
20
|
+
}) => {
|
|
21
|
+
if (!isEditing) {
|
|
22
|
+
return <>{children}</>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<Box sx={{
|
|
27
|
+
position: 'relative',
|
|
28
|
+
border: '1px dashed #ccc',
|
|
29
|
+
borderRadius: 1,
|
|
30
|
+
p: 2,
|
|
31
|
+
pt: 4, // Space for label
|
|
32
|
+
minHeight: isEmpty ? 120 : 'auto',
|
|
33
|
+
width: '100%',
|
|
34
|
+
transition: 'all 0.2s',
|
|
35
|
+
boxSizing: 'border-box',
|
|
36
|
+
backgroundColor: isEmpty ? 'rgba(0, 0, 0, 0.01)' : 'transparent',
|
|
37
|
+
'&:hover': {
|
|
38
|
+
borderColor: '#1976d2',
|
|
39
|
+
backgroundColor: 'rgba(25, 118, 210, 0.04)'
|
|
40
|
+
},
|
|
41
|
+
...sx
|
|
42
|
+
}}>
|
|
43
|
+
<Chip
|
|
44
|
+
label={label}
|
|
45
|
+
size="small"
|
|
46
|
+
sx={{
|
|
47
|
+
position: 'absolute',
|
|
48
|
+
top: 0,
|
|
49
|
+
left: 0,
|
|
50
|
+
borderRadius: '4px 0 4px 0',
|
|
51
|
+
height: 20,
|
|
52
|
+
fontSize: '0.7rem',
|
|
53
|
+
backgroundColor: '#eee',
|
|
54
|
+
color: '#666',
|
|
55
|
+
zIndex: 1
|
|
56
|
+
}}
|
|
57
|
+
/>
|
|
58
|
+
{children}
|
|
59
|
+
{isEmpty && (
|
|
60
|
+
<Box
|
|
61
|
+
display="flex"
|
|
62
|
+
flexDirection="column"
|
|
63
|
+
alignItems="center"
|
|
64
|
+
justifyContent="center"
|
|
65
|
+
height="100%"
|
|
66
|
+
position="absolute"
|
|
67
|
+
top={0}
|
|
68
|
+
left={0}
|
|
69
|
+
right={0}
|
|
70
|
+
bottom={0}
|
|
71
|
+
zIndex={0}
|
|
72
|
+
sx={{ opacity: 0.6 }}
|
|
73
|
+
>
|
|
74
|
+
<AddCircleOutlineIcon sx={{ fontSize: 40, mb: 1, color: 'text.secondary' }} />
|
|
75
|
+
<Typography variant="body2" color="text.secondary" fontWeight="medium">
|
|
76
|
+
Empty {label}
|
|
77
|
+
</Typography>
|
|
78
|
+
<Typography variant="caption" color="text.secondary">
|
|
79
|
+
Drag items from toolbox here
|
|
80
|
+
</Typography>
|
|
81
|
+
</Box>
|
|
82
|
+
)}
|
|
83
|
+
</Box>
|
|
84
|
+
);
|
|
85
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { registerComponent } from './FieldRegistry';
|
|
2
|
+
import { TextField } from './fields/TextField';
|
|
3
|
+
import { NumberField } from './fields/NumberField';
|
|
4
|
+
import { CheckboxField } from './fields/CheckboxField';
|
|
5
|
+
import { SelectField } from './fields/SelectField';
|
|
6
|
+
import { DateField } from './fields/DateField';
|
|
7
|
+
import { FileUploadField } from './fields/FileUploadField';
|
|
8
|
+
import { RichTextField } from './fields/RichTextField';
|
|
9
|
+
import { FormRow } from './layout/FormRow';
|
|
10
|
+
import { FormCol } from './layout/FormCol';
|
|
11
|
+
import { FormTabs } from './layout/FormTabs';
|
|
12
|
+
import { FormTab } from './layout/FormTab';
|
|
13
|
+
import { FormRepeater } from './layout/FormRepeater';
|
|
14
|
+
import { FormPaper } from './layout/FormPaper';
|
|
15
|
+
|
|
16
|
+
export const registerAllComponents = () => {
|
|
17
|
+
registerComponent('text', TextField);
|
|
18
|
+
registerComponent('number', NumberField);
|
|
19
|
+
registerComponent('checkbox', CheckboxField);
|
|
20
|
+
registerComponent('select', SelectField);
|
|
21
|
+
registerComponent('date', DateField);
|
|
22
|
+
registerComponent('file', FileUploadField);
|
|
23
|
+
registerComponent('richtext', RichTextField);
|
|
24
|
+
registerComponent('row', FormRow);
|
|
25
|
+
registerComponent('col', FormCol);
|
|
26
|
+
registerComponent('tabs', FormTabs);
|
|
27
|
+
registerComponent('tab', FormTab);
|
|
28
|
+
registerComponent('repeater', FormRepeater);
|
|
29
|
+
registerComponent('paper', FormPaper);
|
|
30
|
+
};
|
package/src/index.css
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
|
3
|
+
line-height: 1.5;
|
|
4
|
+
font-weight: 400;
|
|
5
|
+
|
|
6
|
+
color: #213547;
|
|
7
|
+
background-color: #ffffff;
|
|
8
|
+
|
|
9
|
+
font-synthesis: none;
|
|
10
|
+
text-rendering: optimizeLegibility;
|
|
11
|
+
-webkit-font-smoothing: antialiased;
|
|
12
|
+
-moz-osx-font-smoothing: grayscale;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
a {
|
|
16
|
+
font-weight: 500;
|
|
17
|
+
color: #646cff;
|
|
18
|
+
text-decoration: inherit;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
a:hover {
|
|
22
|
+
color: #535bf2;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
body {
|
|
26
|
+
margin: 0;
|
|
27
|
+
min-width: 320px;
|
|
28
|
+
min-height: 100vh;
|
|
29
|
+
background-color: #f0f2f5;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
h1 {
|
|
33
|
+
font-size: 3.2em;
|
|
34
|
+
line-height: 1.1;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
h6 {
|
|
38
|
+
color: black !important;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
button {
|
|
42
|
+
border-radius: 8px;
|
|
43
|
+
border: 1px solid transparent;
|
|
44
|
+
padding: 0.6em 1.2em;
|
|
45
|
+
font-size: 1em;
|
|
46
|
+
font-weight: 500;
|
|
47
|
+
font-family: inherit;
|
|
48
|
+
background-color: #1a1a1a;
|
|
49
|
+
cursor: pointer;
|
|
50
|
+
transition: border-color 0.25s;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
button:hover {
|
|
54
|
+
border-color: #646cff;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
button:focus,
|
|
58
|
+
button:focus-visible {
|
|
59
|
+
outline: 4px auto -webkit-focus-ring-color;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
@media (prefers-color-scheme: light) {
|
|
63
|
+
:root {
|
|
64
|
+
color: #213547;
|
|
65
|
+
background-color: #ffffff;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
a:hover {
|
|
69
|
+
color: #747bff;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
button {
|
|
73
|
+
background-color: #f9f9f9;
|
|
74
|
+
}
|
|
75
|
+
}
|
package/src/index.ts
ADDED
package/src/main.tsx
ADDED