@k3-universe/react-kit 0.0.2 → 0.0.4
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/dist/kit/builder/form/components/FormBuilder.d.ts +21 -1
- package/dist/kit/builder/form/components/FormBuilder.d.ts.map +1 -1
- package/dist/kit/builder/form/components/FormBuilderField.d.ts +5 -5
- package/dist/kit/builder/form/components/FormBuilderField.d.ts.map +1 -1
- package/dist/kit/builder/form/components/fields/ArrayField.d.ts +3 -0
- package/dist/kit/builder/form/components/fields/ArrayField.d.ts.map +1 -0
- package/dist/kit/builder/form/components/fields/AutocompleteField.d.ts +3 -0
- package/dist/kit/builder/form/components/fields/AutocompleteField.d.ts.map +1 -0
- package/dist/kit/builder/form/components/fields/CheckboxField.d.ts +3 -0
- package/dist/kit/builder/form/components/fields/CheckboxField.d.ts.map +1 -0
- package/dist/kit/builder/form/components/fields/DateField.d.ts +3 -0
- package/dist/kit/builder/form/components/fields/DateField.d.ts.map +1 -0
- package/dist/kit/builder/form/components/fields/FileField.d.ts +3 -0
- package/dist/kit/builder/form/components/fields/FileField.d.ts.map +1 -0
- package/dist/kit/builder/form/components/fields/NumberField.d.ts +3 -0
- package/dist/kit/builder/form/components/fields/NumberField.d.ts.map +1 -0
- package/dist/kit/builder/form/components/fields/ObjectField.d.ts +3 -0
- package/dist/kit/builder/form/components/fields/ObjectField.d.ts.map +1 -0
- package/dist/kit/builder/form/components/fields/RadioField.d.ts +3 -0
- package/dist/kit/builder/form/components/fields/RadioField.d.ts.map +1 -0
- package/dist/kit/builder/form/components/fields/SelectField.d.ts +3 -0
- package/dist/kit/builder/form/components/fields/SelectField.d.ts.map +1 -0
- package/dist/kit/builder/form/components/fields/SwitchField.d.ts +3 -0
- package/dist/kit/builder/form/components/fields/SwitchField.d.ts.map +1 -0
- package/dist/kit/builder/form/components/fields/TextField.d.ts +3 -0
- package/dist/kit/builder/form/components/fields/TextField.d.ts.map +1 -0
- package/dist/kit/builder/form/components/fields/TextareaField.d.ts +3 -0
- package/dist/kit/builder/form/components/fields/TextareaField.d.ts.map +1 -0
- package/dist/kit/builder/form/components/fields/index.d.ts +14 -0
- package/dist/kit/builder/form/components/fields/index.d.ts.map +1 -0
- package/dist/kit/builder/form/components/fields/types.d.ts +14 -0
- package/dist/kit/builder/form/components/fields/types.d.ts.map +1 -0
- package/dist/kit/builder/form/utils/field-factories.d.ts +1 -0
- package/dist/kit/builder/form/utils/field-factories.d.ts.map +1 -1
- package/dist/kit/themes/clean-slate.css +1 -1
- package/dist/kit/themes/default.css +1 -1
- package/dist/kit/themes/minimal-modern.css +1 -1
- package/dist/kit/themes/spotify.css +1 -1
- package/package.json +1 -1
- package/src/kit/builder/form/components/FormBuilder.tsx +24 -1
- package/src/kit/builder/form/components/FormBuilderField.tsx +143 -340
- package/src/kit/builder/form/components/fields/ArrayField.tsx +222 -0
- package/src/kit/builder/form/components/fields/AutocompleteField.tsx +25 -0
- package/src/kit/builder/form/components/fields/CheckboxField.tsx +56 -0
- package/src/kit/builder/form/components/fields/DateField.tsx +15 -0
- package/src/kit/builder/form/components/fields/FileField.tsx +14 -0
- package/src/kit/builder/form/components/fields/NumberField.tsx +15 -0
- package/src/kit/builder/form/components/fields/ObjectField.tsx +30 -0
- package/src/kit/builder/form/components/fields/RadioField.tsx +29 -0
- package/src/kit/builder/form/components/fields/SelectField.tsx +31 -0
- package/src/kit/builder/form/components/fields/SwitchField.tsx +56 -0
- package/src/kit/builder/form/components/fields/TextField.tsx +18 -0
- package/src/kit/builder/form/components/fields/TextareaField.tsx +15 -0
- package/src/kit/builder/form/components/fields/index.ts +13 -0
- package/src/kit/builder/form/components/fields/types.ts +14 -0
- package/src/kit/builder/form/utils/field-factories.ts +13 -0
- package/src/stories/kit/builder/Form.ArrayLayouts.stories.tsx +153 -0
- package/src/stories/kit/builder/Form.Basic.stories.tsx +2 -0
- package/src/stories/kit/builder/Form.Simple.stories.tsx +4 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { useFieldArray } from 'react-hook-form'
|
|
2
|
+
import type { FieldArrayPath, FieldValues } from 'react-hook-form'
|
|
3
|
+
import { Card, CardContent, CardHeader, CardTitle } from '../../../../../shadcn/ui/card'
|
|
4
|
+
import { Button } from '../../../../../shadcn/ui/button'
|
|
5
|
+
import { Input } from '../../../../../shadcn/ui/input'
|
|
6
|
+
import { GripVertical, Plus, Trash2 } from 'lucide-react'
|
|
7
|
+
import { cn } from '../../../../../shadcn/lib/utils'
|
|
8
|
+
import type { FieldRenderProps } from './types'
|
|
9
|
+
import { FormBuilderField } from '../FormBuilderField'
|
|
10
|
+
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '../../../../../shadcn/ui/table'
|
|
11
|
+
|
|
12
|
+
export function ArrayField({ field, control, fieldPath, value, onChange }: FieldRenderProps) {
|
|
13
|
+
const { fields, append, remove } = useFieldArray({
|
|
14
|
+
control,
|
|
15
|
+
name: fieldPath as FieldArrayPath<FieldValues>,
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
const addItem = () => {
|
|
19
|
+
// For custom layout, prefer appending an object so useFieldArray can generate stable IDs
|
|
20
|
+
if (field.arrayLayout === 'custom') {
|
|
21
|
+
append({} as never)
|
|
22
|
+
return
|
|
23
|
+
}
|
|
24
|
+
if (field.fields && field.fields.length === 1) {
|
|
25
|
+
const defaultValue = field.fields[0].defaultValue ?? ''
|
|
26
|
+
append(defaultValue as never)
|
|
27
|
+
} else if (field.fields) {
|
|
28
|
+
const defaultObject: Record<string, unknown> = {}
|
|
29
|
+
field.fields.forEach((subField) => {
|
|
30
|
+
defaultObject[subField.name] = subField.defaultValue ?? ''
|
|
31
|
+
})
|
|
32
|
+
append(defaultObject as never)
|
|
33
|
+
} else {
|
|
34
|
+
append('' as never)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const removeItem = (index: number) => remove(index)
|
|
39
|
+
|
|
40
|
+
// Custom layout hook
|
|
41
|
+
if (field.arrayLayout === 'custom' && typeof field.arrayRender === 'function') {
|
|
42
|
+
return (
|
|
43
|
+
<>{field.arrayRender({ field, control, fieldPath, value, onChange, addItem, removeItem, disabled: field.disabled, rows: fields })}</>
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Table layout
|
|
48
|
+
if (field.arrayLayout === 'table') {
|
|
49
|
+
const hasNested = Array.isArray(field.fields) && field.fields.length > 0
|
|
50
|
+
const fFields = field.fields ?? []
|
|
51
|
+
const singleNested = hasNested && fFields.length === 1
|
|
52
|
+
const headerBg = field.arrayColors?.headerBgClass ?? 'bg-primary'
|
|
53
|
+
const headerText = field.arrayColors?.headerTextClass ?? 'text-primary-foreground'
|
|
54
|
+
const altRow = field.arrayColors?.rowAltBgClass ?? 'bg-muted/40'
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<Card className={cn(field.className, 'py-3 rounded-md gap-3')}>
|
|
58
|
+
<CardHeader>
|
|
59
|
+
<div className="flex items-center justify-between">
|
|
60
|
+
<div>
|
|
61
|
+
<CardTitle className="text-base">{field.label}</CardTitle>
|
|
62
|
+
{field.description && (
|
|
63
|
+
<p className="text-sm text-muted-foreground">{field.description}</p>
|
|
64
|
+
)}
|
|
65
|
+
</div>
|
|
66
|
+
<Button type="button" variant="outline" size="sm" onClick={addItem} disabled={field.disabled}>
|
|
67
|
+
<Plus className="h-4 w-4 mr-1" />
|
|
68
|
+
Add Item
|
|
69
|
+
</Button>
|
|
70
|
+
</div>
|
|
71
|
+
</CardHeader>
|
|
72
|
+
<CardContent className="space-y-4">
|
|
73
|
+
{fields.length === 0 ? (
|
|
74
|
+
<p className="text-sm text-muted-foreground text-center py-2">
|
|
75
|
+
No items added yet. Click "Add Item" to get started.
|
|
76
|
+
</p>
|
|
77
|
+
) : (
|
|
78
|
+
<Table>
|
|
79
|
+
<TableHeader>
|
|
80
|
+
<TableRow className={cn(headerBg, headerText)}>
|
|
81
|
+
{hasNested ? (
|
|
82
|
+
singleNested ? (
|
|
83
|
+
<TableHead className={cn(headerText)}>{fFields[0]?.label || 'Value'}</TableHead>
|
|
84
|
+
) : (
|
|
85
|
+
fFields.map(sf => (
|
|
86
|
+
<TableHead key={sf.name} className={cn(headerText)}>{sf.label || sf.name}</TableHead>
|
|
87
|
+
))
|
|
88
|
+
)
|
|
89
|
+
) : (
|
|
90
|
+
<TableHead className={cn(headerText)}>Value</TableHead>
|
|
91
|
+
)}
|
|
92
|
+
<TableHead className={cn(headerText)}>Action</TableHead>
|
|
93
|
+
</TableRow>
|
|
94
|
+
</TableHeader>
|
|
95
|
+
<TableBody>
|
|
96
|
+
{fields.map((item, index) => (
|
|
97
|
+
<TableRow key={item.id} className={cn(index % 2 === 1 && altRow)}>
|
|
98
|
+
{hasNested ? (
|
|
99
|
+
singleNested ? (
|
|
100
|
+
<TableCell>
|
|
101
|
+
<FormBuilderField
|
|
102
|
+
field={{ ...fFields[0], name: fFields[0]?.name, label: fFields[0]?.label || 'Value' }}
|
|
103
|
+
control={control}
|
|
104
|
+
parentPath={`${fieldPath}.${index}`}
|
|
105
|
+
/>
|
|
106
|
+
</TableCell>
|
|
107
|
+
) : (
|
|
108
|
+
fFields.map(subField => (
|
|
109
|
+
<TableCell key={subField.name}>
|
|
110
|
+
<FormBuilderField
|
|
111
|
+
field={subField}
|
|
112
|
+
control={control}
|
|
113
|
+
parentPath={`${fieldPath}.${index}`}
|
|
114
|
+
/>
|
|
115
|
+
</TableCell>
|
|
116
|
+
))
|
|
117
|
+
)
|
|
118
|
+
) : (
|
|
119
|
+
<TableCell>
|
|
120
|
+
<Input
|
|
121
|
+
value={String(((value as unknown[] | undefined)?.[index] ?? ''))}
|
|
122
|
+
onChange={(e) => {
|
|
123
|
+
const current = (value as unknown[] | undefined) ?? []
|
|
124
|
+
const newArray = [...current]
|
|
125
|
+
newArray[index] = e.target.value
|
|
126
|
+
onChange(newArray)
|
|
127
|
+
}}
|
|
128
|
+
placeholder={`Item ${index + 1}`}
|
|
129
|
+
disabled={field.disabled}
|
|
130
|
+
/>
|
|
131
|
+
</TableCell>
|
|
132
|
+
)}
|
|
133
|
+
<TableCell className="w-1 text-right">
|
|
134
|
+
<Button type="button" variant="destructive" size="sm" onClick={() => remove(index)} disabled={field.disabled}>
|
|
135
|
+
<Trash2 className="h-4 w-4" />
|
|
136
|
+
</Button>
|
|
137
|
+
</TableCell>
|
|
138
|
+
</TableRow>
|
|
139
|
+
))}
|
|
140
|
+
</TableBody>
|
|
141
|
+
</Table>
|
|
142
|
+
)}
|
|
143
|
+
</CardContent>
|
|
144
|
+
</Card>
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Default: card layout (existing UI)
|
|
149
|
+
return (
|
|
150
|
+
<Card className={cn(field.className, 'py-3 rounded-md gap-3')}>
|
|
151
|
+
<CardHeader>
|
|
152
|
+
<div className="flex items-center justify-between">
|
|
153
|
+
<div>
|
|
154
|
+
<CardTitle className="text-base">{field.label}</CardTitle>
|
|
155
|
+
{field.description && (
|
|
156
|
+
<p className="text-sm text-muted-foreground">{field.description}</p>
|
|
157
|
+
)}
|
|
158
|
+
</div>
|
|
159
|
+
<Button type="button" variant="outline" size="sm" onClick={addItem} disabled={field.disabled}>
|
|
160
|
+
<Plus className="h-4 w-4 mr-1" />
|
|
161
|
+
Add Item
|
|
162
|
+
</Button>
|
|
163
|
+
</div>
|
|
164
|
+
</CardHeader>
|
|
165
|
+
<CardContent className="space-y-4">
|
|
166
|
+
{fields.length === 0 ? (
|
|
167
|
+
<p className="text-sm text-muted-foreground text-center py-2">
|
|
168
|
+
No items added yet. Click "Add Item" to get started.
|
|
169
|
+
</p>
|
|
170
|
+
) : (
|
|
171
|
+
fields.map((item, index) => (
|
|
172
|
+
<Card key={item.id} className="relative py-3 rounded-md gap-3">
|
|
173
|
+
<CardHeader>
|
|
174
|
+
<div className="flex items-center justify-between">
|
|
175
|
+
<div className="flex items-center gap-2">
|
|
176
|
+
<GripVertical className="h-4 w-4 text-muted-foreground" />
|
|
177
|
+
<span className="text-sm font-medium">Item {index + 1}</span>
|
|
178
|
+
</div>
|
|
179
|
+
<Button type="button" variant="ghost" size="sm" onClick={() => remove(index)} disabled={field.disabled}>
|
|
180
|
+
<Trash2 className="h-4 w-4" />
|
|
181
|
+
</Button>
|
|
182
|
+
</div>
|
|
183
|
+
</CardHeader>
|
|
184
|
+
<CardContent>
|
|
185
|
+
{field.fields && field.fields.length === 1 ? (
|
|
186
|
+
<FormBuilderField
|
|
187
|
+
field={{ ...field.fields[0], name: field.fields[0].name, label: field.fields[0].label || 'Value' }}
|
|
188
|
+
control={control}
|
|
189
|
+
parentPath={`${fieldPath}.${index}`}
|
|
190
|
+
/>
|
|
191
|
+
) : field.fields ? (
|
|
192
|
+
<div className="grid gap-2 md:grid-cols-2">
|
|
193
|
+
{field.fields.map(subField => (
|
|
194
|
+
<FormBuilderField
|
|
195
|
+
key={subField.name}
|
|
196
|
+
field={subField}
|
|
197
|
+
control={control}
|
|
198
|
+
parentPath={`${fieldPath}.${index}`}
|
|
199
|
+
/>
|
|
200
|
+
))}
|
|
201
|
+
</div>
|
|
202
|
+
) : (
|
|
203
|
+
<Input
|
|
204
|
+
value={String(((value as unknown[] | undefined)?.[index] ?? ''))}
|
|
205
|
+
onChange={(e) => {
|
|
206
|
+
const current = (value as unknown[] | undefined) ?? []
|
|
207
|
+
const newArray = [...current]
|
|
208
|
+
newArray[index] = e.target.value
|
|
209
|
+
onChange(newArray)
|
|
210
|
+
}}
|
|
211
|
+
placeholder={`Item ${index + 1}`}
|
|
212
|
+
disabled={field.disabled}
|
|
213
|
+
/>
|
|
214
|
+
)}
|
|
215
|
+
</CardContent>
|
|
216
|
+
</Card>
|
|
217
|
+
))
|
|
218
|
+
)}
|
|
219
|
+
</CardContent>
|
|
220
|
+
</Card>
|
|
221
|
+
)
|
|
222
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Autocomplete } from '../../../../../kit/components/autocomplete/Autocomplete'
|
|
2
|
+
import type { AutocompleteOption } from '../../../../../kit/components/autocomplete/types'
|
|
3
|
+
import type { FieldRenderProps } from './types'
|
|
4
|
+
|
|
5
|
+
export function AutocompleteField({ field, value, onChange, className }: FieldRenderProps) {
|
|
6
|
+
const options: AutocompleteOption[] = (field.options ?? [])
|
|
7
|
+
.filter((o): o is { label: string; value: string | number } => o.value !== null && o.value !== undefined)
|
|
8
|
+
.map(o => ({ label: o.label, value: o.value as string | number }))
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
<Autocomplete
|
|
12
|
+
mode={field.autocompleteMode ?? 'client'}
|
|
13
|
+
options={options}
|
|
14
|
+
fetcher={field.fetcher}
|
|
15
|
+
pageSize={field.pageSize}
|
|
16
|
+
value={(value as string | number | null) ?? null}
|
|
17
|
+
onChange={(val) => onChange(val)}
|
|
18
|
+
placeholder={field.placeholder}
|
|
19
|
+
searchPlaceholder={field.searchPlaceholder}
|
|
20
|
+
renderOption={field.renderOption}
|
|
21
|
+
disabled={field.disabled}
|
|
22
|
+
className={className}
|
|
23
|
+
/>
|
|
24
|
+
)
|
|
25
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { Checkbox } from '../../../../../shadcn/ui/checkbox'
|
|
2
|
+
import { Label } from '../../../../../shadcn/ui/label'
|
|
3
|
+
import { cn } from '../../../../../shadcn/lib/utils'
|
|
4
|
+
import type { FieldRenderProps } from './types'
|
|
5
|
+
|
|
6
|
+
export function CheckboxField({ field, fieldPath, value, onChange, className }: FieldRenderProps) {
|
|
7
|
+
const placement = field.labelPlacement ?? 'inline'
|
|
8
|
+
|
|
9
|
+
if (placement === 'stacked') {
|
|
10
|
+
const labelId = `${fieldPath}-label`
|
|
11
|
+
return (
|
|
12
|
+
<div className="space-y-2">
|
|
13
|
+
<Label id={labelId} className="text-sm font-medium">
|
|
14
|
+
{field.label}
|
|
15
|
+
{field.required && <span className="text-destructive ml-1">*</span>}
|
|
16
|
+
</Label>
|
|
17
|
+
<Checkbox
|
|
18
|
+
aria-labelledby={labelId}
|
|
19
|
+
id={fieldPath}
|
|
20
|
+
checked={(value as boolean) || false}
|
|
21
|
+
onCheckedChange={onChange as (val: boolean) => void}
|
|
22
|
+
disabled={field.disabled}
|
|
23
|
+
className={cn(className)}
|
|
24
|
+
/>
|
|
25
|
+
</div>
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (placement === 'hidden') {
|
|
30
|
+
return (
|
|
31
|
+
<Checkbox
|
|
32
|
+
id={fieldPath}
|
|
33
|
+
checked={(value as boolean) || false}
|
|
34
|
+
onCheckedChange={onChange as (val: boolean) => void}
|
|
35
|
+
disabled={field.disabled}
|
|
36
|
+
className={cn(className)}
|
|
37
|
+
/>
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<div className="flex items-center space-x-2">
|
|
43
|
+
<Checkbox
|
|
44
|
+
id={fieldPath}
|
|
45
|
+
checked={(value as boolean) || false}
|
|
46
|
+
onCheckedChange={onChange as (val: boolean) => void}
|
|
47
|
+
disabled={field.disabled}
|
|
48
|
+
className={cn(className)}
|
|
49
|
+
/>
|
|
50
|
+
<Label htmlFor={fieldPath} className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
|
|
51
|
+
{field.label}
|
|
52
|
+
{field.required && <span className="text-destructive ml-1">*</span>}
|
|
53
|
+
</Label>
|
|
54
|
+
</div>
|
|
55
|
+
)
|
|
56
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Input } from '../../../../../shadcn/ui/input'
|
|
2
|
+
import type { FieldRenderProps } from './types'
|
|
3
|
+
|
|
4
|
+
export function DateField({ field, value, onChange, className }: FieldRenderProps) {
|
|
5
|
+
return (
|
|
6
|
+
<Input
|
|
7
|
+
className={className}
|
|
8
|
+
disabled={field.disabled}
|
|
9
|
+
placeholder={field.placeholder}
|
|
10
|
+
type="date"
|
|
11
|
+
value={value ? new Date(value as Date | string).toISOString().split('T')[0] : ''}
|
|
12
|
+
onChange={(e) => onChange(e.target.value ? new Date(e.target.value) : null)}
|
|
13
|
+
/>
|
|
14
|
+
)
|
|
15
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Input } from '../../../../../shadcn/ui/input'
|
|
2
|
+
import type { FieldRenderProps } from './types'
|
|
3
|
+
|
|
4
|
+
export function FileField({ field, onChange, className }: FieldRenderProps) {
|
|
5
|
+
return (
|
|
6
|
+
<Input
|
|
7
|
+
className={className}
|
|
8
|
+
disabled={field.disabled}
|
|
9
|
+
placeholder={field.placeholder}
|
|
10
|
+
type="file"
|
|
11
|
+
onChange={(e) => onChange((e.target as HTMLInputElement).files?.[0] || null)}
|
|
12
|
+
/>
|
|
13
|
+
)
|
|
14
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Input } from '../../../../../shadcn/ui/input'
|
|
2
|
+
import type { FieldRenderProps } from './types'
|
|
3
|
+
|
|
4
|
+
export function NumberField({ field, value, onChange, className }: FieldRenderProps) {
|
|
5
|
+
return (
|
|
6
|
+
<Input
|
|
7
|
+
className={className}
|
|
8
|
+
disabled={field.disabled}
|
|
9
|
+
placeholder={field.placeholder}
|
|
10
|
+
type="number"
|
|
11
|
+
value={(value as number | string) ?? ''}
|
|
12
|
+
onChange={(e) => onChange(Number(e.target.value))}
|
|
13
|
+
/>
|
|
14
|
+
)
|
|
15
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Card, CardContent, CardHeader, CardTitle } from '../../../../../shadcn/ui/card'
|
|
2
|
+
import { cn } from '../../../../../shadcn/lib/utils'
|
|
3
|
+
import type { FieldRenderProps } from './types'
|
|
4
|
+
import { FormBuilderField } from '../FormBuilderField'
|
|
5
|
+
|
|
6
|
+
export function ObjectField({ field, control, fieldPath }: FieldRenderProps) {
|
|
7
|
+
if (!field.fields) return null
|
|
8
|
+
return (
|
|
9
|
+
<Card className={cn(field.className)}>
|
|
10
|
+
<CardHeader className="pb-3">
|
|
11
|
+
<CardTitle className="text-base">{field.label}</CardTitle>
|
|
12
|
+
{field.description && (
|
|
13
|
+
<p className="text-sm text-muted-foreground">{field.description}</p>
|
|
14
|
+
)}
|
|
15
|
+
</CardHeader>
|
|
16
|
+
<CardContent className="space-y-4">
|
|
17
|
+
<div className="grid gap-4 md:grid-cols-2">
|
|
18
|
+
{field.fields.map(subField => (
|
|
19
|
+
<FormBuilderField
|
|
20
|
+
key={subField.name}
|
|
21
|
+
field={subField}
|
|
22
|
+
control={control}
|
|
23
|
+
parentPath={fieldPath}
|
|
24
|
+
/>
|
|
25
|
+
))}
|
|
26
|
+
</div>
|
|
27
|
+
</CardContent>
|
|
28
|
+
</Card>
|
|
29
|
+
)
|
|
30
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { RadioGroup, RadioGroupItem } from '../../../../../shadcn/ui/radio-group'
|
|
2
|
+
import { Label } from '../../../../../shadcn/ui/label'
|
|
3
|
+
import type { FieldRenderProps } from './types'
|
|
4
|
+
|
|
5
|
+
const NULL_SENTINEL = '__NULL__'
|
|
6
|
+
|
|
7
|
+
export function RadioField({ field, value, onChange, className, fieldPath }: FieldRenderProps) {
|
|
8
|
+
const toUiValue = (val: unknown) => (val === null || val === undefined ? NULL_SENTINEL : String(val))
|
|
9
|
+
const fromUiValue = (val: string) => {
|
|
10
|
+
const match = field.options?.find(opt => toUiValue(opt.value) === val)
|
|
11
|
+
return match ? match.value : null
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<RadioGroup
|
|
16
|
+
value={toUiValue(value)}
|
|
17
|
+
onValueChange={(val) => onChange(fromUiValue(val))}
|
|
18
|
+
disabled={field.disabled}
|
|
19
|
+
className={className}
|
|
20
|
+
>
|
|
21
|
+
{field.options?.map(option => (
|
|
22
|
+
<div key={toUiValue(option.value)} className="flex items-center space-x-2">
|
|
23
|
+
<RadioGroupItem value={toUiValue(option.value)} id={`${fieldPath}-${toUiValue(option.value)}`} />
|
|
24
|
+
<Label htmlFor={`${fieldPath}-${toUiValue(option.value)}`}>{option.label}</Label>
|
|
25
|
+
</div>
|
|
26
|
+
))}
|
|
27
|
+
</RadioGroup>
|
|
28
|
+
)
|
|
29
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../../../../../shadcn/ui/select'
|
|
2
|
+
import type { FieldRenderProps } from './types'
|
|
3
|
+
|
|
4
|
+
const NULL_SENTINEL = '__NULL__'
|
|
5
|
+
|
|
6
|
+
export function SelectField({ field, value, onChange, className }: FieldRenderProps) {
|
|
7
|
+
const toUiValue = (val: unknown) => (val === null || val === undefined ? NULL_SENTINEL : String(val))
|
|
8
|
+
const fromUiValue = (val: string) => {
|
|
9
|
+
const match = field.options?.find(opt => toUiValue(opt.value) === val)
|
|
10
|
+
return match ? match.value : null
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<Select
|
|
15
|
+
value={toUiValue(value)}
|
|
16
|
+
onValueChange={(val) => onChange(fromUiValue(val))}
|
|
17
|
+
disabled={field.disabled}
|
|
18
|
+
>
|
|
19
|
+
<SelectTrigger className={className}>
|
|
20
|
+
<SelectValue placeholder={field.placeholder} />
|
|
21
|
+
</SelectTrigger>
|
|
22
|
+
<SelectContent>
|
|
23
|
+
{field.options?.map(option => (
|
|
24
|
+
<SelectItem key={toUiValue(option.value)} value={toUiValue(option.value)}>
|
|
25
|
+
{option.label}
|
|
26
|
+
</SelectItem>
|
|
27
|
+
))}
|
|
28
|
+
</SelectContent>
|
|
29
|
+
</Select>
|
|
30
|
+
)
|
|
31
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { Switch } from '../../../../../shadcn/ui/switch'
|
|
2
|
+
import { Label } from '../../../../../shadcn/ui/label'
|
|
3
|
+
import { cn } from '../../../../../shadcn/lib/utils'
|
|
4
|
+
import type { FieldRenderProps } from './types'
|
|
5
|
+
|
|
6
|
+
export function SwitchField({ field, fieldPath, value, onChange, className }: FieldRenderProps) {
|
|
7
|
+
const placement = field.labelPlacement ?? 'inline'
|
|
8
|
+
|
|
9
|
+
if (placement === 'stacked') {
|
|
10
|
+
const labelId = `${fieldPath}-label`
|
|
11
|
+
return (
|
|
12
|
+
<div className="space-y-2">
|
|
13
|
+
<Label id={labelId} className="text-sm font-medium">
|
|
14
|
+
{field.label}
|
|
15
|
+
{field.required && <span className="text-destructive ml-1">*</span>}
|
|
16
|
+
</Label>
|
|
17
|
+
<Switch
|
|
18
|
+
aria-labelledby={labelId}
|
|
19
|
+
id={fieldPath}
|
|
20
|
+
checked={(value as boolean) || false}
|
|
21
|
+
onCheckedChange={onChange as (val: boolean) => void}
|
|
22
|
+
disabled={field.disabled}
|
|
23
|
+
className={cn(className)}
|
|
24
|
+
/>
|
|
25
|
+
</div>
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (placement === 'hidden') {
|
|
30
|
+
return (
|
|
31
|
+
<Switch
|
|
32
|
+
id={fieldPath}
|
|
33
|
+
checked={(value as boolean) || false}
|
|
34
|
+
onCheckedChange={onChange as (val: boolean) => void}
|
|
35
|
+
disabled={field.disabled}
|
|
36
|
+
className={cn(className)}
|
|
37
|
+
/>
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<div className="flex items-center space-x-2">
|
|
43
|
+
<Switch
|
|
44
|
+
id={fieldPath}
|
|
45
|
+
checked={(value as boolean) || false}
|
|
46
|
+
onCheckedChange={onChange as (val: boolean) => void}
|
|
47
|
+
disabled={field.disabled}
|
|
48
|
+
className={cn(className)}
|
|
49
|
+
/>
|
|
50
|
+
<Label htmlFor={fieldPath} className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
|
|
51
|
+
{field.label}
|
|
52
|
+
{field.required && <span className="text-destructive ml-1">*</span>}
|
|
53
|
+
</Label>
|
|
54
|
+
</div>
|
|
55
|
+
)
|
|
56
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Input } from '../../../../../shadcn/ui/input'
|
|
2
|
+
import type { FieldRenderProps } from './types'
|
|
3
|
+
|
|
4
|
+
export function TextField({ field, fieldPath, value, onChange, className }: FieldRenderProps) {
|
|
5
|
+
const type: 'text' | 'email' | 'password' =
|
|
6
|
+
field.type === 'email' || field.type === 'password' ? field.type : 'text'
|
|
7
|
+
return (
|
|
8
|
+
<Input
|
|
9
|
+
id={fieldPath}
|
|
10
|
+
className={className}
|
|
11
|
+
disabled={field.disabled}
|
|
12
|
+
placeholder={field.placeholder}
|
|
13
|
+
type={type}
|
|
14
|
+
value={(value as string) || ''}
|
|
15
|
+
onChange={(e) => onChange(e.target.value)}
|
|
16
|
+
/>
|
|
17
|
+
)
|
|
18
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Textarea } from '../../../../../shadcn/ui/textarea'
|
|
2
|
+
import type { FieldRenderProps } from './types'
|
|
3
|
+
|
|
4
|
+
export function TextareaField({ field, value, onChange, className }: FieldRenderProps) {
|
|
5
|
+
return (
|
|
6
|
+
<Textarea
|
|
7
|
+
className={className}
|
|
8
|
+
disabled={field.disabled}
|
|
9
|
+
placeholder={field.placeholder}
|
|
10
|
+
value={(value as string) || ''}
|
|
11
|
+
onChange={(e) => onChange(e.target.value)}
|
|
12
|
+
rows={4}
|
|
13
|
+
/>
|
|
14
|
+
)
|
|
15
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export * from './types'
|
|
2
|
+
export * from './AutocompleteField'
|
|
3
|
+
export * from './TextField'
|
|
4
|
+
export * from './NumberField'
|
|
5
|
+
export * from './TextareaField'
|
|
6
|
+
export * from './SelectField'
|
|
7
|
+
export * from './CheckboxField'
|
|
8
|
+
export * from './SwitchField'
|
|
9
|
+
export * from './RadioField'
|
|
10
|
+
export * from './DateField'
|
|
11
|
+
export * from './FileField'
|
|
12
|
+
export * from './ObjectField'
|
|
13
|
+
export * from './ArrayField'
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Control, FieldValues } from 'react-hook-form'
|
|
2
|
+
import type { FormBuilderFieldConfig } from '../FormBuilder'
|
|
3
|
+
|
|
4
|
+
export interface FieldRenderProps {
|
|
5
|
+
field: FormBuilderFieldConfig
|
|
6
|
+
control: Control<FieldValues>
|
|
7
|
+
fieldPath: string
|
|
8
|
+
value: unknown
|
|
9
|
+
onChange: (value: unknown) => void
|
|
10
|
+
className?: string
|
|
11
|
+
disabled?: boolean
|
|
12
|
+
errorMessage?: string
|
|
13
|
+
onFieldChange?: (name: string, value: unknown, allValues: Record<string, unknown>) => void
|
|
14
|
+
}
|
|
@@ -93,6 +93,19 @@ export const createField = {
|
|
|
93
93
|
...options,
|
|
94
94
|
}),
|
|
95
95
|
|
|
96
|
+
switch: (
|
|
97
|
+
name: string,
|
|
98
|
+
label: string,
|
|
99
|
+
options: Partial<FormBuilderFieldConfig> = {},
|
|
100
|
+
): FormBuilderFieldConfig => ({
|
|
101
|
+
name,
|
|
102
|
+
label,
|
|
103
|
+
type: 'switch',
|
|
104
|
+
required: false,
|
|
105
|
+
defaultValue: false,
|
|
106
|
+
...options,
|
|
107
|
+
}),
|
|
108
|
+
|
|
96
109
|
radio: (
|
|
97
110
|
name: string,
|
|
98
111
|
label: string,
|