@squaredr/fieldcraft-pro 0.0.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/dist/chunk-APHGEYG4.mjs +3168 -0
- package/dist/chunk-MQGR25NG.mjs +409 -0
- package/dist/chunk-NIKQEWPG.mjs +467 -0
- package/dist/form-builder/index.d.mts +367 -0
- package/dist/form-builder/index.d.ts +367 -0
- package/dist/form-builder/index.js +3201 -0
- package/dist/form-builder/index.mjs +1 -0
- package/dist/index.d.mts +9 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +4111 -0
- package/dist/index.mjs +24 -0
- package/dist/response-viewer/index.d.mts +21 -0
- package/dist/response-viewer/index.d.ts +21 -0
- package/dist/response-viewer/index.js +469 -0
- package/dist/response-viewer/index.mjs +1 -0
- package/dist/styles.css +2 -0
- package/dist/theme-editor/index.d.mts +32 -0
- package/dist/theme-editor/index.d.ts +32 -0
- package/dist/theme-editor/index.js +413 -0
- package/dist/theme-editor/index.mjs +1 -0
- package/dist/theme-editor-styles.css +256 -0
- package/package.json +74 -0
|
@@ -0,0 +1,3168 @@
|
|
|
1
|
+
import { requireLicense } from '@squaredr/fieldcraft-pro-license';
|
|
2
|
+
import { createContext, Component, useRef, useEffect, useCallback, useState, useMemo, useContext } from 'react';
|
|
3
|
+
import { DndContext, DragOverlay, useSensors, useSensor, MouseSensor, TouchSensor, useDraggable, useDroppable } from '@dnd-kit/core';
|
|
4
|
+
import { Undo2, Redo2, Upload, Download, Save, Search, ChevronRight, Plus, Settings, Copy, Trash2, X, HelpCircle, MoveVertical, Minus, Video, Image, FileText, PartyPopper, Hand, ShieldCheck, SeparatorHorizontal, Info, Heading, Trophy, EyeOff, Calculator, CreditCard, MapPin, Repeat, Grid3X3, Camera, PenTool, Paperclip, CalendarCheck, CalendarRange, Clock, Calendar, ArrowUpDown, Globe, ToggleLeft, ChevronDown, CheckSquare, CircleDot, ListOrdered, TrendingUp, BarChart3, Star, SlidersHorizontal, Hash, UserCheck, Link, PhoneCall, Phone, Mail, AlignLeft, Type, GripVertical } from 'lucide-react';
|
|
5
|
+
import { Button, Separator, Input, Badge, Textarea, Label, Switch } from '@squaredr/fieldcraft-react';
|
|
6
|
+
import { clsx } from 'clsx';
|
|
7
|
+
import { twMerge } from 'tailwind-merge';
|
|
8
|
+
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
9
|
+
|
|
10
|
+
// src/form-builder/components/FormBuilderGated.tsx
|
|
11
|
+
var MAX_HISTORY = 50;
|
|
12
|
+
function useUndoRedo(currentSchema, setSchema) {
|
|
13
|
+
const historyRef = useRef([currentSchema]);
|
|
14
|
+
const [currentIndex, setCurrentIndex] = useState(0);
|
|
15
|
+
const canUndo = currentIndex > 0;
|
|
16
|
+
const canRedo = currentIndex < historyRef.current.length - 1;
|
|
17
|
+
const push = useCallback(
|
|
18
|
+
(schema) => {
|
|
19
|
+
historyRef.current = historyRef.current.slice(0, currentIndex + 1);
|
|
20
|
+
historyRef.current.push(schema);
|
|
21
|
+
if (historyRef.current.length > MAX_HISTORY) {
|
|
22
|
+
historyRef.current.shift();
|
|
23
|
+
setCurrentIndex(historyRef.current.length - 1);
|
|
24
|
+
} else {
|
|
25
|
+
setCurrentIndex((prev) => prev + 1);
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
[currentIndex]
|
|
29
|
+
);
|
|
30
|
+
const undo = useCallback(() => {
|
|
31
|
+
if (currentIndex > 0) {
|
|
32
|
+
const newIndex = currentIndex - 1;
|
|
33
|
+
setCurrentIndex(newIndex);
|
|
34
|
+
setSchema(historyRef.current[newIndex]);
|
|
35
|
+
}
|
|
36
|
+
}, [currentIndex, setSchema]);
|
|
37
|
+
const redo = useCallback(() => {
|
|
38
|
+
if (currentIndex < historyRef.current.length - 1) {
|
|
39
|
+
const newIndex = currentIndex + 1;
|
|
40
|
+
setCurrentIndex(newIndex);
|
|
41
|
+
setSchema(historyRef.current[newIndex]);
|
|
42
|
+
}
|
|
43
|
+
}, [currentIndex, setSchema]);
|
|
44
|
+
const clear = useCallback(() => {
|
|
45
|
+
historyRef.current = [currentSchema];
|
|
46
|
+
setCurrentIndex(0);
|
|
47
|
+
}, [currentSchema]);
|
|
48
|
+
return {
|
|
49
|
+
canUndo,
|
|
50
|
+
canRedo,
|
|
51
|
+
undo,
|
|
52
|
+
redo,
|
|
53
|
+
push,
|
|
54
|
+
clear
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// src/form-builder/utils/id-generator.ts
|
|
59
|
+
var counter = 0;
|
|
60
|
+
function generateId(prefix) {
|
|
61
|
+
const timestamp = Date.now().toString(36);
|
|
62
|
+
const random = Math.random().toString(36).substring(2, 7);
|
|
63
|
+
counter = (counter + 1) % 1e4;
|
|
64
|
+
const count = counter.toString(36);
|
|
65
|
+
return `${prefix}_${timestamp}${count}${random}`;
|
|
66
|
+
}
|
|
67
|
+
function generateSectionId() {
|
|
68
|
+
return generateId("section");
|
|
69
|
+
}
|
|
70
|
+
function generateQuestionId() {
|
|
71
|
+
return generateId("question");
|
|
72
|
+
}
|
|
73
|
+
function generateOptionId() {
|
|
74
|
+
return generateId("option");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// src/form-builder/utils/schema-mutations.ts
|
|
78
|
+
function deepClone(obj) {
|
|
79
|
+
return structuredClone(obj);
|
|
80
|
+
}
|
|
81
|
+
function addSection(schema, section, index) {
|
|
82
|
+
const newSchema = deepClone(schema);
|
|
83
|
+
newSchema.sections.splice(index, 0, section);
|
|
84
|
+
return newSchema;
|
|
85
|
+
}
|
|
86
|
+
function removeSection(schema, sectionId) {
|
|
87
|
+
const newSchema = deepClone(schema);
|
|
88
|
+
newSchema.sections = newSchema.sections.filter((s) => s.id !== sectionId);
|
|
89
|
+
return newSchema;
|
|
90
|
+
}
|
|
91
|
+
function updateSection(schema, sectionId, updates) {
|
|
92
|
+
const newSchema = deepClone(schema);
|
|
93
|
+
const sectionIndex = newSchema.sections.findIndex((s) => s.id === sectionId);
|
|
94
|
+
if (sectionIndex === -1) return schema;
|
|
95
|
+
newSchema.sections[sectionIndex] = {
|
|
96
|
+
...newSchema.sections[sectionIndex],
|
|
97
|
+
...updates
|
|
98
|
+
};
|
|
99
|
+
return newSchema;
|
|
100
|
+
}
|
|
101
|
+
function moveSection(schema, sectionId, newIndex) {
|
|
102
|
+
const newSchema = deepClone(schema);
|
|
103
|
+
const oldIndex = newSchema.sections.findIndex((s) => s.id === sectionId);
|
|
104
|
+
if (oldIndex === -1) return schema;
|
|
105
|
+
const [section] = newSchema.sections.splice(oldIndex, 1);
|
|
106
|
+
newSchema.sections.splice(newIndex, 0, section);
|
|
107
|
+
return newSchema;
|
|
108
|
+
}
|
|
109
|
+
function duplicateSection(schema, sectionId) {
|
|
110
|
+
const newSchema = deepClone(schema);
|
|
111
|
+
const sectionIndex = newSchema.sections.findIndex((s) => s.id === sectionId);
|
|
112
|
+
if (sectionIndex === -1) return schema;
|
|
113
|
+
const original = newSchema.sections[sectionIndex];
|
|
114
|
+
const duplicate = {
|
|
115
|
+
...original,
|
|
116
|
+
id: generateSectionId(),
|
|
117
|
+
title: `${original.title} (Copy)`,
|
|
118
|
+
questions: original.questions.map((q) => ({
|
|
119
|
+
...q,
|
|
120
|
+
id: generateQuestionId()
|
|
121
|
+
}))
|
|
122
|
+
};
|
|
123
|
+
newSchema.sections.splice(sectionIndex + 1, 0, duplicate);
|
|
124
|
+
return newSchema;
|
|
125
|
+
}
|
|
126
|
+
function addQuestion(schema, sectionId, question, index) {
|
|
127
|
+
const newSchema = deepClone(schema);
|
|
128
|
+
const section = newSchema.sections.find((s) => s.id === sectionId);
|
|
129
|
+
if (!section) return schema;
|
|
130
|
+
section.questions.splice(index, 0, question);
|
|
131
|
+
return newSchema;
|
|
132
|
+
}
|
|
133
|
+
function removeQuestion(schema, sectionId, questionId) {
|
|
134
|
+
const newSchema = deepClone(schema);
|
|
135
|
+
const section = newSchema.sections.find((s) => s.id === sectionId);
|
|
136
|
+
if (!section) return schema;
|
|
137
|
+
section.questions = section.questions.filter((q) => q.id !== questionId);
|
|
138
|
+
return newSchema;
|
|
139
|
+
}
|
|
140
|
+
function updateQuestion(schema, sectionId, questionId, updates) {
|
|
141
|
+
const newSchema = deepClone(schema);
|
|
142
|
+
const section = newSchema.sections.find((s) => s.id === sectionId);
|
|
143
|
+
if (!section) return schema;
|
|
144
|
+
const questionIndex = section.questions.findIndex((q) => q.id === questionId);
|
|
145
|
+
if (questionIndex === -1) return schema;
|
|
146
|
+
section.questions[questionIndex] = {
|
|
147
|
+
...section.questions[questionIndex],
|
|
148
|
+
...updates
|
|
149
|
+
};
|
|
150
|
+
return newSchema;
|
|
151
|
+
}
|
|
152
|
+
function moveQuestion(schema, sectionId, questionId, targetSectionId, newIndex) {
|
|
153
|
+
const newSchema = deepClone(schema);
|
|
154
|
+
const sourceSection = newSchema.sections.find((s) => s.id === sectionId);
|
|
155
|
+
const targetSection = newSchema.sections.find((s) => s.id === targetSectionId);
|
|
156
|
+
if (!sourceSection || !targetSection) return schema;
|
|
157
|
+
const questionIndex = sourceSection.questions.findIndex((q) => q.id === questionId);
|
|
158
|
+
if (questionIndex === -1) return schema;
|
|
159
|
+
const [question] = sourceSection.questions.splice(questionIndex, 1);
|
|
160
|
+
targetSection.questions.splice(newIndex, 0, question);
|
|
161
|
+
return newSchema;
|
|
162
|
+
}
|
|
163
|
+
function duplicateQuestion(schema, sectionId, questionId) {
|
|
164
|
+
const newSchema = deepClone(schema);
|
|
165
|
+
const section = newSchema.sections.find((s) => s.id === sectionId);
|
|
166
|
+
if (!section) return schema;
|
|
167
|
+
const questionIndex = section.questions.findIndex((q) => q.id === questionId);
|
|
168
|
+
if (questionIndex === -1) return schema;
|
|
169
|
+
const original = section.questions[questionIndex];
|
|
170
|
+
const duplicate = {
|
|
171
|
+
...original,
|
|
172
|
+
id: generateQuestionId(),
|
|
173
|
+
label: `${original.label} (Copy)`
|
|
174
|
+
};
|
|
175
|
+
section.questions.splice(questionIndex + 1, 0, duplicate);
|
|
176
|
+
return newSchema;
|
|
177
|
+
}
|
|
178
|
+
function addOption(schema, sectionId, questionId, option, index) {
|
|
179
|
+
const newSchema = deepClone(schema);
|
|
180
|
+
const section = newSchema.sections.find((s) => s.id === sectionId);
|
|
181
|
+
if (!section) return schema;
|
|
182
|
+
const question = section.questions.find((q) => q.id === questionId);
|
|
183
|
+
if (!question) return schema;
|
|
184
|
+
if (!question.options) question.options = [];
|
|
185
|
+
question.options.splice(index, 0, option);
|
|
186
|
+
return newSchema;
|
|
187
|
+
}
|
|
188
|
+
function removeOption(schema, sectionId, questionId, optionIndex) {
|
|
189
|
+
const newSchema = deepClone(schema);
|
|
190
|
+
const section = newSchema.sections.find((s) => s.id === sectionId);
|
|
191
|
+
if (!section) return schema;
|
|
192
|
+
const question = section.questions.find((q) => q.id === questionId);
|
|
193
|
+
if (!question || !question.options) return schema;
|
|
194
|
+
question.options.splice(optionIndex, 1);
|
|
195
|
+
return newSchema;
|
|
196
|
+
}
|
|
197
|
+
function updateOption(schema, sectionId, questionId, optionIndex, updates) {
|
|
198
|
+
const newSchema = deepClone(schema);
|
|
199
|
+
const section = newSchema.sections.find((s) => s.id === sectionId);
|
|
200
|
+
if (!section) return schema;
|
|
201
|
+
const question = section.questions.find((q) => q.id === questionId);
|
|
202
|
+
if (!question || !question.options) return schema;
|
|
203
|
+
question.options[optionIndex] = {
|
|
204
|
+
...question.options[optionIndex],
|
|
205
|
+
...updates
|
|
206
|
+
};
|
|
207
|
+
return newSchema;
|
|
208
|
+
}
|
|
209
|
+
function moveOption(schema, sectionId, questionId, oldIndex, newIndex) {
|
|
210
|
+
const newSchema = deepClone(schema);
|
|
211
|
+
const section = newSchema.sections.find((s) => s.id === sectionId);
|
|
212
|
+
if (!section) return schema;
|
|
213
|
+
const question = section.questions.find((q) => q.id === questionId);
|
|
214
|
+
if (!question || !question.options) return schema;
|
|
215
|
+
const [option] = question.options.splice(oldIndex, 1);
|
|
216
|
+
question.options.splice(newIndex, 0, option);
|
|
217
|
+
return newSchema;
|
|
218
|
+
}
|
|
219
|
+
function findQuestion(schema, sectionId, questionId) {
|
|
220
|
+
const section = schema.sections.find((s) => s.id === sectionId);
|
|
221
|
+
if (!section) return null;
|
|
222
|
+
const questionIndex = section.questions.findIndex((q) => q.id === questionId);
|
|
223
|
+
if (questionIndex === -1) return null;
|
|
224
|
+
return { section, question: section.questions[questionIndex], questionIndex };
|
|
225
|
+
}
|
|
226
|
+
function findSection(schema, sectionId) {
|
|
227
|
+
const sectionIndex = schema.sections.findIndex((s) => s.id === sectionId);
|
|
228
|
+
if (sectionIndex === -1) return null;
|
|
229
|
+
return { section: schema.sections[sectionIndex], sectionIndex };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// src/form-builder/hooks/use-builder-state.ts
|
|
233
|
+
function useBuilderState(initialSchema) {
|
|
234
|
+
const [schema, setSchema] = useState(initialSchema);
|
|
235
|
+
const [selectedItem, setSelectedItem] = useState(null);
|
|
236
|
+
const [isDirty, setIsDirty] = useState(false);
|
|
237
|
+
const schemaRef = useRef(schema);
|
|
238
|
+
schemaRef.current = schema;
|
|
239
|
+
const undoRedo = useUndoRedo(schema, (newSchema) => {
|
|
240
|
+
setSchema(newSchema);
|
|
241
|
+
setIsDirty(true);
|
|
242
|
+
});
|
|
243
|
+
const updateSchema = useCallback(
|
|
244
|
+
(newSchema) => {
|
|
245
|
+
setSchema(newSchema);
|
|
246
|
+
undoRedo.push(newSchema);
|
|
247
|
+
setIsDirty(true);
|
|
248
|
+
},
|
|
249
|
+
[undoRedo]
|
|
250
|
+
);
|
|
251
|
+
const applyMutation = useCallback(
|
|
252
|
+
(mutate) => {
|
|
253
|
+
const result = mutate(schemaRef.current);
|
|
254
|
+
updateSchema(result);
|
|
255
|
+
},
|
|
256
|
+
[updateSchema]
|
|
257
|
+
);
|
|
258
|
+
const addSection2 = useCallback(
|
|
259
|
+
(section, index) => {
|
|
260
|
+
applyMutation((s) => addSection(s, section, index));
|
|
261
|
+
},
|
|
262
|
+
[applyMutation]
|
|
263
|
+
);
|
|
264
|
+
const removeSection2 = useCallback(
|
|
265
|
+
(sectionId) => {
|
|
266
|
+
applyMutation((s) => removeSection(s, sectionId));
|
|
267
|
+
setSelectedItem((prev) => {
|
|
268
|
+
if (prev?.type === "section" && prev.sectionId === sectionId) return null;
|
|
269
|
+
return prev;
|
|
270
|
+
});
|
|
271
|
+
},
|
|
272
|
+
[applyMutation]
|
|
273
|
+
);
|
|
274
|
+
const updateSection2 = useCallback(
|
|
275
|
+
(sectionId, updates) => {
|
|
276
|
+
applyMutation((s) => updateSection(s, sectionId, updates));
|
|
277
|
+
},
|
|
278
|
+
[applyMutation]
|
|
279
|
+
);
|
|
280
|
+
const moveSection2 = useCallback(
|
|
281
|
+
(sectionId, newIndex) => {
|
|
282
|
+
applyMutation((s) => moveSection(s, sectionId, newIndex));
|
|
283
|
+
},
|
|
284
|
+
[applyMutation]
|
|
285
|
+
);
|
|
286
|
+
const duplicateSection2 = useCallback(
|
|
287
|
+
(sectionId) => {
|
|
288
|
+
applyMutation((s) => duplicateSection(s, sectionId));
|
|
289
|
+
},
|
|
290
|
+
[applyMutation]
|
|
291
|
+
);
|
|
292
|
+
const addQuestion2 = useCallback(
|
|
293
|
+
(sectionId, question, index) => {
|
|
294
|
+
applyMutation((s) => addQuestion(s, sectionId, question, index));
|
|
295
|
+
},
|
|
296
|
+
[applyMutation]
|
|
297
|
+
);
|
|
298
|
+
const removeQuestion2 = useCallback(
|
|
299
|
+
(sectionId, questionId) => {
|
|
300
|
+
applyMutation((s) => removeQuestion(s, sectionId, questionId));
|
|
301
|
+
setSelectedItem((prev) => {
|
|
302
|
+
if (prev?.type === "question" && prev.sectionId === sectionId && prev.questionId === questionId) {
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
return prev;
|
|
306
|
+
});
|
|
307
|
+
},
|
|
308
|
+
[applyMutation]
|
|
309
|
+
);
|
|
310
|
+
const updateQuestion2 = useCallback(
|
|
311
|
+
(sectionId, questionId, updates) => {
|
|
312
|
+
applyMutation((s) => updateQuestion(s, sectionId, questionId, updates));
|
|
313
|
+
},
|
|
314
|
+
[applyMutation]
|
|
315
|
+
);
|
|
316
|
+
const moveQuestion2 = useCallback(
|
|
317
|
+
(sectionId, questionId, targetSectionId, newIndex) => {
|
|
318
|
+
applyMutation((s) => moveQuestion(s, sectionId, questionId, targetSectionId, newIndex));
|
|
319
|
+
},
|
|
320
|
+
[applyMutation]
|
|
321
|
+
);
|
|
322
|
+
const duplicateQuestion2 = useCallback(
|
|
323
|
+
(sectionId, questionId) => {
|
|
324
|
+
applyMutation((s) => duplicateQuestion(s, sectionId, questionId));
|
|
325
|
+
},
|
|
326
|
+
[applyMutation]
|
|
327
|
+
);
|
|
328
|
+
const selectQuestion = useCallback((sectionId, questionId) => {
|
|
329
|
+
setSelectedItem({ type: "question", sectionId, questionId });
|
|
330
|
+
}, []);
|
|
331
|
+
const selectSection = useCallback((sectionId) => {
|
|
332
|
+
setSelectedItem({ type: "section", sectionId });
|
|
333
|
+
}, []);
|
|
334
|
+
const clearSelection = useCallback(() => {
|
|
335
|
+
setSelectedItem(null);
|
|
336
|
+
}, []);
|
|
337
|
+
const resetSchema = useCallback(
|
|
338
|
+
(newSchema) => {
|
|
339
|
+
setSchema(newSchema);
|
|
340
|
+
undoRedo.clear();
|
|
341
|
+
setIsDirty(false);
|
|
342
|
+
setSelectedItem(null);
|
|
343
|
+
},
|
|
344
|
+
[undoRedo]
|
|
345
|
+
);
|
|
346
|
+
const markClean = useCallback(() => {
|
|
347
|
+
setIsDirty(false);
|
|
348
|
+
}, []);
|
|
349
|
+
return {
|
|
350
|
+
// State
|
|
351
|
+
schema,
|
|
352
|
+
selectedItem,
|
|
353
|
+
isDirty,
|
|
354
|
+
// Schema-level mutation
|
|
355
|
+
updateSchema,
|
|
356
|
+
// Section operations
|
|
357
|
+
addSection: addSection2,
|
|
358
|
+
removeSection: removeSection2,
|
|
359
|
+
updateSection: updateSection2,
|
|
360
|
+
moveSection: moveSection2,
|
|
361
|
+
duplicateSection: duplicateSection2,
|
|
362
|
+
// Question operations
|
|
363
|
+
addQuestion: addQuestion2,
|
|
364
|
+
removeQuestion: removeQuestion2,
|
|
365
|
+
updateQuestion: updateQuestion2,
|
|
366
|
+
moveQuestion: moveQuestion2,
|
|
367
|
+
duplicateQuestion: duplicateQuestion2,
|
|
368
|
+
// Selection
|
|
369
|
+
selectQuestion,
|
|
370
|
+
selectSection,
|
|
371
|
+
clearSelection,
|
|
372
|
+
// Undo/Redo
|
|
373
|
+
canUndo: undoRedo.canUndo,
|
|
374
|
+
canRedo: undoRedo.canRedo,
|
|
375
|
+
undo: undoRedo.undo,
|
|
376
|
+
redo: undoRedo.redo,
|
|
377
|
+
// Reset
|
|
378
|
+
resetSchema,
|
|
379
|
+
markClean
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// src/form-builder/constants.ts
|
|
384
|
+
var QUESTION_TYPE_INFO = {
|
|
385
|
+
// ── Text ──
|
|
386
|
+
short_text: {
|
|
387
|
+
type: "short_text",
|
|
388
|
+
label: "Short Text",
|
|
389
|
+
category: "text",
|
|
390
|
+
icon: "Type",
|
|
391
|
+
description: "Single-line text input",
|
|
392
|
+
defaultConfig: { type: "short_text", maxLength: 255 }
|
|
393
|
+
},
|
|
394
|
+
long_text: {
|
|
395
|
+
type: "long_text",
|
|
396
|
+
label: "Long Text",
|
|
397
|
+
category: "text",
|
|
398
|
+
icon: "AlignLeft",
|
|
399
|
+
description: "Multi-line text area",
|
|
400
|
+
defaultConfig: { type: "long_text", rows: 4 }
|
|
401
|
+
},
|
|
402
|
+
email: {
|
|
403
|
+
type: "email",
|
|
404
|
+
label: "Email",
|
|
405
|
+
category: "text",
|
|
406
|
+
icon: "Mail",
|
|
407
|
+
description: "Email address input with validation"
|
|
408
|
+
},
|
|
409
|
+
phone: {
|
|
410
|
+
type: "phone",
|
|
411
|
+
label: "Phone",
|
|
412
|
+
category: "text",
|
|
413
|
+
icon: "Phone",
|
|
414
|
+
description: "US phone number input"
|
|
415
|
+
},
|
|
416
|
+
url: {
|
|
417
|
+
type: "url",
|
|
418
|
+
label: "URL",
|
|
419
|
+
category: "text",
|
|
420
|
+
icon: "Link",
|
|
421
|
+
description: "Website URL input"
|
|
422
|
+
},
|
|
423
|
+
// ── Numeric ──
|
|
424
|
+
number: {
|
|
425
|
+
type: "number",
|
|
426
|
+
label: "Number",
|
|
427
|
+
category: "numeric",
|
|
428
|
+
icon: "Hash",
|
|
429
|
+
description: "Numeric input with min/max",
|
|
430
|
+
defaultConfig: { type: "number", step: 1 }
|
|
431
|
+
},
|
|
432
|
+
slider: {
|
|
433
|
+
type: "slider",
|
|
434
|
+
label: "Slider",
|
|
435
|
+
category: "numeric",
|
|
436
|
+
icon: "SlidersHorizontal",
|
|
437
|
+
description: "Range slider",
|
|
438
|
+
defaultConfig: { type: "slider", min: 0, max: 100, step: 1 }
|
|
439
|
+
},
|
|
440
|
+
rating: {
|
|
441
|
+
type: "rating",
|
|
442
|
+
label: "Rating",
|
|
443
|
+
category: "numeric",
|
|
444
|
+
icon: "Star",
|
|
445
|
+
description: "Star rating (1-5 or custom)",
|
|
446
|
+
defaultConfig: { type: "rating", max: 5, icon: "star" }
|
|
447
|
+
},
|
|
448
|
+
nps: {
|
|
449
|
+
type: "nps",
|
|
450
|
+
label: "NPS",
|
|
451
|
+
category: "numeric",
|
|
452
|
+
icon: "BarChart3",
|
|
453
|
+
description: "Net Promoter Score (0-10)",
|
|
454
|
+
defaultConfig: { type: "nps", lowLabel: "Not likely", highLabel: "Very likely" }
|
|
455
|
+
},
|
|
456
|
+
opinion_scale: {
|
|
457
|
+
type: "opinion_scale",
|
|
458
|
+
label: "Opinion Scale",
|
|
459
|
+
category: "numeric",
|
|
460
|
+
icon: "TrendingUp",
|
|
461
|
+
description: "Custom numeric scale with labels",
|
|
462
|
+
defaultConfig: { type: "opinion_scale", min: 1, max: 5 }
|
|
463
|
+
},
|
|
464
|
+
// ── Selection ──
|
|
465
|
+
single_select: {
|
|
466
|
+
type: "single_select",
|
|
467
|
+
label: "Single Select",
|
|
468
|
+
category: "selection",
|
|
469
|
+
icon: "CircleDot",
|
|
470
|
+
description: "Radio buttons or vertical list",
|
|
471
|
+
requiresOptions: true,
|
|
472
|
+
defaultConfig: { type: "single_select", layout: "vertical" }
|
|
473
|
+
},
|
|
474
|
+
multi_select: {
|
|
475
|
+
type: "multi_select",
|
|
476
|
+
label: "Multi Select",
|
|
477
|
+
category: "selection",
|
|
478
|
+
icon: "CheckSquare",
|
|
479
|
+
description: "Checkboxes - select multiple",
|
|
480
|
+
requiresOptions: true,
|
|
481
|
+
defaultConfig: { type: "multi_select", layout: "vertical" }
|
|
482
|
+
},
|
|
483
|
+
dropdown: {
|
|
484
|
+
type: "dropdown",
|
|
485
|
+
label: "Dropdown",
|
|
486
|
+
category: "selection",
|
|
487
|
+
icon: "ChevronDown",
|
|
488
|
+
description: "Select from dropdown menu",
|
|
489
|
+
requiresOptions: true,
|
|
490
|
+
defaultConfig: { type: "dropdown", searchable: false }
|
|
491
|
+
},
|
|
492
|
+
boolean: {
|
|
493
|
+
type: "boolean",
|
|
494
|
+
label: "Yes/No",
|
|
495
|
+
category: "selection",
|
|
496
|
+
icon: "ToggleLeft",
|
|
497
|
+
description: "Toggle, radio, or checkbox",
|
|
498
|
+
defaultConfig: { type: "boolean", style: "toggle" }
|
|
499
|
+
},
|
|
500
|
+
ranking: {
|
|
501
|
+
type: "ranking",
|
|
502
|
+
label: "Ranking",
|
|
503
|
+
category: "selection",
|
|
504
|
+
icon: "ArrowUpDown",
|
|
505
|
+
description: "Drag to rank items in order",
|
|
506
|
+
requiresOptions: true,
|
|
507
|
+
defaultConfig: { type: "ranking", items: [] }
|
|
508
|
+
},
|
|
509
|
+
// ── Date/Time ──
|
|
510
|
+
date: {
|
|
511
|
+
type: "date",
|
|
512
|
+
label: "Date",
|
|
513
|
+
category: "datetime",
|
|
514
|
+
icon: "Calendar",
|
|
515
|
+
description: "Date picker",
|
|
516
|
+
defaultConfig: { type: "date" }
|
|
517
|
+
},
|
|
518
|
+
time: {
|
|
519
|
+
type: "time",
|
|
520
|
+
label: "Time",
|
|
521
|
+
category: "datetime",
|
|
522
|
+
icon: "Clock",
|
|
523
|
+
description: "Time picker",
|
|
524
|
+
defaultConfig: { type: "time", format: "12h" }
|
|
525
|
+
},
|
|
526
|
+
// ── Media ──
|
|
527
|
+
file_upload: {
|
|
528
|
+
type: "file_upload",
|
|
529
|
+
label: "File Upload",
|
|
530
|
+
category: "media",
|
|
531
|
+
icon: "Paperclip",
|
|
532
|
+
description: "Upload files",
|
|
533
|
+
defaultConfig: { type: "file_upload", maxFiles: 1, maxSizeMb: 10 }
|
|
534
|
+
},
|
|
535
|
+
// ── Advanced ──
|
|
536
|
+
matrix: {
|
|
537
|
+
type: "matrix",
|
|
538
|
+
label: "Matrix",
|
|
539
|
+
category: "advanced",
|
|
540
|
+
icon: "Grid3X3",
|
|
541
|
+
description: "Grid of inputs (rows x columns)",
|
|
542
|
+
defaultConfig: {
|
|
543
|
+
type: "matrix",
|
|
544
|
+
rows: [{ label: "Row 1", value: "row1" }],
|
|
545
|
+
columns: [{ label: "Column 1", value: "col1" }],
|
|
546
|
+
inputType: "radio"
|
|
547
|
+
}
|
|
548
|
+
},
|
|
549
|
+
calculated: {
|
|
550
|
+
type: "calculated",
|
|
551
|
+
label: "Calculated",
|
|
552
|
+
category: "advanced",
|
|
553
|
+
icon: "Calculator",
|
|
554
|
+
description: "Computed value from other fields",
|
|
555
|
+
defaultConfig: { type: "calculated", expression: "", format: "number" }
|
|
556
|
+
},
|
|
557
|
+
hidden: {
|
|
558
|
+
type: "hidden",
|
|
559
|
+
label: "Hidden Field",
|
|
560
|
+
category: "advanced",
|
|
561
|
+
icon: "EyeOff",
|
|
562
|
+
description: "Hidden value from URL or static",
|
|
563
|
+
defaultConfig: { type: "hidden", source: "static" }
|
|
564
|
+
},
|
|
565
|
+
// ── Structural ──
|
|
566
|
+
section_header: {
|
|
567
|
+
type: "section_header",
|
|
568
|
+
label: "Section Header",
|
|
569
|
+
category: "structural",
|
|
570
|
+
icon: "Heading",
|
|
571
|
+
description: "Heading within a section",
|
|
572
|
+
defaultConfig: { type: "section_header", level: "h3" }
|
|
573
|
+
},
|
|
574
|
+
info_block: {
|
|
575
|
+
type: "info_block",
|
|
576
|
+
label: "Info Block",
|
|
577
|
+
category: "structural",
|
|
578
|
+
icon: "Info",
|
|
579
|
+
description: "Informational message box",
|
|
580
|
+
defaultConfig: { type: "info_block", content: "", variant: "info" }
|
|
581
|
+
},
|
|
582
|
+
page_break: {
|
|
583
|
+
type: "page_break",
|
|
584
|
+
label: "Page Break",
|
|
585
|
+
category: "structural",
|
|
586
|
+
icon: "SeparatorHorizontal",
|
|
587
|
+
description: "Visual separator for print",
|
|
588
|
+
defaultConfig: { type: "page_break" }
|
|
589
|
+
},
|
|
590
|
+
consent: {
|
|
591
|
+
type: "consent",
|
|
592
|
+
label: "Consent",
|
|
593
|
+
category: "structural",
|
|
594
|
+
icon: "ShieldCheck",
|
|
595
|
+
description: "Agreement checkbox",
|
|
596
|
+
defaultConfig: { type: "consent", text: "", checkboxLabel: "I agree" }
|
|
597
|
+
},
|
|
598
|
+
// ── Content & Visual ──
|
|
599
|
+
"welcome-screen": {
|
|
600
|
+
type: "welcome-screen",
|
|
601
|
+
label: "Welcome Screen",
|
|
602
|
+
category: "content",
|
|
603
|
+
icon: "Hand",
|
|
604
|
+
description: "Full-width welcome card",
|
|
605
|
+
defaultConfig: {
|
|
606
|
+
type: "welcome-screen",
|
|
607
|
+
heading: "Welcome",
|
|
608
|
+
buttonText: "Start",
|
|
609
|
+
alignment: "center"
|
|
610
|
+
}
|
|
611
|
+
},
|
|
612
|
+
"thank-you-screen": {
|
|
613
|
+
type: "thank-you-screen",
|
|
614
|
+
label: "Thank You Screen",
|
|
615
|
+
category: "content",
|
|
616
|
+
icon: "PartyPopper",
|
|
617
|
+
description: "Completion screen",
|
|
618
|
+
defaultConfig: {
|
|
619
|
+
type: "thank-you-screen",
|
|
620
|
+
heading: "Thank You!",
|
|
621
|
+
description: "Your response has been recorded."
|
|
622
|
+
}
|
|
623
|
+
},
|
|
624
|
+
"rich-text": {
|
|
625
|
+
type: "rich-text",
|
|
626
|
+
label: "Rich Text",
|
|
627
|
+
category: "content",
|
|
628
|
+
icon: "FileText",
|
|
629
|
+
description: "HTML or Markdown content",
|
|
630
|
+
defaultConfig: { type: "rich-text", content: "", format: "html" }
|
|
631
|
+
},
|
|
632
|
+
image: {
|
|
633
|
+
type: "image",
|
|
634
|
+
label: "Image",
|
|
635
|
+
category: "content",
|
|
636
|
+
icon: "Image",
|
|
637
|
+
description: "Display an image",
|
|
638
|
+
defaultConfig: { type: "image", src: "", alt: "", alignment: "center" }
|
|
639
|
+
},
|
|
640
|
+
video: {
|
|
641
|
+
type: "video",
|
|
642
|
+
label: "Video",
|
|
643
|
+
category: "content",
|
|
644
|
+
icon: "Video",
|
|
645
|
+
description: "Embed YouTube/Vimeo video",
|
|
646
|
+
defaultConfig: { type: "video", src: "", provider: "youtube" }
|
|
647
|
+
},
|
|
648
|
+
divider: {
|
|
649
|
+
type: "divider",
|
|
650
|
+
label: "Divider",
|
|
651
|
+
category: "content",
|
|
652
|
+
icon: "Minus",
|
|
653
|
+
description: "Horizontal line separator",
|
|
654
|
+
defaultConfig: { type: "divider", style: "solid" }
|
|
655
|
+
},
|
|
656
|
+
spacer: {
|
|
657
|
+
type: "spacer",
|
|
658
|
+
label: "Spacer",
|
|
659
|
+
category: "content",
|
|
660
|
+
icon: "MoveVertical",
|
|
661
|
+
description: "Vertical spacing",
|
|
662
|
+
defaultConfig: { type: "spacer", height: 32 }
|
|
663
|
+
}
|
|
664
|
+
};
|
|
665
|
+
var DEFAULT_PALETTE = [
|
|
666
|
+
{
|
|
667
|
+
category: "text",
|
|
668
|
+
label: "Text Input",
|
|
669
|
+
types: ["short_text", "long_text", "email", "phone", "url"]
|
|
670
|
+
},
|
|
671
|
+
{
|
|
672
|
+
category: "numeric",
|
|
673
|
+
label: "Numeric",
|
|
674
|
+
types: ["number", "slider", "rating", "nps", "opinion_scale"]
|
|
675
|
+
},
|
|
676
|
+
{
|
|
677
|
+
category: "selection",
|
|
678
|
+
label: "Selection",
|
|
679
|
+
types: ["single_select", "multi_select", "dropdown", "boolean", "ranking"]
|
|
680
|
+
},
|
|
681
|
+
{
|
|
682
|
+
category: "datetime",
|
|
683
|
+
label: "Date & Time",
|
|
684
|
+
types: ["date", "time"]
|
|
685
|
+
},
|
|
686
|
+
{
|
|
687
|
+
category: "media",
|
|
688
|
+
label: "Media",
|
|
689
|
+
types: ["file_upload"]
|
|
690
|
+
},
|
|
691
|
+
{
|
|
692
|
+
category: "content",
|
|
693
|
+
label: "Content & Visual",
|
|
694
|
+
types: ["welcome-screen", "thank-you-screen", "rich-text", "image", "video", "divider", "spacer"]
|
|
695
|
+
},
|
|
696
|
+
{
|
|
697
|
+
category: "structural",
|
|
698
|
+
label: "Structural",
|
|
699
|
+
types: ["section_header", "info_block", "page_break", "consent"]
|
|
700
|
+
},
|
|
701
|
+
{
|
|
702
|
+
category: "advanced",
|
|
703
|
+
label: "Advanced",
|
|
704
|
+
types: ["matrix", "calculated", "hidden"]
|
|
705
|
+
}
|
|
706
|
+
];
|
|
707
|
+
|
|
708
|
+
// src/form-builder/hooks/use-drag-drop.ts
|
|
709
|
+
function useDragDrop(builderState) {
|
|
710
|
+
const [activeDragItem, setActiveDragItem] = useState(null);
|
|
711
|
+
const sensors = useSensors(
|
|
712
|
+
useSensor(MouseSensor, {
|
|
713
|
+
activationConstraint: {
|
|
714
|
+
distance: 8
|
|
715
|
+
// 8px movement to activate drag
|
|
716
|
+
}
|
|
717
|
+
}),
|
|
718
|
+
useSensor(TouchSensor, {
|
|
719
|
+
activationConstraint: {
|
|
720
|
+
delay: 200,
|
|
721
|
+
tolerance: 5
|
|
722
|
+
}
|
|
723
|
+
})
|
|
724
|
+
);
|
|
725
|
+
const parseDragItem = (active) => {
|
|
726
|
+
const data = active.data.current;
|
|
727
|
+
if (!data) return null;
|
|
728
|
+
if (data.type === "palette-item") {
|
|
729
|
+
return { type: "palette-item", questionType: data.questionType };
|
|
730
|
+
}
|
|
731
|
+
if (data.type === "question") {
|
|
732
|
+
return {
|
|
733
|
+
type: "question",
|
|
734
|
+
sectionId: data.sectionId,
|
|
735
|
+
questionId: data.questionId,
|
|
736
|
+
questionIndex: data.questionIndex
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
if (data.type === "section") {
|
|
740
|
+
return {
|
|
741
|
+
type: "section",
|
|
742
|
+
sectionId: data.sectionId,
|
|
743
|
+
sectionIndex: data.sectionIndex
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
return null;
|
|
747
|
+
};
|
|
748
|
+
const parseDropTarget = (over) => {
|
|
749
|
+
if (!over) return null;
|
|
750
|
+
const data = over.data.current;
|
|
751
|
+
if (!data) return null;
|
|
752
|
+
if (data.type === "section") {
|
|
753
|
+
return { type: "section", sectionId: data.sectionId, index: data.index };
|
|
754
|
+
}
|
|
755
|
+
if (data.type === "canvas") {
|
|
756
|
+
return { type: "canvas", index: data.index };
|
|
757
|
+
}
|
|
758
|
+
return null;
|
|
759
|
+
};
|
|
760
|
+
const handleDragStart = useCallback((event) => {
|
|
761
|
+
const item = parseDragItem(event.active);
|
|
762
|
+
setActiveDragItem(item);
|
|
763
|
+
}, []);
|
|
764
|
+
const handleDragCancel = useCallback(() => {
|
|
765
|
+
setActiveDragItem(null);
|
|
766
|
+
}, []);
|
|
767
|
+
const handleDragEnd = (event) => {
|
|
768
|
+
setActiveDragItem(null);
|
|
769
|
+
const { active, over } = event;
|
|
770
|
+
if (!over) return;
|
|
771
|
+
const dragItem = parseDragItem(active);
|
|
772
|
+
const dropTarget = parseDropTarget(over);
|
|
773
|
+
if (!dragItem || !dropTarget) return;
|
|
774
|
+
if (dragItem.type === "palette-item") {
|
|
775
|
+
if (dropTarget.type === "section") {
|
|
776
|
+
const typeInfo = QUESTION_TYPE_INFO[dragItem.questionType];
|
|
777
|
+
const newQuestion = {
|
|
778
|
+
id: generateQuestionId(),
|
|
779
|
+
type: dragItem.questionType,
|
|
780
|
+
label: typeInfo?.label ?? "New Question",
|
|
781
|
+
config: typeInfo?.defaultConfig,
|
|
782
|
+
options: typeInfo?.requiresOptions ? [
|
|
783
|
+
{ label: "Option 1", value: "option1" },
|
|
784
|
+
{ label: "Option 2", value: "option2" }
|
|
785
|
+
] : void 0
|
|
786
|
+
};
|
|
787
|
+
builderState.addQuestion(dropTarget.sectionId, newQuestion, dropTarget.index);
|
|
788
|
+
builderState.selectQuestion(dropTarget.sectionId, newQuestion.id);
|
|
789
|
+
}
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
if (dragItem.type === "question" && dropTarget.type === "section") {
|
|
793
|
+
if (dragItem.sectionId === dropTarget.sectionId) {
|
|
794
|
+
if (dragItem.questionIndex !== dropTarget.index) {
|
|
795
|
+
builderState.moveQuestion(
|
|
796
|
+
dragItem.sectionId,
|
|
797
|
+
dragItem.questionId,
|
|
798
|
+
dropTarget.sectionId,
|
|
799
|
+
dropTarget.index
|
|
800
|
+
);
|
|
801
|
+
}
|
|
802
|
+
} else {
|
|
803
|
+
builderState.moveQuestion(
|
|
804
|
+
dragItem.sectionId,
|
|
805
|
+
dragItem.questionId,
|
|
806
|
+
dropTarget.sectionId,
|
|
807
|
+
dropTarget.index
|
|
808
|
+
);
|
|
809
|
+
}
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
if (dragItem.type === "section" && dropTarget.type === "canvas") {
|
|
813
|
+
if (dragItem.sectionIndex !== dropTarget.index) {
|
|
814
|
+
builderState.moveSection(dragItem.sectionId, dropTarget.index);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
};
|
|
818
|
+
return {
|
|
819
|
+
sensors,
|
|
820
|
+
handleDragStart,
|
|
821
|
+
handleDragEnd,
|
|
822
|
+
handleDragCancel,
|
|
823
|
+
activeDragItem
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
// src/form-builder/default-schema.ts
|
|
828
|
+
var DEFAULT_SCHEMA = {
|
|
829
|
+
id: "new-form",
|
|
830
|
+
version: "1.0.0",
|
|
831
|
+
title: "Untitled Form",
|
|
832
|
+
description: "Create your form by dragging fields from the palette.",
|
|
833
|
+
sections: [
|
|
834
|
+
{
|
|
835
|
+
id: "section_default",
|
|
836
|
+
title: "Section 1",
|
|
837
|
+
description: "",
|
|
838
|
+
questions: [
|
|
839
|
+
{
|
|
840
|
+
id: "question_default",
|
|
841
|
+
type: "short_text",
|
|
842
|
+
label: "Your first question",
|
|
843
|
+
placeholder: "Enter your answer here",
|
|
844
|
+
required: false,
|
|
845
|
+
config: {
|
|
846
|
+
type: "short_text",
|
|
847
|
+
maxLength: 255
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
]
|
|
851
|
+
}
|
|
852
|
+
],
|
|
853
|
+
submitAction: {
|
|
854
|
+
type: "callback"
|
|
855
|
+
}
|
|
856
|
+
};
|
|
857
|
+
var ICON_MAP = {
|
|
858
|
+
Type,
|
|
859
|
+
AlignLeft,
|
|
860
|
+
Mail,
|
|
861
|
+
Phone,
|
|
862
|
+
PhoneCall,
|
|
863
|
+
Link,
|
|
864
|
+
UserCheck,
|
|
865
|
+
Hash,
|
|
866
|
+
SlidersHorizontal,
|
|
867
|
+
Star,
|
|
868
|
+
BarChart3,
|
|
869
|
+
TrendingUp,
|
|
870
|
+
ListOrdered,
|
|
871
|
+
CircleDot,
|
|
872
|
+
CheckSquare,
|
|
873
|
+
ChevronDown,
|
|
874
|
+
ToggleLeft,
|
|
875
|
+
Globe,
|
|
876
|
+
ArrowUpDown,
|
|
877
|
+
Calendar,
|
|
878
|
+
Clock,
|
|
879
|
+
CalendarRange,
|
|
880
|
+
CalendarCheck,
|
|
881
|
+
Paperclip,
|
|
882
|
+
PenTool,
|
|
883
|
+
Camera,
|
|
884
|
+
Grid3X3,
|
|
885
|
+
Repeat,
|
|
886
|
+
MapPin,
|
|
887
|
+
CreditCard,
|
|
888
|
+
Calculator,
|
|
889
|
+
EyeOff,
|
|
890
|
+
Trophy,
|
|
891
|
+
Heading,
|
|
892
|
+
Info,
|
|
893
|
+
SeparatorHorizontal,
|
|
894
|
+
ShieldCheck,
|
|
895
|
+
Hand,
|
|
896
|
+
PartyPopper,
|
|
897
|
+
FileText,
|
|
898
|
+
Image,
|
|
899
|
+
Video,
|
|
900
|
+
Minus,
|
|
901
|
+
MoveVertical,
|
|
902
|
+
HelpCircle
|
|
903
|
+
};
|
|
904
|
+
function getIcon(name) {
|
|
905
|
+
return ICON_MAP[name] ?? HelpCircle;
|
|
906
|
+
}
|
|
907
|
+
function cn(...inputs) {
|
|
908
|
+
return twMerge(clsx(inputs));
|
|
909
|
+
}
|
|
910
|
+
function PaletteItem({ questionType, typeInfo }) {
|
|
911
|
+
const { attributes, listeners, setNodeRef, isDragging } = useDraggable({
|
|
912
|
+
id: `palette-${questionType}`,
|
|
913
|
+
data: { type: "palette-item", questionType }
|
|
914
|
+
});
|
|
915
|
+
const IconComponent = getIcon(typeInfo.icon);
|
|
916
|
+
return /* @__PURE__ */ jsxs(
|
|
917
|
+
"div",
|
|
918
|
+
{
|
|
919
|
+
ref: setNodeRef,
|
|
920
|
+
...listeners,
|
|
921
|
+
...attributes,
|
|
922
|
+
className: cn(
|
|
923
|
+
"group/item flex items-center gap-3 px-2 py-1.5 mb-0.5 rounded-md text-sm text-foreground cursor-grab transition-colors",
|
|
924
|
+
isDragging ? "fcb-dragging border border-primary" : "border border-transparent hover:bg-accent hover:border-border"
|
|
925
|
+
),
|
|
926
|
+
children: [
|
|
927
|
+
/* @__PURE__ */ jsx(IconComponent, { size: 14, className: "shrink-0 text-muted-foreground group-hover/item:text-primary transition-colors", strokeWidth: 1.75 }),
|
|
928
|
+
/* @__PURE__ */ jsx("span", { className: "font-medium", children: typeInfo.label })
|
|
929
|
+
]
|
|
930
|
+
}
|
|
931
|
+
);
|
|
932
|
+
}
|
|
933
|
+
function QuestionPalette({ questionTypes, palette }) {
|
|
934
|
+
const [collapsed, setCollapsed] = useState({});
|
|
935
|
+
const [search, setSearch] = useState("");
|
|
936
|
+
const mergedPalette = useMemo(
|
|
937
|
+
() => palette ? [...DEFAULT_PALETTE, ...palette] : DEFAULT_PALETTE,
|
|
938
|
+
[palette]
|
|
939
|
+
);
|
|
940
|
+
const toggleCategory = (category) => {
|
|
941
|
+
setCollapsed((prev) => ({ ...prev, [category]: !prev[category] }));
|
|
942
|
+
};
|
|
943
|
+
const searchLower = search.toLowerCase();
|
|
944
|
+
return /* @__PURE__ */ jsxs("div", { className: "w-60 h-full flex flex-col border-r border-border bg-card", role: "region", "aria-label": "Field palette", children: [
|
|
945
|
+
/* @__PURE__ */ jsxs("div", { className: "p-4 pb-3", children: [
|
|
946
|
+
/* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold text-foreground mb-3", children: "Fields" }),
|
|
947
|
+
/* @__PURE__ */ jsxs("div", { className: "relative", children: [
|
|
948
|
+
/* @__PURE__ */ jsx(
|
|
949
|
+
Search,
|
|
950
|
+
{
|
|
951
|
+
size: 13,
|
|
952
|
+
className: "absolute left-2.5 top-1/2 -translate-y-1/2 text-muted-foreground pointer-events-none"
|
|
953
|
+
}
|
|
954
|
+
),
|
|
955
|
+
/* @__PURE__ */ jsx(
|
|
956
|
+
Input,
|
|
957
|
+
{
|
|
958
|
+
value: search,
|
|
959
|
+
onChange: (e) => setSearch(e.target.value),
|
|
960
|
+
placeholder: "Search fields...",
|
|
961
|
+
className: "h-8 pl-8 pr-3 text-xs",
|
|
962
|
+
"aria-label": "Search field types"
|
|
963
|
+
}
|
|
964
|
+
)
|
|
965
|
+
] })
|
|
966
|
+
] }),
|
|
967
|
+
/* @__PURE__ */ jsx("div", { className: "flex-1 overflow-y-auto px-3 pb-4 scrollbar-thin", children: mergedPalette.map((category) => {
|
|
968
|
+
const filteredTypes = search ? category.types.filter((t) => {
|
|
969
|
+
const info = questionTypes[t];
|
|
970
|
+
return info && (info.label.toLowerCase().includes(searchLower) || info.description.toLowerCase().includes(searchLower));
|
|
971
|
+
}) : category.types;
|
|
972
|
+
if (filteredTypes.length === 0) return null;
|
|
973
|
+
const isCollapsed = collapsed[category.category] && !search;
|
|
974
|
+
return /* @__PURE__ */ jsxs("div", { className: "mb-3", children: [
|
|
975
|
+
/* @__PURE__ */ jsxs(
|
|
976
|
+
"button",
|
|
977
|
+
{
|
|
978
|
+
type: "button",
|
|
979
|
+
onClick: () => toggleCategory(category.category),
|
|
980
|
+
className: "flex items-center gap-1.5 w-full px-1 py-1 text-left text-[11px] font-semibold uppercase tracking-widest text-muted-foreground bg-transparent border-0 cursor-pointer",
|
|
981
|
+
"aria-expanded": !isCollapsed,
|
|
982
|
+
children: [
|
|
983
|
+
/* @__PURE__ */ jsx(
|
|
984
|
+
ChevronRight,
|
|
985
|
+
{
|
|
986
|
+
size: 12,
|
|
987
|
+
className: cn(
|
|
988
|
+
"shrink-0 transition-transform duration-150",
|
|
989
|
+
!isCollapsed && "rotate-90"
|
|
990
|
+
)
|
|
991
|
+
}
|
|
992
|
+
),
|
|
993
|
+
category.label
|
|
994
|
+
]
|
|
995
|
+
}
|
|
996
|
+
),
|
|
997
|
+
!isCollapsed && /* @__PURE__ */ jsx("div", { children: filteredTypes.map((type) => {
|
|
998
|
+
const info = questionTypes[type];
|
|
999
|
+
if (!info) return null;
|
|
1000
|
+
return /* @__PURE__ */ jsx(PaletteItem, { questionType: type, typeInfo: info }, type);
|
|
1001
|
+
}) })
|
|
1002
|
+
] }, category.category);
|
|
1003
|
+
}) })
|
|
1004
|
+
] });
|
|
1005
|
+
}
|
|
1006
|
+
function QuestionBlock({
|
|
1007
|
+
question,
|
|
1008
|
+
sectionId,
|
|
1009
|
+
questionIndex,
|
|
1010
|
+
isSelected,
|
|
1011
|
+
builderState
|
|
1012
|
+
}) {
|
|
1013
|
+
const typeInfo = QUESTION_TYPE_INFO[question.type];
|
|
1014
|
+
const IconComponent = typeInfo ? getIcon(typeInfo.icon) : null;
|
|
1015
|
+
const [isEditing, setIsEditing] = useState(false);
|
|
1016
|
+
const [editValue, setEditValue] = useState(question.label);
|
|
1017
|
+
const inputRef = useRef(null);
|
|
1018
|
+
useEffect(() => {
|
|
1019
|
+
if (!isEditing) setEditValue(question.label);
|
|
1020
|
+
}, [question.label, isEditing]);
|
|
1021
|
+
useEffect(() => {
|
|
1022
|
+
if (isEditing && inputRef.current) {
|
|
1023
|
+
inputRef.current.focus();
|
|
1024
|
+
inputRef.current.select();
|
|
1025
|
+
}
|
|
1026
|
+
}, [isEditing]);
|
|
1027
|
+
const { attributes, listeners, setNodeRef: setDragRef, isDragging } = useDraggable({
|
|
1028
|
+
id: `question-${question.id}`,
|
|
1029
|
+
data: { type: "question", sectionId, questionId: question.id, questionIndex }
|
|
1030
|
+
});
|
|
1031
|
+
const { setNodeRef: setDropRef } = useDroppable({
|
|
1032
|
+
id: `question-drop-${question.id}`,
|
|
1033
|
+
data: { type: "section", sectionId, index: questionIndex }
|
|
1034
|
+
});
|
|
1035
|
+
const handleSelect = () => builderState.selectQuestion(sectionId, question.id);
|
|
1036
|
+
const handleDelete = (e) => {
|
|
1037
|
+
e.stopPropagation();
|
|
1038
|
+
builderState.removeQuestion(sectionId, question.id);
|
|
1039
|
+
};
|
|
1040
|
+
const handleDuplicate = (e) => {
|
|
1041
|
+
e.stopPropagation();
|
|
1042
|
+
builderState.duplicateQuestion(sectionId, question.id);
|
|
1043
|
+
};
|
|
1044
|
+
const handleLabelDoubleClick = (e) => {
|
|
1045
|
+
e.stopPropagation();
|
|
1046
|
+
setIsEditing(true);
|
|
1047
|
+
};
|
|
1048
|
+
const commitEdit = () => {
|
|
1049
|
+
const trimmed = editValue.trim();
|
|
1050
|
+
if (trimmed && trimmed !== question.label) {
|
|
1051
|
+
builderState.updateQuestion(sectionId, question.id, { label: trimmed });
|
|
1052
|
+
} else {
|
|
1053
|
+
setEditValue(question.label);
|
|
1054
|
+
}
|
|
1055
|
+
setIsEditing(false);
|
|
1056
|
+
};
|
|
1057
|
+
const handleEditKeyDown = (e) => {
|
|
1058
|
+
if (e.key === "Enter") {
|
|
1059
|
+
e.preventDefault();
|
|
1060
|
+
commitEdit();
|
|
1061
|
+
}
|
|
1062
|
+
if (e.key === "Escape") {
|
|
1063
|
+
setEditValue(question.label);
|
|
1064
|
+
setIsEditing(false);
|
|
1065
|
+
}
|
|
1066
|
+
e.stopPropagation();
|
|
1067
|
+
};
|
|
1068
|
+
return /* @__PURE__ */ jsx("div", { ref: setDropRef, children: /* @__PURE__ */ jsxs(
|
|
1069
|
+
"div",
|
|
1070
|
+
{
|
|
1071
|
+
ref: setDragRef,
|
|
1072
|
+
onClick: handleSelect,
|
|
1073
|
+
className: cn(
|
|
1074
|
+
"group p-3 mb-1.5 rounded-md border cursor-pointer transition-colors",
|
|
1075
|
+
isSelected ? "fcb-selected border-primary" : "bg-secondary border-border hover:border-fcb-border-strong hover:bg-accent",
|
|
1076
|
+
isDragging && "opacity-40"
|
|
1077
|
+
),
|
|
1078
|
+
children: [
|
|
1079
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between mb-1.5", children: [
|
|
1080
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
1081
|
+
/* @__PURE__ */ jsx(
|
|
1082
|
+
"div",
|
|
1083
|
+
{
|
|
1084
|
+
...listeners,
|
|
1085
|
+
...attributes,
|
|
1086
|
+
className: "cursor-grab text-muted-foreground opacity-40 group-hover:opacity-100 transition-opacity",
|
|
1087
|
+
children: /* @__PURE__ */ jsx(GripVertical, { size: 14, strokeWidth: 1.5 })
|
|
1088
|
+
}
|
|
1089
|
+
),
|
|
1090
|
+
/* @__PURE__ */ jsxs(Badge, { variant: "secondary", className: "gap-1.5 text-muted-foreground", children: [
|
|
1091
|
+
IconComponent && /* @__PURE__ */ jsx(IconComponent, { size: 11, strokeWidth: 1.75 }),
|
|
1092
|
+
typeInfo?.label ?? question.type
|
|
1093
|
+
] })
|
|
1094
|
+
] }),
|
|
1095
|
+
/* @__PURE__ */ jsxs("div", { className: cn(
|
|
1096
|
+
"flex gap-0.5 transition-opacity",
|
|
1097
|
+
isSelected ? "opacity-100" : "opacity-0 group-hover:opacity-100"
|
|
1098
|
+
), children: [
|
|
1099
|
+
/* @__PURE__ */ jsx(
|
|
1100
|
+
Button,
|
|
1101
|
+
{
|
|
1102
|
+
variant: "ghost",
|
|
1103
|
+
size: "icon-xs",
|
|
1104
|
+
onClick: handleDuplicate,
|
|
1105
|
+
title: "Duplicate",
|
|
1106
|
+
"aria-label": "Duplicate field",
|
|
1107
|
+
children: /* @__PURE__ */ jsx(Copy, { size: 12, strokeWidth: 1.75 })
|
|
1108
|
+
}
|
|
1109
|
+
),
|
|
1110
|
+
/* @__PURE__ */ jsx(
|
|
1111
|
+
Button,
|
|
1112
|
+
{
|
|
1113
|
+
variant: "ghost",
|
|
1114
|
+
size: "icon-xs",
|
|
1115
|
+
onClick: handleDelete,
|
|
1116
|
+
className: "hover:bg-destructive/10 hover:text-destructive",
|
|
1117
|
+
title: "Delete",
|
|
1118
|
+
"aria-label": "Delete field",
|
|
1119
|
+
children: /* @__PURE__ */ jsx(Trash2, { size: 12, strokeWidth: 1.75 })
|
|
1120
|
+
}
|
|
1121
|
+
)
|
|
1122
|
+
] })
|
|
1123
|
+
] }),
|
|
1124
|
+
isEditing ? /* @__PURE__ */ jsx(
|
|
1125
|
+
"input",
|
|
1126
|
+
{
|
|
1127
|
+
ref: inputRef,
|
|
1128
|
+
value: editValue,
|
|
1129
|
+
onChange: (e) => setEditValue(e.target.value),
|
|
1130
|
+
onBlur: commitEdit,
|
|
1131
|
+
onKeyDown: handleEditKeyDown,
|
|
1132
|
+
onClick: (e) => e.stopPropagation(),
|
|
1133
|
+
className: "w-full text-sm font-medium text-foreground bg-transparent border-0 border-b border-primary outline-none py-0.5 px-0"
|
|
1134
|
+
}
|
|
1135
|
+
) : /* @__PURE__ */ jsxs(
|
|
1136
|
+
"div",
|
|
1137
|
+
{
|
|
1138
|
+
className: "text-sm font-medium text-foreground cursor-text",
|
|
1139
|
+
onDoubleClick: handleLabelDoubleClick,
|
|
1140
|
+
title: "Double-click to edit",
|
|
1141
|
+
children: [
|
|
1142
|
+
question.label,
|
|
1143
|
+
question.required && /* @__PURE__ */ jsx("span", { className: "ml-1 text-xs text-primary", children: "*" })
|
|
1144
|
+
]
|
|
1145
|
+
}
|
|
1146
|
+
),
|
|
1147
|
+
question.helpText && /* @__PURE__ */ jsx("div", { className: "mt-0.5 text-xs text-muted-foreground", children: question.helpText })
|
|
1148
|
+
]
|
|
1149
|
+
}
|
|
1150
|
+
) });
|
|
1151
|
+
}
|
|
1152
|
+
function ConfirmDialog({ open, title, message, onConfirm, onCancel }) {
|
|
1153
|
+
if (!open) return null;
|
|
1154
|
+
return /* @__PURE__ */ jsx(
|
|
1155
|
+
"div",
|
|
1156
|
+
{
|
|
1157
|
+
className: "fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm",
|
|
1158
|
+
onClick: onCancel,
|
|
1159
|
+
children: /* @__PURE__ */ jsxs(
|
|
1160
|
+
"div",
|
|
1161
|
+
{
|
|
1162
|
+
className: "w-96 max-w-[90vw] bg-card border border-border rounded-xl p-6 fcb-shadow-lg",
|
|
1163
|
+
onClick: (e) => e.stopPropagation(),
|
|
1164
|
+
children: [
|
|
1165
|
+
/* @__PURE__ */ jsx("h3", { className: "text-base font-semibold text-foreground mb-2", children: title }),
|
|
1166
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground mb-6 leading-relaxed", children: message }),
|
|
1167
|
+
/* @__PURE__ */ jsxs("div", { className: "flex gap-2 justify-end", children: [
|
|
1168
|
+
/* @__PURE__ */ jsx(Button, { variant: "outline", onClick: onCancel, children: "Cancel" }),
|
|
1169
|
+
/* @__PURE__ */ jsx(Button, { variant: "destructive", onClick: onConfirm, children: "Delete" })
|
|
1170
|
+
] })
|
|
1171
|
+
]
|
|
1172
|
+
}
|
|
1173
|
+
)
|
|
1174
|
+
}
|
|
1175
|
+
);
|
|
1176
|
+
}
|
|
1177
|
+
function SectionBlock({ section, builderState }) {
|
|
1178
|
+
const [confirmDelete, setConfirmDelete] = useState(false);
|
|
1179
|
+
const { setNodeRef } = useDroppable({
|
|
1180
|
+
id: `section-end-${section.id}`,
|
|
1181
|
+
data: { type: "section", sectionId: section.id, index: section.questions.length }
|
|
1182
|
+
});
|
|
1183
|
+
const handleAddQuestion = () => {
|
|
1184
|
+
const newQuestion = {
|
|
1185
|
+
id: generateQuestionId(),
|
|
1186
|
+
type: "short_text",
|
|
1187
|
+
label: "New Question",
|
|
1188
|
+
config: { type: "short_text", maxLength: 255 }
|
|
1189
|
+
};
|
|
1190
|
+
builderState.addQuestion(section.id, newQuestion, section.questions.length);
|
|
1191
|
+
builderState.selectQuestion(section.id, newQuestion.id);
|
|
1192
|
+
};
|
|
1193
|
+
const isSelected = builderState.selectedItem?.type === "section" && builderState.selectedItem.sectionId === section.id;
|
|
1194
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1195
|
+
/* @__PURE__ */ jsxs(
|
|
1196
|
+
"div",
|
|
1197
|
+
{
|
|
1198
|
+
className: cn(
|
|
1199
|
+
"mb-4 p-4 rounded-lg border transition-colors",
|
|
1200
|
+
isSelected ? "fcb-selected border-primary" : "bg-card border-fcb-border-strong"
|
|
1201
|
+
),
|
|
1202
|
+
onClick: (e) => {
|
|
1203
|
+
if (e.target === e.currentTarget) builderState.selectSection(section.id);
|
|
1204
|
+
},
|
|
1205
|
+
children: [
|
|
1206
|
+
/* @__PURE__ */ jsxs("div", { className: "flex justify-between items-center mb-4 pb-3 border-b border-fcb-border-strong", children: [
|
|
1207
|
+
/* @__PURE__ */ jsxs("div", { className: "flex-1", children: [
|
|
1208
|
+
/* @__PURE__ */ jsx("h3", { className: "text-base font-semibold text-foreground mb-0.5", children: section.title }),
|
|
1209
|
+
section.description && /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: section.description })
|
|
1210
|
+
] }),
|
|
1211
|
+
/* @__PURE__ */ jsxs("div", { className: "flex gap-2 shrink-0 ml-3", children: [
|
|
1212
|
+
/* @__PURE__ */ jsxs(Button, { variant: "secondary", size: "sm", onClick: handleAddQuestion, children: [
|
|
1213
|
+
/* @__PURE__ */ jsx(Plus, { size: 13, strokeWidth: 2 }),
|
|
1214
|
+
"Add Field"
|
|
1215
|
+
] }),
|
|
1216
|
+
/* @__PURE__ */ jsx(
|
|
1217
|
+
Button,
|
|
1218
|
+
{
|
|
1219
|
+
variant: "ghost",
|
|
1220
|
+
size: "icon-sm",
|
|
1221
|
+
onClick: () => builderState.duplicateSection(section.id),
|
|
1222
|
+
title: "Duplicate section",
|
|
1223
|
+
"aria-label": "Duplicate section",
|
|
1224
|
+
children: /* @__PURE__ */ jsx(Copy, { size: 14, strokeWidth: 1.75 })
|
|
1225
|
+
}
|
|
1226
|
+
),
|
|
1227
|
+
/* @__PURE__ */ jsx(
|
|
1228
|
+
Button,
|
|
1229
|
+
{
|
|
1230
|
+
variant: "ghost",
|
|
1231
|
+
size: "icon-sm",
|
|
1232
|
+
onClick: () => setConfirmDelete(true),
|
|
1233
|
+
className: "hover:bg-destructive/10 hover:text-destructive",
|
|
1234
|
+
title: "Delete section",
|
|
1235
|
+
"aria-label": "Delete section",
|
|
1236
|
+
children: /* @__PURE__ */ jsx(Trash2, { size: 14, strokeWidth: 1.75 })
|
|
1237
|
+
}
|
|
1238
|
+
)
|
|
1239
|
+
] })
|
|
1240
|
+
] }),
|
|
1241
|
+
section.questions.length === 0 ? /* @__PURE__ */ jsx(
|
|
1242
|
+
"div",
|
|
1243
|
+
{
|
|
1244
|
+
ref: setNodeRef,
|
|
1245
|
+
className: "py-8 text-center text-sm text-muted-foreground border border-dashed border-fcb-border-strong rounded-md",
|
|
1246
|
+
children: 'Drag a field from the palette or click "Add Field"'
|
|
1247
|
+
}
|
|
1248
|
+
) : /* @__PURE__ */ jsxs("div", { children: [
|
|
1249
|
+
section.questions.map((question, index) => {
|
|
1250
|
+
const isQuestionSelected = builderState.selectedItem?.type === "question" && builderState.selectedItem.sectionId === section.id && builderState.selectedItem.questionId === question.id;
|
|
1251
|
+
return /* @__PURE__ */ jsx(
|
|
1252
|
+
QuestionBlock,
|
|
1253
|
+
{
|
|
1254
|
+
question,
|
|
1255
|
+
sectionId: section.id,
|
|
1256
|
+
questionIndex: index,
|
|
1257
|
+
isSelected: isQuestionSelected,
|
|
1258
|
+
builderState
|
|
1259
|
+
},
|
|
1260
|
+
question.id
|
|
1261
|
+
);
|
|
1262
|
+
}),
|
|
1263
|
+
/* @__PURE__ */ jsx(
|
|
1264
|
+
"div",
|
|
1265
|
+
{
|
|
1266
|
+
ref: setNodeRef,
|
|
1267
|
+
className: "h-2 mt-1 rounded-sm transition-colors"
|
|
1268
|
+
}
|
|
1269
|
+
)
|
|
1270
|
+
] })
|
|
1271
|
+
]
|
|
1272
|
+
}
|
|
1273
|
+
),
|
|
1274
|
+
/* @__PURE__ */ jsx(
|
|
1275
|
+
ConfirmDialog,
|
|
1276
|
+
{
|
|
1277
|
+
open: confirmDelete,
|
|
1278
|
+
title: "Delete Section",
|
|
1279
|
+
message: `Delete "${section.title}" and all its ${section.questions.length} question${section.questions.length !== 1 ? "s" : ""}? This cannot be undone.`,
|
|
1280
|
+
onConfirm: () => {
|
|
1281
|
+
setConfirmDelete(false);
|
|
1282
|
+
builderState.removeSection(section.id);
|
|
1283
|
+
},
|
|
1284
|
+
onCancel: () => setConfirmDelete(false)
|
|
1285
|
+
}
|
|
1286
|
+
)
|
|
1287
|
+
] });
|
|
1288
|
+
}
|
|
1289
|
+
function FormCanvas({ builderState }) {
|
|
1290
|
+
const { schema } = builderState;
|
|
1291
|
+
const handleAddSection = () => {
|
|
1292
|
+
const newSection = {
|
|
1293
|
+
id: generateSectionId(),
|
|
1294
|
+
title: `Section ${schema.sections.length + 1}`,
|
|
1295
|
+
description: "",
|
|
1296
|
+
questions: []
|
|
1297
|
+
};
|
|
1298
|
+
builderState.addSection(newSection, schema.sections.length);
|
|
1299
|
+
builderState.selectSection(newSection.id);
|
|
1300
|
+
};
|
|
1301
|
+
return /* @__PURE__ */ jsx("div", { className: "flex-1 h-full overflow-y-auto bg-background fcb-canvas scrollbar-thin", children: /* @__PURE__ */ jsxs("div", { className: "p-6 max-w-3xl mx-auto", children: [
|
|
1302
|
+
/* @__PURE__ */ jsxs("div", { className: "mb-8", children: [
|
|
1303
|
+
/* @__PURE__ */ jsx(
|
|
1304
|
+
"input",
|
|
1305
|
+
{
|
|
1306
|
+
type: "text",
|
|
1307
|
+
value: schema.title,
|
|
1308
|
+
onChange: (e) => {
|
|
1309
|
+
builderState.updateSchema({ ...schema, title: e.target.value });
|
|
1310
|
+
},
|
|
1311
|
+
className: "w-full text-xl font-bold bg-transparent text-foreground border-0 outline-none p-1.5 rounded-md focus:bg-card transition-colors",
|
|
1312
|
+
placeholder: "Form Title"
|
|
1313
|
+
}
|
|
1314
|
+
),
|
|
1315
|
+
/* @__PURE__ */ jsx(
|
|
1316
|
+
"textarea",
|
|
1317
|
+
{
|
|
1318
|
+
value: schema.description ?? "",
|
|
1319
|
+
onChange: (e) => {
|
|
1320
|
+
builderState.updateSchema({ ...schema, description: e.target.value });
|
|
1321
|
+
},
|
|
1322
|
+
className: "w-full text-sm text-muted-foreground bg-transparent border-0 outline-none p-1.5 resize-y min-h-12 rounded-md leading-relaxed focus:bg-card transition-colors",
|
|
1323
|
+
placeholder: "Form description (optional)"
|
|
1324
|
+
}
|
|
1325
|
+
)
|
|
1326
|
+
] }),
|
|
1327
|
+
schema.sections.length === 0 ? /* @__PURE__ */ jsxs("div", { className: "py-16 text-center border border-dashed border-fcb-border-strong rounded-lg text-muted-foreground", children: [
|
|
1328
|
+
/* @__PURE__ */ jsx("p", { className: "text-base mb-4", children: "No sections yet" }),
|
|
1329
|
+
/* @__PURE__ */ jsxs(Button, { onClick: handleAddSection, className: "fcb-glow", children: [
|
|
1330
|
+
/* @__PURE__ */ jsx(Plus, { size: 16, strokeWidth: 2 }),
|
|
1331
|
+
"Add First Section"
|
|
1332
|
+
] })
|
|
1333
|
+
] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1334
|
+
schema.sections.map((section, index) => /* @__PURE__ */ jsx(
|
|
1335
|
+
SectionBlock,
|
|
1336
|
+
{
|
|
1337
|
+
section,
|
|
1338
|
+
sectionIndex: index,
|
|
1339
|
+
builderState
|
|
1340
|
+
},
|
|
1341
|
+
section.id
|
|
1342
|
+
)),
|
|
1343
|
+
/* @__PURE__ */ jsxs(
|
|
1344
|
+
"button",
|
|
1345
|
+
{
|
|
1346
|
+
type: "button",
|
|
1347
|
+
onClick: handleAddSection,
|
|
1348
|
+
className: "w-full py-4 text-sm font-medium rounded-lg border border-dashed border-fcb-border-strong text-muted-foreground bg-transparent flex items-center justify-center gap-2 cursor-pointer transition-colors hover:border-primary hover:text-primary hover:bg-primary/5",
|
|
1349
|
+
children: [
|
|
1350
|
+
/* @__PURE__ */ jsx(Plus, { size: 14, strokeWidth: 2 }),
|
|
1351
|
+
"Add Section"
|
|
1352
|
+
]
|
|
1353
|
+
}
|
|
1354
|
+
)
|
|
1355
|
+
] })
|
|
1356
|
+
] }) });
|
|
1357
|
+
}
|
|
1358
|
+
function useConfigUpdater(question, onUpdate) {
|
|
1359
|
+
return (field, value) => {
|
|
1360
|
+
const current = question.config ?? {};
|
|
1361
|
+
onUpdate({ config: { ...current, type: question.type, [field]: value } });
|
|
1362
|
+
};
|
|
1363
|
+
}
|
|
1364
|
+
function QuestionConfigEditor({ question, onUpdate }) {
|
|
1365
|
+
const updateConfig = useConfigUpdater(question, onUpdate);
|
|
1366
|
+
const config = question.config ?? {};
|
|
1367
|
+
switch (question.type) {
|
|
1368
|
+
// ── Text ──
|
|
1369
|
+
case "short_text":
|
|
1370
|
+
return /* @__PURE__ */ jsxs(ConfigSection, { title: "Text Settings", children: [
|
|
1371
|
+
/* @__PURE__ */ jsx(NumberField, { label: "Max Length", value: config.maxLength, placeholder: "255", onChange: (v) => updateConfig("maxLength", v) }),
|
|
1372
|
+
/* @__PURE__ */ jsx(SelectField, { label: "Input Type", value: config.inputType ?? "text", options: [{ label: "Text", value: "text" }, { label: "Password", value: "password" }], onChange: (v) => updateConfig("inputType", v) }),
|
|
1373
|
+
/* @__PURE__ */ jsx(TextField, { label: "Prefix", value: config.prefix, placeholder: "e.g. $", onChange: (v) => updateConfig("prefix", v) }),
|
|
1374
|
+
/* @__PURE__ */ jsx(TextField, { label: "Suffix", value: config.suffix, placeholder: "e.g. USD", onChange: (v) => updateConfig("suffix", v) })
|
|
1375
|
+
] });
|
|
1376
|
+
case "long_text":
|
|
1377
|
+
return /* @__PURE__ */ jsxs(ConfigSection, { title: "Text Settings", children: [
|
|
1378
|
+
/* @__PURE__ */ jsx(NumberField, { label: "Max Length", value: config.maxLength, placeholder: "No limit", onChange: (v) => updateConfig("maxLength", v) }),
|
|
1379
|
+
/* @__PURE__ */ jsx(NumberField, { label: "Rows", value: config.rows, placeholder: "4", onChange: (v) => updateConfig("rows", v) }),
|
|
1380
|
+
/* @__PURE__ */ jsx(ToggleField, { label: "Show Character Count", checked: !!config.showCharCount, onChange: (v) => updateConfig("showCharCount", v) })
|
|
1381
|
+
] });
|
|
1382
|
+
case "legal_name":
|
|
1383
|
+
return /* @__PURE__ */ jsxs(ConfigSection, { title: "Name Fields", children: [
|
|
1384
|
+
/* @__PURE__ */ jsx(ToggleField, { label: "Show Middle Name", checked: !!config.showMiddleName, onChange: (v) => updateConfig("showMiddleName", v) }),
|
|
1385
|
+
/* @__PURE__ */ jsx(ToggleField, { label: "Show Suffix", checked: !!config.showSuffix, onChange: (v) => updateConfig("showSuffix", v) })
|
|
1386
|
+
] });
|
|
1387
|
+
// ── Numeric ──
|
|
1388
|
+
case "number":
|
|
1389
|
+
return /* @__PURE__ */ jsxs(ConfigSection, { title: "Number Settings", children: [
|
|
1390
|
+
/* @__PURE__ */ jsx(NumberField, { label: "Min", value: config.min, onChange: (v) => updateConfig("min", v) }),
|
|
1391
|
+
/* @__PURE__ */ jsx(NumberField, { label: "Max", value: config.max, onChange: (v) => updateConfig("max", v) }),
|
|
1392
|
+
/* @__PURE__ */ jsx(NumberField, { label: "Step", value: config.step, placeholder: "1", onChange: (v) => updateConfig("step", v) }),
|
|
1393
|
+
/* @__PURE__ */ jsx(NumberField, { label: "Decimal Places", value: config.decimalPlaces, placeholder: "0", onChange: (v) => updateConfig("decimalPlaces", v) }),
|
|
1394
|
+
/* @__PURE__ */ jsx(TextField, { label: "Prefix", value: config.prefix, placeholder: "e.g. $", onChange: (v) => updateConfig("prefix", v) }),
|
|
1395
|
+
/* @__PURE__ */ jsx(TextField, { label: "Suffix", value: config.suffix, placeholder: "e.g. kg", onChange: (v) => updateConfig("suffix", v) })
|
|
1396
|
+
] });
|
|
1397
|
+
case "slider":
|
|
1398
|
+
return /* @__PURE__ */ jsxs(ConfigSection, { title: "Slider Settings", children: [
|
|
1399
|
+
/* @__PURE__ */ jsx(NumberField, { label: "Min", value: config.min, placeholder: "0", onChange: (v) => updateConfig("min", v) }),
|
|
1400
|
+
/* @__PURE__ */ jsx(NumberField, { label: "Max", value: config.max, placeholder: "100", onChange: (v) => updateConfig("max", v) }),
|
|
1401
|
+
/* @__PURE__ */ jsx(NumberField, { label: "Step", value: config.step, placeholder: "1", onChange: (v) => updateConfig("step", v) }),
|
|
1402
|
+
/* @__PURE__ */ jsx(ToggleField, { label: "Show Value", checked: !!config.showValue, onChange: (v) => updateConfig("showValue", v) }),
|
|
1403
|
+
/* @__PURE__ */ jsx(TextField, { label: "Min Label", value: config.minLabel, onChange: (v) => updateConfig("minLabel", v) }),
|
|
1404
|
+
/* @__PURE__ */ jsx(TextField, { label: "Max Label", value: config.maxLabel, onChange: (v) => updateConfig("maxLabel", v) })
|
|
1405
|
+
] });
|
|
1406
|
+
case "rating":
|
|
1407
|
+
return /* @__PURE__ */ jsxs(ConfigSection, { title: "Rating Settings", children: [
|
|
1408
|
+
/* @__PURE__ */ jsx(NumberField, { label: "Max Stars", value: config.max, placeholder: "5", onChange: (v) => updateConfig("max", v) }),
|
|
1409
|
+
/* @__PURE__ */ jsx(SelectField, { label: "Icon", value: config.icon ?? "star", options: [{ label: "Star", value: "star" }, { label: "Heart", value: "heart" }, { label: "Circle", value: "circle" }], onChange: (v) => updateConfig("icon", v) }),
|
|
1410
|
+
/* @__PURE__ */ jsx(ToggleField, { label: "Show Labels", checked: !!config.showLabels, onChange: (v) => updateConfig("showLabels", v) })
|
|
1411
|
+
] });
|
|
1412
|
+
case "nps":
|
|
1413
|
+
return /* @__PURE__ */ jsxs(ConfigSection, { title: "NPS Settings", children: [
|
|
1414
|
+
/* @__PURE__ */ jsx(TextField, { label: "Low Label", value: config.lowLabel ?? "Not likely", onChange: (v) => updateConfig("lowLabel", v) }),
|
|
1415
|
+
/* @__PURE__ */ jsx(TextField, { label: "High Label", value: config.highLabel ?? "Very likely", onChange: (v) => updateConfig("highLabel", v) })
|
|
1416
|
+
] });
|
|
1417
|
+
case "opinion_scale":
|
|
1418
|
+
return /* @__PURE__ */ jsxs(ConfigSection, { title: "Scale Settings", children: [
|
|
1419
|
+
/* @__PURE__ */ jsx(NumberField, { label: "Min", value: config.min, placeholder: "1", onChange: (v) => updateConfig("min", v) }),
|
|
1420
|
+
/* @__PURE__ */ jsx(NumberField, { label: "Max", value: config.max, placeholder: "5", onChange: (v) => updateConfig("max", v) }),
|
|
1421
|
+
/* @__PURE__ */ jsx(NumberField, { label: "Step", value: config.step, placeholder: "1", onChange: (v) => updateConfig("step", v) }),
|
|
1422
|
+
/* @__PURE__ */ jsx(TextField, { label: "Min Label", value: config.minLabel, onChange: (v) => updateConfig("minLabel", v) }),
|
|
1423
|
+
/* @__PURE__ */ jsx(TextField, { label: "Max Label", value: config.maxLabel, onChange: (v) => updateConfig("maxLabel", v) })
|
|
1424
|
+
] });
|
|
1425
|
+
case "likert":
|
|
1426
|
+
return /* @__PURE__ */ jsx(ConfigSection, { title: "Likert Settings", children: /* @__PURE__ */ jsx(LikertLabelsEditor, { labels: config.labels ?? ["Strongly Disagree", "Disagree", "Neutral", "Agree", "Strongly Agree"], onChange: (v) => updateConfig("labels", v) }) });
|
|
1427
|
+
// ── Selection ──
|
|
1428
|
+
case "single_select":
|
|
1429
|
+
return /* @__PURE__ */ jsxs(ConfigSection, { title: "Select Settings", children: [
|
|
1430
|
+
/* @__PURE__ */ jsx(SelectField, { label: "Layout", value: config.layout ?? "vertical", options: [{ label: "Vertical", value: "vertical" }, { label: "Horizontal", value: "horizontal" }, { label: "Grid", value: "grid" }], onChange: (v) => updateConfig("layout", v) }),
|
|
1431
|
+
/* @__PURE__ */ jsx(ToggleField, { label: "Allow Other", checked: !!config.allowOther, onChange: (v) => updateConfig("allowOther", v) }),
|
|
1432
|
+
!!config.allowOther && /* @__PURE__ */ jsx(TextField, { label: "Other Label", value: config.otherLabel ?? "Other", onChange: (v) => updateConfig("otherLabel", v) })
|
|
1433
|
+
] });
|
|
1434
|
+
case "multi_select":
|
|
1435
|
+
return /* @__PURE__ */ jsxs(ConfigSection, { title: "Multi-Select Settings", children: [
|
|
1436
|
+
/* @__PURE__ */ jsx(SelectField, { label: "Layout", value: config.layout ?? "vertical", options: [{ label: "Vertical", value: "vertical" }, { label: "Horizontal", value: "horizontal" }, { label: "Grid", value: "grid" }], onChange: (v) => updateConfig("layout", v) }),
|
|
1437
|
+
/* @__PURE__ */ jsx(NumberField, { label: "Min Selections", value: config.minSelections, onChange: (v) => updateConfig("minSelections", v) }),
|
|
1438
|
+
/* @__PURE__ */ jsx(NumberField, { label: "Max Selections", value: config.maxSelections, onChange: (v) => updateConfig("maxSelections", v) }),
|
|
1439
|
+
/* @__PURE__ */ jsx(ToggleField, { label: "Allow Other", checked: !!config.allowOther, onChange: (v) => updateConfig("allowOther", v) }),
|
|
1440
|
+
!!config.allowOther && /* @__PURE__ */ jsx(TextField, { label: "Other Label", value: config.otherLabel ?? "Other", onChange: (v) => updateConfig("otherLabel", v) })
|
|
1441
|
+
] });
|
|
1442
|
+
case "dropdown":
|
|
1443
|
+
return /* @__PURE__ */ jsxs(ConfigSection, { title: "Dropdown Settings", children: [
|
|
1444
|
+
/* @__PURE__ */ jsx(ToggleField, { label: "Searchable", checked: !!config.searchable, onChange: (v) => updateConfig("searchable", v) }),
|
|
1445
|
+
/* @__PURE__ */ jsx(ToggleField, { label: "Allow Other", checked: !!config.allowOther, onChange: (v) => updateConfig("allowOther", v) }),
|
|
1446
|
+
/* @__PURE__ */ jsx(ToggleField, { label: "Multiple", checked: !!config.multiple, onChange: (v) => updateConfig("multiple", v) })
|
|
1447
|
+
] });
|
|
1448
|
+
case "boolean":
|
|
1449
|
+
return /* @__PURE__ */ jsxs(ConfigSection, { title: "Yes/No Settings", children: [
|
|
1450
|
+
/* @__PURE__ */ jsx(SelectField, { label: "Style", value: config.style ?? "toggle", options: [{ label: "Toggle", value: "toggle" }, { label: "Radio", value: "radio" }, { label: "Checkbox", value: "checkbox" }], onChange: (v) => updateConfig("style", v) }),
|
|
1451
|
+
/* @__PURE__ */ jsx(TextField, { label: "True Label", value: config.trueLabel ?? "Yes", onChange: (v) => updateConfig("trueLabel", v) }),
|
|
1452
|
+
/* @__PURE__ */ jsx(TextField, { label: "False Label", value: config.falseLabel ?? "No", onChange: (v) => updateConfig("falseLabel", v) })
|
|
1453
|
+
] });
|
|
1454
|
+
case "country_select":
|
|
1455
|
+
return /* @__PURE__ */ jsxs(ConfigSection, { title: "Country Settings", children: [
|
|
1456
|
+
/* @__PURE__ */ jsx(ToggleField, { label: "Show Flags", checked: config.showFlags !== false, onChange: (v) => updateConfig("showFlags", v) }),
|
|
1457
|
+
/* @__PURE__ */ jsx(TextField, { label: "Priority Countries", value: config.priorityCountries?.join(", "), placeholder: "e.g. US, GB, CA", onChange: (v) => updateConfig("priorityCountries", v ? v.split(",").map((s) => s.trim()).filter(Boolean) : void 0) }),
|
|
1458
|
+
/* @__PURE__ */ jsx(TextField, { label: "Exclude Countries", value: config.excludeCountries?.join(", "), placeholder: "e.g. XX, YY", onChange: (v) => updateConfig("excludeCountries", v ? v.split(",").map((s) => s.trim()).filter(Boolean) : void 0) })
|
|
1459
|
+
] });
|
|
1460
|
+
// ── Date/Time ──
|
|
1461
|
+
case "date":
|
|
1462
|
+
return /* @__PURE__ */ jsxs(ConfigSection, { title: "Date Settings", children: [
|
|
1463
|
+
/* @__PURE__ */ jsx(TextField, { label: "Min Date", value: config.minDate, placeholder: "YYYY-MM-DD", onChange: (v) => updateConfig("minDate", v) }),
|
|
1464
|
+
/* @__PURE__ */ jsx(TextField, { label: "Max Date", value: config.maxDate, placeholder: "YYYY-MM-DD", onChange: (v) => updateConfig("maxDate", v) }),
|
|
1465
|
+
/* @__PURE__ */ jsx(ToggleField, { label: "Disable Past Dates", checked: !!config.disablePast, onChange: (v) => updateConfig("disablePast", v) }),
|
|
1466
|
+
/* @__PURE__ */ jsx(ToggleField, { label: "Disable Future Dates", checked: !!config.disableFuture, onChange: (v) => updateConfig("disableFuture", v) }),
|
|
1467
|
+
/* @__PURE__ */ jsx(TextField, { label: "Format", value: config.format, placeholder: "e.g. MM/DD/YYYY", onChange: (v) => updateConfig("format", v) })
|
|
1468
|
+
] });
|
|
1469
|
+
case "time":
|
|
1470
|
+
return /* @__PURE__ */ jsxs(ConfigSection, { title: "Time Settings", children: [
|
|
1471
|
+
/* @__PURE__ */ jsx(SelectField, { label: "Format", value: config.format ?? "12h", options: [{ label: "12 Hour", value: "12h" }, { label: "24 Hour", value: "24h" }], onChange: (v) => updateConfig("format", v) }),
|
|
1472
|
+
/* @__PURE__ */ jsx(NumberField, { label: "Minute Step", value: config.minuteStep, placeholder: "1", onChange: (v) => updateConfig("minuteStep", v) })
|
|
1473
|
+
] });
|
|
1474
|
+
case "date_range":
|
|
1475
|
+
return /* @__PURE__ */ jsxs(ConfigSection, { title: "Date Range Settings", children: [
|
|
1476
|
+
/* @__PURE__ */ jsx(TextField, { label: "Min Date", value: config.minDate, placeholder: "YYYY-MM-DD", onChange: (v) => updateConfig("minDate", v) }),
|
|
1477
|
+
/* @__PURE__ */ jsx(TextField, { label: "Max Date", value: config.maxDate, placeholder: "YYYY-MM-DD", onChange: (v) => updateConfig("maxDate", v) }),
|
|
1478
|
+
/* @__PURE__ */ jsx(NumberField, { label: "Max Range (days)", value: config.maxRangeDays, onChange: (v) => updateConfig("maxRangeDays", v) })
|
|
1479
|
+
] });
|
|
1480
|
+
case "appointment":
|
|
1481
|
+
return /* @__PURE__ */ jsxs(ConfigSection, { title: "Appointment Settings", children: [
|
|
1482
|
+
/* @__PURE__ */ jsx(NumberField, { label: "Duration (min)", value: config.duration ?? 30, onChange: (v) => updateConfig("duration", v) }),
|
|
1483
|
+
/* @__PURE__ */ jsx(TextField, { label: "Timezone", value: config.timezone, placeholder: "e.g. America/New_York", onChange: (v) => updateConfig("timezone", v) }),
|
|
1484
|
+
/* @__PURE__ */ jsx(TextField, { label: "Slots URL", value: config.slotsUrl, placeholder: "API endpoint for available slots", onChange: (v) => updateConfig("slotsUrl", v) })
|
|
1485
|
+
] });
|
|
1486
|
+
// ── Media ──
|
|
1487
|
+
case "file_upload":
|
|
1488
|
+
return /* @__PURE__ */ jsxs(ConfigSection, { title: "Upload Settings", children: [
|
|
1489
|
+
/* @__PURE__ */ jsx(NumberField, { label: "Max Files", value: config.maxFiles ?? 1, onChange: (v) => updateConfig("maxFiles", v) }),
|
|
1490
|
+
/* @__PURE__ */ jsx(NumberField, { label: "Max Size (MB)", value: config.maxSizeMb ?? 10, onChange: (v) => updateConfig("maxSizeMb", v) }),
|
|
1491
|
+
/* @__PURE__ */ jsx(TextField, { label: "Accepted Types", value: config.accept?.join(", "), placeholder: "e.g. .pdf, .jpg, .png", onChange: (v) => updateConfig("accept", v ? v.split(",").map((s) => s.trim()) : void 0) }),
|
|
1492
|
+
/* @__PURE__ */ jsx(TextField, { label: "Upload URL", value: config.uploadUrl, placeholder: "Server upload endpoint", onChange: (v) => updateConfig("uploadUrl", v) })
|
|
1493
|
+
] });
|
|
1494
|
+
case "signature":
|
|
1495
|
+
return /* @__PURE__ */ jsxs(ConfigSection, { title: "Signature Settings", children: [
|
|
1496
|
+
/* @__PURE__ */ jsx(TextField, { label: "Pen Color", value: config.penColor, placeholder: "#000000", onChange: (v) => updateConfig("penColor", v) }),
|
|
1497
|
+
/* @__PURE__ */ jsx(TextField, { label: "Background", value: config.backgroundColor, placeholder: "#ffffff", onChange: (v) => updateConfig("backgroundColor", v) }),
|
|
1498
|
+
/* @__PURE__ */ jsx(NumberField, { label: "Width (px)", value: config.width, onChange: (v) => updateConfig("width", v) }),
|
|
1499
|
+
/* @__PURE__ */ jsx(NumberField, { label: "Height (px)", value: config.height, onChange: (v) => updateConfig("height", v) })
|
|
1500
|
+
] });
|
|
1501
|
+
case "image_capture":
|
|
1502
|
+
return /* @__PURE__ */ jsxs(ConfigSection, { title: "Camera Settings", children: [
|
|
1503
|
+
/* @__PURE__ */ jsx(SelectField, { label: "Camera", value: config.camera ?? "any", options: [{ label: "Any", value: "any" }, { label: "Front", value: "front" }, { label: "Back", value: "back" }], onChange: (v) => updateConfig("camera", v) }),
|
|
1504
|
+
/* @__PURE__ */ jsx(NumberField, { label: "Max Size (MB)", value: config.maxSizeMb, onChange: (v) => updateConfig("maxSizeMb", v) }),
|
|
1505
|
+
/* @__PURE__ */ jsx(ToggleField, { label: "Allow Gallery", checked: config.allowGallery !== false, onChange: (v) => updateConfig("allowGallery", v) })
|
|
1506
|
+
] });
|
|
1507
|
+
// ── Content & Visual ──
|
|
1508
|
+
case "welcome-screen":
|
|
1509
|
+
return /* @__PURE__ */ jsxs(ConfigSection, { title: "Welcome Screen", children: [
|
|
1510
|
+
/* @__PURE__ */ jsx(TextField, { label: "Heading", value: config.heading ?? "Welcome", onChange: (v) => updateConfig("heading", v) }),
|
|
1511
|
+
/* @__PURE__ */ jsx(TextareaField, { label: "Description", value: config.description, onChange: (v) => updateConfig("description", v) }),
|
|
1512
|
+
/* @__PURE__ */ jsx(TextField, { label: "Button Text", value: config.buttonText ?? "Start", onChange: (v) => updateConfig("buttonText", v) }),
|
|
1513
|
+
/* @__PURE__ */ jsx(TextField, { label: "Image URL", value: config.imageUrl, placeholder: "https://...", onChange: (v) => updateConfig("imageUrl", v) }),
|
|
1514
|
+
/* @__PURE__ */ jsx(SelectField, { label: "Alignment", value: config.alignment ?? "center", options: [{ label: "Left", value: "left" }, { label: "Center", value: "center" }, { label: "Right", value: "right" }], onChange: (v) => updateConfig("alignment", v) })
|
|
1515
|
+
] });
|
|
1516
|
+
case "thank-you-screen":
|
|
1517
|
+
return /* @__PURE__ */ jsxs(ConfigSection, { title: "Thank You Screen", children: [
|
|
1518
|
+
/* @__PURE__ */ jsx(TextField, { label: "Heading", value: config.heading ?? "Thank You!", onChange: (v) => updateConfig("heading", v) }),
|
|
1519
|
+
/* @__PURE__ */ jsx(TextareaField, { label: "Description", value: config.description ?? "", onChange: (v) => updateConfig("description", v) }),
|
|
1520
|
+
/* @__PURE__ */ jsx(TextField, { label: "Image URL", value: config.imageUrl, placeholder: "https://...", onChange: (v) => updateConfig("imageUrl", v) }),
|
|
1521
|
+
/* @__PURE__ */ jsx(TextField, { label: "Redirect URL", value: config.redirectUrl, placeholder: "https://...", onChange: (v) => updateConfig("redirectUrl", v) }),
|
|
1522
|
+
/* @__PURE__ */ jsx(NumberField, { label: "Redirect Delay (s)", value: config.redirectDelay, placeholder: "0", onChange: (v) => updateConfig("redirectDelay", v) }),
|
|
1523
|
+
/* @__PURE__ */ jsx(ToggleField, { label: "Show Response Summary", checked: !!config.showSummary, onChange: (v) => updateConfig("showSummary", v) })
|
|
1524
|
+
] });
|
|
1525
|
+
case "rich-text":
|
|
1526
|
+
return /* @__PURE__ */ jsxs(ConfigSection, { title: "Rich Text", children: [
|
|
1527
|
+
/* @__PURE__ */ jsx(TextareaField, { label: "Content", value: config.content ?? "", rows: 6, onChange: (v) => updateConfig("content", v) }),
|
|
1528
|
+
/* @__PURE__ */ jsx(SelectField, { label: "Format", value: config.format ?? "html", options: [{ label: "HTML", value: "html" }, { label: "Markdown", value: "markdown" }], onChange: (v) => updateConfig("format", v) })
|
|
1529
|
+
] });
|
|
1530
|
+
case "image":
|
|
1531
|
+
return /* @__PURE__ */ jsxs(ConfigSection, { title: "Image Settings", children: [
|
|
1532
|
+
/* @__PURE__ */ jsx(TextField, { label: "Source URL", value: config.src ?? "", placeholder: "https://...", onChange: (v) => updateConfig("src", v) }),
|
|
1533
|
+
/* @__PURE__ */ jsx(TextField, { label: "Alt Text", value: config.alt ?? "", onChange: (v) => updateConfig("alt", v) }),
|
|
1534
|
+
/* @__PURE__ */ jsx(TextField, { label: "Caption", value: config.caption, onChange: (v) => updateConfig("caption", v) }),
|
|
1535
|
+
/* @__PURE__ */ jsx(SelectField, { label: "Alignment", value: config.alignment ?? "center", options: [{ label: "Left", value: "left" }, { label: "Center", value: "center" }, { label: "Right", value: "right" }], onChange: (v) => updateConfig("alignment", v) }),
|
|
1536
|
+
/* @__PURE__ */ jsx(TextField, { label: "Width", value: config.width, placeholder: "e.g. 100% or 300px", onChange: (v) => updateConfig("width", v) }),
|
|
1537
|
+
/* @__PURE__ */ jsx(TextField, { label: "Link URL", value: config.link, placeholder: "Click opens this URL", onChange: (v) => updateConfig("link", v) })
|
|
1538
|
+
] });
|
|
1539
|
+
case "video":
|
|
1540
|
+
return /* @__PURE__ */ jsxs(ConfigSection, { title: "Video Settings", children: [
|
|
1541
|
+
/* @__PURE__ */ jsx(TextField, { label: "Source URL", value: config.src ?? "", placeholder: "YouTube/Vimeo URL", onChange: (v) => updateConfig("src", v) }),
|
|
1542
|
+
/* @__PURE__ */ jsx(SelectField, { label: "Provider", value: config.provider ?? "youtube", options: [{ label: "YouTube", value: "youtube" }, { label: "Vimeo", value: "vimeo" }, { label: "Direct URL", value: "url" }], onChange: (v) => updateConfig("provider", v) }),
|
|
1543
|
+
/* @__PURE__ */ jsx(ToggleField, { label: "Autoplay", checked: !!config.autoplay, onChange: (v) => updateConfig("autoplay", v) }),
|
|
1544
|
+
/* @__PURE__ */ jsx(ToggleField, { label: "Muted", checked: !!config.muted, onChange: (v) => updateConfig("muted", v) }),
|
|
1545
|
+
/* @__PURE__ */ jsx(TextField, { label: "Width", value: config.width, placeholder: "e.g. 100% or 640px", onChange: (v) => updateConfig("width", v) }),
|
|
1546
|
+
/* @__PURE__ */ jsx(TextField, { label: "Height", value: config.height, placeholder: "e.g. 360px", onChange: (v) => updateConfig("height", v) })
|
|
1547
|
+
] });
|
|
1548
|
+
case "divider":
|
|
1549
|
+
return /* @__PURE__ */ jsxs(ConfigSection, { title: "Divider Settings", children: [
|
|
1550
|
+
/* @__PURE__ */ jsx(SelectField, { label: "Style", value: config.style ?? "solid", options: [{ label: "Solid", value: "solid" }, { label: "Dashed", value: "dashed" }, { label: "Dotted", value: "dotted" }], onChange: (v) => updateConfig("style", v) }),
|
|
1551
|
+
/* @__PURE__ */ jsx(TextField, { label: "Color", value: config.color, placeholder: "#e5e7eb", onChange: (v) => updateConfig("color", v) }),
|
|
1552
|
+
/* @__PURE__ */ jsx(NumberField, { label: "Thickness (px)", value: config.thickness, placeholder: "1", onChange: (v) => updateConfig("thickness", v) }),
|
|
1553
|
+
/* @__PURE__ */ jsx(NumberField, { label: "Spacing (px)", value: config.spacing, placeholder: "16", onChange: (v) => updateConfig("spacing", v) })
|
|
1554
|
+
] });
|
|
1555
|
+
case "spacer":
|
|
1556
|
+
return /* @__PURE__ */ jsx(ConfigSection, { title: "Spacer Settings", children: /* @__PURE__ */ jsx(NumberField, { label: "Height (px)", value: config.height ?? 32, onChange: (v) => updateConfig("height", v) }) });
|
|
1557
|
+
// ── Structural ──
|
|
1558
|
+
case "section_header":
|
|
1559
|
+
return /* @__PURE__ */ jsxs(ConfigSection, { title: "Header Settings", children: [
|
|
1560
|
+
/* @__PURE__ */ jsx(SelectField, { label: "Level", value: config.level ?? "h3", options: [{ label: "H2", value: "h2" }, { label: "H3", value: "h3" }, { label: "H4", value: "h4" }], onChange: (v) => updateConfig("level", v) }),
|
|
1561
|
+
/* @__PURE__ */ jsx(ToggleField, { label: "Show Divider", checked: !!config.showDivider, onChange: (v) => updateConfig("showDivider", v) })
|
|
1562
|
+
] });
|
|
1563
|
+
case "info_block":
|
|
1564
|
+
return /* @__PURE__ */ jsxs(ConfigSection, { title: "Info Block", children: [
|
|
1565
|
+
/* @__PURE__ */ jsx(TextareaField, { label: "Content", value: config.content ?? "", rows: 4, onChange: (v) => updateConfig("content", v) }),
|
|
1566
|
+
/* @__PURE__ */ jsx(SelectField, { label: "Variant", value: config.variant ?? "info", options: [{ label: "Info", value: "info" }, { label: "Warning", value: "warning" }, { label: "Success", value: "success" }, { label: "Error", value: "error" }], onChange: (v) => updateConfig("variant", v) })
|
|
1567
|
+
] });
|
|
1568
|
+
case "page_break":
|
|
1569
|
+
return /* @__PURE__ */ jsx(ConfigSection, { title: "Page Break", children: /* @__PURE__ */ jsx(TextField, { label: "Label", value: config.label, placeholder: "Next page label", onChange: (v) => updateConfig("label", v) }) });
|
|
1570
|
+
case "consent":
|
|
1571
|
+
return /* @__PURE__ */ jsxs(ConfigSection, { title: "Consent Settings", children: [
|
|
1572
|
+
/* @__PURE__ */ jsx(TextareaField, { label: "Consent Text", value: config.text ?? "", rows: 4, onChange: (v) => updateConfig("text", v) }),
|
|
1573
|
+
/* @__PURE__ */ jsx(TextField, { label: "Checkbox Label", value: config.checkboxLabel ?? "I agree", onChange: (v) => updateConfig("checkboxLabel", v) }),
|
|
1574
|
+
/* @__PURE__ */ jsx(TextareaField, { label: "Expandable Text", value: config.expandableText, rows: 3, placeholder: "Additional text shown on expand", onChange: (v) => updateConfig("expandableText", v) })
|
|
1575
|
+
] });
|
|
1576
|
+
// ── Advanced ──
|
|
1577
|
+
case "matrix":
|
|
1578
|
+
return /* @__PURE__ */ jsxs(ConfigSection, { title: "Matrix Settings", children: [
|
|
1579
|
+
/* @__PURE__ */ jsx(SelectField, { label: "Input Type", value: config.inputType ?? "radio", options: [{ label: "Radio", value: "radio" }, { label: "Checkbox", value: "checkbox" }, { label: "Text", value: "text" }, { label: "Number", value: "number" }], onChange: (v) => updateConfig("inputType", v) }),
|
|
1580
|
+
/* @__PURE__ */ jsx(SelectField, { label: "Required", value: config.required ?? "none", options: [{ label: "All rows", value: "all" }, { label: "Any row", value: "any" }, { label: "None", value: "none" }], onChange: (v) => updateConfig("required", v) }),
|
|
1581
|
+
/* @__PURE__ */ jsx(MatrixItemsEditor, { label: "Rows", items: config.rows ?? [], onChange: (v) => updateConfig("rows", v) }),
|
|
1582
|
+
/* @__PURE__ */ jsx(MatrixItemsEditor, { label: "Columns", items: config.columns ?? [], onChange: (v) => updateConfig("columns", v) })
|
|
1583
|
+
] });
|
|
1584
|
+
case "repeater":
|
|
1585
|
+
return /* @__PURE__ */ jsxs(ConfigSection, { title: "Repeater Settings", children: [
|
|
1586
|
+
/* @__PURE__ */ jsx(NumberField, { label: "Min Entries", value: config.minEntries, placeholder: "1", onChange: (v) => updateConfig("minEntries", v) }),
|
|
1587
|
+
/* @__PURE__ */ jsx(NumberField, { label: "Max Entries", value: config.maxEntries, onChange: (v) => updateConfig("maxEntries", v) }),
|
|
1588
|
+
/* @__PURE__ */ jsx(NumberField, { label: "Default Entries", value: config.defaultEntries, placeholder: "1", onChange: (v) => updateConfig("defaultEntries", v) }),
|
|
1589
|
+
/* @__PURE__ */ jsx(TextField, { label: "Add Button Label", value: config.addLabel, placeholder: "Add Entry", onChange: (v) => updateConfig("addLabel", v) }),
|
|
1590
|
+
/* @__PURE__ */ jsx(TextField, { label: "Remove Button Label", value: config.removeLabel, placeholder: "Remove", onChange: (v) => updateConfig("removeLabel", v) }),
|
|
1591
|
+
/* @__PURE__ */ jsx("div", { className: "mt-1 text-[11px] text-muted-foreground leading-relaxed", children: "Sub-fields for each repeater entry are configured by nesting questions inside the repeater in the schema JSON." })
|
|
1592
|
+
] });
|
|
1593
|
+
case "address": {
|
|
1594
|
+
const ADDRESS_FIELDS = [
|
|
1595
|
+
{ label: "Street", value: "street" },
|
|
1596
|
+
{ label: "Street 2", value: "street2" },
|
|
1597
|
+
{ label: "City", value: "city" },
|
|
1598
|
+
{ label: "State", value: "state" },
|
|
1599
|
+
{ label: "ZIP Code", value: "zip" },
|
|
1600
|
+
{ label: "Country", value: "country" }
|
|
1601
|
+
];
|
|
1602
|
+
const activeFields = config.fields ?? ["street", "city", "state", "zip", "country"];
|
|
1603
|
+
return /* @__PURE__ */ jsxs(ConfigSection, { title: "Address Settings", children: [
|
|
1604
|
+
/* @__PURE__ */ jsx(SelectField, { label: "Provider", value: config.provider ?? "none", options: [{ label: "None", value: "none" }, { label: "Google", value: "google" }, { label: "Mapbox", value: "mapbox" }], onChange: (v) => updateConfig("provider", v) }),
|
|
1605
|
+
config.provider !== "none" && /* @__PURE__ */ jsx(TextField, { label: "API Key", value: config.apiKey, placeholder: "Provider API key", onChange: (v) => updateConfig("apiKey", v) }),
|
|
1606
|
+
/* @__PURE__ */ jsx(TextField, { label: "Default Country", value: config.defaultCountry, placeholder: "e.g. US", onChange: (v) => updateConfig("defaultCountry", v) }),
|
|
1607
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
1608
|
+
/* @__PURE__ */ jsx(Label, { className: "text-xs text-muted-foreground mb-2 block", children: "Fields" }),
|
|
1609
|
+
/* @__PURE__ */ jsx("div", { className: "space-y-1.5", children: ADDRESS_FIELDS.map((f) => /* @__PURE__ */ jsx(
|
|
1610
|
+
ToggleField,
|
|
1611
|
+
{
|
|
1612
|
+
label: f.label,
|
|
1613
|
+
checked: activeFields.includes(f.value),
|
|
1614
|
+
onChange: (checked) => {
|
|
1615
|
+
const next = checked ? [...activeFields, f.value] : activeFields.filter((v) => v !== f.value);
|
|
1616
|
+
updateConfig("fields", next.length > 0 ? next : void 0);
|
|
1617
|
+
}
|
|
1618
|
+
},
|
|
1619
|
+
f.value
|
|
1620
|
+
)) })
|
|
1621
|
+
] })
|
|
1622
|
+
] });
|
|
1623
|
+
}
|
|
1624
|
+
case "payment":
|
|
1625
|
+
return /* @__PURE__ */ jsxs(ConfigSection, { title: "Payment Settings", children: [
|
|
1626
|
+
/* @__PURE__ */ jsx(SelectField, { label: "Provider", value: config.provider ?? "stripe", options: [{ label: "Stripe", value: "stripe" }, { label: "PayPal", value: "paypal" }], onChange: (v) => updateConfig("provider", v) }),
|
|
1627
|
+
/* @__PURE__ */ jsx(TextField, { label: "Public Key", value: config.publicKey ?? "", placeholder: "Provider public/client key", onChange: (v) => updateConfig("publicKey", v) }),
|
|
1628
|
+
/* @__PURE__ */ jsx(NumberField, { label: "Amount", value: config.amount, onChange: (v) => updateConfig("amount", v) }),
|
|
1629
|
+
/* @__PURE__ */ jsx(TextField, { label: "Amount Field", value: config.amountField, placeholder: "Field ID for dynamic amount", onChange: (v) => updateConfig("amountField", v) }),
|
|
1630
|
+
/* @__PURE__ */ jsx(TextField, { label: "Currency", value: config.currency, placeholder: "USD", onChange: (v) => updateConfig("currency", v) }),
|
|
1631
|
+
/* @__PURE__ */ jsx(TextField, { label: "Description", value: config.description, onChange: (v) => updateConfig("description", v) })
|
|
1632
|
+
] });
|
|
1633
|
+
case "calculated":
|
|
1634
|
+
return /* @__PURE__ */ jsxs(ConfigSection, { title: "Calculated Field", children: [
|
|
1635
|
+
/* @__PURE__ */ jsx(TextField, { label: "Expression", value: config.expression ?? "", placeholder: "e.g. {q1} + {q2}", onChange: (v) => updateConfig("expression", v) }),
|
|
1636
|
+
/* @__PURE__ */ jsx(SelectField, { label: "Format", value: config.format ?? "number", options: [{ label: "Number", value: "number" }, { label: "Currency", value: "currency" }, { label: "Percentage", value: "percentage" }], onChange: (v) => updateConfig("format", v) }),
|
|
1637
|
+
/* @__PURE__ */ jsx(NumberField, { label: "Decimal Places", value: config.decimalPlaces, placeholder: "2", onChange: (v) => updateConfig("decimalPlaces", v) }),
|
|
1638
|
+
/* @__PURE__ */ jsx(TextField, { label: "Prefix", value: config.prefix, onChange: (v) => updateConfig("prefix", v) }),
|
|
1639
|
+
/* @__PURE__ */ jsx(TextField, { label: "Suffix", value: config.suffix, onChange: (v) => updateConfig("suffix", v) }),
|
|
1640
|
+
/* @__PURE__ */ jsx(ToggleField, { label: "Visible to User", checked: config.visible !== false, onChange: (v) => updateConfig("visible", v) })
|
|
1641
|
+
] });
|
|
1642
|
+
case "hidden":
|
|
1643
|
+
return /* @__PURE__ */ jsxs(ConfigSection, { title: "Hidden Field", children: [
|
|
1644
|
+
/* @__PURE__ */ jsx(SelectField, { label: "Source", value: config.source ?? "static", options: [{ label: "Static Value", value: "static" }, { label: "URL Parameter", value: "url_param" }, { label: "Cookie", value: "cookie" }, { label: "Referrer", value: "referrer" }], onChange: (v) => updateConfig("source", v) }),
|
|
1645
|
+
config.source === "static" && /* @__PURE__ */ jsx(TextField, { label: "Default Value", value: config.defaultValue, onChange: (v) => updateConfig("defaultValue", v) }),
|
|
1646
|
+
config.source === "url_param" && /* @__PURE__ */ jsx(TextField, { label: "Parameter Name", value: config.paramName, placeholder: "e.g. utm_source", onChange: (v) => updateConfig("paramName", v) }),
|
|
1647
|
+
config.source === "cookie" && /* @__PURE__ */ jsx(TextField, { label: "Cookie Name", value: config.cookieName, onChange: (v) => updateConfig("cookieName", v) })
|
|
1648
|
+
] });
|
|
1649
|
+
case "scoring":
|
|
1650
|
+
return /* @__PURE__ */ jsxs(ConfigSection, { title: "Scoring Settings", children: [
|
|
1651
|
+
/* @__PURE__ */ jsx(ToggleField, { label: "Show Score", checked: !!config.showScore, onChange: (v) => updateConfig("showScore", v) }),
|
|
1652
|
+
/* @__PURE__ */ jsx(ScoringOptionsEditor, { options: config.options ?? [], onChange: (v) => updateConfig("options", v) }),
|
|
1653
|
+
/* @__PURE__ */ jsx(ScoreRangesEditor, { ranges: config.scoreRanges ?? [], onChange: (v) => updateConfig("scoreRanges", v) })
|
|
1654
|
+
] });
|
|
1655
|
+
case "ranking":
|
|
1656
|
+
return /* @__PURE__ */ jsx(ConfigSection, { title: "Ranking Settings", children: /* @__PURE__ */ jsx(MatrixItemsEditor, { label: "Items", items: config.items ?? [], onChange: (v) => updateConfig("items", v) }) });
|
|
1657
|
+
// Types with no additional config
|
|
1658
|
+
case "email":
|
|
1659
|
+
case "phone":
|
|
1660
|
+
case "phone_international":
|
|
1661
|
+
case "url":
|
|
1662
|
+
return null;
|
|
1663
|
+
default:
|
|
1664
|
+
return null;
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
function ConfigSection({ title, children }) {
|
|
1668
|
+
return /* @__PURE__ */ jsxs("div", { children: [
|
|
1669
|
+
/* @__PURE__ */ jsx(Separator, { className: "mb-4" }),
|
|
1670
|
+
/* @__PURE__ */ jsx(Label, { className: "text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-3 block", children: title }),
|
|
1671
|
+
/* @__PURE__ */ jsx("div", { className: "space-y-3", children })
|
|
1672
|
+
] });
|
|
1673
|
+
}
|
|
1674
|
+
function TextField({
|
|
1675
|
+
label,
|
|
1676
|
+
value,
|
|
1677
|
+
placeholder,
|
|
1678
|
+
onChange
|
|
1679
|
+
}) {
|
|
1680
|
+
return /* @__PURE__ */ jsxs("div", { children: [
|
|
1681
|
+
/* @__PURE__ */ jsx(Label, { className: "text-xs text-muted-foreground mb-1", children: label }),
|
|
1682
|
+
/* @__PURE__ */ jsx(
|
|
1683
|
+
Input,
|
|
1684
|
+
{
|
|
1685
|
+
value: value ?? "",
|
|
1686
|
+
onChange: (e) => onChange(e.target.value || void 0),
|
|
1687
|
+
placeholder
|
|
1688
|
+
}
|
|
1689
|
+
)
|
|
1690
|
+
] });
|
|
1691
|
+
}
|
|
1692
|
+
function TextareaField({
|
|
1693
|
+
label,
|
|
1694
|
+
value,
|
|
1695
|
+
placeholder,
|
|
1696
|
+
rows,
|
|
1697
|
+
onChange
|
|
1698
|
+
}) {
|
|
1699
|
+
return /* @__PURE__ */ jsxs("div", { children: [
|
|
1700
|
+
/* @__PURE__ */ jsx(Label, { className: "text-xs text-muted-foreground mb-1", children: label }),
|
|
1701
|
+
/* @__PURE__ */ jsx(
|
|
1702
|
+
Textarea,
|
|
1703
|
+
{
|
|
1704
|
+
value: value ?? "",
|
|
1705
|
+
onChange: (e) => onChange(e.target.value || void 0),
|
|
1706
|
+
placeholder,
|
|
1707
|
+
rows: rows ?? 3
|
|
1708
|
+
}
|
|
1709
|
+
)
|
|
1710
|
+
] });
|
|
1711
|
+
}
|
|
1712
|
+
function NumberField({
|
|
1713
|
+
label,
|
|
1714
|
+
value,
|
|
1715
|
+
placeholder,
|
|
1716
|
+
onChange
|
|
1717
|
+
}) {
|
|
1718
|
+
return /* @__PURE__ */ jsxs("div", { children: [
|
|
1719
|
+
/* @__PURE__ */ jsx(Label, { className: "text-xs text-muted-foreground mb-1", children: label }),
|
|
1720
|
+
/* @__PURE__ */ jsx(
|
|
1721
|
+
Input,
|
|
1722
|
+
{
|
|
1723
|
+
type: "number",
|
|
1724
|
+
value: value ?? "",
|
|
1725
|
+
onChange: (e) => {
|
|
1726
|
+
const v = e.target.value;
|
|
1727
|
+
onChange(v === "" ? void 0 : Number(v));
|
|
1728
|
+
},
|
|
1729
|
+
placeholder
|
|
1730
|
+
}
|
|
1731
|
+
)
|
|
1732
|
+
] });
|
|
1733
|
+
}
|
|
1734
|
+
function ToggleField({
|
|
1735
|
+
label,
|
|
1736
|
+
checked,
|
|
1737
|
+
onChange
|
|
1738
|
+
}) {
|
|
1739
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
|
|
1740
|
+
/* @__PURE__ */ jsx(Label, { className: "text-xs text-muted-foreground", children: label }),
|
|
1741
|
+
/* @__PURE__ */ jsx(Switch, { checked, onCheckedChange: onChange })
|
|
1742
|
+
] });
|
|
1743
|
+
}
|
|
1744
|
+
function SelectField({
|
|
1745
|
+
label,
|
|
1746
|
+
value,
|
|
1747
|
+
options,
|
|
1748
|
+
onChange
|
|
1749
|
+
}) {
|
|
1750
|
+
return /* @__PURE__ */ jsxs("div", { children: [
|
|
1751
|
+
/* @__PURE__ */ jsx(Label, { className: "text-xs text-muted-foreground mb-1", children: label }),
|
|
1752
|
+
/* @__PURE__ */ jsxs("div", { className: "relative", children: [
|
|
1753
|
+
/* @__PURE__ */ jsx(
|
|
1754
|
+
"select",
|
|
1755
|
+
{
|
|
1756
|
+
value,
|
|
1757
|
+
onChange: (e) => onChange(e.target.value),
|
|
1758
|
+
className: "flex h-9 w-full appearance-none rounded-md border border-input bg-card px-3 py-1 pr-8 text-sm shadow-xs transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring text-foreground cursor-pointer",
|
|
1759
|
+
children: options.map((opt) => /* @__PURE__ */ jsx("option", { value: opt.value, children: opt.label }, opt.value))
|
|
1760
|
+
}
|
|
1761
|
+
),
|
|
1762
|
+
/* @__PURE__ */ jsx("svg", { className: "pointer-events-none absolute right-2.5 top-1/2 -translate-y-1/2 size-4 text-muted-foreground", xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsx("path", { d: "m6 9 6 6 6-6" }) })
|
|
1763
|
+
] })
|
|
1764
|
+
] });
|
|
1765
|
+
}
|
|
1766
|
+
function LikertLabelsEditor({
|
|
1767
|
+
labels,
|
|
1768
|
+
onChange
|
|
1769
|
+
}) {
|
|
1770
|
+
const handleUpdate = (index, value) => {
|
|
1771
|
+
const updated = labels.map((l, i) => i === index ? value : l);
|
|
1772
|
+
onChange(updated);
|
|
1773
|
+
};
|
|
1774
|
+
const handleAdd = () => {
|
|
1775
|
+
onChange([...labels, `Label ${labels.length + 1}`]);
|
|
1776
|
+
};
|
|
1777
|
+
const handleRemove = (index) => {
|
|
1778
|
+
if (labels.length <= 2) return;
|
|
1779
|
+
onChange(labels.filter((_, i) => i !== index));
|
|
1780
|
+
};
|
|
1781
|
+
return /* @__PURE__ */ jsxs("div", { children: [
|
|
1782
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between mb-2", children: [
|
|
1783
|
+
/* @__PURE__ */ jsx(Label, { className: "text-xs text-muted-foreground", children: "Scale Labels" }),
|
|
1784
|
+
/* @__PURE__ */ jsx(
|
|
1785
|
+
"button",
|
|
1786
|
+
{
|
|
1787
|
+
type: "button",
|
|
1788
|
+
onClick: handleAdd,
|
|
1789
|
+
className: "text-xs text-primary hover:underline",
|
|
1790
|
+
children: "+ Add"
|
|
1791
|
+
}
|
|
1792
|
+
)
|
|
1793
|
+
] }),
|
|
1794
|
+
/* @__PURE__ */ jsx("div", { className: "space-y-1.5", children: labels.map((label, index) => /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 group", children: [
|
|
1795
|
+
/* @__PURE__ */ jsx("span", { className: "text-[10px] text-muted-foreground w-4 text-right shrink-0", children: index + 1 }),
|
|
1796
|
+
/* @__PURE__ */ jsx(
|
|
1797
|
+
Input,
|
|
1798
|
+
{
|
|
1799
|
+
value: label,
|
|
1800
|
+
onChange: (e) => handleUpdate(index, e.target.value),
|
|
1801
|
+
className: "h-7 text-xs flex-1"
|
|
1802
|
+
}
|
|
1803
|
+
),
|
|
1804
|
+
labels.length > 2 && /* @__PURE__ */ jsx(
|
|
1805
|
+
"button",
|
|
1806
|
+
{
|
|
1807
|
+
type: "button",
|
|
1808
|
+
onClick: () => handleRemove(index),
|
|
1809
|
+
className: "text-xs text-destructive opacity-0 group-hover:opacity-100 transition-opacity px-1",
|
|
1810
|
+
children: "x"
|
|
1811
|
+
}
|
|
1812
|
+
)
|
|
1813
|
+
] }, index)) })
|
|
1814
|
+
] });
|
|
1815
|
+
}
|
|
1816
|
+
function MatrixItemsEditor({
|
|
1817
|
+
label,
|
|
1818
|
+
items,
|
|
1819
|
+
onChange
|
|
1820
|
+
}) {
|
|
1821
|
+
const handleUpdate = (index, newLabel) => {
|
|
1822
|
+
const updated = items.map(
|
|
1823
|
+
(item, i) => i === index ? { label: newLabel, value: newLabel.toLowerCase().replace(/\s+/g, "_") } : item
|
|
1824
|
+
);
|
|
1825
|
+
onChange(updated);
|
|
1826
|
+
};
|
|
1827
|
+
const handleAdd = () => {
|
|
1828
|
+
const n = items.length + 1;
|
|
1829
|
+
onChange([...items, { label: `${label.slice(0, -1)} ${n}`, value: `${label.toLowerCase().slice(0, -1)}${n}` }]);
|
|
1830
|
+
};
|
|
1831
|
+
const handleRemove = (index) => {
|
|
1832
|
+
if (items.length <= 1) return;
|
|
1833
|
+
onChange(items.filter((_, i) => i !== index));
|
|
1834
|
+
};
|
|
1835
|
+
return /* @__PURE__ */ jsxs("div", { children: [
|
|
1836
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between mb-2", children: [
|
|
1837
|
+
/* @__PURE__ */ jsx(Label, { className: "text-xs text-muted-foreground", children: label }),
|
|
1838
|
+
/* @__PURE__ */ jsx(
|
|
1839
|
+
"button",
|
|
1840
|
+
{
|
|
1841
|
+
type: "button",
|
|
1842
|
+
onClick: handleAdd,
|
|
1843
|
+
className: "text-xs text-primary hover:underline",
|
|
1844
|
+
children: "+ Add"
|
|
1845
|
+
}
|
|
1846
|
+
)
|
|
1847
|
+
] }),
|
|
1848
|
+
/* @__PURE__ */ jsx("div", { className: "space-y-1.5", children: items.map((item, index) => /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 group", children: [
|
|
1849
|
+
/* @__PURE__ */ jsx(
|
|
1850
|
+
Input,
|
|
1851
|
+
{
|
|
1852
|
+
value: item.label,
|
|
1853
|
+
onChange: (e) => handleUpdate(index, e.target.value),
|
|
1854
|
+
className: "h-7 text-xs flex-1"
|
|
1855
|
+
}
|
|
1856
|
+
),
|
|
1857
|
+
items.length > 1 && /* @__PURE__ */ jsx(
|
|
1858
|
+
"button",
|
|
1859
|
+
{
|
|
1860
|
+
type: "button",
|
|
1861
|
+
onClick: () => handleRemove(index),
|
|
1862
|
+
className: "text-xs text-destructive opacity-0 group-hover:opacity-100 transition-opacity px-1",
|
|
1863
|
+
children: "x"
|
|
1864
|
+
}
|
|
1865
|
+
)
|
|
1866
|
+
] }, index)) })
|
|
1867
|
+
] });
|
|
1868
|
+
}
|
|
1869
|
+
function ScoringOptionsEditor({
|
|
1870
|
+
options,
|
|
1871
|
+
onChange
|
|
1872
|
+
}) {
|
|
1873
|
+
const handleUpdate = (index, field, val) => {
|
|
1874
|
+
const updated = options.map((opt, i) => {
|
|
1875
|
+
if (i !== index) return opt;
|
|
1876
|
+
if (field === "label") {
|
|
1877
|
+
const label = val;
|
|
1878
|
+
return { ...opt, label, value: label.toLowerCase().replace(/\s+/g, "_") };
|
|
1879
|
+
}
|
|
1880
|
+
return { ...opt, score: val };
|
|
1881
|
+
});
|
|
1882
|
+
onChange(updated);
|
|
1883
|
+
};
|
|
1884
|
+
const handleAdd = () => {
|
|
1885
|
+
const n = options.length + 1;
|
|
1886
|
+
onChange([...options, { label: `Option ${n}`, value: `option_${n}`, score: 0 }]);
|
|
1887
|
+
};
|
|
1888
|
+
const handleRemove = (index) => {
|
|
1889
|
+
if (options.length <= 1) return;
|
|
1890
|
+
onChange(options.filter((_, i) => i !== index));
|
|
1891
|
+
};
|
|
1892
|
+
return /* @__PURE__ */ jsxs("div", { children: [
|
|
1893
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between mb-2", children: [
|
|
1894
|
+
/* @__PURE__ */ jsx(Label, { className: "text-xs text-muted-foreground", children: "Score Options" }),
|
|
1895
|
+
/* @__PURE__ */ jsx("button", { type: "button", onClick: handleAdd, className: "text-xs text-primary hover:underline", children: "+ Add" })
|
|
1896
|
+
] }),
|
|
1897
|
+
/* @__PURE__ */ jsx("div", { className: "space-y-1.5", children: options.map((opt, index) => /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 group", children: [
|
|
1898
|
+
/* @__PURE__ */ jsx(
|
|
1899
|
+
Input,
|
|
1900
|
+
{
|
|
1901
|
+
value: opt.label,
|
|
1902
|
+
onChange: (e) => handleUpdate(index, "label", e.target.value),
|
|
1903
|
+
className: "h-7 text-xs flex-1",
|
|
1904
|
+
placeholder: "Label"
|
|
1905
|
+
}
|
|
1906
|
+
),
|
|
1907
|
+
/* @__PURE__ */ jsx(
|
|
1908
|
+
Input,
|
|
1909
|
+
{
|
|
1910
|
+
type: "number",
|
|
1911
|
+
value: opt.score,
|
|
1912
|
+
onChange: (e) => handleUpdate(index, "score", Number(e.target.value)),
|
|
1913
|
+
className: "h-7 text-xs w-16",
|
|
1914
|
+
placeholder: "Score"
|
|
1915
|
+
}
|
|
1916
|
+
),
|
|
1917
|
+
options.length > 1 && /* @__PURE__ */ jsx("button", { type: "button", onClick: () => handleRemove(index), className: "text-xs text-destructive opacity-0 group-hover:opacity-100 transition-opacity px-1", children: "x" })
|
|
1918
|
+
] }, index)) })
|
|
1919
|
+
] });
|
|
1920
|
+
}
|
|
1921
|
+
function ScoreRangesEditor({
|
|
1922
|
+
ranges,
|
|
1923
|
+
onChange
|
|
1924
|
+
}) {
|
|
1925
|
+
const handleUpdate = (index, updates) => {
|
|
1926
|
+
const updated = ranges.map((r, i) => i === index ? { ...r, ...updates } : r);
|
|
1927
|
+
onChange(updated);
|
|
1928
|
+
};
|
|
1929
|
+
const handleAdd = () => {
|
|
1930
|
+
const lastMax = ranges.length > 0 ? ranges[ranges.length - 1].max : 0;
|
|
1931
|
+
onChange([...ranges, { min: lastMax, max: lastMax + 10, label: `Range ${ranges.length + 1}` }]);
|
|
1932
|
+
};
|
|
1933
|
+
const handleRemove = (index) => {
|
|
1934
|
+
onChange(ranges.filter((_, i) => i !== index));
|
|
1935
|
+
};
|
|
1936
|
+
return /* @__PURE__ */ jsxs("div", { children: [
|
|
1937
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between mb-2", children: [
|
|
1938
|
+
/* @__PURE__ */ jsx(Label, { className: "text-xs text-muted-foreground", children: "Score Ranges" }),
|
|
1939
|
+
/* @__PURE__ */ jsx("button", { type: "button", onClick: handleAdd, className: "text-xs text-primary hover:underline", children: "+ Add" })
|
|
1940
|
+
] }),
|
|
1941
|
+
/* @__PURE__ */ jsx("div", { className: "space-y-2", children: ranges.map((range, index) => /* @__PURE__ */ jsxs("div", { className: "p-2 rounded-md border border-border space-y-1.5 group", children: [
|
|
1942
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5", children: [
|
|
1943
|
+
/* @__PURE__ */ jsx(Input, { type: "number", value: range.min, onChange: (e) => handleUpdate(index, { min: Number(e.target.value) }), className: "h-7 text-xs w-16", placeholder: "Min" }),
|
|
1944
|
+
/* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: "to" }),
|
|
1945
|
+
/* @__PURE__ */ jsx(Input, { type: "number", value: range.max, onChange: (e) => handleUpdate(index, { max: Number(e.target.value) }), className: "h-7 text-xs w-16", placeholder: "Max" }),
|
|
1946
|
+
/* @__PURE__ */ jsx(Input, { value: range.label, onChange: (e) => handleUpdate(index, { label: e.target.value }), className: "h-7 text-xs flex-1", placeholder: "Label" }),
|
|
1947
|
+
/* @__PURE__ */ jsx("button", { type: "button", onClick: () => handleRemove(index), className: "text-xs text-destructive opacity-0 group-hover:opacity-100 transition-opacity px-1", children: "x" })
|
|
1948
|
+
] }),
|
|
1949
|
+
/* @__PURE__ */ jsx(Input, { value: range.description ?? "", onChange: (e) => handleUpdate(index, { description: e.target.value || void 0 }), className: "h-7 text-xs", placeholder: "Description (optional)" })
|
|
1950
|
+
] }, index)) })
|
|
1951
|
+
] });
|
|
1952
|
+
}
|
|
1953
|
+
var RULE_TYPES = [
|
|
1954
|
+
{ value: "minLength", label: "Min Length", description: "Minimum character count" },
|
|
1955
|
+
{ value: "maxLength", label: "Max Length", description: "Maximum character count" },
|
|
1956
|
+
{ value: "min", label: "Min Value", description: "Minimum numeric value" },
|
|
1957
|
+
{ value: "max", label: "Max Value", description: "Maximum numeric value" },
|
|
1958
|
+
{ value: "pattern", label: "Pattern", description: "Regex pattern match" },
|
|
1959
|
+
{ value: "email", label: "Email Format", description: "Valid email address" },
|
|
1960
|
+
{ value: "phone", label: "Phone Format", description: "Valid phone number" },
|
|
1961
|
+
{ value: "url", label: "URL Format", description: "Valid URL" },
|
|
1962
|
+
{ value: "date", label: "Date Range", description: "Date within range" },
|
|
1963
|
+
{ value: "fileSize", label: "File Size", description: "Max file size in MB" },
|
|
1964
|
+
{ value: "fileType", label: "File Type", description: "Accepted file types" },
|
|
1965
|
+
{ value: "custom", label: "Custom", description: "Named custom validator" }
|
|
1966
|
+
];
|
|
1967
|
+
function ValidationRulesEditor({ question, onUpdate }) {
|
|
1968
|
+
const rules = question.validation ?? [];
|
|
1969
|
+
const updateRules = (next) => {
|
|
1970
|
+
onUpdate({ validation: next.length > 0 ? next : void 0 });
|
|
1971
|
+
};
|
|
1972
|
+
const addRule = (type) => {
|
|
1973
|
+
const newRule = createDefaultRule(type);
|
|
1974
|
+
if (newRule) updateRules([...rules, newRule]);
|
|
1975
|
+
};
|
|
1976
|
+
const updateRule = (index, updated) => {
|
|
1977
|
+
updateRules(rules.map((r, i) => i === index ? updated : r));
|
|
1978
|
+
};
|
|
1979
|
+
const removeRule = (index) => {
|
|
1980
|
+
updateRules(rules.filter((_, i) => i !== index));
|
|
1981
|
+
};
|
|
1982
|
+
const usedTypes = new Set(rules.map((r) => r.type));
|
|
1983
|
+
const availableTypes = RULE_TYPES.filter((t) => !usedTypes.has(t.value));
|
|
1984
|
+
return /* @__PURE__ */ jsxs("div", { children: [
|
|
1985
|
+
/* @__PURE__ */ jsx(Separator, { className: "mb-4" }),
|
|
1986
|
+
/* @__PURE__ */ jsx(Label, { className: "text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-3 block", children: "Validation Rules" }),
|
|
1987
|
+
rules.length === 0 && /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground mb-3", children: "No validation rules configured." }),
|
|
1988
|
+
/* @__PURE__ */ jsx("div", { className: "space-y-2", children: rules.map((rule, index) => /* @__PURE__ */ jsx(
|
|
1989
|
+
RuleRow,
|
|
1990
|
+
{
|
|
1991
|
+
rule,
|
|
1992
|
+
onChange: (updated) => updateRule(index, updated),
|
|
1993
|
+
onRemove: () => removeRule(index)
|
|
1994
|
+
},
|
|
1995
|
+
`${rule.type}-${index}`
|
|
1996
|
+
)) }),
|
|
1997
|
+
availableTypes.length > 0 && /* @__PURE__ */ jsx("div", { className: "mt-3", children: /* @__PURE__ */ jsxs("div", { className: "relative", children: [
|
|
1998
|
+
/* @__PURE__ */ jsxs(
|
|
1999
|
+
"select",
|
|
2000
|
+
{
|
|
2001
|
+
value: "",
|
|
2002
|
+
onChange: (e) => {
|
|
2003
|
+
if (e.target.value) addRule(e.target.value);
|
|
2004
|
+
},
|
|
2005
|
+
className: "flex h-8 w-full appearance-none rounded-md border border-input bg-card px-3 py-1 pr-8 text-xs shadow-xs transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring text-muted-foreground cursor-pointer",
|
|
2006
|
+
children: [
|
|
2007
|
+
/* @__PURE__ */ jsxs("option", { value: "", children: [
|
|
2008
|
+
/* @__PURE__ */ jsx(Plus, { size: 12 }),
|
|
2009
|
+
" Add validation rule..."
|
|
2010
|
+
] }),
|
|
2011
|
+
availableTypes.map((t) => /* @__PURE__ */ jsx("option", { value: t.value, children: t.label }, t.value))
|
|
2012
|
+
]
|
|
2013
|
+
}
|
|
2014
|
+
),
|
|
2015
|
+
/* @__PURE__ */ jsx("svg", { className: "pointer-events-none absolute right-2.5 top-1/2 -translate-y-1/2 size-3.5 text-muted-foreground", xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsx("path", { d: "m6 9 6 6 6-6" }) })
|
|
2016
|
+
] }) })
|
|
2017
|
+
] });
|
|
2018
|
+
}
|
|
2019
|
+
function RuleRow({
|
|
2020
|
+
rule,
|
|
2021
|
+
onChange,
|
|
2022
|
+
onRemove
|
|
2023
|
+
}) {
|
|
2024
|
+
const ruleInfo = RULE_TYPES.find((t) => t.value === rule.type);
|
|
2025
|
+
const label = ruleInfo?.label ?? rule.type;
|
|
2026
|
+
return /* @__PURE__ */ jsxs("div", { className: "p-2 rounded-md border border-border space-y-1.5 group", children: [
|
|
2027
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
|
|
2028
|
+
/* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-foreground", children: label }),
|
|
2029
|
+
/* @__PURE__ */ jsx(
|
|
2030
|
+
"button",
|
|
2031
|
+
{
|
|
2032
|
+
type: "button",
|
|
2033
|
+
onClick: onRemove,
|
|
2034
|
+
className: "opacity-0 group-hover:opacity-100 transition-opacity",
|
|
2035
|
+
title: "Remove rule",
|
|
2036
|
+
children: /* @__PURE__ */ jsx(Trash2, { size: 12, className: "text-destructive", strokeWidth: 1.75 })
|
|
2037
|
+
}
|
|
2038
|
+
)
|
|
2039
|
+
] }),
|
|
2040
|
+
/* @__PURE__ */ jsx(RuleFields, { rule, onChange }),
|
|
2041
|
+
/* @__PURE__ */ jsx(
|
|
2042
|
+
Input,
|
|
2043
|
+
{
|
|
2044
|
+
value: rule.message ?? "",
|
|
2045
|
+
onChange: (e) => onChange({ ...rule, message: e.target.value || void 0 }),
|
|
2046
|
+
className: "h-7 text-xs",
|
|
2047
|
+
placeholder: "Custom error message (optional)"
|
|
2048
|
+
}
|
|
2049
|
+
)
|
|
2050
|
+
] });
|
|
2051
|
+
}
|
|
2052
|
+
function RuleFields({
|
|
2053
|
+
rule,
|
|
2054
|
+
onChange
|
|
2055
|
+
}) {
|
|
2056
|
+
switch (rule.type) {
|
|
2057
|
+
case "min":
|
|
2058
|
+
case "max":
|
|
2059
|
+
return /* @__PURE__ */ jsx(
|
|
2060
|
+
Input,
|
|
2061
|
+
{
|
|
2062
|
+
type: "number",
|
|
2063
|
+
value: rule.value,
|
|
2064
|
+
onChange: (e) => onChange({ ...rule, value: Number(e.target.value) }),
|
|
2065
|
+
className: "h-7 text-xs",
|
|
2066
|
+
placeholder: rule.type === "min" ? "Minimum value" : "Maximum value"
|
|
2067
|
+
}
|
|
2068
|
+
);
|
|
2069
|
+
case "minLength":
|
|
2070
|
+
case "maxLength":
|
|
2071
|
+
return /* @__PURE__ */ jsx(
|
|
2072
|
+
Input,
|
|
2073
|
+
{
|
|
2074
|
+
type: "number",
|
|
2075
|
+
value: rule.value,
|
|
2076
|
+
onChange: (e) => onChange({ ...rule, value: Number(e.target.value) }),
|
|
2077
|
+
className: "h-7 text-xs",
|
|
2078
|
+
placeholder: rule.type === "minLength" ? "Minimum characters" : "Maximum characters"
|
|
2079
|
+
}
|
|
2080
|
+
);
|
|
2081
|
+
case "pattern":
|
|
2082
|
+
return /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
|
|
2083
|
+
/* @__PURE__ */ jsx(
|
|
2084
|
+
Input,
|
|
2085
|
+
{
|
|
2086
|
+
value: rule.regex,
|
|
2087
|
+
onChange: (e) => onChange({ ...rule, regex: e.target.value }),
|
|
2088
|
+
className: "h-7 text-xs font-mono",
|
|
2089
|
+
placeholder: "Regex pattern, e.g. ^[A-Z]+"
|
|
2090
|
+
}
|
|
2091
|
+
),
|
|
2092
|
+
/* @__PURE__ */ jsx(
|
|
2093
|
+
Input,
|
|
2094
|
+
{
|
|
2095
|
+
value: rule.flags ?? "",
|
|
2096
|
+
onChange: (e) => onChange({ ...rule, flags: e.target.value || void 0 }),
|
|
2097
|
+
className: "h-7 text-xs font-mono",
|
|
2098
|
+
placeholder: "Flags, e.g. gi"
|
|
2099
|
+
}
|
|
2100
|
+
)
|
|
2101
|
+
] });
|
|
2102
|
+
case "date":
|
|
2103
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5", children: [
|
|
2104
|
+
/* @__PURE__ */ jsx(
|
|
2105
|
+
Input,
|
|
2106
|
+
{
|
|
2107
|
+
value: rule.min ?? "",
|
|
2108
|
+
onChange: (e) => onChange({ ...rule, min: e.target.value || void 0 }),
|
|
2109
|
+
className: "h-7 text-xs flex-1",
|
|
2110
|
+
placeholder: "Min date (YYYY-MM-DD)"
|
|
2111
|
+
}
|
|
2112
|
+
),
|
|
2113
|
+
/* @__PURE__ */ jsx(
|
|
2114
|
+
Input,
|
|
2115
|
+
{
|
|
2116
|
+
value: rule.max ?? "",
|
|
2117
|
+
onChange: (e) => onChange({ ...rule, max: e.target.value || void 0 }),
|
|
2118
|
+
className: "h-7 text-xs flex-1",
|
|
2119
|
+
placeholder: "Max date"
|
|
2120
|
+
}
|
|
2121
|
+
)
|
|
2122
|
+
] });
|
|
2123
|
+
case "fileSize":
|
|
2124
|
+
return /* @__PURE__ */ jsx(
|
|
2125
|
+
Input,
|
|
2126
|
+
{
|
|
2127
|
+
type: "number",
|
|
2128
|
+
value: rule.maxMb,
|
|
2129
|
+
onChange: (e) => onChange({ ...rule, maxMb: Number(e.target.value) }),
|
|
2130
|
+
className: "h-7 text-xs",
|
|
2131
|
+
placeholder: "Max size in MB"
|
|
2132
|
+
}
|
|
2133
|
+
);
|
|
2134
|
+
case "fileType":
|
|
2135
|
+
return /* @__PURE__ */ jsx(
|
|
2136
|
+
Input,
|
|
2137
|
+
{
|
|
2138
|
+
value: rule.accept.join(", "),
|
|
2139
|
+
onChange: (e) => onChange({ ...rule, accept: e.target.value.split(",").map((s) => s.trim()).filter(Boolean) }),
|
|
2140
|
+
className: "h-7 text-xs",
|
|
2141
|
+
placeholder: ".pdf, .jpg, .png"
|
|
2142
|
+
}
|
|
2143
|
+
);
|
|
2144
|
+
case "custom":
|
|
2145
|
+
return /* @__PURE__ */ jsx(
|
|
2146
|
+
Input,
|
|
2147
|
+
{
|
|
2148
|
+
value: rule.name,
|
|
2149
|
+
onChange: (e) => onChange({ ...rule, name: e.target.value }),
|
|
2150
|
+
className: "h-7 text-xs",
|
|
2151
|
+
placeholder: "Validator name"
|
|
2152
|
+
}
|
|
2153
|
+
);
|
|
2154
|
+
// email, phone, url — no extra fields needed
|
|
2155
|
+
case "email":
|
|
2156
|
+
case "phone":
|
|
2157
|
+
case "url":
|
|
2158
|
+
return null;
|
|
2159
|
+
default:
|
|
2160
|
+
return null;
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
function createDefaultRule(type) {
|
|
2164
|
+
switch (type) {
|
|
2165
|
+
case "required":
|
|
2166
|
+
return { type: "required" };
|
|
2167
|
+
case "min":
|
|
2168
|
+
return { type: "min", value: 0 };
|
|
2169
|
+
case "max":
|
|
2170
|
+
return { type: "max", value: 100 };
|
|
2171
|
+
case "minLength":
|
|
2172
|
+
return { type: "minLength", value: 1 };
|
|
2173
|
+
case "maxLength":
|
|
2174
|
+
return { type: "maxLength", value: 255 };
|
|
2175
|
+
case "pattern":
|
|
2176
|
+
return { type: "pattern", regex: "" };
|
|
2177
|
+
case "email":
|
|
2178
|
+
return { type: "email" };
|
|
2179
|
+
case "phone":
|
|
2180
|
+
return { type: "phone" };
|
|
2181
|
+
case "url":
|
|
2182
|
+
return { type: "url" };
|
|
2183
|
+
case "date":
|
|
2184
|
+
return { type: "date" };
|
|
2185
|
+
case "fileSize":
|
|
2186
|
+
return { type: "fileSize", maxMb: 10 };
|
|
2187
|
+
case "fileType":
|
|
2188
|
+
return { type: "fileType", accept: [] };
|
|
2189
|
+
case "custom":
|
|
2190
|
+
return { type: "custom", name: "" };
|
|
2191
|
+
default:
|
|
2192
|
+
return null;
|
|
2193
|
+
}
|
|
2194
|
+
}
|
|
2195
|
+
var OPERATORS = [
|
|
2196
|
+
{ value: "eq", label: "equals" },
|
|
2197
|
+
{ value: "neq", label: "not equals" },
|
|
2198
|
+
{ value: "gt", label: "greater than" },
|
|
2199
|
+
{ value: "gte", label: "greater or equal" },
|
|
2200
|
+
{ value: "lt", label: "less than" },
|
|
2201
|
+
{ value: "lte", label: "less or equal" },
|
|
2202
|
+
{ value: "contains", label: "contains" },
|
|
2203
|
+
{ value: "notContains", label: "not contains" },
|
|
2204
|
+
{ value: "startsWith", label: "starts with" },
|
|
2205
|
+
{ value: "endsWith", label: "ends with" },
|
|
2206
|
+
{ value: "in", label: "in list" },
|
|
2207
|
+
{ value: "notIn", label: "not in list" },
|
|
2208
|
+
{ value: "exists", label: "has value" },
|
|
2209
|
+
{ value: "notExists", label: "is empty" },
|
|
2210
|
+
{ value: "between", label: "between" },
|
|
2211
|
+
{ value: "matches", label: "matches regex" }
|
|
2212
|
+
];
|
|
2213
|
+
function getFieldOptions(schema, excludeId) {
|
|
2214
|
+
const fields = [];
|
|
2215
|
+
for (const section of schema.sections) {
|
|
2216
|
+
for (const q of section.questions) {
|
|
2217
|
+
if (q.id !== excludeId) {
|
|
2218
|
+
fields.push({ id: q.id, label: q.label || q.id });
|
|
2219
|
+
}
|
|
2220
|
+
}
|
|
2221
|
+
}
|
|
2222
|
+
return fields;
|
|
2223
|
+
}
|
|
2224
|
+
function ConditionEditor({ question, schema, onUpdate }) {
|
|
2225
|
+
const showIf = question.showIf;
|
|
2226
|
+
const fieldOptions = getFieldOptions(schema, question.id);
|
|
2227
|
+
const updateShowIf = (next) => {
|
|
2228
|
+
onUpdate({ showIf: next });
|
|
2229
|
+
};
|
|
2230
|
+
const hasConditions = showIf && showIf.conditions && showIf.conditions.length > 0;
|
|
2231
|
+
const addCondition = () => {
|
|
2232
|
+
const firstField = fieldOptions.length > 0 ? fieldOptions[0].id : "";
|
|
2233
|
+
const newCondition = {
|
|
2234
|
+
field: firstField,
|
|
2235
|
+
operator: "eq",
|
|
2236
|
+
value: ""
|
|
2237
|
+
};
|
|
2238
|
+
if (!showIf) {
|
|
2239
|
+
updateShowIf({ combine: "AND", conditions: [newCondition] });
|
|
2240
|
+
} else {
|
|
2241
|
+
updateShowIf({
|
|
2242
|
+
...showIf,
|
|
2243
|
+
conditions: [...showIf.conditions ?? [], newCondition]
|
|
2244
|
+
});
|
|
2245
|
+
}
|
|
2246
|
+
};
|
|
2247
|
+
const updateCondition = (index, updates) => {
|
|
2248
|
+
if (!showIf?.conditions) return;
|
|
2249
|
+
const updated = showIf.conditions.map((c, i) => i === index ? { ...c, ...updates } : c);
|
|
2250
|
+
updateShowIf({ ...showIf, conditions: updated });
|
|
2251
|
+
};
|
|
2252
|
+
const removeCondition = (index) => {
|
|
2253
|
+
if (!showIf?.conditions) return;
|
|
2254
|
+
const updated = showIf.conditions.filter((_, i) => i !== index);
|
|
2255
|
+
if (updated.length === 0) {
|
|
2256
|
+
updateShowIf(void 0);
|
|
2257
|
+
} else {
|
|
2258
|
+
updateShowIf({ ...showIf, conditions: updated });
|
|
2259
|
+
}
|
|
2260
|
+
};
|
|
2261
|
+
const toggleCombine = () => {
|
|
2262
|
+
if (!showIf) return;
|
|
2263
|
+
updateShowIf({ ...showIf, combine: showIf.combine === "AND" ? "OR" : "AND" });
|
|
2264
|
+
};
|
|
2265
|
+
return /* @__PURE__ */ jsxs("div", { children: [
|
|
2266
|
+
/* @__PURE__ */ jsx(Separator, { className: "mb-4" }),
|
|
2267
|
+
/* @__PURE__ */ jsx(Label, { className: "text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-3 block", children: "Visibility Rules" }),
|
|
2268
|
+
!hasConditions && /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground mb-3", children: "Always visible. Add a rule to show this field conditionally." }),
|
|
2269
|
+
hasConditions && /* @__PURE__ */ jsx("div", { className: "space-y-2 mb-3", children: showIf.conditions.map((cond, index) => /* @__PURE__ */ jsxs("div", { children: [
|
|
2270
|
+
index > 0 && /* @__PURE__ */ jsx(
|
|
2271
|
+
"button",
|
|
2272
|
+
{
|
|
2273
|
+
type: "button",
|
|
2274
|
+
onClick: toggleCombine,
|
|
2275
|
+
className: "text-[10px] font-semibold uppercase tracking-wider text-primary mb-1.5 block cursor-pointer hover:underline",
|
|
2276
|
+
children: showIf.combine ?? "AND"
|
|
2277
|
+
}
|
|
2278
|
+
),
|
|
2279
|
+
/* @__PURE__ */ jsxs("div", { className: "p-2 rounded-md border border-border space-y-1.5 group", children: [
|
|
2280
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
|
|
2281
|
+
/* @__PURE__ */ jsx("span", { className: "text-[10px] text-muted-foreground uppercase tracking-wider", children: "When" }),
|
|
2282
|
+
/* @__PURE__ */ jsx(
|
|
2283
|
+
"button",
|
|
2284
|
+
{
|
|
2285
|
+
type: "button",
|
|
2286
|
+
onClick: () => removeCondition(index),
|
|
2287
|
+
className: "opacity-0 group-hover:opacity-100 transition-opacity",
|
|
2288
|
+
title: "Remove condition",
|
|
2289
|
+
children: /* @__PURE__ */ jsx(Trash2, { size: 12, className: "text-destructive", strokeWidth: 1.75 })
|
|
2290
|
+
}
|
|
2291
|
+
)
|
|
2292
|
+
] }),
|
|
2293
|
+
/* @__PURE__ */ jsxs("div", { className: "relative", children: [
|
|
2294
|
+
/* @__PURE__ */ jsxs(
|
|
2295
|
+
"select",
|
|
2296
|
+
{
|
|
2297
|
+
value: cond.field ?? "",
|
|
2298
|
+
onChange: (e) => updateCondition(index, { field: e.target.value }),
|
|
2299
|
+
className: "flex h-7 w-full appearance-none rounded-md border border-input bg-card px-2 py-1 pr-7 text-xs shadow-xs transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring text-foreground cursor-pointer",
|
|
2300
|
+
children: [
|
|
2301
|
+
/* @__PURE__ */ jsx("option", { value: "", children: "Select field..." }),
|
|
2302
|
+
fieldOptions.map((f) => /* @__PURE__ */ jsx("option", { value: f.id, children: f.label }, f.id))
|
|
2303
|
+
]
|
|
2304
|
+
}
|
|
2305
|
+
),
|
|
2306
|
+
/* @__PURE__ */ jsx("svg", { className: "pointer-events-none absolute right-2 top-1/2 -translate-y-1/2 size-3 text-muted-foreground", xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsx("path", { d: "m6 9 6 6 6-6" }) })
|
|
2307
|
+
] }),
|
|
2308
|
+
/* @__PURE__ */ jsxs("div", { className: "relative", children: [
|
|
2309
|
+
/* @__PURE__ */ jsx(
|
|
2310
|
+
"select",
|
|
2311
|
+
{
|
|
2312
|
+
value: cond.operator ?? "eq",
|
|
2313
|
+
onChange: (e) => updateCondition(index, { operator: e.target.value }),
|
|
2314
|
+
className: "flex h-7 w-full appearance-none rounded-md border border-input bg-card px-2 py-1 pr-7 text-xs shadow-xs transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring text-foreground cursor-pointer",
|
|
2315
|
+
children: OPERATORS.map((op) => /* @__PURE__ */ jsx("option", { value: op.value, children: op.label }, op.value))
|
|
2316
|
+
}
|
|
2317
|
+
),
|
|
2318
|
+
/* @__PURE__ */ jsx("svg", { className: "pointer-events-none absolute right-2 top-1/2 -translate-y-1/2 size-3 text-muted-foreground", xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsx("path", { d: "m6 9 6 6 6-6" }) })
|
|
2319
|
+
] }),
|
|
2320
|
+
cond.operator !== "exists" && cond.operator !== "notExists" && /* @__PURE__ */ jsx(
|
|
2321
|
+
Input,
|
|
2322
|
+
{
|
|
2323
|
+
value: String(cond.value ?? ""),
|
|
2324
|
+
onChange: (e) => updateCondition(index, { value: e.target.value }),
|
|
2325
|
+
className: "h-7 text-xs",
|
|
2326
|
+
placeholder: "Value"
|
|
2327
|
+
}
|
|
2328
|
+
)
|
|
2329
|
+
] })
|
|
2330
|
+
] }, index)) }),
|
|
2331
|
+
fieldOptions.length > 0 ? /* @__PURE__ */ jsxs(
|
|
2332
|
+
"button",
|
|
2333
|
+
{
|
|
2334
|
+
type: "button",
|
|
2335
|
+
onClick: addCondition,
|
|
2336
|
+
className: "flex items-center gap-1.5 text-xs text-primary hover:underline",
|
|
2337
|
+
children: [
|
|
2338
|
+
/* @__PURE__ */ jsx(Plus, { size: 12, strokeWidth: 2 }),
|
|
2339
|
+
"Add condition"
|
|
2340
|
+
]
|
|
2341
|
+
}
|
|
2342
|
+
) : /* @__PURE__ */ jsx("p", { className: "text-[11px] text-muted-foreground", children: "Add other fields to create visibility rules." })
|
|
2343
|
+
] });
|
|
2344
|
+
}
|
|
2345
|
+
function FormSettingsPanel({ schema, onUpdate }) {
|
|
2346
|
+
const settings = schema.settings ?? {};
|
|
2347
|
+
const updateSettings = (updates) => {
|
|
2348
|
+
onUpdate({ ...schema, settings: { ...settings, ...updates } });
|
|
2349
|
+
};
|
|
2350
|
+
const updateSubmitButton = (updates) => {
|
|
2351
|
+
onUpdate({
|
|
2352
|
+
...schema,
|
|
2353
|
+
settings: {
|
|
2354
|
+
...settings,
|
|
2355
|
+
submitButton: { ...settings.submitButton, ...updates }
|
|
2356
|
+
}
|
|
2357
|
+
});
|
|
2358
|
+
};
|
|
2359
|
+
const updateNavigation = (updates) => {
|
|
2360
|
+
onUpdate({
|
|
2361
|
+
...schema,
|
|
2362
|
+
settings: {
|
|
2363
|
+
...settings,
|
|
2364
|
+
navigation: { ...settings.navigation, ...updates }
|
|
2365
|
+
}
|
|
2366
|
+
});
|
|
2367
|
+
};
|
|
2368
|
+
return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
|
|
2369
|
+
/* @__PURE__ */ jsxs(SettingsSection, { title: "Display", children: [
|
|
2370
|
+
/* @__PURE__ */ jsx(
|
|
2371
|
+
SettingsSelect,
|
|
2372
|
+
{
|
|
2373
|
+
label: "Mode",
|
|
2374
|
+
value: settings.displayMode ?? "classic",
|
|
2375
|
+
options: [
|
|
2376
|
+
{ label: "Classic", value: "classic" },
|
|
2377
|
+
{ label: "Stepped", value: "stepped" },
|
|
2378
|
+
{ label: "Conversational", value: "conversational" }
|
|
2379
|
+
],
|
|
2380
|
+
onChange: (v) => updateSettings({ displayMode: v })
|
|
2381
|
+
}
|
|
2382
|
+
),
|
|
2383
|
+
/* @__PURE__ */ jsx(SettingsToggle, { label: "Show Progress", checked: settings.showProgress !== false, onChange: (v) => updateSettings({ showProgress: v }) }),
|
|
2384
|
+
settings.showProgress !== false && /* @__PURE__ */ jsx(
|
|
2385
|
+
SettingsSelect,
|
|
2386
|
+
{
|
|
2387
|
+
label: "Progress Style",
|
|
2388
|
+
value: settings.progressStyle ?? "bar",
|
|
2389
|
+
options: [
|
|
2390
|
+
{ label: "Bar", value: "bar" },
|
|
2391
|
+
{ label: "Steps", value: "steps" },
|
|
2392
|
+
{ label: "Percentage", value: "percentage" }
|
|
2393
|
+
],
|
|
2394
|
+
onChange: (v) => updateSettings({ progressStyle: v })
|
|
2395
|
+
}
|
|
2396
|
+
)
|
|
2397
|
+
] }),
|
|
2398
|
+
/* @__PURE__ */ jsxs(SettingsSection, { title: "Submit Button", children: [
|
|
2399
|
+
/* @__PURE__ */ jsx(SettingsField, { label: "Label", value: settings.submitButton?.label ?? "", placeholder: "Submit", onChange: (v) => updateSubmitButton({ label: v || void 0 }) }),
|
|
2400
|
+
/* @__PURE__ */ jsx(SettingsField, { label: "Loading Label", value: settings.submitButton?.loadingLabel ?? "", placeholder: "Submitting...", onChange: (v) => updateSubmitButton({ loadingLabel: v || void 0 }) }),
|
|
2401
|
+
/* @__PURE__ */ jsx(SettingsField, { label: "Success Label", value: settings.submitButton?.successLabel ?? "", placeholder: "Submitted!", onChange: (v) => updateSubmitButton({ successLabel: v || void 0 }) })
|
|
2402
|
+
] }),
|
|
2403
|
+
/* @__PURE__ */ jsxs(SettingsSection, { title: "Navigation", children: [
|
|
2404
|
+
/* @__PURE__ */ jsx(SettingsToggle, { label: "Show Back Button", checked: settings.navigation?.showBack !== false, onChange: (v) => updateNavigation({ showBack: v }) }),
|
|
2405
|
+
/* @__PURE__ */ jsx(SettingsToggle, { label: "Show Section List", checked: !!settings.navigation?.showSectionList, onChange: (v) => updateNavigation({ showSectionList: v }) }),
|
|
2406
|
+
/* @__PURE__ */ jsx(SettingsToggle, { label: "Allow Skip", checked: !!settings.navigation?.allowSkip, onChange: (v) => updateNavigation({ allowSkip: v }) }),
|
|
2407
|
+
/* @__PURE__ */ jsx(SettingsField, { label: "Next Label", value: settings.navigation?.nextLabel ?? "", placeholder: "Next", onChange: (v) => updateNavigation({ nextLabel: v || void 0 }) }),
|
|
2408
|
+
/* @__PURE__ */ jsx(SettingsField, { label: "Back Label", value: settings.navigation?.backLabel ?? "", placeholder: "Back", onChange: (v) => updateNavigation({ backLabel: v || void 0 }) })
|
|
2409
|
+
] }),
|
|
2410
|
+
/* @__PURE__ */ jsxs(SettingsSection, { title: "Drafts", children: [
|
|
2411
|
+
/* @__PURE__ */ jsx(SettingsToggle, { label: "Allow Draft Save", checked: !!settings.allowDraftSave, onChange: (v) => updateSettings({ allowDraftSave: v }) }),
|
|
2412
|
+
settings.allowDraftSave && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
2413
|
+
/* @__PURE__ */ jsx(
|
|
2414
|
+
SettingsSelect,
|
|
2415
|
+
{
|
|
2416
|
+
label: "Storage",
|
|
2417
|
+
value: settings.draftStorage ?? "local",
|
|
2418
|
+
options: [
|
|
2419
|
+
{ label: "Local", value: "local" },
|
|
2420
|
+
{ label: "Server", value: "server" },
|
|
2421
|
+
{ label: "Both", value: "both" }
|
|
2422
|
+
],
|
|
2423
|
+
onChange: (v) => updateSettings({ draftStorage: v })
|
|
2424
|
+
}
|
|
2425
|
+
),
|
|
2426
|
+
/* @__PURE__ */ jsx(SettingsNumber, { label: "Draft TTL (hours)", value: settings.draftTtlHours, placeholder: "24", onChange: (v) => updateSettings({ draftTtlHours: v }) })
|
|
2427
|
+
] })
|
|
2428
|
+
] }),
|
|
2429
|
+
/* @__PURE__ */ jsxs(SettingsSection, { title: "Advanced", children: [
|
|
2430
|
+
/* @__PURE__ */ jsx(SettingsField, { label: "Locale", value: settings.locale ?? "", placeholder: "en", onChange: (v) => updateSettings({ locale: v || void 0 }) }),
|
|
2431
|
+
/* @__PURE__ */ jsx(SettingsField, { label: "Server URL", value: settings.serverUrl ?? "", placeholder: "https://api.example.com/submit", onChange: (v) => updateSettings({ serverUrl: v || void 0 }) }),
|
|
2432
|
+
/* @__PURE__ */ jsx(SettingsToggle, { label: "No PII in Logs", checked: !!settings.noPiiInLogs, onChange: (v) => updateSettings({ noPiiInLogs: v }) })
|
|
2433
|
+
] })
|
|
2434
|
+
] });
|
|
2435
|
+
}
|
|
2436
|
+
function SettingsSection({ title, children }) {
|
|
2437
|
+
return /* @__PURE__ */ jsxs("div", { children: [
|
|
2438
|
+
/* @__PURE__ */ jsx(Separator, { className: "mb-4" }),
|
|
2439
|
+
/* @__PURE__ */ jsx(Label, { className: "text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-3 block", children: title }),
|
|
2440
|
+
/* @__PURE__ */ jsx("div", { className: "space-y-3", children })
|
|
2441
|
+
] });
|
|
2442
|
+
}
|
|
2443
|
+
function SettingsField({
|
|
2444
|
+
label,
|
|
2445
|
+
value,
|
|
2446
|
+
placeholder,
|
|
2447
|
+
onChange
|
|
2448
|
+
}) {
|
|
2449
|
+
return /* @__PURE__ */ jsxs("div", { children: [
|
|
2450
|
+
/* @__PURE__ */ jsx(Label, { className: "text-xs text-muted-foreground mb-1", children: label }),
|
|
2451
|
+
/* @__PURE__ */ jsx(
|
|
2452
|
+
Input,
|
|
2453
|
+
{
|
|
2454
|
+
value,
|
|
2455
|
+
onChange: (e) => onChange(e.target.value),
|
|
2456
|
+
placeholder
|
|
2457
|
+
}
|
|
2458
|
+
)
|
|
2459
|
+
] });
|
|
2460
|
+
}
|
|
2461
|
+
function SettingsNumber({
|
|
2462
|
+
label,
|
|
2463
|
+
value,
|
|
2464
|
+
placeholder,
|
|
2465
|
+
onChange
|
|
2466
|
+
}) {
|
|
2467
|
+
return /* @__PURE__ */ jsxs("div", { children: [
|
|
2468
|
+
/* @__PURE__ */ jsx(Label, { className: "text-xs text-muted-foreground mb-1", children: label }),
|
|
2469
|
+
/* @__PURE__ */ jsx(
|
|
2470
|
+
Input,
|
|
2471
|
+
{
|
|
2472
|
+
type: "number",
|
|
2473
|
+
value: value ?? "",
|
|
2474
|
+
onChange: (e) => onChange(e.target.value === "" ? void 0 : Number(e.target.value)),
|
|
2475
|
+
placeholder
|
|
2476
|
+
}
|
|
2477
|
+
)
|
|
2478
|
+
] });
|
|
2479
|
+
}
|
|
2480
|
+
function SettingsToggle({
|
|
2481
|
+
label,
|
|
2482
|
+
checked,
|
|
2483
|
+
onChange
|
|
2484
|
+
}) {
|
|
2485
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
|
|
2486
|
+
/* @__PURE__ */ jsx(Label, { className: "text-xs text-muted-foreground", children: label }),
|
|
2487
|
+
/* @__PURE__ */ jsx(Switch, { checked, onCheckedChange: onChange })
|
|
2488
|
+
] });
|
|
2489
|
+
}
|
|
2490
|
+
function SettingsSelect({
|
|
2491
|
+
label,
|
|
2492
|
+
value,
|
|
2493
|
+
options,
|
|
2494
|
+
onChange
|
|
2495
|
+
}) {
|
|
2496
|
+
return /* @__PURE__ */ jsxs("div", { children: [
|
|
2497
|
+
/* @__PURE__ */ jsx(Label, { className: "text-xs text-muted-foreground mb-1", children: label }),
|
|
2498
|
+
/* @__PURE__ */ jsxs("div", { className: "relative", children: [
|
|
2499
|
+
/* @__PURE__ */ jsx(
|
|
2500
|
+
"select",
|
|
2501
|
+
{
|
|
2502
|
+
value,
|
|
2503
|
+
onChange: (e) => onChange(e.target.value),
|
|
2504
|
+
className: "flex h-9 w-full appearance-none rounded-md border border-input bg-card px-3 py-1 pr-8 text-sm shadow-xs transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring text-foreground cursor-pointer",
|
|
2505
|
+
children: options.map((opt) => /* @__PURE__ */ jsx("option", { value: opt.value, children: opt.label }, opt.value))
|
|
2506
|
+
}
|
|
2507
|
+
),
|
|
2508
|
+
/* @__PURE__ */ jsx("svg", { className: "pointer-events-none absolute right-2.5 top-1/2 -translate-y-1/2 size-4 text-muted-foreground", xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsx("path", { d: "m6 9 6 6 6-6" }) })
|
|
2509
|
+
] })
|
|
2510
|
+
] });
|
|
2511
|
+
}
|
|
2512
|
+
function PropertiesPanel({ builderState }) {
|
|
2513
|
+
const { schema, selectedItem } = builderState;
|
|
2514
|
+
const [showSettings, setShowSettings] = useState(false);
|
|
2515
|
+
if (showSettings) {
|
|
2516
|
+
return /* @__PURE__ */ jsxs("div", { className: "w-80 h-full border-l border-border bg-card flex flex-col", children: [
|
|
2517
|
+
/* @__PURE__ */ jsx(PanelHeader, { title: "Form Settings", onClose: () => setShowSettings(false) }),
|
|
2518
|
+
/* @__PURE__ */ jsx("div", { className: "flex-1 overflow-y-auto p-4", children: /* @__PURE__ */ jsx(FormSettingsPanel, { schema, onUpdate: builderState.updateSchema }) })
|
|
2519
|
+
] });
|
|
2520
|
+
}
|
|
2521
|
+
if (!selectedItem) {
|
|
2522
|
+
return /* @__PURE__ */ jsxs("div", { className: "w-80 h-full border-l border-border bg-card flex flex-col", children: [
|
|
2523
|
+
/* @__PURE__ */ jsxs("div", { className: "shrink-0 px-4 py-3 border-b border-border flex items-center justify-between", children: [
|
|
2524
|
+
/* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold text-foreground", children: "Properties" }),
|
|
2525
|
+
/* @__PURE__ */ jsx(
|
|
2526
|
+
Button,
|
|
2527
|
+
{
|
|
2528
|
+
type: "button",
|
|
2529
|
+
variant: "ghost",
|
|
2530
|
+
size: "icon-xs",
|
|
2531
|
+
onClick: () => setShowSettings(true),
|
|
2532
|
+
title: "Form settings",
|
|
2533
|
+
children: /* @__PURE__ */ jsx(Settings, { size: 14, strokeWidth: 1.75 })
|
|
2534
|
+
}
|
|
2535
|
+
)
|
|
2536
|
+
] }),
|
|
2537
|
+
/* @__PURE__ */ jsx("div", { className: "flex-1 flex items-center justify-center", children: /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: "Select a field or section to edit" }) })
|
|
2538
|
+
] });
|
|
2539
|
+
}
|
|
2540
|
+
if (selectedItem.type === "section") {
|
|
2541
|
+
const found2 = findSection(schema, selectedItem.sectionId);
|
|
2542
|
+
if (!found2) return null;
|
|
2543
|
+
return /* @__PURE__ */ jsx(
|
|
2544
|
+
SectionProperties,
|
|
2545
|
+
{
|
|
2546
|
+
section: found2.section,
|
|
2547
|
+
onUpdate: (updates) => builderState.updateSection(selectedItem.sectionId, updates),
|
|
2548
|
+
onClose: builderState.clearSelection,
|
|
2549
|
+
onOpenSettings: () => setShowSettings(true)
|
|
2550
|
+
}
|
|
2551
|
+
);
|
|
2552
|
+
}
|
|
2553
|
+
const found = findQuestion(schema, selectedItem.sectionId, selectedItem.questionId);
|
|
2554
|
+
if (!found) return null;
|
|
2555
|
+
return /* @__PURE__ */ jsx(
|
|
2556
|
+
QuestionProperties,
|
|
2557
|
+
{
|
|
2558
|
+
question: found.question,
|
|
2559
|
+
sectionId: selectedItem.sectionId,
|
|
2560
|
+
builderState,
|
|
2561
|
+
onClose: builderState.clearSelection,
|
|
2562
|
+
onOpenSettings: () => setShowSettings(true)
|
|
2563
|
+
}
|
|
2564
|
+
);
|
|
2565
|
+
}
|
|
2566
|
+
function SectionProperties({ section, onUpdate, onClose, onOpenSettings }) {
|
|
2567
|
+
return /* @__PURE__ */ jsxs("div", { className: "w-80 h-full border-l border-border bg-card flex flex-col", children: [
|
|
2568
|
+
/* @__PURE__ */ jsx(PanelHeader, { title: "Section Properties", onClose, children: /* @__PURE__ */ jsx(Button, { type: "button", variant: "ghost", size: "icon-xs", onClick: onOpenSettings, title: "Form settings", children: /* @__PURE__ */ jsx(Settings, { size: 13, strokeWidth: 1.75 }) }) }),
|
|
2569
|
+
/* @__PURE__ */ jsxs("div", { className: "flex-1 overflow-y-auto p-4 space-y-4", children: [
|
|
2570
|
+
/* @__PURE__ */ jsx(FieldGroup, { label: "Title", children: /* @__PURE__ */ jsx(
|
|
2571
|
+
Input,
|
|
2572
|
+
{
|
|
2573
|
+
value: section.title,
|
|
2574
|
+
onChange: (e) => onUpdate({ title: e.target.value })
|
|
2575
|
+
}
|
|
2576
|
+
) }),
|
|
2577
|
+
/* @__PURE__ */ jsx(FieldGroup, { label: "Description", children: /* @__PURE__ */ jsx(
|
|
2578
|
+
Textarea,
|
|
2579
|
+
{
|
|
2580
|
+
value: section.description ?? "",
|
|
2581
|
+
onChange: (e) => onUpdate({ description: e.target.value }),
|
|
2582
|
+
rows: 3
|
|
2583
|
+
}
|
|
2584
|
+
) })
|
|
2585
|
+
] })
|
|
2586
|
+
] });
|
|
2587
|
+
}
|
|
2588
|
+
function QuestionProperties({ question, sectionId, builderState, onClose, onOpenSettings }) {
|
|
2589
|
+
const [activeTab, setActiveTab] = useState("basic");
|
|
2590
|
+
const typeInfo = QUESTION_TYPE_INFO[question.type];
|
|
2591
|
+
const IconComponent = typeInfo ? getIcon(typeInfo.icon) : null;
|
|
2592
|
+
const hasOptions = typeInfo?.requiresOptions || question.options && question.options.length > 0;
|
|
2593
|
+
const updateQuestion2 = useCallback(
|
|
2594
|
+
(updates) => {
|
|
2595
|
+
builderState.updateQuestion(sectionId, question.id, updates);
|
|
2596
|
+
},
|
|
2597
|
+
[builderState, sectionId, question.id]
|
|
2598
|
+
);
|
|
2599
|
+
const tabs = [
|
|
2600
|
+
{ key: "basic", label: "Basic" },
|
|
2601
|
+
{ key: "validation", label: "Rules" },
|
|
2602
|
+
{ key: "logic", label: "Logic" }
|
|
2603
|
+
];
|
|
2604
|
+
return /* @__PURE__ */ jsxs("div", { className: "w-80 h-full border-l border-border bg-card flex flex-col", children: [
|
|
2605
|
+
/* @__PURE__ */ jsx(PanelHeader, { title: "Field Properties", onClose, children: /* @__PURE__ */ jsx(Button, { type: "button", variant: "ghost", size: "icon-xs", onClick: onOpenSettings, title: "Form settings", children: /* @__PURE__ */ jsx(Settings, { size: 13, strokeWidth: 1.75 }) }) }),
|
|
2606
|
+
IconComponent && /* @__PURE__ */ jsx("div", { className: "px-4 pt-3 pb-0", children: /* @__PURE__ */ jsxs(Badge, { variant: "secondary", className: "gap-1.5", children: [
|
|
2607
|
+
/* @__PURE__ */ jsx(IconComponent, { size: 11, strokeWidth: 1.75 }),
|
|
2608
|
+
typeInfo?.label
|
|
2609
|
+
] }) }),
|
|
2610
|
+
/* @__PURE__ */ jsx("div", { className: "shrink-0 px-4 pt-3 flex gap-0.5", children: tabs.map((tab) => /* @__PURE__ */ jsx(
|
|
2611
|
+
"button",
|
|
2612
|
+
{
|
|
2613
|
+
type: "button",
|
|
2614
|
+
onClick: () => setActiveTab(tab.key),
|
|
2615
|
+
className: cn(
|
|
2616
|
+
"px-3 py-1.5 text-xs font-medium rounded-md border-0 cursor-pointer transition-colors",
|
|
2617
|
+
activeTab === tab.key ? "bg-primary text-primary-foreground" : "bg-transparent text-muted-foreground hover:bg-accent hover:text-foreground"
|
|
2618
|
+
),
|
|
2619
|
+
children: tab.label
|
|
2620
|
+
},
|
|
2621
|
+
tab.key
|
|
2622
|
+
)) }),
|
|
2623
|
+
/* @__PURE__ */ jsxs("div", { className: "flex-1 overflow-y-auto p-4 space-y-4", children: [
|
|
2624
|
+
activeTab === "basic" && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
2625
|
+
/* @__PURE__ */ jsx(FieldGroup, { label: "Label", children: /* @__PURE__ */ jsx(
|
|
2626
|
+
Input,
|
|
2627
|
+
{
|
|
2628
|
+
value: question.label,
|
|
2629
|
+
onChange: (e) => updateQuestion2({ label: e.target.value })
|
|
2630
|
+
}
|
|
2631
|
+
) }),
|
|
2632
|
+
/* @__PURE__ */ jsx(FieldGroup, { label: "Help Text", children: /* @__PURE__ */ jsx(
|
|
2633
|
+
Input,
|
|
2634
|
+
{
|
|
2635
|
+
value: question.helpText ?? "",
|
|
2636
|
+
onChange: (e) => updateQuestion2({ helpText: e.target.value || void 0 }),
|
|
2637
|
+
placeholder: "Optional help text below the field"
|
|
2638
|
+
}
|
|
2639
|
+
) }),
|
|
2640
|
+
/* @__PURE__ */ jsx(FieldGroup, { label: "Placeholder", children: /* @__PURE__ */ jsx(
|
|
2641
|
+
Input,
|
|
2642
|
+
{
|
|
2643
|
+
value: question.placeholder ?? "",
|
|
2644
|
+
onChange: (e) => updateQuestion2({ placeholder: e.target.value || void 0 }),
|
|
2645
|
+
placeholder: "Input placeholder text"
|
|
2646
|
+
}
|
|
2647
|
+
) }),
|
|
2648
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
|
|
2649
|
+
/* @__PURE__ */ jsx(Label, { htmlFor: "required-switch", children: "Required" }),
|
|
2650
|
+
/* @__PURE__ */ jsx(
|
|
2651
|
+
Switch,
|
|
2652
|
+
{
|
|
2653
|
+
id: "required-switch",
|
|
2654
|
+
checked: !!question.required,
|
|
2655
|
+
onCheckedChange: (checked) => updateQuestion2({ required: checked })
|
|
2656
|
+
}
|
|
2657
|
+
)
|
|
2658
|
+
] }),
|
|
2659
|
+
/* @__PURE__ */ jsx(QuestionConfigEditor, { question, onUpdate: updateQuestion2 }),
|
|
2660
|
+
hasOptions && /* @__PURE__ */ jsx(
|
|
2661
|
+
OptionsEditor,
|
|
2662
|
+
{
|
|
2663
|
+
options: question.options ?? [],
|
|
2664
|
+
sectionId,
|
|
2665
|
+
questionId: question.id,
|
|
2666
|
+
builderState
|
|
2667
|
+
}
|
|
2668
|
+
)
|
|
2669
|
+
] }),
|
|
2670
|
+
activeTab === "validation" && /* @__PURE__ */ jsx(ValidationRulesEditor, { question, onUpdate: updateQuestion2 }),
|
|
2671
|
+
activeTab === "logic" && /* @__PURE__ */ jsx(ConditionEditor, { question, schema: builderState.schema, onUpdate: updateQuestion2 })
|
|
2672
|
+
] })
|
|
2673
|
+
] });
|
|
2674
|
+
}
|
|
2675
|
+
function OptionsEditor({ options, sectionId, questionId, builderState }) {
|
|
2676
|
+
const handleAddOption = () => {
|
|
2677
|
+
const index = options.length;
|
|
2678
|
+
const newOption = {
|
|
2679
|
+
label: `Option ${index + 1}`,
|
|
2680
|
+
value: `option${index + 1}`
|
|
2681
|
+
};
|
|
2682
|
+
builderState.updateQuestion(sectionId, questionId, {
|
|
2683
|
+
options: [...options, newOption]
|
|
2684
|
+
});
|
|
2685
|
+
};
|
|
2686
|
+
const handleUpdateOption = (index, updates) => {
|
|
2687
|
+
const updated = options.map(
|
|
2688
|
+
(opt, i) => i === index ? { ...opt, ...updates } : opt
|
|
2689
|
+
);
|
|
2690
|
+
builderState.updateQuestion(sectionId, questionId, { options: updated });
|
|
2691
|
+
};
|
|
2692
|
+
const handleRemoveOption = (index) => {
|
|
2693
|
+
if (options.length <= 1) return;
|
|
2694
|
+
const updated = options.filter((_, i) => i !== index);
|
|
2695
|
+
builderState.updateQuestion(sectionId, questionId, { options: updated });
|
|
2696
|
+
};
|
|
2697
|
+
return /* @__PURE__ */ jsxs("div", { children: [
|
|
2698
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between mb-2", children: [
|
|
2699
|
+
/* @__PURE__ */ jsx(Label, { children: "Options" }),
|
|
2700
|
+
/* @__PURE__ */ jsxs(Button, { type: "button", variant: "secondary", size: "xs", onClick: handleAddOption, children: [
|
|
2701
|
+
/* @__PURE__ */ jsx(Plus, { size: 12, strokeWidth: 2 }),
|
|
2702
|
+
"Add"
|
|
2703
|
+
] })
|
|
2704
|
+
] }),
|
|
2705
|
+
/* @__PURE__ */ jsx("div", { className: "space-y-1.5", children: options.map((option, index) => /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 group", children: [
|
|
2706
|
+
/* @__PURE__ */ jsx("div", { className: "text-muted-foreground cursor-grab shrink-0", children: /* @__PURE__ */ jsx(GripVertical, { size: 12, strokeWidth: 1.5 }) }),
|
|
2707
|
+
/* @__PURE__ */ jsx(
|
|
2708
|
+
Input,
|
|
2709
|
+
{
|
|
2710
|
+
value: option.label,
|
|
2711
|
+
onChange: (e) => {
|
|
2712
|
+
const label = e.target.value;
|
|
2713
|
+
handleUpdateOption(index, {
|
|
2714
|
+
label,
|
|
2715
|
+
value: label.toLowerCase().replace(/\s+/g, "_")
|
|
2716
|
+
});
|
|
2717
|
+
},
|
|
2718
|
+
className: "h-8 flex-1"
|
|
2719
|
+
}
|
|
2720
|
+
),
|
|
2721
|
+
/* @__PURE__ */ jsx(
|
|
2722
|
+
Button,
|
|
2723
|
+
{
|
|
2724
|
+
type: "button",
|
|
2725
|
+
variant: "ghost",
|
|
2726
|
+
size: "icon-xs",
|
|
2727
|
+
onClick: () => handleRemoveOption(index),
|
|
2728
|
+
disabled: options.length <= 1,
|
|
2729
|
+
className: "shrink-0 opacity-0 group-hover:opacity-100 transition-opacity disabled:opacity-0 hover:bg-destructive/10 hover:text-destructive",
|
|
2730
|
+
title: "Remove option",
|
|
2731
|
+
children: /* @__PURE__ */ jsx(Trash2, { size: 12, strokeWidth: 1.75 })
|
|
2732
|
+
}
|
|
2733
|
+
)
|
|
2734
|
+
] }, index)) })
|
|
2735
|
+
] });
|
|
2736
|
+
}
|
|
2737
|
+
function PanelHeader({
|
|
2738
|
+
title,
|
|
2739
|
+
onClose,
|
|
2740
|
+
children
|
|
2741
|
+
}) {
|
|
2742
|
+
return /* @__PURE__ */ jsxs("div", { className: "shrink-0 px-4 py-3 border-b border-border flex items-center justify-between", children: [
|
|
2743
|
+
/* @__PURE__ */ jsx("div", { className: "flex items-center gap-2", children: /* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold text-foreground", children: title }) }),
|
|
2744
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-0.5", children: [
|
|
2745
|
+
children,
|
|
2746
|
+
/* @__PURE__ */ jsx(
|
|
2747
|
+
Button,
|
|
2748
|
+
{
|
|
2749
|
+
type: "button",
|
|
2750
|
+
variant: "ghost",
|
|
2751
|
+
size: "icon-xs",
|
|
2752
|
+
onClick: onClose,
|
|
2753
|
+
title: "Close panel",
|
|
2754
|
+
"aria-label": "Close properties panel",
|
|
2755
|
+
children: /* @__PURE__ */ jsx(X, { size: 14, strokeWidth: 1.75 })
|
|
2756
|
+
}
|
|
2757
|
+
)
|
|
2758
|
+
] })
|
|
2759
|
+
] });
|
|
2760
|
+
}
|
|
2761
|
+
function FieldGroup({ label, children }) {
|
|
2762
|
+
return /* @__PURE__ */ jsxs("div", { children: [
|
|
2763
|
+
/* @__PURE__ */ jsx(Label, { className: "text-xs text-muted-foreground mb-1.5", children: label }),
|
|
2764
|
+
children
|
|
2765
|
+
] });
|
|
2766
|
+
}
|
|
2767
|
+
var ThemeCtx = createContext({});
|
|
2768
|
+
function useBuilderTheme() {
|
|
2769
|
+
return useContext(ThemeCtx);
|
|
2770
|
+
}
|
|
2771
|
+
function themeToCssVars(theme) {
|
|
2772
|
+
const vars = {};
|
|
2773
|
+
if (theme.background) vars["--background"] = theme.background;
|
|
2774
|
+
if (theme.foreground) vars["--foreground"] = theme.foreground;
|
|
2775
|
+
if (theme.card) {
|
|
2776
|
+
vars["--card"] = theme.card;
|
|
2777
|
+
vars["--card-foreground"] = theme.foreground ?? "";
|
|
2778
|
+
vars["--popover"] = theme.card;
|
|
2779
|
+
vars["--popover-foreground"] = theme.foreground ?? "";
|
|
2780
|
+
}
|
|
2781
|
+
if (theme.primary) vars["--primary"] = theme.primary;
|
|
2782
|
+
if (theme.primaryForeground) vars["--primary-foreground"] = theme.primaryForeground;
|
|
2783
|
+
if (theme.secondary) vars["--secondary"] = theme.secondary;
|
|
2784
|
+
if (theme.secondaryForeground) vars["--secondary-foreground"] = theme.secondaryForeground;
|
|
2785
|
+
if (theme.muted) vars["--muted"] = theme.muted;
|
|
2786
|
+
if (theme.mutedForeground) vars["--muted-foreground"] = theme.mutedForeground;
|
|
2787
|
+
if (theme.accent) vars["--accent"] = theme.accent;
|
|
2788
|
+
if (theme.accentForeground) vars["--accent-foreground"] = theme.accentForeground;
|
|
2789
|
+
if (theme.destructive) vars["--destructive"] = theme.destructive;
|
|
2790
|
+
if (theme.destructiveForeground) vars["--destructive-foreground"] = theme.destructiveForeground;
|
|
2791
|
+
if (theme.border) vars["--border"] = theme.border;
|
|
2792
|
+
if (theme.input) vars["--input"] = theme.input;
|
|
2793
|
+
if (theme.ring) vars["--ring"] = theme.ring;
|
|
2794
|
+
if (theme.radius) vars["--radius"] = theme.radius;
|
|
2795
|
+
if (theme.surface) vars["--fcb-surface"] = theme.surface;
|
|
2796
|
+
if (theme.surfaceHover) vars["--fcb-surface-hover"] = theme.surfaceHover;
|
|
2797
|
+
if (theme.canvas) vars["--fcb-canvas"] = theme.canvas;
|
|
2798
|
+
if (theme.panel) vars["--fcb-panel"] = theme.panel;
|
|
2799
|
+
if (theme.borderStrong) vars["--fcb-border-strong"] = theme.borderStrong;
|
|
2800
|
+
if (theme.textDim) vars["--fcb-text-dim"] = theme.textDim;
|
|
2801
|
+
return vars;
|
|
2802
|
+
}
|
|
2803
|
+
function FormBuilderThemeProvider({ theme, children }) {
|
|
2804
|
+
const resolved = theme ?? {};
|
|
2805
|
+
const cssVars = useMemo(() => themeToCssVars(resolved), [resolved]);
|
|
2806
|
+
return /* @__PURE__ */ jsx(ThemeCtx.Provider, { value: resolved, children: /* @__PURE__ */ jsx("div", { "data-fcb-root": "", style: cssVars, className: "w-full h-full", children }) });
|
|
2807
|
+
}
|
|
2808
|
+
var FormBuilderErrorBoundary = class extends Component {
|
|
2809
|
+
constructor(props) {
|
|
2810
|
+
super(props);
|
|
2811
|
+
this.state = { hasError: false, error: null };
|
|
2812
|
+
}
|
|
2813
|
+
static getDerivedStateFromError(error) {
|
|
2814
|
+
return { hasError: true, error };
|
|
2815
|
+
}
|
|
2816
|
+
componentDidCatch(error, info) {
|
|
2817
|
+
console.error("[FormBuilder] Render error:", error, info.componentStack);
|
|
2818
|
+
}
|
|
2819
|
+
render() {
|
|
2820
|
+
if (this.state.hasError) {
|
|
2821
|
+
return /* @__PURE__ */ jsx("div", { className: "flex flex-col items-center justify-center h-full bg-background text-foreground p-8", children: /* @__PURE__ */ jsxs("div", { className: "max-w-md text-center space-y-4", children: [
|
|
2822
|
+
/* @__PURE__ */ jsx("h2", { className: "text-lg font-semibold", children: "Something went wrong" }),
|
|
2823
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: "The form builder encountered an unexpected error. Your schema data is preserved." }),
|
|
2824
|
+
/* @__PURE__ */ jsx("pre", { className: "text-xs text-destructive bg-destructive/10 rounded-md p-3 text-left overflow-auto max-h-32", children: this.state.error?.message }),
|
|
2825
|
+
/* @__PURE__ */ jsx(
|
|
2826
|
+
Button,
|
|
2827
|
+
{
|
|
2828
|
+
onClick: () => this.setState({ hasError: false, error: null }),
|
|
2829
|
+
variant: "outline",
|
|
2830
|
+
children: "Try Again"
|
|
2831
|
+
}
|
|
2832
|
+
)
|
|
2833
|
+
] }) });
|
|
2834
|
+
}
|
|
2835
|
+
return this.props.children;
|
|
2836
|
+
}
|
|
2837
|
+
};
|
|
2838
|
+
function FormBuilderCore(props) {
|
|
2839
|
+
const { initialSchema = DEFAULT_SCHEMA, onChange, onSave, height = "100vh", theme, className, toolbarExtra, questionTypes, palette } = props;
|
|
2840
|
+
const mergedQuestionTypes = questionTypes ? { ...QUESTION_TYPE_INFO, ...questionTypes } : QUESTION_TYPE_INFO;
|
|
2841
|
+
const builderState = useBuilderState(initialSchema);
|
|
2842
|
+
const dragDrop = useDragDrop(builderState);
|
|
2843
|
+
const fileInputRef = useRef(null);
|
|
2844
|
+
useEffect(() => {
|
|
2845
|
+
if (onChange && builderState.isDirty) {
|
|
2846
|
+
onChange(builderState.schema);
|
|
2847
|
+
}
|
|
2848
|
+
}, [builderState.schema, builderState.isDirty, onChange]);
|
|
2849
|
+
const handleSave = useCallback(() => {
|
|
2850
|
+
if (onSave) {
|
|
2851
|
+
onSave(builderState.schema);
|
|
2852
|
+
builderState.markClean();
|
|
2853
|
+
}
|
|
2854
|
+
}, [onSave, builderState]);
|
|
2855
|
+
const handleExport = useCallback(() => {
|
|
2856
|
+
const json = JSON.stringify(builderState.schema, null, 2);
|
|
2857
|
+
const blob = new Blob([json], { type: "application/json" });
|
|
2858
|
+
const url = URL.createObjectURL(blob);
|
|
2859
|
+
const a = document.createElement("a");
|
|
2860
|
+
a.href = url;
|
|
2861
|
+
a.download = `${builderState.schema.title?.replace(/\s+/g, "-").toLowerCase() || "form"}-schema.json`;
|
|
2862
|
+
a.click();
|
|
2863
|
+
URL.revokeObjectURL(url);
|
|
2864
|
+
}, [builderState.schema]);
|
|
2865
|
+
const handleImport = useCallback(() => {
|
|
2866
|
+
fileInputRef.current?.click();
|
|
2867
|
+
}, []);
|
|
2868
|
+
const handleFileChange = useCallback(
|
|
2869
|
+
(e) => {
|
|
2870
|
+
const file = e.target.files?.[0];
|
|
2871
|
+
if (!file) return;
|
|
2872
|
+
const reader = new FileReader();
|
|
2873
|
+
reader.onload = (event) => {
|
|
2874
|
+
try {
|
|
2875
|
+
const parsed = JSON.parse(event.target?.result);
|
|
2876
|
+
if (parsed && parsed.sections && Array.isArray(parsed.sections)) {
|
|
2877
|
+
builderState.resetSchema(parsed);
|
|
2878
|
+
} else {
|
|
2879
|
+
alert("Invalid schema: must contain a 'sections' array.");
|
|
2880
|
+
}
|
|
2881
|
+
} catch {
|
|
2882
|
+
alert("Failed to parse JSON file.");
|
|
2883
|
+
}
|
|
2884
|
+
};
|
|
2885
|
+
reader.readAsText(file);
|
|
2886
|
+
e.target.value = "";
|
|
2887
|
+
},
|
|
2888
|
+
[builderState]
|
|
2889
|
+
);
|
|
2890
|
+
const handleKeyDown = useCallback(
|
|
2891
|
+
(e) => {
|
|
2892
|
+
const mod = e.metaKey || e.ctrlKey;
|
|
2893
|
+
const tag = e.target.tagName;
|
|
2894
|
+
const isEditing = tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT";
|
|
2895
|
+
if (mod && e.key === "z" && !e.shiftKey) {
|
|
2896
|
+
e.preventDefault();
|
|
2897
|
+
builderState.undo();
|
|
2898
|
+
return;
|
|
2899
|
+
}
|
|
2900
|
+
if (mod && e.key === "z" && e.shiftKey) {
|
|
2901
|
+
e.preventDefault();
|
|
2902
|
+
builderState.redo();
|
|
2903
|
+
return;
|
|
2904
|
+
}
|
|
2905
|
+
if (mod && e.key === "s") {
|
|
2906
|
+
e.preventDefault();
|
|
2907
|
+
handleSave();
|
|
2908
|
+
return;
|
|
2909
|
+
}
|
|
2910
|
+
if (isEditing) return;
|
|
2911
|
+
if (e.key === "Delete" || e.key === "Backspace") {
|
|
2912
|
+
const sel = builderState.selectedItem;
|
|
2913
|
+
if (!sel) return;
|
|
2914
|
+
e.preventDefault();
|
|
2915
|
+
if (sel.type === "question") {
|
|
2916
|
+
builderState.removeQuestion(sel.sectionId, sel.questionId);
|
|
2917
|
+
} else if (sel.type === "section") {
|
|
2918
|
+
builderState.removeSection(sel.sectionId);
|
|
2919
|
+
}
|
|
2920
|
+
return;
|
|
2921
|
+
}
|
|
2922
|
+
if (e.key === "Escape") {
|
|
2923
|
+
if (builderState.selectedItem) {
|
|
2924
|
+
e.preventDefault();
|
|
2925
|
+
builderState.clearSelection();
|
|
2926
|
+
}
|
|
2927
|
+
return;
|
|
2928
|
+
}
|
|
2929
|
+
if (mod && e.key === "d") {
|
|
2930
|
+
const sel = builderState.selectedItem;
|
|
2931
|
+
if (!sel) return;
|
|
2932
|
+
e.preventDefault();
|
|
2933
|
+
if (sel.type === "question") {
|
|
2934
|
+
builderState.duplicateQuestion(sel.sectionId, sel.questionId);
|
|
2935
|
+
} else if (sel.type === "section") {
|
|
2936
|
+
builderState.duplicateSection(sel.sectionId);
|
|
2937
|
+
}
|
|
2938
|
+
}
|
|
2939
|
+
},
|
|
2940
|
+
[builderState, handleSave]
|
|
2941
|
+
);
|
|
2942
|
+
return /* @__PURE__ */ jsx(FormBuilderThemeProvider, { theme, children: /* @__PURE__ */ jsxs(
|
|
2943
|
+
DndContext,
|
|
2944
|
+
{
|
|
2945
|
+
sensors: dragDrop.sensors,
|
|
2946
|
+
onDragStart: dragDrop.handleDragStart,
|
|
2947
|
+
onDragEnd: dragDrop.handleDragEnd,
|
|
2948
|
+
onDragCancel: dragDrop.handleDragCancel,
|
|
2949
|
+
children: [
|
|
2950
|
+
/* @__PURE__ */ jsxs(
|
|
2951
|
+
"div",
|
|
2952
|
+
{
|
|
2953
|
+
className: cn("flex flex-col bg-background text-foreground", className),
|
|
2954
|
+
style: { height: typeof height === "number" ? `${height}px` : height },
|
|
2955
|
+
onKeyDown: handleKeyDown,
|
|
2956
|
+
tabIndex: -1,
|
|
2957
|
+
role: "application",
|
|
2958
|
+
"aria-label": "Form Builder",
|
|
2959
|
+
children: [
|
|
2960
|
+
/* @__PURE__ */ jsxs("div", { className: "h-12 shrink-0 grid px-4 bg-card border-b border-border", style: { gridTemplateColumns: "1fr auto 1fr" }, role: "toolbar", "aria-label": "Builder toolbar", children: [
|
|
2961
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
|
|
2962
|
+
/* @__PURE__ */ jsx(
|
|
2963
|
+
Button,
|
|
2964
|
+
{
|
|
2965
|
+
variant: "ghost",
|
|
2966
|
+
size: "icon-sm",
|
|
2967
|
+
onClick: builderState.undo,
|
|
2968
|
+
disabled: !builderState.canUndo,
|
|
2969
|
+
className: "disabled:opacity-30",
|
|
2970
|
+
title: "Undo (Ctrl+Z)",
|
|
2971
|
+
"aria-label": "Undo",
|
|
2972
|
+
children: /* @__PURE__ */ jsx(Undo2, { size: 16, strokeWidth: 1.75 })
|
|
2973
|
+
}
|
|
2974
|
+
),
|
|
2975
|
+
/* @__PURE__ */ jsx(
|
|
2976
|
+
Button,
|
|
2977
|
+
{
|
|
2978
|
+
variant: "ghost",
|
|
2979
|
+
size: "icon-sm",
|
|
2980
|
+
onClick: builderState.redo,
|
|
2981
|
+
disabled: !builderState.canRedo,
|
|
2982
|
+
className: "disabled:opacity-30",
|
|
2983
|
+
title: "Redo (Ctrl+Shift+Z)",
|
|
2984
|
+
"aria-label": "Redo",
|
|
2985
|
+
children: /* @__PURE__ */ jsx(Redo2, { size: 16, strokeWidth: 1.75 })
|
|
2986
|
+
}
|
|
2987
|
+
),
|
|
2988
|
+
/* @__PURE__ */ jsx(Separator, { orientation: "vertical", className: "mx-2 h-5" }),
|
|
2989
|
+
builderState.isDirty && /* @__PURE__ */ jsx("span", { className: "text-xs text-primary", role: "status", children: "Unsaved changes" })
|
|
2990
|
+
] }),
|
|
2991
|
+
/* @__PURE__ */ jsx("div", { className: "flex items-center justify-center", children: toolbarExtra }),
|
|
2992
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-end gap-1.5", children: [
|
|
2993
|
+
/* @__PURE__ */ jsx(
|
|
2994
|
+
Button,
|
|
2995
|
+
{
|
|
2996
|
+
variant: "ghost",
|
|
2997
|
+
size: "icon-sm",
|
|
2998
|
+
onClick: handleImport,
|
|
2999
|
+
title: "Import schema JSON",
|
|
3000
|
+
"aria-label": "Import schema",
|
|
3001
|
+
children: /* @__PURE__ */ jsx(Upload, { size: 15, strokeWidth: 1.75 })
|
|
3002
|
+
}
|
|
3003
|
+
),
|
|
3004
|
+
/* @__PURE__ */ jsx(
|
|
3005
|
+
Button,
|
|
3006
|
+
{
|
|
3007
|
+
variant: "ghost",
|
|
3008
|
+
size: "icon-sm",
|
|
3009
|
+
onClick: handleExport,
|
|
3010
|
+
title: "Export schema JSON",
|
|
3011
|
+
"aria-label": "Export schema",
|
|
3012
|
+
children: /* @__PURE__ */ jsx(Download, { size: 15, strokeWidth: 1.75 })
|
|
3013
|
+
}
|
|
3014
|
+
),
|
|
3015
|
+
/* @__PURE__ */ jsx(Separator, { orientation: "vertical", className: "mx-1 h-5" }),
|
|
3016
|
+
/* @__PURE__ */ jsxs(Button, { onClick: handleSave, className: "border-0 shadow-sm fcb-glow", "aria-label": "Save form", children: [
|
|
3017
|
+
/* @__PURE__ */ jsx(Save, { size: 14, strokeWidth: 2 }),
|
|
3018
|
+
"Save"
|
|
3019
|
+
] })
|
|
3020
|
+
] }),
|
|
3021
|
+
/* @__PURE__ */ jsx(
|
|
3022
|
+
"input",
|
|
3023
|
+
{
|
|
3024
|
+
ref: fileInputRef,
|
|
3025
|
+
type: "file",
|
|
3026
|
+
accept: ".json,application/json",
|
|
3027
|
+
onChange: handleFileChange,
|
|
3028
|
+
className: "hidden",
|
|
3029
|
+
"aria-hidden": "true"
|
|
3030
|
+
}
|
|
3031
|
+
)
|
|
3032
|
+
] }),
|
|
3033
|
+
/* @__PURE__ */ jsxs("div", { className: "flex-1 flex overflow-hidden", children: [
|
|
3034
|
+
/* @__PURE__ */ jsx(QuestionPalette, { questionTypes: mergedQuestionTypes, palette }),
|
|
3035
|
+
/* @__PURE__ */ jsx(FormCanvas, { builderState }),
|
|
3036
|
+
/* @__PURE__ */ jsx(PropertiesPanel, { builderState })
|
|
3037
|
+
] })
|
|
3038
|
+
]
|
|
3039
|
+
}
|
|
3040
|
+
),
|
|
3041
|
+
/* @__PURE__ */ jsx(DragOverlay, { dropAnimation: null, children: dragDrop.activeDragItem && /* @__PURE__ */ jsx(DragOverlayContent, { item: dragDrop.activeDragItem, schema: builderState.schema, questionTypes: mergedQuestionTypes }) })
|
|
3042
|
+
]
|
|
3043
|
+
}
|
|
3044
|
+
) });
|
|
3045
|
+
}
|
|
3046
|
+
function DragOverlayContent({
|
|
3047
|
+
item,
|
|
3048
|
+
schema,
|
|
3049
|
+
questionTypes: types
|
|
3050
|
+
}) {
|
|
3051
|
+
if (item.type === "palette-item") {
|
|
3052
|
+
const typeInfo = types[item.questionType];
|
|
3053
|
+
if (!typeInfo) return null;
|
|
3054
|
+
const IconComponent = getIcon(typeInfo.icon);
|
|
3055
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 px-3 py-2 rounded-md border border-primary bg-card text-foreground text-sm fcb-shadow-lg cursor-grabbing", children: [
|
|
3056
|
+
/* @__PURE__ */ jsx(IconComponent, { size: 14, className: "shrink-0 text-primary", strokeWidth: 1.75 }),
|
|
3057
|
+
/* @__PURE__ */ jsx("span", { className: "font-medium", children: typeInfo.label })
|
|
3058
|
+
] });
|
|
3059
|
+
}
|
|
3060
|
+
if (item.type === "question") {
|
|
3061
|
+
const found = findQuestion(schema, item.sectionId, item.questionId);
|
|
3062
|
+
if (!found) return null;
|
|
3063
|
+
const typeInfo = types[found.question.type];
|
|
3064
|
+
const IconComponent = typeInfo ? getIcon(typeInfo.icon) : null;
|
|
3065
|
+
return /* @__PURE__ */ jsxs("div", { className: "p-3 rounded-md border border-primary bg-card text-foreground fcb-shadow-lg cursor-grabbing max-w-sm", children: [
|
|
3066
|
+
/* @__PURE__ */ jsx("div", { className: "flex items-center gap-2 mb-1", children: /* @__PURE__ */ jsxs(Badge, { variant: "secondary", className: "gap-1.5 text-muted-foreground", children: [
|
|
3067
|
+
IconComponent && /* @__PURE__ */ jsx(IconComponent, { size: 11, strokeWidth: 1.75 }),
|
|
3068
|
+
typeInfo?.label ?? found.question.type
|
|
3069
|
+
] }) }),
|
|
3070
|
+
/* @__PURE__ */ jsx("div", { className: "text-sm font-medium", children: found.question.label })
|
|
3071
|
+
] });
|
|
3072
|
+
}
|
|
3073
|
+
if (item.type === "section") {
|
|
3074
|
+
const section = schema.sections.find((s) => s.id === item.sectionId);
|
|
3075
|
+
if (!section) return null;
|
|
3076
|
+
return /* @__PURE__ */ jsxs("div", { className: "px-4 py-3 rounded-md border border-primary bg-card text-foreground fcb-shadow-lg cursor-grabbing max-w-sm", children: [
|
|
3077
|
+
/* @__PURE__ */ jsx("div", { className: "text-sm font-semibold", children: section.title }),
|
|
3078
|
+
/* @__PURE__ */ jsxs("div", { className: "text-xs text-muted-foreground mt-0.5", children: [
|
|
3079
|
+
section.questions.length,
|
|
3080
|
+
" field",
|
|
3081
|
+
section.questions.length !== 1 ? "s" : ""
|
|
3082
|
+
] })
|
|
3083
|
+
] });
|
|
3084
|
+
}
|
|
3085
|
+
return null;
|
|
3086
|
+
}
|
|
3087
|
+
function FormBuilderInner(props) {
|
|
3088
|
+
return /* @__PURE__ */ jsx(FormBuilderErrorBoundary, { children: /* @__PURE__ */ jsx(FormBuilderCore, { ...props }) });
|
|
3089
|
+
}
|
|
3090
|
+
|
|
3091
|
+
// src/form-builder/components/FormBuilderGated.tsx
|
|
3092
|
+
var FormBuilder = requireLicense(FormBuilderInner, "FormBuilder");
|
|
3093
|
+
|
|
3094
|
+
// src/form-builder/theme/presets.ts
|
|
3095
|
+
var squaredrDarkPreset = {
|
|
3096
|
+
background: "#0a0a0b",
|
|
3097
|
+
// ink-950
|
|
3098
|
+
foreground: "#e8e8ea",
|
|
3099
|
+
// ink-100
|
|
3100
|
+
card: "#111113",
|
|
3101
|
+
// ink-900
|
|
3102
|
+
primary: "oklch(0.82 0.14 210)",
|
|
3103
|
+
// sr-accent-cyan
|
|
3104
|
+
primaryForeground: "#0a0a0b",
|
|
3105
|
+
// ink-950
|
|
3106
|
+
secondary: "#17171a",
|
|
3107
|
+
// ink-850
|
|
3108
|
+
secondaryForeground: "#e8e8ea",
|
|
3109
|
+
// ink-100
|
|
3110
|
+
muted: "#111113",
|
|
3111
|
+
// ink-900
|
|
3112
|
+
mutedForeground: "#8a8a95",
|
|
3113
|
+
// ink-400
|
|
3114
|
+
accent: "#17171a",
|
|
3115
|
+
// ink-850
|
|
3116
|
+
accentForeground: "#e8e8ea",
|
|
3117
|
+
// ink-100
|
|
3118
|
+
destructive: "oklch(0.68 0.22 25)",
|
|
3119
|
+
// sr-error
|
|
3120
|
+
destructiveForeground: "#e8e8ea",
|
|
3121
|
+
// ink-100
|
|
3122
|
+
border: "#1c1c20",
|
|
3123
|
+
// ink-800
|
|
3124
|
+
input: "#1c1c20",
|
|
3125
|
+
// ink-800
|
|
3126
|
+
ring: "oklch(0.82 0.14 210)",
|
|
3127
|
+
// sr-accent-cyan
|
|
3128
|
+
radius: "6px",
|
|
3129
|
+
surface: "#111113",
|
|
3130
|
+
// ink-900
|
|
3131
|
+
surfaceHover: "#17171a",
|
|
3132
|
+
// ink-850
|
|
3133
|
+
canvas: "#0a0a0b",
|
|
3134
|
+
// ink-950
|
|
3135
|
+
panel: "#111113",
|
|
3136
|
+
// ink-900
|
|
3137
|
+
borderStrong: "#26262c",
|
|
3138
|
+
// ink-700
|
|
3139
|
+
textDim: "#5a5a66"
|
|
3140
|
+
// ink-500
|
|
3141
|
+
};
|
|
3142
|
+
var cleanPreset = {
|
|
3143
|
+
background: "#ffffff",
|
|
3144
|
+
foreground: "#111113",
|
|
3145
|
+
card: "#f9fafb",
|
|
3146
|
+
primary: "#0d9488",
|
|
3147
|
+
primaryForeground: "#ffffff",
|
|
3148
|
+
secondary: "#f3f4f6",
|
|
3149
|
+
secondaryForeground: "#111113",
|
|
3150
|
+
muted: "#f3f4f6",
|
|
3151
|
+
mutedForeground: "#6b7280",
|
|
3152
|
+
accent: "#f3f4f6",
|
|
3153
|
+
accentForeground: "#111113",
|
|
3154
|
+
destructive: "#ef4444",
|
|
3155
|
+
destructiveForeground: "#ffffff",
|
|
3156
|
+
border: "#e5e7eb",
|
|
3157
|
+
input: "#e5e7eb",
|
|
3158
|
+
ring: "#0d9488",
|
|
3159
|
+
radius: "6px",
|
|
3160
|
+
surface: "#f9fafb",
|
|
3161
|
+
surfaceHover: "#f3f4f6",
|
|
3162
|
+
canvas: "#ffffff",
|
|
3163
|
+
panel: "#ffffff",
|
|
3164
|
+
borderStrong: "#d1d5db",
|
|
3165
|
+
textDim: "#9ca3af"
|
|
3166
|
+
};
|
|
3167
|
+
|
|
3168
|
+
export { DEFAULT_PALETTE, DEFAULT_SCHEMA, FormBuilder, FormBuilderThemeProvider, QUESTION_TYPE_INFO, addOption, addQuestion, addSection, cleanPreset, cn, duplicateQuestion, duplicateSection, findQuestion, findSection, generateId, generateOptionId, generateQuestionId, generateSectionId, moveOption, moveQuestion, moveSection, removeOption, removeQuestion, removeSection, squaredrDarkPreset, updateOption, updateQuestion, updateSection, useBuilderState, useBuilderTheme, useDragDrop, useUndoRedo };
|