@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.
@@ -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 };