@squaredr/fieldcraft-pro 1.3.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -8,6 +8,8 @@ var fieldcraftReact = require('@squaredr/fieldcraft-react');
8
8
  var fieldcraftCore = require('@squaredr/fieldcraft-core');
9
9
  var clsx = require('clsx');
10
10
  var tailwindMerge = require('tailwind-merge');
11
+ var react$1 = require('@xyflow/react');
12
+ require('@xyflow/react/dist/style.css');
11
13
 
12
14
  // ../license/dist/index.mjs
13
15
  var LEGACY_TIER_MAP = {
@@ -555,19 +557,19 @@ function UnlicensedOverlay({ featureName, reason, children }) {
555
557
  )
556
558
  ] });
557
559
  }
558
- function requireLicense(Component2, featureName) {
560
+ function requireLicense(Component3, featureName) {
559
561
  function LicenseGated(props) {
560
562
  const { status, tier } = useLicense();
561
563
  const isProduction = isProductionEnvironment();
562
564
  if (!isProduction) {
563
- return /* @__PURE__ */ jsxRuntime.jsx(Component2, { ...props });
565
+ return /* @__PURE__ */ jsxRuntime.jsx(Component3, { ...props });
564
566
  }
565
567
  if (status === "validating") {
566
568
  return null;
567
569
  }
568
570
  const isLicensed = status === "valid" && tier != null && tierHasFeature(tier, featureName);
569
571
  if (isLicensed) {
570
- return /* @__PURE__ */ jsxRuntime.jsx(Component2, { ...props });
572
+ return /* @__PURE__ */ jsxRuntime.jsx(Component3, { ...props });
571
573
  }
572
574
  let reason = "A valid license is required for production use.";
573
575
  if (status === "invalid") {
@@ -581,9 +583,9 @@ function requireLicense(Component2, featureName) {
581
583
  } else if (!tier || !tierHasFeature(tier, featureName)) {
582
584
  reason = "Your license does not include this feature. Please upgrade your plan.";
583
585
  }
584
- return /* @__PURE__ */ jsxRuntime.jsx(UnlicensedOverlay, { featureName, reason, children: /* @__PURE__ */ jsxRuntime.jsx(Component2, { ...props }) });
586
+ return /* @__PURE__ */ jsxRuntime.jsx(UnlicensedOverlay, { featureName, reason, children: /* @__PURE__ */ jsxRuntime.jsx(Component3, { ...props }) });
585
587
  }
586
- LicenseGated.displayName = `requireLicense(${Component2.displayName || Component2.name || "Component"})`;
588
+ LicenseGated.displayName = `requireLicense(${Component3.displayName || Component3.name || "Component"})`;
587
589
  return LicenseGated;
588
590
  }
589
591
  var MAX_HISTORY = 50;
@@ -1149,7 +1151,81 @@ var QUESTION_TYPE_INFO = {
1149
1151
  description: "Upload files",
1150
1152
  defaultConfig: { type: "file_upload", maxFiles: 1, maxSizeMb: 10 }
1151
1153
  },
1154
+ // ── Compound ──
1155
+ legal_name: {
1156
+ type: "legal_name",
1157
+ label: "Legal Name",
1158
+ category: "text",
1159
+ icon: "UserCheck",
1160
+ description: "First, middle, and last name",
1161
+ defaultConfig: { type: "legal_name" }
1162
+ },
1163
+ address: {
1164
+ type: "address",
1165
+ label: "Address",
1166
+ category: "text",
1167
+ icon: "MapPin",
1168
+ description: "Street, city, state, ZIP",
1169
+ defaultConfig: { type: "address", includeCountry: false }
1170
+ },
1171
+ signature: {
1172
+ type: "signature",
1173
+ label: "Signature",
1174
+ category: "media",
1175
+ icon: "PenTool",
1176
+ description: "Canvas signature pad",
1177
+ defaultConfig: { type: "signature" }
1178
+ },
1179
+ date_range: {
1180
+ type: "date_range",
1181
+ label: "Date Range",
1182
+ category: "datetime",
1183
+ icon: "CalendarRange",
1184
+ description: "Start and end date picker",
1185
+ defaultConfig: { type: "date_range" }
1186
+ },
1152
1187
  // ── Advanced ──
1188
+ repeater: {
1189
+ type: "repeater",
1190
+ label: "Repeater",
1191
+ category: "advanced",
1192
+ icon: "ListPlus",
1193
+ description: "Repeatable group of fields",
1194
+ defaultConfig: { type: "repeater", fields: [], minItems: 1, maxItems: 10 }
1195
+ },
1196
+ likert: {
1197
+ type: "likert",
1198
+ label: "Likert Scale",
1199
+ category: "advanced",
1200
+ icon: "AlignHorizontalSpaceAround",
1201
+ description: "Agreement scale (e.g. Strongly Disagree to Strongly Agree)",
1202
+ requiresOptions: true,
1203
+ defaultConfig: {
1204
+ type: "likert",
1205
+ scale: [
1206
+ { label: "Strongly Disagree", value: "1" },
1207
+ { label: "Disagree", value: "2" },
1208
+ { label: "Neutral", value: "3" },
1209
+ { label: "Agree", value: "4" },
1210
+ { label: "Strongly Agree", value: "5" }
1211
+ ]
1212
+ }
1213
+ },
1214
+ scoring: {
1215
+ type: "scoring",
1216
+ label: "Scoring",
1217
+ category: "advanced",
1218
+ icon: "Award",
1219
+ description: "Scored question with point values per option",
1220
+ requiresOptions: true,
1221
+ defaultConfig: {
1222
+ type: "scoring",
1223
+ options: [
1224
+ { label: "Option 1", value: "1", score: 1 },
1225
+ { label: "Option 2", value: "2", score: 2 }
1226
+ ]
1227
+ }
1228
+ },
1153
1229
  matrix: {
1154
1230
  type: "matrix",
1155
1231
  label: "Matrix",
@@ -1283,7 +1359,7 @@ var DEFAULT_PALETTE = [
1283
1359
  {
1284
1360
  category: "text",
1285
1361
  label: "Text Input",
1286
- types: ["short_text", "long_text", "email", "phone", "url"]
1362
+ types: ["short_text", "long_text", "email", "phone", "url", "legal_name", "address"]
1287
1363
  },
1288
1364
  {
1289
1365
  category: "numeric",
@@ -1298,12 +1374,12 @@ var DEFAULT_PALETTE = [
1298
1374
  {
1299
1375
  category: "datetime",
1300
1376
  label: "Date & Time",
1301
- types: ["date", "time"]
1377
+ types: ["date", "time", "date_range"]
1302
1378
  },
1303
1379
  {
1304
1380
  category: "media",
1305
1381
  label: "Media",
1306
- types: ["file_upload"]
1382
+ types: ["file_upload", "signature"]
1307
1383
  },
1308
1384
  {
1309
1385
  category: "content",
@@ -1318,7 +1394,7 @@ var DEFAULT_PALETTE = [
1318
1394
  {
1319
1395
  category: "advanced",
1320
1396
  label: "Advanced",
1321
- types: ["matrix", "calculated", "hidden"]
1397
+ types: ["repeater", "likert", "scoring", "matrix", "calculated", "hidden"]
1322
1398
  }
1323
1399
  ];
1324
1400
 
@@ -2835,7 +2911,8 @@ function getFieldOptions(schema, excludeId) {
2835
2911
  return fields;
2836
2912
  }
2837
2913
  function ConditionEditor({ question, schema, onUpdate }) {
2838
- const showIf = question.showIf;
2914
+ const rawShowIf = question.showIf;
2915
+ const showIf = rawShowIf && rawShowIf.field && !rawShowIf.conditions ? { combine: "AND", conditions: [rawShowIf] } : rawShowIf;
2839
2916
  const fieldOptions = getFieldOptions(schema, question.id);
2840
2917
  const updateShowIf = (next) => {
2841
2918
  onUpdate({ showIf: next });
@@ -2957,6 +3034,24 @@ function ConditionEditor({ question, schema, onUpdate }) {
2957
3034
  }
2958
3035
  function FormSettingsPanel({ schema, onUpdate }) {
2959
3036
  const settings = schema.settings ?? {};
3037
+ const hasConditions = schema.sections.some(
3038
+ (s) => s.showIf || s.questions.some((q) => q.showIf)
3039
+ );
3040
+ const displayModeOptions = hasConditions ? [
3041
+ { label: "Stepped", value: "stepped" },
3042
+ { label: "Conversational", value: "conversational" }
3043
+ ] : [
3044
+ { label: "Stepped", value: "stepped" },
3045
+ { label: "Classic", value: "classic" },
3046
+ { label: "Conversational", value: "conversational" }
3047
+ ];
3048
+ const displayMode = settings.displayMode ?? "stepped";
3049
+ const effectiveDisplayMode = hasConditions && displayMode === "classic" ? "stepped" : displayMode;
3050
+ react.useEffect(() => {
3051
+ if (effectiveDisplayMode !== displayMode) {
3052
+ onUpdate({ ...schema, settings: { ...settings, displayMode: effectiveDisplayMode } });
3053
+ }
3054
+ }, [effectiveDisplayMode, displayMode]);
2960
3055
  const updateSettings = (updates) => {
2961
3056
  onUpdate({ ...schema, settings: { ...settings, ...updates } });
2962
3057
  };
@@ -2984,12 +3079,8 @@ function FormSettingsPanel({ schema, onUpdate }) {
2984
3079
  SettingsSelect,
2985
3080
  {
2986
3081
  label: "Mode",
2987
- value: settings.displayMode ?? "classic",
2988
- options: [
2989
- { label: "Classic", value: "classic" },
2990
- { label: "Stepped", value: "stepped" },
2991
- { label: "Conversational", value: "conversational" }
2992
- ],
3082
+ value: effectiveDisplayMode,
3083
+ options: displayModeOptions,
2993
3084
  onChange: (v) => updateSettings({ displayMode: v })
2994
3085
  }
2995
3086
  ),
@@ -3367,228 +3458,1241 @@ function FieldGroup({ label, children }) {
3367
3458
  children
3368
3459
  ] });
3369
3460
  }
3370
- var ThemeCtx = react.createContext({});
3371
- function useBuilderTheme() {
3372
- return react.useContext(ThemeCtx);
3373
- }
3374
- function themeToCssVars(theme) {
3375
- const vars = {};
3376
- if (theme.background) vars["--background"] = theme.background;
3377
- if (theme.foreground) vars["--foreground"] = theme.foreground;
3378
- if (theme.card) {
3379
- vars["--card"] = theme.card;
3380
- vars["--card-foreground"] = theme.foreground ?? "";
3381
- vars["--popover"] = theme.card;
3382
- vars["--popover-foreground"] = theme.foreground ?? "";
3383
- }
3384
- if (theme.primary) vars["--primary"] = theme.primary;
3385
- if (theme.primaryForeground) vars["--primary-foreground"] = theme.primaryForeground;
3386
- if (theme.secondary) vars["--secondary"] = theme.secondary;
3387
- if (theme.secondaryForeground) vars["--secondary-foreground"] = theme.secondaryForeground;
3388
- if (theme.muted) vars["--muted"] = theme.muted;
3389
- if (theme.mutedForeground) vars["--muted-foreground"] = theme.mutedForeground;
3390
- if (theme.accent) vars["--accent"] = theme.accent;
3391
- if (theme.accentForeground) vars["--accent-foreground"] = theme.accentForeground;
3392
- if (theme.destructive) vars["--destructive"] = theme.destructive;
3393
- if (theme.destructiveForeground) vars["--destructive-foreground"] = theme.destructiveForeground;
3394
- if (theme.border) vars["--border"] = theme.border;
3395
- if (theme.input) vars["--input"] = theme.input;
3396
- if (theme.ring) vars["--ring"] = theme.ring;
3397
- if (theme.radius) vars["--radius"] = theme.radius;
3398
- if (theme.surface) vars["--fcb-surface"] = theme.surface;
3399
- if (theme.surfaceHover) vars["--fcb-surface-hover"] = theme.surfaceHover;
3400
- if (theme.canvas) vars["--fcb-canvas"] = theme.canvas;
3401
- if (theme.panel) vars["--fcb-panel"] = theme.panel;
3402
- if (theme.borderStrong) vars["--fcb-border-strong"] = theme.borderStrong;
3403
- if (theme.textDim) vars["--fcb-text-dim"] = theme.textDim;
3404
- return vars;
3405
- }
3406
- function FormBuilderThemeProvider({ theme, children }) {
3407
- const resolved = theme ?? {};
3408
- const cssVars = react.useMemo(() => themeToCssVars(resolved), [resolved]);
3409
- return /* @__PURE__ */ jsxRuntime.jsx(ThemeCtx.Provider, { value: resolved, children: /* @__PURE__ */ jsxRuntime.jsx("div", { "data-fcb-root": "", style: cssVars, className: "w-full h-full", children }) });
3410
- }
3411
- var FormBuilderErrorBoundary = class extends react.Component {
3412
- constructor(props) {
3413
- super(props);
3414
- this.state = { hasError: false, error: null };
3415
- }
3461
+ var PreviewErrorBoundary = class extends react.Component {
3462
+ state = { error: null };
3416
3463
  static getDerivedStateFromError(error) {
3417
- return { hasError: true, error };
3464
+ return { error };
3418
3465
  }
3419
3466
  componentDidCatch(error, info) {
3420
- console.error("[FormBuilder] Render error:", error, info.componentStack);
3467
+ console.error("[FormBuilder Preview]", error, info);
3468
+ }
3469
+ componentDidUpdate(prevProps) {
3470
+ if (prevProps.resetKey !== this.props.resetKey && this.state.error) {
3471
+ this.setState({ error: null });
3472
+ }
3421
3473
  }
3422
3474
  render() {
3423
- if (this.state.hasError) {
3424
- return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-col items-center justify-center h-full bg-background text-foreground p-8", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "max-w-md text-center space-y-4", children: [
3425
- /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "text-lg font-semibold", children: "Something went wrong" }),
3426
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-muted-foreground", children: "The form builder encountered an unexpected error. Your schema data is preserved." }),
3427
- /* @__PURE__ */ jsxRuntime.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 }),
3428
- /* @__PURE__ */ jsxRuntime.jsx(
3429
- fieldcraftReact.Button,
3430
- {
3431
- onClick: () => this.setState({ hasError: false, error: null }),
3432
- variant: "outline",
3433
- children: "Try Again"
3434
- }
3435
- )
3436
- ] }) });
3475
+ if (this.state.error) {
3476
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "p-4 text-destructive text-sm font-mono", children: [
3477
+ /* @__PURE__ */ jsxRuntime.jsx("strong", { children: "Render Error:" }),
3478
+ " ",
3479
+ this.state.error.message
3480
+ ] });
3437
3481
  }
3438
3482
  return this.props.children;
3439
3483
  }
3440
3484
  };
3441
- function FormBuilderCore(props) {
3442
- const { initialSchema = DEFAULT_SCHEMA, onChange, onSave, height = "100vh", theme, className, toolbarExtra, questionTypes, palette } = props;
3443
- const mergedQuestionTypes = questionTypes ? { ...QUESTION_TYPE_INFO, ...questionTypes } : QUESTION_TYPE_INFO;
3444
- const builderState = useBuilderState(initialSchema);
3445
- const dragDrop = useDragDrop(builderState);
3446
- const fileInputRef = react.useRef(null);
3447
- const [mobilePanel, setMobilePanel] = react.useState("none");
3448
- react.useEffect(() => {
3449
- if (builderState.selectedItem && window.innerWidth < 768) {
3450
- setMobilePanel("properties");
3451
- }
3452
- }, [builderState.selectedItem]);
3453
- react.useEffect(() => {
3454
- if (!dragDrop.activeDragItem && mobilePanel === "palette") {
3455
- setMobilePanel("none");
3485
+ function FormPreviewPanel({ schema, preview }) {
3486
+ const hasSections = schema.sections && schema.sections.length > 0;
3487
+ const hasFields = hasSections && schema.sections.some((s) => s.questions.length > 0);
3488
+ if (!hasFields) {
3489
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1 flex items-center justify-center text-muted-foreground text-sm", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-center space-y-2", children: [
3490
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "font-medium", children: "No fields to preview" }),
3491
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs", children: "Add fields in the Design view to see a live preview here." })
3492
+ ] }) });
3493
+ }
3494
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1 overflow-y-auto", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "max-w-2xl mx-auto py-8 px-4", children: /* @__PURE__ */ jsxRuntime.jsx(PreviewErrorBoundary, { resetKey: schema.id + (schema.version ?? ""), children: /* @__PURE__ */ jsxRuntime.jsx(
3495
+ fieldcraftReact.FormEngineRenderer,
3496
+ {
3497
+ schema,
3498
+ theme: preview?.theme,
3499
+ components: preview?.components,
3500
+ onReady: preview?.onReady,
3501
+ onFieldChange: preview?.onFieldChange,
3502
+ onSubmit: preview?.onSubmit ?? (() => {
3503
+ })
3456
3504
  }
3457
- }, [dragDrop.activeDragItem]);
3505
+ ) }) }) });
3506
+ }
3507
+
3508
+ // src/form-builder/utils/validation-markers.ts
3509
+ var MarkerSeverity = {
3510
+ Error: 8};
3511
+ function errorsToMarkers(errors) {
3512
+ return errors.map((error) => ({
3513
+ severity: MarkerSeverity.Error,
3514
+ message: error.message,
3515
+ startLineNumber: error.line ?? 1,
3516
+ startColumn: error.column ?? 1,
3517
+ endLineNumber: error.endLine ?? error.line ?? 1,
3518
+ endColumn: error.endColumn ?? error.column ?? 1,
3519
+ source: "FieldCraft Schema Validator"
3520
+ }));
3521
+ }
3522
+ var Editor = react.lazy(() => import('@monaco-editor/react').then((m) => ({ default: m.default })));
3523
+ function MonacoWrapper({ value, onChange, errors }) {
3524
+ const editorRef = react.useRef(null);
3525
+ const monacoRef = react.useRef(null);
3526
+ const handleMount = (editor, monaco) => {
3527
+ editorRef.current = editor;
3528
+ monacoRef.current = monaco;
3529
+ };
3458
3530
  react.useEffect(() => {
3459
- if (onChange && builderState.isDirty) {
3460
- onChange(builderState.schema);
3461
- }
3462
- }, [builderState.schema, builderState.isDirty, onChange]);
3463
- const handleSave = react.useCallback(() => {
3464
- if (onSave) {
3465
- onSave(builderState.schema);
3466
- builderState.markClean();
3531
+ const editor = editorRef.current;
3532
+ const monaco = monacoRef.current;
3533
+ if (!editor || !monaco) return;
3534
+ const model = editor.getModel();
3535
+ if (!model) return;
3536
+ const markers = errorsToMarkers(errors);
3537
+ monaco.editor.setModelMarkers(model, "fieldcraft-schema", markers);
3538
+ }, [errors]);
3539
+ return /* @__PURE__ */ jsxRuntime.jsx(
3540
+ Editor,
3541
+ {
3542
+ height: "100%",
3543
+ language: "json",
3544
+ theme: "vs-dark",
3545
+ value,
3546
+ onChange: (val) => onChange(val ?? ""),
3547
+ onMount: handleMount,
3548
+ options: {
3549
+ minimap: { enabled: false },
3550
+ fontSize: 13,
3551
+ wordWrap: "on",
3552
+ formatOnPaste: true,
3553
+ automaticLayout: true,
3554
+ tabSize: 2,
3555
+ scrollBeyondLastLine: false,
3556
+ padding: { top: 12 }
3557
+ }
3467
3558
  }
3468
- }, [onSave, builderState]);
3469
- const handleExport = react.useCallback(() => {
3470
- const json = JSON.stringify(builderState.schema, null, 2);
3471
- const blob = new Blob([json], { type: "application/json" });
3472
- const url = URL.createObjectURL(blob);
3473
- const a = document.createElement("a");
3474
- a.href = url;
3475
- a.download = `${builderState.schema.title?.replace(/\s+/g, "-").toLowerCase() || "form"}-schema.json`;
3476
- a.click();
3477
- URL.revokeObjectURL(url);
3478
- }, [builderState.schema]);
3479
- const handleImport = react.useCallback(() => {
3480
- fileInputRef.current?.click();
3481
- }, []);
3482
- const handleFileChange = react.useCallback(
3483
- (e) => {
3484
- const file = e.target.files?.[0];
3485
- if (!file) return;
3486
- const reader = new FileReader();
3487
- reader.onload = (event) => {
3488
- try {
3489
- const parsed = JSON.parse(event.target?.result);
3490
- const validated = fieldcraftCore.validateSchema(parsed);
3491
- builderState.resetSchema(validated);
3492
- } catch (err) {
3493
- if (err instanceof fieldcraftCore.FormEngineSchemaError) {
3494
- const details = err.issues.slice(0, 3).map((i) => `\u2022 ${i.path.join(".")}: ${i.message}`).join("\n");
3495
- alert(`Invalid schema:
3496
- ${details}`);
3497
- } else if (err instanceof SyntaxError) {
3498
- alert("Failed to parse JSON file.");
3499
- } else {
3500
- alert("Invalid schema file.");
3501
- }
3502
- }
3503
- };
3504
- reader.readAsText(file);
3505
- e.target.value = "";
3559
+ );
3560
+ }
3561
+ function JsonEditorPanel({ value, onChange, errors, onValidate }) {
3562
+ const handleChange = react.useCallback(
3563
+ (newValue) => {
3564
+ onChange(newValue);
3565
+ onValidate(newValue);
3506
3566
  },
3507
- [builderState]
3567
+ [onChange, onValidate]
3508
3568
  );
3509
- const handleKeyDown = react.useCallback(
3510
- (e) => {
3511
- const mod = e.metaKey || e.ctrlKey;
3512
- const tag = e.target.tagName;
3513
- const isEditing = tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT";
3514
- if (mod && e.key === "z" && !e.shiftKey) {
3515
- e.preventDefault();
3516
- builderState.undo();
3517
- return;
3518
- }
3519
- if (mod && e.key === "z" && e.shiftKey) {
3520
- e.preventDefault();
3521
- builderState.redo();
3522
- return;
3569
+ const handleFormat = react.useCallback(() => {
3570
+ try {
3571
+ const formatted = JSON.stringify(JSON.parse(value), null, 2);
3572
+ onChange(formatted);
3573
+ } catch {
3574
+ }
3575
+ }, [value, onChange]);
3576
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 flex flex-col overflow-hidden", children: [
3577
+ errors.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "shrink-0 px-3 py-1.5 bg-destructive/10 border-b border-destructive/20 text-destructive text-xs font-mono overflow-x-auto", children: [
3578
+ errors.slice(0, 3).map((e, i) => /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
3579
+ e.line ? `Line ${e.line}: ` : "",
3580
+ e.path ? `${e.path}: ` : "",
3581
+ e.message
3582
+ ] }, i)),
3583
+ errors.length > 3 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-muted-foreground", children: [
3584
+ "...and ",
3585
+ errors.length - 3,
3586
+ " more"
3587
+ ] })
3588
+ ] }),
3589
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "shrink-0 flex items-center justify-between px-3 py-1.5 bg-card border-b border-border text-xs", children: [
3590
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground", children: errors.length === 0 ? "Valid schema" : `${errors.length} error${errors.length !== 1 ? "s" : ""}` }),
3591
+ /* @__PURE__ */ jsxRuntime.jsx(
3592
+ "button",
3593
+ {
3594
+ type: "button",
3595
+ onClick: handleFormat,
3596
+ className: "text-primary hover:underline",
3597
+ children: "Format JSON"
3598
+ }
3599
+ )
3600
+ ] }),
3601
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1 overflow-hidden", children: /* @__PURE__ */ jsxRuntime.jsx(
3602
+ react.Suspense,
3603
+ {
3604
+ fallback: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center justify-center h-full text-sm text-muted-foreground", children: "Loading editor..." }),
3605
+ children: /* @__PURE__ */ jsxRuntime.jsx(MonacoWrapper, { value, onChange: handleChange, errors })
3523
3606
  }
3524
- if (mod && e.key === "s") {
3525
- e.preventDefault();
3526
- handleSave();
3527
- return;
3607
+ ) })
3608
+ ] });
3609
+ }
3610
+
3611
+ // src/form-builder/utils/logic-graph.ts
3612
+ function extractFieldRefs(expr) {
3613
+ const refs = [];
3614
+ if (expr.field) {
3615
+ refs.push({ field: expr.field, operator: expr.operator, value: expr.value });
3616
+ }
3617
+ if (expr.conditions) {
3618
+ for (const child of expr.conditions) {
3619
+ refs.push(...extractFieldRefs(child));
3620
+ }
3621
+ }
3622
+ return refs;
3623
+ }
3624
+ function buildLogicGraph(schema) {
3625
+ const nodes = [];
3626
+ const edges = [];
3627
+ const nodeIds = /* @__PURE__ */ new Set();
3628
+ for (const section of schema.sections) {
3629
+ nodes.push({
3630
+ id: section.id,
3631
+ label: section.title || "Untitled Section",
3632
+ type: "section",
3633
+ sectionId: section.id,
3634
+ sectionTitle: section.title || "Untitled Section",
3635
+ hasCondition: !!section.showIf
3636
+ });
3637
+ nodeIds.add(section.id);
3638
+ if (section.showIf) {
3639
+ const refs = extractFieldRefs(section.showIf);
3640
+ for (const ref of refs) {
3641
+ edges.push({ from: ref.field, to: section.id, edgeType: "showIf", operator: ref.operator, value: ref.value });
3528
3642
  }
3529
- if (isEditing) return;
3530
- if (e.key === "Delete" || e.key === "Backspace") {
3531
- const sel = builderState.selectedItem;
3532
- if (!sel) return;
3533
- e.preventDefault();
3534
- if (sel.type === "question") {
3535
- builderState.removeQuestion(sel.sectionId, sel.questionId);
3536
- } else if (sel.type === "section") {
3537
- builderState.removeSection(sel.sectionId);
3643
+ }
3644
+ if (section.onExit) {
3645
+ for (const rule of section.onExit.rules) {
3646
+ if (rule.jumpTo) {
3647
+ edges.push({ from: section.id, to: rule.jumpTo, edgeType: "onExit", jumpTo: rule.jumpTo });
3538
3648
  }
3539
- return;
3540
3649
  }
3541
- if (e.key === "Escape") {
3542
- if (builderState.selectedItem) {
3543
- e.preventDefault();
3544
- builderState.clearSelection();
3545
- }
3546
- return;
3650
+ if (section.onExit.default) {
3651
+ edges.push({ from: section.id, to: section.onExit.default, edgeType: "onExit", jumpTo: section.onExit.default });
3547
3652
  }
3548
- if (mod && e.key === "d") {
3549
- const sel = builderState.selectedItem;
3550
- if (!sel) return;
3551
- e.preventDefault();
3552
- if (sel.type === "question") {
3553
- builderState.duplicateQuestion(sel.sectionId, sel.questionId);
3554
- } else if (sel.type === "section") {
3555
- builderState.duplicateSection(sel.sectionId);
3653
+ }
3654
+ for (const question of section.questions) {
3655
+ nodes.push({
3656
+ id: question.id,
3657
+ label: question.label || question.id,
3658
+ type: "field",
3659
+ fieldType: question.type,
3660
+ sectionId: section.id,
3661
+ sectionTitle: section.title || "Untitled Section",
3662
+ hasCondition: !!question.showIf
3663
+ });
3664
+ nodeIds.add(question.id);
3665
+ if (question.showIf) {
3666
+ const refs = extractFieldRefs(question.showIf);
3667
+ for (const ref of refs) {
3668
+ edges.push({ from: ref.field, to: question.id, edgeType: "showIf", operator: ref.operator, value: ref.value });
3556
3669
  }
3557
3670
  }
3558
- },
3559
- [builderState, handleSave]
3560
- );
3561
- return /* @__PURE__ */ jsxRuntime.jsx(FormBuilderThemeProvider, { theme, children: /* @__PURE__ */ jsxRuntime.jsxs(
3562
- core.DndContext,
3563
- {
3564
- sensors: dragDrop.sensors,
3565
- onDragStart: dragDrop.handleDragStart,
3566
- onDragEnd: dragDrop.handleDragEnd,
3567
- onDragCancel: dragDrop.handleDragCancel,
3568
- children: [
3569
- /* @__PURE__ */ jsxRuntime.jsxs(
3570
- "div",
3571
- {
3572
- className: cn("flex flex-col bg-background text-foreground", className),
3573
- style: { height: typeof height === "number" ? `${height}px` : height },
3574
- onKeyDown: handleKeyDown,
3575
- tabIndex: -1,
3576
- role: "application",
3577
- "aria-label": "Form Builder",
3578
- children: [
3579
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "h-12 shrink-0 hidden md:grid px-4 bg-card border-b border-border", style: { gridTemplateColumns: "1fr auto 1fr" }, role: "toolbar", "aria-label": "Builder toolbar", children: [
3671
+ }
3672
+ }
3673
+ const validEdges = edges.filter((e) => nodeIds.has(e.from) && nodeIds.has(e.to));
3674
+ return { nodes, edges: validEdges };
3675
+ }
3676
+ var SECTION_COLORS = [
3677
+ "#3b82f6",
3678
+ // blue
3679
+ "#8b5cf6",
3680
+ // violet
3681
+ "#06b6d4",
3682
+ // cyan
3683
+ "#f59e0b",
3684
+ // amber
3685
+ "#10b981",
3686
+ // emerald
3687
+ "#ef4444",
3688
+ // red
3689
+ "#ec4899",
3690
+ // pink
3691
+ "#6366f1"
3692
+ // indigo
3693
+ ];
3694
+ var FIELD_NODE_WIDTH = 220;
3695
+ var FIELD_NODE_HEIGHT = 52;
3696
+ var SECTION_HEADER_HEIGHT = 40;
3697
+ var SECTION_PADDING_X = 16;
3698
+ var SECTION_PADDING_TOP = 12;
3699
+ var SECTION_PADDING_BOTTOM = 16;
3700
+ var FIELD_GAP = 10;
3701
+ var SECTION_GAP = 80;
3702
+ function formatEdgeLabel(edge) {
3703
+ if (edge.edgeType === "onExit") return "jump";
3704
+ if (!edge.operator) return "";
3705
+ const op = edge.operator;
3706
+ const val = edge.value;
3707
+ if (op === "exists") return "has value";
3708
+ if (op === "notExists") return "is empty";
3709
+ if (val === void 0 || val === null || val === "") return op;
3710
+ const short = String(val).length > 12 ? String(val).slice(0, 12) + "\u2026" : String(val);
3711
+ return `${op} ${short}`;
3712
+ }
3713
+ function buildReactFlowGraph(schema) {
3714
+ const graph = buildLogicGraph(schema);
3715
+ const nodes = [];
3716
+ const edges = [];
3717
+ let sectionX = 0;
3718
+ let maxSectionHeight = 0;
3719
+ for (const section of schema.sections) {
3720
+ const fieldCount = section.questions.length;
3721
+ const contentHeight = SECTION_HEADER_HEIGHT + SECTION_PADDING_TOP + fieldCount * FIELD_NODE_HEIGHT + Math.max(0, fieldCount - 1) * FIELD_GAP + SECTION_PADDING_BOTTOM;
3722
+ if (contentHeight > maxSectionHeight) maxSectionHeight = contentHeight;
3723
+ }
3724
+ if (maxSectionHeight < SECTION_HEADER_HEIGHT + SECTION_PADDING_TOP + SECTION_PADDING_BOTTOM + FIELD_NODE_HEIGHT) {
3725
+ maxSectionHeight = SECTION_HEADER_HEIGHT + SECTION_PADDING_TOP + SECTION_PADDING_BOTTOM + FIELD_NODE_HEIGHT;
3726
+ }
3727
+ const sectionWidth = FIELD_NODE_WIDTH + SECTION_PADDING_X * 2;
3728
+ for (let sIdx = 0; sIdx < schema.sections.length; sIdx++) {
3729
+ const section = schema.sections[sIdx];
3730
+ const colorIndex = sIdx;
3731
+ nodes.push({
3732
+ id: section.id,
3733
+ type: "sectionNode",
3734
+ position: { x: sectionX, y: 0 },
3735
+ data: {
3736
+ label: section.title || "Untitled Section",
3737
+ sectionId: section.id,
3738
+ fieldCount: section.questions.length,
3739
+ hasCondition: !!section.showIf,
3740
+ hasExitLogic: !!section.onExit,
3741
+ colorIndex
3742
+ },
3743
+ style: { width: sectionWidth, height: maxSectionHeight }
3744
+ });
3745
+ for (let fIdx = 0; fIdx < section.questions.length; fIdx++) {
3746
+ const question = section.questions[fIdx];
3747
+ const fieldY = SECTION_HEADER_HEIGHT + SECTION_PADDING_TOP + fIdx * (FIELD_NODE_HEIGHT + FIELD_GAP);
3748
+ nodes.push({
3749
+ id: question.id,
3750
+ type: "fieldNode",
3751
+ position: { x: SECTION_PADDING_X, y: fieldY },
3752
+ parentId: section.id,
3753
+ extent: "parent",
3754
+ data: {
3755
+ label: question.label || question.id,
3756
+ fieldType: question.type,
3757
+ sectionId: section.id,
3758
+ hasCondition: !!question.showIf,
3759
+ colorIndex
3760
+ },
3761
+ style: { width: FIELD_NODE_WIDTH }
3762
+ });
3763
+ }
3764
+ if (sIdx < schema.sections.length - 1) {
3765
+ const nextSection = schema.sections[sIdx + 1];
3766
+ edges.push({
3767
+ id: `pipeline-${section.id}-${nextSection.id}`,
3768
+ source: section.id,
3769
+ target: nextSection.id,
3770
+ type: "pipelineEdge",
3771
+ data: { edgeType: "pipeline" }
3772
+ });
3773
+ }
3774
+ sectionX += sectionWidth + SECTION_GAP;
3775
+ }
3776
+ for (let i = 0; i < graph.edges.length; i++) {
3777
+ const edge = graph.edges[i];
3778
+ edges.push({
3779
+ id: `e-${edge.from}-${edge.to}-${i}`,
3780
+ source: edge.from,
3781
+ target: edge.to,
3782
+ type: "conditionEdge",
3783
+ animated: edge.edgeType === "showIf",
3784
+ style: edge.edgeType === "onExit" ? { strokeDasharray: "6 3", stroke: "#f59e0b" } : void 0,
3785
+ label: formatEdgeLabel(edge),
3786
+ data: {
3787
+ edgeType: edge.edgeType,
3788
+ operator: edge.operator,
3789
+ value: edge.value,
3790
+ sourceId: edge.from,
3791
+ targetId: edge.to
3792
+ }
3793
+ });
3794
+ }
3795
+ return { nodes, edges };
3796
+ }
3797
+ function SectionNodeInner({ data }) {
3798
+ const d = data;
3799
+ const color = SECTION_COLORS[d.colorIndex % SECTION_COLORS.length];
3800
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fc-flow-section-group", style: { borderColor: color }, children: [
3801
+ /* @__PURE__ */ jsxRuntime.jsx(react$1.Handle, { type: "target", position: react$1.Position.Left, className: "fc-flow-handle fc-flow-handle--section" }),
3802
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fc-flow-section-group__header", style: { background: color }, children: [
3803
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "fc-flow-section-group__label", children: d.label.length > 30 ? d.label.slice(0, 30) + "\u2026" : d.label }),
3804
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "fc-flow-section-group__meta", children: [
3805
+ d.fieldCount,
3806
+ " field",
3807
+ d.fieldCount !== 1 ? "s" : "",
3808
+ d.hasCondition && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "fc-flow-section-group__badge", title: "Has showIf condition", children: "?" }),
3809
+ d.hasExitLogic && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "fc-flow-section-group__badge fc-flow-section-group__badge--jump", title: "Has onExit jump", children: "\u2934" })
3810
+ ] })
3811
+ ] }),
3812
+ /* @__PURE__ */ jsxRuntime.jsx(react$1.Handle, { type: "source", position: react$1.Position.Right, className: "fc-flow-handle fc-flow-handle--section" })
3813
+ ] });
3814
+ }
3815
+ var SectionNode = react.memo(SectionNodeInner);
3816
+ function FieldNodeInner({ data }) {
3817
+ const d = data;
3818
+ const color = SECTION_COLORS[d.colorIndex % SECTION_COLORS.length];
3819
+ return /* @__PURE__ */ jsxRuntime.jsxs(
3820
+ "div",
3821
+ {
3822
+ className: "fc-flow-field",
3823
+ style: {
3824
+ borderColor: d.hasCondition ? color : void 0,
3825
+ borderWidth: d.hasCondition ? 2 : 1
3826
+ },
3827
+ children: [
3828
+ /* @__PURE__ */ jsxRuntime.jsx(react$1.Handle, { type: "target", position: react$1.Position.Left, className: "fc-flow-handle" }),
3829
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fc-flow-field__content", children: [
3830
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "fc-flow-field__label", children: d.label.length > 26 ? d.label.slice(0, 26) + "\u2026" : d.label }),
3831
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "fc-flow-field__type", children: d.fieldType })
3832
+ ] }),
3833
+ d.hasCondition && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "fc-flow-badge", style: { background: color }, title: "Has showIf condition", children: "?" }),
3834
+ /* @__PURE__ */ jsxRuntime.jsx(react$1.Handle, { type: "source", position: react$1.Position.Right, className: "fc-flow-handle" })
3835
+ ]
3836
+ }
3837
+ );
3838
+ }
3839
+ var FieldNode = react.memo(FieldNodeInner);
3840
+ var logicFlowNodeTypes = {
3841
+ sectionNode: SectionNode,
3842
+ fieldNode: FieldNode
3843
+ };
3844
+ function ConditionEdgeInner({
3845
+ id,
3846
+ sourceX,
3847
+ sourceY,
3848
+ targetX,
3849
+ targetY,
3850
+ sourcePosition,
3851
+ targetPosition,
3852
+ label,
3853
+ style,
3854
+ data,
3855
+ markerEnd
3856
+ }) {
3857
+ const [edgePath, labelX, labelY] = react$1.getBezierPath({
3858
+ sourceX,
3859
+ sourceY,
3860
+ sourcePosition,
3861
+ targetX,
3862
+ targetY,
3863
+ targetPosition
3864
+ });
3865
+ const isJump = data?.edgeType === "onExit";
3866
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
3867
+ /* @__PURE__ */ jsxRuntime.jsx(
3868
+ react$1.BaseEdge,
3869
+ {
3870
+ id,
3871
+ path: edgePath,
3872
+ markerEnd,
3873
+ style: {
3874
+ stroke: isJump ? "#f59e0b" : "var(--primary, #3b82f6)",
3875
+ strokeWidth: 1.5,
3876
+ strokeOpacity: 0.7,
3877
+ ...style
3878
+ }
3879
+ }
3880
+ ),
3881
+ label && /* @__PURE__ */ jsxRuntime.jsx(react$1.EdgeLabelRenderer, { children: /* @__PURE__ */ jsxRuntime.jsx(
3882
+ "div",
3883
+ {
3884
+ className: "fc-flow-edge-label",
3885
+ style: {
3886
+ transform: `translate(-50%, -50%) translate(${labelX}px, ${labelY}px)`,
3887
+ background: isJump ? "#f59e0b" : "var(--primary, #3b82f6)"
3888
+ },
3889
+ children: label
3890
+ }
3891
+ ) })
3892
+ ] });
3893
+ }
3894
+ var ConditionEdge = react.memo(ConditionEdgeInner);
3895
+ function PipelineEdgeInner({
3896
+ id,
3897
+ sourceX,
3898
+ sourceY,
3899
+ targetX,
3900
+ targetY,
3901
+ sourcePosition,
3902
+ targetPosition,
3903
+ markerEnd
3904
+ }) {
3905
+ const [edgePath] = react$1.getBezierPath({
3906
+ sourceX,
3907
+ sourceY,
3908
+ sourcePosition,
3909
+ targetX,
3910
+ targetY,
3911
+ targetPosition
3912
+ });
3913
+ return /* @__PURE__ */ jsxRuntime.jsx(
3914
+ react$1.BaseEdge,
3915
+ {
3916
+ id,
3917
+ path: edgePath,
3918
+ markerEnd,
3919
+ style: {
3920
+ stroke: "var(--muted-foreground, #71717a)",
3921
+ strokeWidth: 2,
3922
+ strokeOpacity: 0.4
3923
+ }
3924
+ }
3925
+ );
3926
+ }
3927
+ var PipelineEdge = react.memo(PipelineEdgeInner);
3928
+ var logicFlowEdgeTypes = {
3929
+ conditionEdge: ConditionEdge,
3930
+ pipelineEdge: PipelineEdge
3931
+ };
3932
+ function findQuestion2(schema, fieldId) {
3933
+ for (const section of schema.sections) {
3934
+ for (const q of section.questions) {
3935
+ if (q.id === fieldId) return q;
3936
+ }
3937
+ }
3938
+ return void 0;
3939
+ }
3940
+ function updateQuestionInSchema(schema, fieldId, updates) {
3941
+ return {
3942
+ ...schema,
3943
+ sections: schema.sections.map((section) => ({
3944
+ ...section,
3945
+ questions: section.questions.map(
3946
+ (q) => q.id === fieldId ? { ...q, ...updates } : q
3947
+ )
3948
+ }))
3949
+ };
3950
+ }
3951
+ function removeConditionForSource(showIf, sourceFieldId) {
3952
+ if (!showIf) return void 0;
3953
+ if (showIf.field === sourceFieldId) return void 0;
3954
+ if (showIf.field && showIf.field !== sourceFieldId) return showIf;
3955
+ if (showIf.conditions) {
3956
+ const filtered = showIf.conditions.map((c) => removeConditionForSource(c, sourceFieldId)).filter((c) => c !== void 0);
3957
+ if (filtered.length === 0) return void 0;
3958
+ if (filtered.length === 1) return filtered[0];
3959
+ return { ...showIf, conditions: filtered };
3960
+ }
3961
+ return showIf;
3962
+ }
3963
+ var defaultEdgeOptions = {
3964
+ markerEnd: {
3965
+ type: react$1.MarkerType.ArrowClosed,
3966
+ width: 16,
3967
+ height: 12
3968
+ }
3969
+ };
3970
+ function LogicFlowEditor({ schema, onChange, onClose }) {
3971
+ const noSections = schema.sections.length === 0;
3972
+ const flowGraph = react.useMemo(() => buildReactFlowGraph(schema), [schema]);
3973
+ const [nodes, setNodes, onNodesChange] = react$1.useNodesState(flowGraph.nodes);
3974
+ const [edges, setEdges, onEdgesChange] = react$1.useEdgesState(flowGraph.edges);
3975
+ const [selectedFieldId, setSelectedFieldId] = react.useState(null);
3976
+ const selectedQuestion = selectedFieldId ? findQuestion2(schema, selectedFieldId) : void 0;
3977
+ react.useEffect(() => {
3978
+ setNodes(flowGraph.nodes);
3979
+ setEdges(flowGraph.edges);
3980
+ }, [flowGraph, setNodes, setEdges]);
3981
+ const sectionIds = react.useMemo(() => {
3982
+ const ids = [];
3983
+ for (const section of schema.sections) {
3984
+ ids.push(section.id);
3985
+ }
3986
+ return ids;
3987
+ }, [schema]);
3988
+ const onNodeClick = react.useCallback(
3989
+ (_event, node) => {
3990
+ if (node.type === "fieldNode") {
3991
+ setSelectedFieldId(node.id);
3992
+ }
3993
+ },
3994
+ []
3995
+ );
3996
+ const onConnect = react.useCallback(
3997
+ (connection) => {
3998
+ if (!connection.source || !connection.target) return;
3999
+ const targetQuestion = findQuestion2(schema, connection.target);
4000
+ if (!targetQuestion) return;
4001
+ const newCondition = {
4002
+ field: connection.source,
4003
+ operator: "exists"
4004
+ };
4005
+ let updatedShowIf;
4006
+ const existing = targetQuestion.showIf;
4007
+ if (!existing) {
4008
+ updatedShowIf = { combine: "AND", conditions: [newCondition] };
4009
+ } else {
4010
+ updatedShowIf = {
4011
+ ...existing,
4012
+ conditions: [...existing.conditions ?? [], newCondition]
4013
+ };
4014
+ }
4015
+ const updatedSchema = updateQuestionInSchema(schema, connection.target, {
4016
+ showIf: updatedShowIf
4017
+ });
4018
+ onChange(updatedSchema);
4019
+ setSelectedFieldId(connection.target);
4020
+ },
4021
+ [schema, onChange]
4022
+ );
4023
+ const onEdgesDelete = react.useCallback(
4024
+ (deletedEdges) => {
4025
+ let updatedSchema = schema;
4026
+ for (const edge of deletedEdges) {
4027
+ const edgeData = edge.data;
4028
+ if (!edgeData) continue;
4029
+ const targetId = edge.target;
4030
+ const sourceId = edge.source;
4031
+ const edgeType = edgeData.edgeType;
4032
+ if (edgeType === "showIf") {
4033
+ const question = findQuestion2(updatedSchema, targetId);
4034
+ if (!question) continue;
4035
+ const updatedShowIf = removeConditionForSource(
4036
+ question.showIf,
4037
+ sourceId
4038
+ );
4039
+ updatedSchema = updateQuestionInSchema(updatedSchema, targetId, {
4040
+ showIf: updatedShowIf
4041
+ });
4042
+ }
4043
+ }
4044
+ onChange(updatedSchema);
4045
+ },
4046
+ [schema, onChange]
4047
+ );
4048
+ const handleConditionUpdate = react.useCallback(
4049
+ (updates) => {
4050
+ if (!selectedFieldId) return;
4051
+ const updatedSchema = updateQuestionInSchema(schema, selectedFieldId, updates);
4052
+ onChange(updatedSchema);
4053
+ },
4054
+ [schema, selectedFieldId, onChange]
4055
+ );
4056
+ const showIfCount = flowGraph.edges.filter(
4057
+ (e) => e.data?.edgeType === "showIf"
4058
+ ).length;
4059
+ const jumpCount = flowGraph.edges.filter(
4060
+ (e) => e.data?.edgeType === "onExit"
4061
+ ).length;
4062
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fixed inset-0 z-50 flex flex-col bg-background/95 backdrop-blur-sm", children: [
4063
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "h-12 shrink-0 flex items-center justify-between px-4 border-b border-border bg-card", children: [
4064
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3", children: [
4065
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.GitBranch, { size: 16, className: "text-primary" }),
4066
+ /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "text-sm font-semibold", children: "Logic Flow" }),
4067
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-xs text-muted-foreground", children: [
4068
+ showIfCount,
4069
+ " condition",
4070
+ showIfCount !== 1 ? "s" : "",
4071
+ jumpCount > 0 && ` \xB7 ${jumpCount} jump${jumpCount !== 1 ? "s" : ""}`,
4072
+ " \xB7 ",
4073
+ sectionIds.length,
4074
+ " section",
4075
+ sectionIds.length !== 1 ? "s" : ""
4076
+ ] })
4077
+ ] }),
4078
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1", children: [
4079
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-[10px] text-muted-foreground mr-2", children: "Drag between nodes to create conditions \xB7 Select edges and press Delete to remove" }),
4080
+ /* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.Button, { variant: "ghost", size: "icon-sm", onClick: onClose, title: "Close logic flow", children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { size: 16, strokeWidth: 1.75 }) })
4081
+ ] })
4082
+ ] }),
4083
+ noSections ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1 flex items-center justify-center text-muted-foreground text-sm", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-center space-y-2", children: [
4084
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.GitBranch, { size: 32, className: "mx-auto opacity-40" }),
4085
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "font-medium", children: "No sections found" }),
4086
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs max-w-xs", children: "Add sections and fields to your form to see the logic flow pipeline." })
4087
+ ] }) }) : /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 flex", children: [
4088
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1", children: /* @__PURE__ */ jsxRuntime.jsxs(
4089
+ react$1.ReactFlow,
4090
+ {
4091
+ nodes,
4092
+ edges,
4093
+ onNodesChange,
4094
+ onEdgesChange,
4095
+ onNodeClick,
4096
+ onConnect,
4097
+ onEdgesDelete,
4098
+ nodeTypes: logicFlowNodeTypes,
4099
+ edgeTypes: logicFlowEdgeTypes,
4100
+ defaultEdgeOptions,
4101
+ fitView: true,
4102
+ fitViewOptions: { padding: 0.2 },
4103
+ deleteKeyCode: ["Backspace", "Delete"],
4104
+ proOptions: { hideAttribution: true },
4105
+ children: [
4106
+ /* @__PURE__ */ jsxRuntime.jsx(react$1.Background, { gap: 20, size: 1 }),
4107
+ /* @__PURE__ */ jsxRuntime.jsx(react$1.Controls, { showInteractive: false }),
4108
+ /* @__PURE__ */ jsxRuntime.jsx(
4109
+ react$1.MiniMap,
4110
+ {
4111
+ nodeColor: (node) => {
4112
+ const d = node.data;
4113
+ const idx = d.colorIndex ?? 0;
4114
+ return SECTION_COLORS[idx % SECTION_COLORS.length];
4115
+ },
4116
+ maskColor: "rgba(0, 0, 0, 0.3)"
4117
+ }
4118
+ )
4119
+ ]
4120
+ }
4121
+ ) }),
4122
+ selectedFieldId && selectedQuestion && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "w-72 border-l border-border bg-card overflow-auto", children: [
4123
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-3 py-3 border-b border-border flex items-center justify-between", children: [
4124
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
4125
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs font-semibold truncate", children: selectedQuestion.label || selectedQuestion.id }),
4126
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-[10px] text-muted-foreground", children: selectedQuestion.type })
4127
+ ] }),
4128
+ /* @__PURE__ */ jsxRuntime.jsx(
4129
+ fieldcraftReact.Button,
4130
+ {
4131
+ variant: "ghost",
4132
+ size: "icon-xs",
4133
+ onClick: () => setSelectedFieldId(null),
4134
+ title: "Close panel",
4135
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { size: 14, strokeWidth: 1.75 })
4136
+ }
4137
+ )
4138
+ ] }),
4139
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-3 py-3", children: /* @__PURE__ */ jsxRuntime.jsx(
4140
+ ConditionEditor,
4141
+ {
4142
+ question: selectedQuestion,
4143
+ schema,
4144
+ onUpdate: handleConditionUpdate
4145
+ }
4146
+ ) })
4147
+ ] })
4148
+ ] }),
4149
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "shrink-0 px-4 py-2 border-t border-border bg-card flex items-center gap-4 text-xs text-muted-foreground flex-wrap", children: [
4150
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5", children: [
4151
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-5 h-0.5 rounded", style: { background: "var(--muted-foreground)", opacity: 0.4 } }),
4152
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: "flow" })
4153
+ ] }),
4154
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5", children: [
4155
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-5 h-0.5 bg-primary rounded" }),
4156
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: "showIf" })
4157
+ ] }),
4158
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5", children: [
4159
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-5 h-0.5 rounded", style: { background: "#f59e0b" } }),
4160
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: "jump" })
4161
+ ] }),
4162
+ sectionIds.map((id, i) => {
4163
+ const section = schema.sections.find((s) => s.id === id);
4164
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5", children: [
4165
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-3 h-3 rounded-sm", style: { background: SECTION_COLORS[i % SECTION_COLORS.length] } }),
4166
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: section?.title || id })
4167
+ ] }, id);
4168
+ })
4169
+ ] })
4170
+ ] });
4171
+ }
4172
+ function TemplateGallery({ templates, onSelect, onClose }) {
4173
+ const [search, setSearch] = react.useState("");
4174
+ const [activeCategory, setActiveCategory] = react.useState("all");
4175
+ const [confirmTemplate, setConfirmTemplate] = react.useState(null);
4176
+ const categories = react.useMemo(() => {
4177
+ const cats = /* @__PURE__ */ new Set();
4178
+ for (const t of templates) cats.add(t.meta.category);
4179
+ return ["all", ...Array.from(cats)];
4180
+ }, [templates]);
4181
+ const filtered = react.useMemo(() => {
4182
+ return templates.filter((t) => {
4183
+ const matchesCategory = activeCategory === "all" || t.meta.category === activeCategory;
4184
+ const matchesSearch = !search || t.meta.name.toLowerCase().includes(search.toLowerCase()) || t.meta.description.toLowerCase().includes(search.toLowerCase()) || t.meta.tags?.some((tag) => tag.toLowerCase().includes(search.toLowerCase()));
4185
+ return matchesCategory && matchesSearch;
4186
+ });
4187
+ }, [templates, activeCategory, search]);
4188
+ const handleConfirm = () => {
4189
+ if (confirmTemplate) {
4190
+ onSelect(confirmTemplate);
4191
+ setConfirmTemplate(null);
4192
+ onClose();
4193
+ }
4194
+ };
4195
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "w-full max-w-3xl max-h-[85vh] mx-4 bg-card border border-border rounded-lg shadow-xl flex flex-col overflow-hidden", children: [
4196
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "shrink-0 flex items-center justify-between px-4 py-3 border-b border-border", children: [
4197
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
4198
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.LayoutTemplate, { size: 18, className: "text-primary", strokeWidth: 1.75 }),
4199
+ /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "text-sm font-semibold", children: "Template Gallery" }),
4200
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-xs text-muted-foreground", children: [
4201
+ "(",
4202
+ templates.length,
4203
+ " templates)"
4204
+ ] })
4205
+ ] }),
4206
+ /* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.Button, { variant: "ghost", size: "icon-sm", onClick: onClose, title: "Close", children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { size: 16, strokeWidth: 1.75 }) })
4207
+ ] }),
4208
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "shrink-0 px-4 py-2 space-y-2 border-b border-border", children: [
4209
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative", children: [
4210
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Search, { size: 14, className: "absolute left-2.5 top-1/2 -translate-y-1/2 text-muted-foreground", strokeWidth: 1.75 }),
4211
+ /* @__PURE__ */ jsxRuntime.jsx(
4212
+ "input",
4213
+ {
4214
+ type: "text",
4215
+ placeholder: "Search templates...",
4216
+ value: search,
4217
+ onChange: (e) => setSearch(e.target.value),
4218
+ className: "w-full pl-8 pr-3 py-1.5 text-sm bg-background border border-border rounded-md text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-primary"
4219
+ }
4220
+ )
4221
+ ] }),
4222
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center gap-1 overflow-x-auto pb-0.5", children: categories.map((cat) => /* @__PURE__ */ jsxRuntime.jsx(
4223
+ "button",
4224
+ {
4225
+ type: "button",
4226
+ onClick: () => setActiveCategory(cat),
4227
+ className: `shrink-0 px-2.5 py-1 rounded-md text-xs font-medium transition-colors ${activeCategory === cat ? "bg-primary text-primary-foreground" : "bg-muted text-muted-foreground hover:text-foreground"}`,
4228
+ children: cat === "all" ? "All" : cat.charAt(0).toUpperCase() + cat.slice(1)
4229
+ },
4230
+ cat
4231
+ )) })
4232
+ ] }),
4233
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1 overflow-y-auto p-4", children: filtered.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-col items-center justify-center h-40 text-muted-foreground text-sm", children: /* @__PURE__ */ jsxRuntime.jsx("p", { children: "No templates match your search." }) }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-1 sm:grid-cols-2 gap-3", children: filtered.map((template) => /* @__PURE__ */ jsxRuntime.jsxs(
4234
+ "button",
4235
+ {
4236
+ type: "button",
4237
+ onClick: () => setConfirmTemplate(template),
4238
+ className: "text-left p-3 rounded-lg border border-border bg-background hover:border-primary/50 hover:bg-primary/5 transition-colors group",
4239
+ children: [
4240
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-start justify-between gap-2 mb-1.5", children: [
4241
+ /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-sm font-medium text-foreground group-hover:text-primary transition-colors", children: template.meta.name }),
4242
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "shrink-0 px-1.5 py-0.5 rounded text-[10px] font-medium bg-muted text-muted-foreground", children: template.meta.category })
4243
+ ] }),
4244
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-muted-foreground mb-2 line-clamp-2", children: template.meta.description }),
4245
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3 text-xs text-muted-foreground", children: [
4246
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex items-center gap-1", children: [
4247
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.FileText, { size: 11, strokeWidth: 1.75 }),
4248
+ template.meta.fieldCount,
4249
+ " field",
4250
+ template.meta.fieldCount !== 1 ? "s" : ""
4251
+ ] }),
4252
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex items-center gap-1", children: [
4253
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Layers, { size: 11, strokeWidth: 1.75 }),
4254
+ template.meta.sectionCount,
4255
+ " section",
4256
+ template.meta.sectionCount !== 1 ? "s" : ""
4257
+ ] })
4258
+ ] })
4259
+ ]
4260
+ },
4261
+ template.meta.id
4262
+ )) }) }),
4263
+ confirmTemplate && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "shrink-0 px-4 py-3 border-t border-border bg-muted/50 flex items-center justify-between gap-3", children: [
4264
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-sm", children: [
4265
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground", children: "Use " }),
4266
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium text-foreground", children: confirmTemplate.meta.name }),
4267
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground", children: "? This will replace the current form." })
4268
+ ] }),
4269
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 shrink-0", children: [
4270
+ /* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.Button, { variant: "ghost", size: "sm", onClick: () => setConfirmTemplate(null), children: "Cancel" }),
4271
+ /* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.Button, { size: "sm", onClick: handleConfirm, children: "Use Template" })
4272
+ ] })
4273
+ ] })
4274
+ ] }) });
4275
+ }
4276
+ function useDebouncedValidation(delayMs = 500) {
4277
+ const [errors, setErrors] = react.useState([]);
4278
+ const timerRef = react.useRef(null);
4279
+ const validate = react.useCallback(
4280
+ (text) => {
4281
+ if (timerRef.current) {
4282
+ clearTimeout(timerRef.current);
4283
+ }
4284
+ timerRef.current = setTimeout(() => {
4285
+ const result = [];
4286
+ let parsed;
4287
+ try {
4288
+ parsed = JSON.parse(text);
4289
+ } catch (err) {
4290
+ if (err instanceof SyntaxError) {
4291
+ const posMatch = err.message.match(/position\s+(\d+)/i);
4292
+ let line = 1;
4293
+ let column = 1;
4294
+ if (posMatch) {
4295
+ const pos = parseInt(posMatch[1], 10);
4296
+ const upToPos = text.slice(0, pos);
4297
+ line = (upToPos.match(/\n/g) || []).length + 1;
4298
+ column = pos - upToPos.lastIndexOf("\n");
4299
+ }
4300
+ result.push({ message: err.message, line, column, endLine: line, endColumn: column + 1 });
4301
+ } else {
4302
+ result.push({ message: "Invalid JSON" });
4303
+ }
4304
+ setErrors(result);
4305
+ return;
4306
+ }
4307
+ try {
4308
+ fieldcraftCore.validateSchema(parsed);
4309
+ } catch (err) {
4310
+ if (err instanceof fieldcraftCore.FormEngineSchemaError) {
4311
+ for (const issue of err.issues) {
4312
+ result.push({
4313
+ message: issue.message,
4314
+ path: issue.path?.join(".")
4315
+ });
4316
+ }
4317
+ } else if (err instanceof Error) {
4318
+ result.push({ message: err.message });
4319
+ }
4320
+ }
4321
+ setErrors(result);
4322
+ }, delayMs);
4323
+ },
4324
+ [delayMs]
4325
+ );
4326
+ return { errors, validate };
4327
+ }
4328
+ var ThemeCtx = react.createContext({});
4329
+ function useBuilderTheme() {
4330
+ return react.useContext(ThemeCtx);
4331
+ }
4332
+ function themeToCssVars(theme) {
4333
+ const vars = {};
4334
+ if (theme.background) vars["--background"] = theme.background;
4335
+ if (theme.foreground) vars["--foreground"] = theme.foreground;
4336
+ if (theme.card) {
4337
+ vars["--card"] = theme.card;
4338
+ vars["--card-foreground"] = theme.foreground ?? "";
4339
+ vars["--popover"] = theme.card;
4340
+ vars["--popover-foreground"] = theme.foreground ?? "";
4341
+ }
4342
+ if (theme.primary) vars["--primary"] = theme.primary;
4343
+ if (theme.primaryForeground) vars["--primary-foreground"] = theme.primaryForeground;
4344
+ if (theme.secondary) vars["--secondary"] = theme.secondary;
4345
+ if (theme.secondaryForeground) vars["--secondary-foreground"] = theme.secondaryForeground;
4346
+ if (theme.muted) vars["--muted"] = theme.muted;
4347
+ if (theme.mutedForeground) vars["--muted-foreground"] = theme.mutedForeground;
4348
+ if (theme.accent) vars["--accent"] = theme.accent;
4349
+ if (theme.accentForeground) vars["--accent-foreground"] = theme.accentForeground;
4350
+ if (theme.destructive) vars["--destructive"] = theme.destructive;
4351
+ if (theme.destructiveForeground) vars["--destructive-foreground"] = theme.destructiveForeground;
4352
+ if (theme.border) vars["--border"] = theme.border;
4353
+ if (theme.input) vars["--input"] = theme.input;
4354
+ if (theme.ring) vars["--ring"] = theme.ring;
4355
+ if (theme.radius) vars["--radius"] = theme.radius;
4356
+ if (theme.surface) vars["--fcb-surface"] = theme.surface;
4357
+ if (theme.surfaceHover) vars["--fcb-surface-hover"] = theme.surfaceHover;
4358
+ if (theme.canvas) vars["--fcb-canvas"] = theme.canvas;
4359
+ if (theme.panel) vars["--fcb-panel"] = theme.panel;
4360
+ if (theme.borderStrong) vars["--fcb-border-strong"] = theme.borderStrong;
4361
+ if (theme.textDim) vars["--fcb-text-dim"] = theme.textDim;
4362
+ return vars;
4363
+ }
4364
+ function FormBuilderThemeProvider({ theme, children }) {
4365
+ const resolved = theme ?? {};
4366
+ const cssVars = react.useMemo(() => themeToCssVars(resolved), [resolved]);
4367
+ return /* @__PURE__ */ jsxRuntime.jsx(ThemeCtx.Provider, { value: resolved, children: /* @__PURE__ */ jsxRuntime.jsx("div", { "data-fcb-root": "", style: cssVars, className: "w-full h-full", children }) });
4368
+ }
4369
+ var FormBuilderErrorBoundary = class extends react.Component {
4370
+ constructor(props) {
4371
+ super(props);
4372
+ this.state = { hasError: false, error: null };
4373
+ }
4374
+ static getDerivedStateFromError(error) {
4375
+ return { hasError: true, error };
4376
+ }
4377
+ componentDidCatch(error, info) {
4378
+ console.error("[FormBuilder] Render error:", error, info.componentStack);
4379
+ }
4380
+ render() {
4381
+ if (this.state.hasError) {
4382
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-col items-center justify-center h-full bg-background text-foreground p-8", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "max-w-md text-center space-y-4", children: [
4383
+ /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "text-lg font-semibold", children: "Something went wrong" }),
4384
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-muted-foreground", children: "The form builder encountered an unexpected error. Your schema data is preserved." }),
4385
+ /* @__PURE__ */ jsxRuntime.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 }),
4386
+ /* @__PURE__ */ jsxRuntime.jsx(
4387
+ fieldcraftReact.Button,
4388
+ {
4389
+ onClick: () => this.setState({ hasError: false, error: null }),
4390
+ variant: "outline",
4391
+ children: "Try Again"
4392
+ }
4393
+ )
4394
+ ] }) });
4395
+ }
4396
+ return this.props.children;
4397
+ }
4398
+ };
4399
+ function FormBuilderCore(props) {
4400
+ const { initialSchema = DEFAULT_SCHEMA, onChange, onSave, height = "100vh", theme, className, toolbarExtra, questionTypes, palette, preview, templates, schemaUrl } = props;
4401
+ const [viewMode, setViewMode] = react.useState("design");
4402
+ const [jsonText, setJsonText] = react.useState("");
4403
+ const [jsonSwitchError, setJsonSwitchError] = react.useState(null);
4404
+ const [showLogicMap, setShowLogicMap] = react.useState(false);
4405
+ const [showTemplateGallery, setShowTemplateGallery] = react.useState(false);
4406
+ const [saveValidationErrors, setSaveValidationErrors] = react.useState(null);
4407
+ const [schemaUrlLoading, setSchemaUrlLoading] = react.useState(!!schemaUrl);
4408
+ const [schemaUrlError, setSchemaUrlError] = react.useState(null);
4409
+ const { errors: jsonErrors, validate: validateJson } = useDebouncedValidation(400);
4410
+ const mergedQuestionTypes = questionTypes ? { ...QUESTION_TYPE_INFO, ...questionTypes } : QUESTION_TYPE_INFO;
4411
+ const builderState = useBuilderState(initialSchema);
4412
+ const dragDrop = useDragDrop(builderState);
4413
+ const fileInputRef = react.useRef(null);
4414
+ const [mobilePanel, setMobilePanel] = react.useState("none");
4415
+ react.useEffect(() => {
4416
+ if (builderState.selectedItem && window.innerWidth < 768) {
4417
+ setMobilePanel("properties");
4418
+ }
4419
+ }, [builderState.selectedItem]);
4420
+ react.useEffect(() => {
4421
+ if (!dragDrop.activeDragItem && mobilePanel === "palette") {
4422
+ setMobilePanel("none");
4423
+ }
4424
+ }, [dragDrop.activeDragItem]);
4425
+ react.useEffect(() => {
4426
+ if (!schemaUrl) return;
4427
+ let cancelled = false;
4428
+ setSchemaUrlLoading(true);
4429
+ setSchemaUrlError(null);
4430
+ fetch(schemaUrl).then((res) => {
4431
+ if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`);
4432
+ return res.json();
4433
+ }).then((json) => {
4434
+ if (cancelled) return;
4435
+ try {
4436
+ const validated = fieldcraftCore.validateSchema(json);
4437
+ builderState.resetSchema(validated);
4438
+ } catch (err) {
4439
+ if (err instanceof fieldcraftCore.FormEngineSchemaError) {
4440
+ setSchemaUrlError(`Invalid schema: ${err.issues[0]?.message ?? "validation failed"}`);
4441
+ } else {
4442
+ setSchemaUrlError(err.message);
4443
+ }
4444
+ }
4445
+ setSchemaUrlLoading(false);
4446
+ }).catch((err) => {
4447
+ if (cancelled) return;
4448
+ setSchemaUrlError(err.message ?? "Failed to fetch schema");
4449
+ setSchemaUrlLoading(false);
4450
+ });
4451
+ return () => {
4452
+ cancelled = true;
4453
+ };
4454
+ }, [schemaUrl]);
4455
+ react.useEffect(() => {
4456
+ if (onChange && builderState.isDirty) {
4457
+ onChange(builderState.schema);
4458
+ }
4459
+ }, [builderState.schema, builderState.isDirty, onChange]);
4460
+ const handleSave = react.useCallback(() => {
4461
+ if (!onSave) return;
4462
+ try {
4463
+ fieldcraftCore.validateSchema(builderState.schema);
4464
+ setSaveValidationErrors(null);
4465
+ onSave(builderState.schema);
4466
+ builderState.markClean();
4467
+ } catch (err) {
4468
+ if (err instanceof fieldcraftCore.FormEngineSchemaError) {
4469
+ setSaveValidationErrors(err.issues.map((issue) => issue.message));
4470
+ } else {
4471
+ setSaveValidationErrors([err.message ?? "Unknown validation error"]);
4472
+ }
4473
+ }
4474
+ }, [onSave, builderState]);
4475
+ const handleExport = react.useCallback(() => {
4476
+ const json = JSON.stringify(builderState.schema, null, 2);
4477
+ const blob = new Blob([json], { type: "application/json" });
4478
+ const url = URL.createObjectURL(blob);
4479
+ const a = document.createElement("a");
4480
+ a.href = url;
4481
+ a.download = `${builderState.schema.title?.replace(/\s+/g, "-").toLowerCase() || "form"}-schema.json`;
4482
+ a.click();
4483
+ URL.revokeObjectURL(url);
4484
+ }, [builderState.schema]);
4485
+ const handleImport = react.useCallback(() => {
4486
+ fileInputRef.current?.click();
4487
+ }, []);
4488
+ const handleFileChange = react.useCallback(
4489
+ (e) => {
4490
+ const file = e.target.files?.[0];
4491
+ if (!file) return;
4492
+ const reader = new FileReader();
4493
+ reader.onload = (event) => {
4494
+ try {
4495
+ const parsed = JSON.parse(event.target?.result);
4496
+ const validated = fieldcraftCore.validateSchema(parsed);
4497
+ builderState.resetSchema(validated);
4498
+ } catch (err) {
4499
+ if (err instanceof fieldcraftCore.FormEngineSchemaError) {
4500
+ const details = err.issues.slice(0, 3).map((i) => `\u2022 ${i.path.join(".")}: ${i.message}`).join("\n");
4501
+ alert(`Invalid schema:
4502
+ ${details}`);
4503
+ } else if (err instanceof SyntaxError) {
4504
+ alert("Failed to parse JSON file.");
4505
+ } else {
4506
+ alert("Invalid schema file.");
4507
+ }
4508
+ }
4509
+ };
4510
+ reader.readAsText(file);
4511
+ e.target.value = "";
4512
+ },
4513
+ [builderState]
4514
+ );
4515
+ const handleViewModeChange = react.useCallback(
4516
+ (newMode) => {
4517
+ setJsonSwitchError(null);
4518
+ if (viewMode === "json" && newMode !== "json") {
4519
+ try {
4520
+ const parsed = JSON.parse(jsonText);
4521
+ const validated = fieldcraftCore.validateSchema(parsed);
4522
+ builderState.resetSchema(validated);
4523
+ } catch (err) {
4524
+ if (err instanceof SyntaxError) {
4525
+ setJsonSwitchError("Cannot switch: JSON has syntax errors");
4526
+ } else if (err instanceof fieldcraftCore.FormEngineSchemaError) {
4527
+ setJsonSwitchError(`Cannot switch: ${err.issues[0]?.message ?? "Invalid schema"}`);
4528
+ } else {
4529
+ setJsonSwitchError("Cannot switch: Invalid schema");
4530
+ }
4531
+ return;
4532
+ }
4533
+ }
4534
+ if (newMode === "json") {
4535
+ setJsonText(JSON.stringify(builderState.schema, null, 2));
4536
+ }
4537
+ setViewMode(newMode);
4538
+ },
4539
+ [viewMode, jsonText, builderState]
4540
+ );
4541
+ const handleKeyDown = react.useCallback(
4542
+ (e) => {
4543
+ const mod = e.metaKey || e.ctrlKey;
4544
+ const tag = e.target.tagName;
4545
+ const isEditing = tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT";
4546
+ if (mod && e.key === "z" && !e.shiftKey) {
4547
+ e.preventDefault();
4548
+ builderState.undo();
4549
+ return;
4550
+ }
4551
+ if (mod && e.key === "z" && e.shiftKey) {
4552
+ e.preventDefault();
4553
+ builderState.redo();
4554
+ return;
4555
+ }
4556
+ if (mod && e.key === "s") {
4557
+ e.preventDefault();
4558
+ handleSave();
4559
+ return;
4560
+ }
4561
+ if (isEditing) return;
4562
+ if (e.key === "Delete" || e.key === "Backspace") {
4563
+ const sel = builderState.selectedItem;
4564
+ if (!sel) return;
4565
+ e.preventDefault();
4566
+ if (sel.type === "question") {
4567
+ builderState.removeQuestion(sel.sectionId, sel.questionId);
4568
+ } else if (sel.type === "section") {
4569
+ builderState.removeSection(sel.sectionId);
4570
+ }
4571
+ return;
4572
+ }
4573
+ if (e.key === "Escape") {
4574
+ if (builderState.selectedItem) {
4575
+ e.preventDefault();
4576
+ builderState.clearSelection();
4577
+ }
4578
+ return;
4579
+ }
4580
+ if (mod && e.key === "d") {
4581
+ const sel = builderState.selectedItem;
4582
+ if (!sel) return;
4583
+ e.preventDefault();
4584
+ if (sel.type === "question") {
4585
+ builderState.duplicateQuestion(sel.sectionId, sel.questionId);
4586
+ } else if (sel.type === "section") {
4587
+ builderState.duplicateSection(sel.sectionId);
4588
+ }
4589
+ }
4590
+ },
4591
+ [builderState, handleSave]
4592
+ );
4593
+ return /* @__PURE__ */ jsxRuntime.jsx(FormBuilderThemeProvider, { theme, children: /* @__PURE__ */ jsxRuntime.jsxs(
4594
+ core.DndContext,
4595
+ {
4596
+ sensors: dragDrop.sensors,
4597
+ onDragStart: dragDrop.handleDragStart,
4598
+ onDragEnd: dragDrop.handleDragEnd,
4599
+ onDragCancel: dragDrop.handleDragCancel,
4600
+ children: [
4601
+ /* @__PURE__ */ jsxRuntime.jsxs(
4602
+ "div",
4603
+ {
4604
+ className: cn("flex flex-col bg-background text-foreground", className),
4605
+ style: { height: typeof height === "number" ? `${height}px` : height },
4606
+ onKeyDown: handleKeyDown,
4607
+ tabIndex: -1,
4608
+ role: "application",
4609
+ "aria-label": "Form Builder",
4610
+ children: [
4611
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "h-12 shrink-0 hidden md:grid px-4 bg-card border-b border-border", style: { gridTemplateColumns: "1fr auto 1fr" }, role: "toolbar", "aria-label": "Builder toolbar", children: [
3580
4612
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1", children: [
3581
4613
  /* @__PURE__ */ jsxRuntime.jsx(
3582
4614
  fieldcraftReact.Button,
3583
4615
  {
3584
4616
  variant: "ghost",
3585
4617
  size: "icon-sm",
3586
- onClick: builderState.undo,
3587
- disabled: !builderState.canUndo,
3588
- className: "disabled:opacity-30",
3589
- title: "Undo (Ctrl+Z)",
3590
- "aria-label": "Undo",
3591
- children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Undo2, { size: 16, strokeWidth: 1.75 })
4618
+ onClick: builderState.undo,
4619
+ disabled: !builderState.canUndo,
4620
+ className: "disabled:opacity-30",
4621
+ title: "Undo (Ctrl+Z)",
4622
+ "aria-label": "Undo",
4623
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Undo2, { size: 16, strokeWidth: 1.75 })
4624
+ }
4625
+ ),
4626
+ /* @__PURE__ */ jsxRuntime.jsx(
4627
+ fieldcraftReact.Button,
4628
+ {
4629
+ variant: "ghost",
4630
+ size: "icon-sm",
4631
+ onClick: builderState.redo,
4632
+ disabled: !builderState.canRedo,
4633
+ className: "disabled:opacity-30",
4634
+ title: "Redo (Ctrl+Shift+Z)",
4635
+ "aria-label": "Redo",
4636
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Redo2, { size: 16, strokeWidth: 1.75 })
4637
+ }
4638
+ ),
4639
+ /* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.Separator, { orientation: "vertical", className: "mx-2 h-5" }),
4640
+ builderState.isDirty && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-primary", role: "status", children: "Unsaved changes" })
4641
+ ] }),
4642
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-center gap-2", children: [
4643
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-0.5 bg-muted rounded-md p-0.5", children: [
4644
+ /* @__PURE__ */ jsxRuntime.jsxs(
4645
+ fieldcraftReact.Button,
4646
+ {
4647
+ variant: viewMode === "design" ? "secondary" : "ghost",
4648
+ size: "sm",
4649
+ onClick: () => handleViewModeChange("design"),
4650
+ className: "gap-1.5 h-7 px-2.5 text-xs",
4651
+ children: [
4652
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.PencilRuler, { size: 13, strokeWidth: 1.75 }),
4653
+ "Design"
4654
+ ]
4655
+ }
4656
+ ),
4657
+ /* @__PURE__ */ jsxRuntime.jsxs(
4658
+ fieldcraftReact.Button,
4659
+ {
4660
+ variant: viewMode === "preview" ? "secondary" : "ghost",
4661
+ size: "sm",
4662
+ onClick: () => handleViewModeChange("preview"),
4663
+ className: "gap-1.5 h-7 px-2.5 text-xs",
4664
+ children: [
4665
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Eye, { size: 13, strokeWidth: 1.75 }),
4666
+ "Preview"
4667
+ ]
4668
+ }
4669
+ ),
4670
+ /* @__PURE__ */ jsxRuntime.jsxs(
4671
+ fieldcraftReact.Button,
4672
+ {
4673
+ variant: viewMode === "json" ? "secondary" : "ghost",
4674
+ size: "sm",
4675
+ onClick: () => handleViewModeChange("json"),
4676
+ className: "gap-1.5 h-7 px-2.5 text-xs",
4677
+ children: [
4678
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Code, { size: 13, strokeWidth: 1.75 }),
4679
+ "JSON"
4680
+ ]
4681
+ }
4682
+ )
4683
+ ] }),
4684
+ toolbarExtra
4685
+ ] }),
4686
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-end gap-1.5", children: [
4687
+ templates && templates.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(
4688
+ fieldcraftReact.Button,
4689
+ {
4690
+ variant: "ghost",
4691
+ size: "icon-sm",
4692
+ onClick: () => setShowTemplateGallery(true),
4693
+ title: "Browse templates",
4694
+ "aria-label": "Template gallery",
4695
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.LayoutTemplate, { size: 15, strokeWidth: 1.75 })
3592
4696
  }
3593
4697
  ),
3594
4698
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -3596,19 +4700,13 @@ ${details}`);
3596
4700
  {
3597
4701
  variant: "ghost",
3598
4702
  size: "icon-sm",
3599
- onClick: builderState.redo,
3600
- disabled: !builderState.canRedo,
3601
- className: "disabled:opacity-30",
3602
- title: "Redo (Ctrl+Shift+Z)",
3603
- "aria-label": "Redo",
3604
- children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Redo2, { size: 16, strokeWidth: 1.75 })
4703
+ onClick: () => setShowLogicMap(true),
4704
+ title: "View branching logic map",
4705
+ "aria-label": "Logic map",
4706
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.GitBranch, { size: 15, strokeWidth: 1.75 })
3605
4707
  }
3606
4708
  ),
3607
- /* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.Separator, { orientation: "vertical", className: "mx-2 h-5" }),
3608
- builderState.isDirty && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-primary", role: "status", children: "Unsaved changes" })
3609
- ] }),
3610
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center justify-center", children: toolbarExtra }),
3611
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-end gap-1.5", children: [
4709
+ /* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.Separator, { orientation: "vertical", className: "mx-1 h-5" }),
3612
4710
  /* @__PURE__ */ jsxRuntime.jsx(
3613
4711
  fieldcraftReact.Button,
3614
4712
  {
@@ -3652,6 +4750,47 @@ ${details}`);
3652
4750
  }
3653
4751
  ) }),
3654
4752
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-center gap-1", children: [
4753
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-0.5 bg-muted rounded-md p-0.5", children: [
4754
+ /* @__PURE__ */ jsxRuntime.jsxs(
4755
+ fieldcraftReact.Button,
4756
+ {
4757
+ variant: viewMode === "design" ? "secondary" : "ghost",
4758
+ size: "sm",
4759
+ onClick: () => handleViewModeChange("design"),
4760
+ className: "gap-1 h-7 px-2 text-xs",
4761
+ children: [
4762
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.PencilRuler, { size: 12, strokeWidth: 1.75 }),
4763
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "hidden sm:inline", children: "Design" })
4764
+ ]
4765
+ }
4766
+ ),
4767
+ /* @__PURE__ */ jsxRuntime.jsxs(
4768
+ fieldcraftReact.Button,
4769
+ {
4770
+ variant: viewMode === "preview" ? "secondary" : "ghost",
4771
+ size: "sm",
4772
+ onClick: () => handleViewModeChange("preview"),
4773
+ className: "gap-1 h-7 px-2 text-xs",
4774
+ children: [
4775
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Eye, { size: 12, strokeWidth: 1.75 }),
4776
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "hidden sm:inline", children: "Preview" })
4777
+ ]
4778
+ }
4779
+ ),
4780
+ /* @__PURE__ */ jsxRuntime.jsxs(
4781
+ fieldcraftReact.Button,
4782
+ {
4783
+ variant: viewMode === "json" ? "secondary" : "ghost",
4784
+ size: "sm",
4785
+ onClick: () => handleViewModeChange("json"),
4786
+ className: "gap-1 h-7 px-2 text-xs",
4787
+ children: [
4788
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Code, { size: 12, strokeWidth: 1.75 }),
4789
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "hidden sm:inline", children: "JSON" })
4790
+ ]
4791
+ }
4792
+ )
4793
+ ] }),
3655
4794
  toolbarExtra,
3656
4795
  /* @__PURE__ */ jsxRuntime.jsxs(fieldcraftReact.Button, { onClick: handleSave, size: "sm", className: "border-0 shadow-sm fcb-glow ml-1", "aria-label": "Save form", children: [
3657
4796
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Save, { size: 14, strokeWidth: 2 }),
@@ -3740,7 +4879,50 @@ ${details}`);
3740
4879
  "aria-hidden": "true"
3741
4880
  }
3742
4881
  ),
3743
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 flex overflow-hidden relative", children: [
4882
+ jsonSwitchError && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "shrink-0 px-4 py-2 bg-destructive/10 border-b border-destructive/20 text-destructive text-xs flex items-center justify-between", children: [
4883
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: jsonSwitchError }),
4884
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", onClick: () => setJsonSwitchError(null), className: "ml-2 hover:underline", children: "Dismiss" })
4885
+ ] }),
4886
+ saveValidationErrors && saveValidationErrors.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "shrink-0 px-4 py-2 bg-destructive/10 border-b border-destructive/20 text-xs", children: [
4887
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between mb-1", children: [
4888
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "font-semibold text-destructive", children: [
4889
+ "Schema validation failed (",
4890
+ saveValidationErrors.length,
4891
+ " issue",
4892
+ saveValidationErrors.length !== 1 ? "s" : "",
4893
+ ")"
4894
+ ] }),
4895
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", onClick: () => setSaveValidationErrors(null), className: "text-destructive hover:underline", children: "Dismiss" })
4896
+ ] }),
4897
+ /* @__PURE__ */ jsxRuntime.jsxs("ul", { className: "list-disc pl-4 text-destructive/80 space-y-0.5", children: [
4898
+ saveValidationErrors.slice(0, 10).map((msg, i) => /* @__PURE__ */ jsxRuntime.jsx("li", { children: msg }, i)),
4899
+ saveValidationErrors.length > 10 && /* @__PURE__ */ jsxRuntime.jsxs("li", { children: [
4900
+ "...and ",
4901
+ saveValidationErrors.length - 10,
4902
+ " more"
4903
+ ] })
4904
+ ] })
4905
+ ] }),
4906
+ schemaUrlLoading && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "shrink-0 px-4 py-2 border-b border-border bg-muted/50 text-xs text-muted-foreground flex items-center gap-2", children: [
4907
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "inline-block w-3 h-3 border-2 border-primary border-t-transparent rounded-full animate-spin" }),
4908
+ "Loading schema from URL..."
4909
+ ] }),
4910
+ schemaUrlError && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "shrink-0 px-4 py-2 bg-destructive/10 border-b border-destructive/20 text-destructive text-xs flex items-center justify-between", children: [
4911
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
4912
+ "Failed to load schema: ",
4913
+ schemaUrlError
4914
+ ] }),
4915
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", onClick: () => setSchemaUrlError(null), className: "ml-2 hover:underline", children: "Dismiss" })
4916
+ ] }),
4917
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1 flex overflow-hidden relative", children: viewMode === "json" ? /* @__PURE__ */ jsxRuntime.jsx(
4918
+ JsonEditorPanel,
4919
+ {
4920
+ value: jsonText,
4921
+ onChange: setJsonText,
4922
+ errors: jsonErrors,
4923
+ onValidate: validateJson
4924
+ }
4925
+ ) : viewMode === "preview" ? /* @__PURE__ */ jsxRuntime.jsx(FormPreviewPanel, { schema: builderState.schema, preview }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
3744
4926
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "hidden md:flex", children: /* @__PURE__ */ jsxRuntime.jsx(QuestionPalette, { questionTypes: mergedQuestionTypes, palette }) }),
3745
4927
  /* @__PURE__ */ jsxRuntime.jsx(FormCanvas, { builderState }),
3746
4928
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "hidden md:flex", children: /* @__PURE__ */ jsxRuntime.jsx(PropertiesPanel, { builderState }) }),
@@ -3754,11 +4936,27 @@ ${details}`);
3754
4936
  "aria-hidden": "true"
3755
4937
  }
3756
4938
  )
3757
- ] })
4939
+ ] }) })
3758
4940
  ]
3759
4941
  }
3760
4942
  ),
3761
- /* @__PURE__ */ jsxRuntime.jsx(core.DragOverlay, { dropAnimation: null, children: dragDrop.activeDragItem && /* @__PURE__ */ jsxRuntime.jsx(DragOverlayContent, { item: dragDrop.activeDragItem, schema: builderState.schema, questionTypes: mergedQuestionTypes }) })
4943
+ /* @__PURE__ */ jsxRuntime.jsx(core.DragOverlay, { dropAnimation: null, children: dragDrop.activeDragItem && /* @__PURE__ */ jsxRuntime.jsx(DragOverlayContent, { item: dragDrop.activeDragItem, schema: builderState.schema, questionTypes: mergedQuestionTypes }) }),
4944
+ showLogicMap && /* @__PURE__ */ jsxRuntime.jsx(
4945
+ LogicFlowEditor,
4946
+ {
4947
+ schema: builderState.schema,
4948
+ onChange: (updated) => builderState.resetSchema(updated),
4949
+ onClose: () => setShowLogicMap(false)
4950
+ }
4951
+ ),
4952
+ showTemplateGallery && templates && /* @__PURE__ */ jsxRuntime.jsx(
4953
+ TemplateGallery,
4954
+ {
4955
+ templates,
4956
+ onSelect: (template) => builderState.resetSchema(template.schema),
4957
+ onClose: () => setShowTemplateGallery(false)
4958
+ }
4959
+ )
3762
4960
  ]
3763
4961
  }
3764
4962
  ) });
@@ -3813,76 +5011,54 @@ var FormBuilder = requireLicense(FormBuilderInner, "FormBuilder");
3813
5011
 
3814
5012
  // src/form-builder/theme/presets.ts
3815
5013
  var squaredrDarkPreset = {
3816
- background: "#0a0a0b",
3817
- // ink-950
3818
- foreground: "#e8e8ea",
3819
- // ink-100
3820
- card: "#111113",
3821
- // ink-900
3822
- primary: "oklch(0.82 0.14 210)",
3823
- // sr-accent-cyan
3824
- primaryForeground: "#0a0a0b",
3825
- // ink-950
3826
- secondary: "#17171a",
3827
- // ink-850
3828
- secondaryForeground: "#e8e8ea",
3829
- // ink-100
3830
- muted: "#111113",
3831
- // ink-900
3832
- mutedForeground: "#8a8a95",
3833
- // ink-400
3834
- accent: "#17171a",
3835
- // ink-850
3836
- accentForeground: "#e8e8ea",
3837
- // ink-100
3838
- destructive: "oklch(0.68 0.22 25)",
3839
- // sr-error
3840
- destructiveForeground: "#e8e8ea",
3841
- // ink-100
3842
- border: "#1c1c20",
3843
- // ink-800
3844
- input: "#1c1c20",
3845
- // ink-800
3846
- ring: "oklch(0.82 0.14 210)",
3847
- // sr-accent-cyan
3848
- radius: "6px",
3849
- surface: "#111113",
3850
- // ink-900
3851
- surfaceHover: "#17171a",
3852
- // ink-850
3853
- canvas: "#0a0a0b",
3854
- // ink-950
3855
- panel: "#111113",
3856
- // ink-900
3857
- borderStrong: "#26262c",
3858
- // ink-700
3859
- textDim: "#5a5a66"
3860
- // ink-500
5014
+ background: "#0F1A1F",
5015
+ foreground: "#E8EFF1",
5016
+ card: "#16242A",
5017
+ primary: "#63BDB4",
5018
+ primaryForeground: "#0F1A1F",
5019
+ secondary: "#182F31",
5020
+ secondaryForeground: "#E8EFF1",
5021
+ muted: "#182F31",
5022
+ mutedForeground: "#8CA1A9",
5023
+ accent: "#182F31",
5024
+ accentForeground: "#E8EFF1",
5025
+ destructive: "#E08072",
5026
+ destructiveForeground: "#0F1A1F",
5027
+ border: "#2A3B42",
5028
+ input: "#2A3B42",
5029
+ ring: "#63BDB4",
5030
+ radius: "0px",
5031
+ surface: "#16242A",
5032
+ surfaceHover: "#182F31",
5033
+ canvas: "#0F1A1F",
5034
+ panel: "#16242A",
5035
+ borderStrong: "#2F4F4C",
5036
+ textDim: "#5E7680"
3861
5037
  };
3862
5038
  var cleanPreset = {
3863
- background: "#ffffff",
3864
- foreground: "#111113",
3865
- card: "#f9fafb",
3866
- primary: "#0d9488",
3867
- primaryForeground: "#ffffff",
3868
- secondary: "#f3f4f6",
3869
- secondaryForeground: "#111113",
3870
- muted: "#f3f4f6",
3871
- mutedForeground: "#6b7280",
3872
- accent: "#f3f4f6",
3873
- accentForeground: "#111113",
3874
- destructive: "#ef4444",
3875
- destructiveForeground: "#ffffff",
3876
- border: "#e5e7eb",
3877
- input: "#e5e7eb",
3878
- ring: "#0d9488",
3879
- radius: "6px",
3880
- surface: "#f9fafb",
3881
- surfaceHover: "#f3f4f6",
3882
- canvas: "#ffffff",
3883
- panel: "#ffffff",
3884
- borderStrong: "#d1d5db",
3885
- textDim: "#9ca3af"
5039
+ background: "#F4F7F8",
5040
+ foreground: "#12222A",
5041
+ card: "#FFFFFF",
5042
+ primary: "#1F6B6E",
5043
+ primaryForeground: "#FFFFFF",
5044
+ secondary: "#EDF3F2",
5045
+ secondaryForeground: "#12222A",
5046
+ muted: "#EDF3F2",
5047
+ mutedForeground: "#6A7B85",
5048
+ accent: "#EDF3F2",
5049
+ accentForeground: "#12222A",
5050
+ destructive: "#B04A3C",
5051
+ destructiveForeground: "#FFFFFF",
5052
+ border: "#DCE4E8",
5053
+ input: "#DCE4E8",
5054
+ ring: "#1F6B6E",
5055
+ radius: "0px",
5056
+ surface: "#FAFCFC",
5057
+ surfaceHover: "#EDF3F2",
5058
+ canvas: "#F4F7F8",
5059
+ panel: "#FFFFFF",
5060
+ borderStrong: "#B9D1CF",
5061
+ textDim: "#6A7B85"
3886
5062
  };
3887
5063
 
3888
5064
  // src/response-viewer/clinical-display-data.ts
@@ -4021,10 +5197,30 @@ function getScoreSeverity(instrumentKey, score) {
4021
5197
  if (!thresholds) return void 0;
4022
5198
  return thresholds.find((t) => score >= t.min && score <= t.max);
4023
5199
  }
4024
- function ResponseTable({ schema, responses, onRowClick }) {
5200
+ function ResponseTable({
5201
+ schema,
5202
+ responses,
5203
+ onRowClick,
5204
+ selectable,
5205
+ selectedIds,
5206
+ onToggleSelect,
5207
+ onSelectAll
5208
+ }) {
4025
5209
  const questions = getAllQuestions(schema);
5210
+ const allPageSelected = selectable && responses.length > 0 && responses.every(
5211
+ (r) => r.sessionToken && selectedIds?.has(r.sessionToken)
5212
+ );
4026
5213
  return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "overflow-auto", children: /* @__PURE__ */ jsxRuntime.jsxs("table", { className: "w-full border-collapse text-[13px]", children: [
4027
5214
  /* @__PURE__ */ jsxRuntime.jsx("thead", { children: /* @__PURE__ */ jsxRuntime.jsxs("tr", { className: "bg-muted", children: [
5215
+ selectable && /* @__PURE__ */ jsxRuntime.jsx("th", { className: "px-3 py-2 w-8 border-b-2 border-border", children: /* @__PURE__ */ jsxRuntime.jsx(
5216
+ "input",
5217
+ {
5218
+ type: "checkbox",
5219
+ checked: allPageSelected,
5220
+ onChange: () => onSelectAll?.(),
5221
+ className: "accent-primary"
5222
+ }
5223
+ ) }),
4028
5224
  /* @__PURE__ */ jsxRuntime.jsx("th", { className: "px-3 py-2 text-left font-semibold text-foreground border-b-2 border-border whitespace-nowrap", children: "Submitted" }),
4029
5225
  questions.map((q) => /* @__PURE__ */ jsxRuntime.jsx(
4030
5226
  "th",
@@ -4043,6 +5239,19 @@ function ResponseTable({ schema, responses, onRowClick }) {
4043
5239
  onClick: () => onRowClick?.(response),
4044
5240
  className: onRowClick ? "cursor-pointer border-b border-border hover:bg-accent transition-colors" : "border-b border-border",
4045
5241
  children: [
5242
+ selectable && /* @__PURE__ */ jsxRuntime.jsx("td", { className: "px-3 py-2 w-8", children: /* @__PURE__ */ jsxRuntime.jsx(
5243
+ "input",
5244
+ {
5245
+ type: "checkbox",
5246
+ checked: !!(response.sessionToken && selectedIds?.has(response.sessionToken)),
5247
+ onChange: (e) => {
5248
+ e.stopPropagation();
5249
+ if (response.sessionToken) onToggleSelect?.(response.sessionToken);
5250
+ },
5251
+ onClick: (e) => e.stopPropagation(),
5252
+ className: "accent-primary"
5253
+ }
5254
+ ) }),
4046
5255
  /* @__PURE__ */ jsxRuntime.jsx("td", { className: "px-3 py-2 text-foreground max-w-50 overflow-hidden text-ellipsis whitespace-nowrap", children: new Date(response.submittedAt).toLocaleString() }),
4047
5256
  questions.map((q) => /* @__PURE__ */ jsxRuntime.jsx(
4048
5257
  "td",
@@ -4060,7 +5269,7 @@ function ResponseTable({ schema, responses, onRowClick }) {
4060
5269
  responses.length === 0 && /* @__PURE__ */ jsxRuntime.jsx("tr", { children: /* @__PURE__ */ jsxRuntime.jsx(
4061
5270
  "td",
4062
5271
  {
4063
- colSpan: questions.length + 2,
5272
+ colSpan: questions.length + 2 + (selectable ? 1 : 0),
4064
5273
  className: "px-3 py-8 text-center text-muted-foreground",
4065
5274
  children: "No responses yet"
4066
5275
  }
@@ -4148,15 +5357,37 @@ function formatCellValue(value, type) {
4148
5357
  if (typeof value === "object") return JSON.stringify(value);
4149
5358
  return String(value);
4150
5359
  }
4151
- function ResponseCard({ response, fields, onClick }) {
5360
+ function ResponseCard({
5361
+ response,
5362
+ fields,
5363
+ onClick,
5364
+ selectable,
5365
+ selected,
5366
+ onToggleSelect
5367
+ }) {
4152
5368
  return /* @__PURE__ */ jsxRuntime.jsxs(
4153
5369
  "div",
4154
5370
  {
4155
5371
  onClick,
4156
- className: onClick ? "border border-border rounded-lg p-4 bg-card cursor-pointer transition-shadow hover:shadow-md" : "border border-border rounded-lg p-4 bg-card",
5372
+ className: (onClick ? "border border-border rounded-lg p-4 bg-card cursor-pointer transition-shadow hover:shadow-md" : "border border-border rounded-lg p-4 bg-card") + (selected ? " ring-2 ring-primary" : ""),
4157
5373
  children: [
4158
5374
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex justify-between mb-3 text-xs text-muted-foreground", children: [
4159
- /* @__PURE__ */ jsxRuntime.jsx("span", { children: new Date(response.submittedAt).toLocaleString() }),
5375
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
5376
+ selectable && /* @__PURE__ */ jsxRuntime.jsx(
5377
+ "input",
5378
+ {
5379
+ type: "checkbox",
5380
+ checked: !!selected,
5381
+ onChange: (e) => {
5382
+ e.stopPropagation();
5383
+ onToggleSelect?.();
5384
+ },
5385
+ onClick: (e) => e.stopPropagation(),
5386
+ className: "accent-primary"
5387
+ }
5388
+ ),
5389
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: new Date(response.submittedAt).toLocaleString() })
5390
+ ] }),
4160
5391
  response.completionTimeMs != null && /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
4161
5392
  Math.round(response.completionTimeMs / 1e3),
4162
5393
  "s"
@@ -4247,18 +5478,19 @@ function formatCardValue(value, type) {
4247
5478
  if (typeof value === "object") return JSON.stringify(value);
4248
5479
  return String(value);
4249
5480
  }
4250
- function ResponseDetail({ response, fields, onBack }) {
5481
+ function ResponseDetail({ response, fields, onBack, onDelete }) {
4251
5482
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
4252
5483
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3 mb-5 pb-3 border-b border-border", children: [
4253
5484
  onBack && /* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.Button, { variant: "outline", size: "sm", onClick: onBack, children: "Back" }),
4254
- /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
5485
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1", children: [
4255
5486
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-sm font-semibold text-foreground", children: "Response Detail" }),
4256
5487
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-xs text-muted-foreground", children: [
4257
5488
  "Submitted ",
4258
5489
  new Date(response.submittedAt).toLocaleString(),
4259
5490
  response.completionTimeMs != null && ` \u2014 ${Math.round(response.completionTimeMs / 1e3)}s`
4260
5491
  ] })
4261
- ] })
5492
+ ] }),
5493
+ onDelete && /* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.Button, { variant: "destructive", size: "sm", onClick: onDelete, children: "Delete" })
4262
5494
  ] }),
4263
5495
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-col gap-4", children: fields.map((field) => /* @__PURE__ */ jsxRuntime.jsxs(
4264
5496
  "div",
@@ -4611,6 +5843,491 @@ function formatFallbackValue(value) {
4611
5843
  if (typeof value === "object") return JSON.stringify(value, null, 2);
4612
5844
  return String(value);
4613
5845
  }
5846
+ function TimelineView({ responses, questions, onSelect }) {
5847
+ const grouped = react.useMemo(() => groupByDay(responses), [responses]);
5848
+ if (responses.length === 0) {
5849
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-8 text-center text-muted-foreground text-sm", children: "No responses to display." });
5850
+ }
5851
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-4 space-y-6", children: grouped.map((group) => /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
5852
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3 mb-3", children: [
5853
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-px flex-1 bg-border" }),
5854
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs font-medium text-muted-foreground shrink-0", children: group.dateLabel }),
5855
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-xs text-muted-foreground shrink-0", children: [
5856
+ "(",
5857
+ group.responses.length,
5858
+ ")"
5859
+ ] }),
5860
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-px flex-1 bg-border" })
5861
+ ] }),
5862
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "space-y-2 ml-4 border-l-2 border-border/50 pl-4", children: group.responses.map((response, idx) => /* @__PURE__ */ jsxRuntime.jsx(
5863
+ TimelineEntry,
5864
+ {
5865
+ response,
5866
+ questions,
5867
+ onView: () => onSelect(response)
5868
+ },
5869
+ response.sessionToken || idx
5870
+ )) })
5871
+ ] }, group.dateLabel)) });
5872
+ }
5873
+ function TimelineEntry({
5874
+ response,
5875
+ questions,
5876
+ onView
5877
+ }) {
5878
+ const time = new Date(response.submittedAt).toLocaleTimeString([], {
5879
+ hour: "2-digit",
5880
+ minute: "2-digit"
5881
+ });
5882
+ const preview = [];
5883
+ for (const q of questions) {
5884
+ if (preview.length >= 3) break;
5885
+ const val = response.values[q.id];
5886
+ if (val != null && val !== "") {
5887
+ preview.push({ questionId: q.id, label: q.label, type: q.type, value: val });
5888
+ }
5889
+ }
5890
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative flex items-start gap-3 group", children: [
5891
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute -left-[21px] top-2.5 w-2 h-2 rounded-full bg-border group-hover:bg-primary transition-colors" }),
5892
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 rounded border border-border bg-card p-3 hover:border-primary/40 transition-colors", children: [
5893
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between mb-2", children: [
5894
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs font-medium text-foreground", children: time }),
5895
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
5896
+ response.totalScore != null && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-xs text-muted-foreground", children: [
5897
+ "Score: ",
5898
+ response.totalScore
5899
+ ] }),
5900
+ /* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.Button, { variant: "ghost", size: "sm", className: "h-6 text-xs", onClick: onView, children: "View" })
5901
+ ] })
5902
+ ] }),
5903
+ preview.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-wrap gap-x-4 gap-y-1", children: preview.map((field) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-xs", children: [
5904
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-muted-foreground", children: [
5905
+ field.label,
5906
+ ": "
5907
+ ] }),
5908
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-foreground", children: formatPreviewValue(field.value, field.type) })
5909
+ ] }, field.questionId)) })
5910
+ ] })
5911
+ ] });
5912
+ }
5913
+ function groupByDay(responses) {
5914
+ const sorted = [...responses].sort(
5915
+ (a, b) => new Date(b.submittedAt).getTime() - new Date(a.submittedAt).getTime()
5916
+ );
5917
+ const groups = /* @__PURE__ */ new Map();
5918
+ for (const r of sorted) {
5919
+ const date = new Date(r.submittedAt).toLocaleDateString(void 0, {
5920
+ weekday: "long",
5921
+ year: "numeric",
5922
+ month: "long",
5923
+ day: "numeric"
5924
+ });
5925
+ const existing = groups.get(date);
5926
+ if (existing) {
5927
+ existing.push(r);
5928
+ } else {
5929
+ groups.set(date, [r]);
5930
+ }
5931
+ }
5932
+ return Array.from(groups.entries()).map(([dateLabel, responses2]) => ({
5933
+ dateLabel,
5934
+ responses: responses2
5935
+ }));
5936
+ }
5937
+ function formatPreviewValue(value, type) {
5938
+ if (value == null) return "\u2014";
5939
+ if (typeof value === "boolean") return value ? "Yes" : "No";
5940
+ if (Array.isArray(value)) return value.length > 0 ? value.slice(0, 2).join(", ") + (value.length > 2 ? "..." : "") : "\u2014";
5941
+ if (typeof value === "object") {
5942
+ if (type === "legal_name") {
5943
+ const n = value;
5944
+ return [n.first, n.last].filter(Boolean).join(" ") || "\u2014";
5945
+ }
5946
+ return "...";
5947
+ }
5948
+ const str = String(value);
5949
+ return str.length > 40 ? str.slice(0, 40) + "..." : str;
5950
+ }
5951
+
5952
+ // src/response-viewer/stats-utils.ts
5953
+ function computeStats(responses, schema) {
5954
+ if (responses.length === 0) {
5955
+ return {
5956
+ totalCount: 0,
5957
+ completionRate: 0,
5958
+ avgCompletionTimeMs: null,
5959
+ dateRange: null,
5960
+ fieldSummaries: []
5961
+ };
5962
+ }
5963
+ const questions = getStatsQuestions(schema);
5964
+ const requiredIds = new Set(questions.filter((q) => q.required).map((q) => q.id));
5965
+ let completedCount = 0;
5966
+ for (const r of responses) {
5967
+ let allFilled = true;
5968
+ for (const id of requiredIds) {
5969
+ if (r.values[id] == null || r.values[id] === "") {
5970
+ allFilled = false;
5971
+ break;
5972
+ }
5973
+ }
5974
+ if (allFilled) completedCount++;
5975
+ }
5976
+ const completionRate = requiredIds.size > 0 ? completedCount / responses.length : 1;
5977
+ const completionTimes = responses.map((r) => r.completionTimeMs).filter((t) => t != null && t > 0);
5978
+ const avgCompletionTimeMs = completionTimes.length > 0 ? completionTimes.reduce((a, b) => a + b, 0) / completionTimes.length : null;
5979
+ const timestamps = responses.map((r) => new Date(r.submittedAt).getTime());
5980
+ const earliest = new Date(Math.min(...timestamps)).toISOString();
5981
+ const latest = new Date(Math.max(...timestamps)).toISOString();
5982
+ const fieldSummaries = questions.map((q) => computeFieldSummary(q, responses));
5983
+ return {
5984
+ totalCount: responses.length,
5985
+ completionRate,
5986
+ avgCompletionTimeMs,
5987
+ dateRange: { earliest, latest },
5988
+ fieldSummaries
5989
+ };
5990
+ }
5991
+ function computeFieldSummary(question, responses) {
5992
+ const values = responses.map((r) => r.values[question.id]).filter((v) => v != null && v !== "");
5993
+ const filledCount = values.length;
5994
+ const filledRate = responses.length > 0 ? filledCount / responses.length : 0;
5995
+ const base = {
5996
+ fieldId: question.id,
5997
+ label: question.label,
5998
+ type: question.type,
5999
+ filledCount,
6000
+ filledRate
6001
+ };
6002
+ if (isNumericType(question.type)) {
6003
+ const nums = values.map((v) => typeof v === "number" ? v : Number(v)).filter((n) => !isNaN(n));
6004
+ if (nums.length > 0) {
6005
+ return {
6006
+ ...base,
6007
+ kind: "numeric",
6008
+ mean: nums.reduce((a, b) => a + b, 0) / nums.length,
6009
+ min: Math.min(...nums),
6010
+ max: Math.max(...nums)
6011
+ };
6012
+ }
6013
+ return { ...base, kind: "other" };
6014
+ }
6015
+ if (isCategoricalType(question.type)) {
6016
+ const counts = /* @__PURE__ */ new Map();
6017
+ for (const v of values) {
6018
+ const key = String(v);
6019
+ counts.set(key, (counts.get(key) ?? 0) + 1);
6020
+ }
6021
+ let mode = "";
6022
+ let modeCount = 0;
6023
+ for (const [key, count] of counts) {
6024
+ if (count > modeCount) {
6025
+ mode = key;
6026
+ modeCount = count;
6027
+ }
6028
+ }
6029
+ return {
6030
+ ...base,
6031
+ kind: "categorical",
6032
+ mode,
6033
+ modeCount,
6034
+ uniqueCount: counts.size
6035
+ };
6036
+ }
6037
+ return { ...base, kind: "other" };
6038
+ }
6039
+ function isNumericType(type) {
6040
+ return [
6041
+ "number",
6042
+ "slider",
6043
+ "rating",
6044
+ "nps",
6045
+ "opinion_scale",
6046
+ "pain_scale"
6047
+ ].includes(type);
6048
+ }
6049
+ function isCategoricalType(type) {
6050
+ return [
6051
+ "single_select",
6052
+ "dropdown",
6053
+ "country_select",
6054
+ "boolean",
6055
+ "consent",
6056
+ "multi_select",
6057
+ "checkbox_group"
6058
+ ].includes(type);
6059
+ }
6060
+ function getStatsQuestions(schema) {
6061
+ const questions = [];
6062
+ for (const section of schema.sections) {
6063
+ for (const q of section.questions) {
6064
+ if (q.type === "info-block" || q.type === "section-header" || q.type === "page-break") {
6065
+ continue;
6066
+ }
6067
+ questions.push(q);
6068
+ }
6069
+ }
6070
+ return questions;
6071
+ }
6072
+ function StatsPanel({ schema, responses, onClose }) {
6073
+ const stats = react.useMemo(() => computeStats(responses, schema), [responses, schema]);
6074
+ if (stats.totalCount === 0) {
6075
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-4 py-3 text-sm text-muted-foreground", children: "No responses to compute statistics." });
6076
+ }
6077
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "border-b border-border bg-muted/30", children: [
6078
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between px-4 py-2 border-b border-border/50", children: [
6079
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs font-medium text-foreground", children: "Statistics" }),
6080
+ /* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.Button, { variant: "ghost", size: "sm", className: "h-6 text-xs", onClick: onClose, children: "Close" })
6081
+ ] }),
6082
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-4 py-3", children: [
6083
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-2 sm:grid-cols-4 gap-3", children: [
6084
+ /* @__PURE__ */ jsxRuntime.jsx(
6085
+ StatCard,
6086
+ {
6087
+ label: "Total Responses",
6088
+ value: String(stats.totalCount)
6089
+ }
6090
+ ),
6091
+ /* @__PURE__ */ jsxRuntime.jsx(
6092
+ StatCard,
6093
+ {
6094
+ label: "Completion Rate",
6095
+ value: `${Math.round(stats.completionRate * 100)}%`
6096
+ }
6097
+ ),
6098
+ /* @__PURE__ */ jsxRuntime.jsx(
6099
+ StatCard,
6100
+ {
6101
+ label: "Avg. Completion Time",
6102
+ value: formatTime(stats.avgCompletionTimeMs)
6103
+ }
6104
+ ),
6105
+ /* @__PURE__ */ jsxRuntime.jsx(
6106
+ StatCard,
6107
+ {
6108
+ label: "Date Range",
6109
+ value: formatDateRange(stats.dateRange)
6110
+ }
6111
+ )
6112
+ ] }),
6113
+ stats.fieldSummaries.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-3 grid grid-cols-2 sm:grid-cols-3 gap-2", children: stats.fieldSummaries.slice(0, 6).map((f) => /* @__PURE__ */ jsxRuntime.jsxs(
6114
+ "div",
6115
+ {
6116
+ className: "px-2.5 py-2 rounded border border-border bg-background text-xs",
6117
+ children: [
6118
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-muted-foreground truncate mb-1", children: f.label }),
6119
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-baseline gap-2", children: [
6120
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-foreground font-medium", children: [
6121
+ Math.round(f.filledRate * 100),
6122
+ "% filled"
6123
+ ] }),
6124
+ f.kind === "numeric" && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-muted-foreground", children: [
6125
+ "avg ",
6126
+ f.mean.toFixed(1),
6127
+ " (",
6128
+ f.min,
6129
+ "\u2013",
6130
+ f.max,
6131
+ ")"
6132
+ ] }),
6133
+ f.kind === "categorical" && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-muted-foreground truncate", children: [
6134
+ "top: ",
6135
+ f.mode,
6136
+ " (",
6137
+ f.modeCount,
6138
+ ")"
6139
+ ] })
6140
+ ] })
6141
+ ]
6142
+ },
6143
+ f.fieldId
6144
+ )) })
6145
+ ] })
6146
+ ] });
6147
+ }
6148
+ function StatCard({ label, value }) {
6149
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-3 py-2 rounded border border-border bg-background", children: [
6150
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-[11px] text-muted-foreground mb-0.5", children: label }),
6151
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-sm font-semibold text-foreground", children: value })
6152
+ ] });
6153
+ }
6154
+ function formatTime(ms) {
6155
+ if (ms == null) return "\u2014";
6156
+ const seconds = Math.round(ms / 1e3);
6157
+ if (seconds < 60) return `${seconds}s`;
6158
+ const minutes = Math.floor(seconds / 60);
6159
+ const remainingSeconds = seconds % 60;
6160
+ return `${minutes}m ${remainingSeconds}s`;
6161
+ }
6162
+ function formatDateRange(range) {
6163
+ if (!range) return "\u2014";
6164
+ const fmt = (iso) => new Date(iso).toLocaleDateString();
6165
+ return `${fmt(range.earliest)} \u2013 ${fmt(range.latest)}`;
6166
+ }
6167
+
6168
+ // src/response-viewer/chart-utils.ts
6169
+ function computeFieldDistribution(responses, fieldId) {
6170
+ const counts = /* @__PURE__ */ new Map();
6171
+ let total = 0;
6172
+ for (const r of responses) {
6173
+ const raw = r.values[fieldId];
6174
+ if (raw == null || raw === "") continue;
6175
+ total++;
6176
+ if (typeof raw === "boolean") {
6177
+ const key = raw ? "Yes" : "No";
6178
+ counts.set(key, (counts.get(key) ?? 0) + 1);
6179
+ } else if (Array.isArray(raw)) {
6180
+ for (const item of raw) {
6181
+ const key = String(item);
6182
+ counts.set(key, (counts.get(key) ?? 0) + 1);
6183
+ }
6184
+ } else {
6185
+ const key = String(raw);
6186
+ counts.set(key, (counts.get(key) ?? 0) + 1);
6187
+ }
6188
+ }
6189
+ if (total === 0) return [];
6190
+ const entries = [];
6191
+ for (const [value, count] of counts) {
6192
+ entries.push({
6193
+ value,
6194
+ count,
6195
+ percentage: count / total * 100
6196
+ });
6197
+ }
6198
+ entries.sort((a, b) => b.count - a.count);
6199
+ return entries;
6200
+ }
6201
+ function getChartableFieldIds(schema) {
6202
+ const chartable = [];
6203
+ const chartTypes = /* @__PURE__ */ new Set([
6204
+ "single_select",
6205
+ "dropdown",
6206
+ "country_select",
6207
+ "boolean",
6208
+ "consent",
6209
+ "multi_select",
6210
+ "checkbox_group",
6211
+ "rating",
6212
+ "nps",
6213
+ "opinion_scale",
6214
+ "pain_scale"
6215
+ ]);
6216
+ for (const section of schema.sections) {
6217
+ for (const q of section.questions) {
6218
+ if (chartTypes.has(q.type)) {
6219
+ chartable.push({ id: q.id, label: q.label, type: q.type });
6220
+ }
6221
+ }
6222
+ }
6223
+ return chartable;
6224
+ }
6225
+ function ChartPanel({ schema, responses, onClose }) {
6226
+ const chartableFields = react.useMemo(() => getChartableFieldIds(schema), [schema]);
6227
+ const [selectedFieldId, setSelectedFieldId] = react.useState(chartableFields[0]?.id ?? "");
6228
+ const distribution = react.useMemo(() => {
6229
+ if (!selectedFieldId) return [];
6230
+ return computeFieldDistribution(responses, selectedFieldId);
6231
+ }, [responses, selectedFieldId]);
6232
+ const selectedField = chartableFields.find((f) => f.id === selectedFieldId);
6233
+ const maxCount = distribution.length > 0 ? Math.max(...distribution.map((d) => d.count)) : 1;
6234
+ if (chartableFields.length === 0) {
6235
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-4 py-6 border-b border-border bg-muted/30 text-center", children: [
6236
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-muted-foreground", children: "No chartable fields found in this schema." }),
6237
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-muted-foreground mt-1", children: "Charts work with select, dropdown, boolean, rating, and similar field types." })
6238
+ ] });
6239
+ }
6240
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "border-b border-border bg-muted/30", children: [
6241
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between px-4 py-2 border-b border-border/50", children: [
6242
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3", children: [
6243
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs font-medium text-foreground", children: "Field Distribution" }),
6244
+ /* @__PURE__ */ jsxRuntime.jsx(
6245
+ "select",
6246
+ {
6247
+ value: selectedFieldId,
6248
+ onChange: (e) => setSelectedFieldId(e.target.value),
6249
+ className: "text-xs bg-background border border-border rounded px-2 py-1 text-foreground",
6250
+ children: chartableFields.map((f) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: f.id, children: f.label }, f.id))
6251
+ }
6252
+ )
6253
+ ] }),
6254
+ /* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.Button, { variant: "ghost", size: "sm", className: "h-6 text-xs", onClick: onClose, children: "Close" })
6255
+ ] }),
6256
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-4 py-3", children: [
6257
+ distribution.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-muted-foreground text-center py-4", children: "No data for this field." }) : /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-1.5", children: [
6258
+ distribution.slice(0, 10).map((entry) => /* @__PURE__ */ jsxRuntime.jsx(BarRow, { entry, maxCount }, entry.value)),
6259
+ distribution.length > 10 && /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-xs text-muted-foreground mt-2", children: [
6260
+ "+",
6261
+ distribution.length - 10,
6262
+ " more values"
6263
+ ] })
6264
+ ] }),
6265
+ selectedField && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-3 text-[11px] text-muted-foreground", children: [
6266
+ responses.length,
6267
+ " response",
6268
+ responses.length !== 1 ? "s" : "",
6269
+ " \xB7 ",
6270
+ distribution.length,
6271
+ " unique value",
6272
+ distribution.length !== 1 ? "s" : ""
6273
+ ] })
6274
+ ] })
6275
+ ] });
6276
+ }
6277
+ function BarRow({ entry, maxCount }) {
6278
+ const widthPct = maxCount > 0 ? entry.count / maxCount * 100 : 0;
6279
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
6280
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-24 shrink-0 text-xs text-foreground truncate", title: entry.value, children: entry.value }),
6281
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1 h-5 bg-background rounded overflow-hidden border border-border/50", children: /* @__PURE__ */ jsxRuntime.jsx(
6282
+ "div",
6283
+ {
6284
+ className: "h-full bg-primary/60 rounded transition-all",
6285
+ style: { width: `${widthPct}%` }
6286
+ }
6287
+ ) }),
6288
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "w-16 shrink-0 text-xs text-muted-foreground text-right", children: [
6289
+ entry.count,
6290
+ " (",
6291
+ Math.round(entry.percentage),
6292
+ "%)"
6293
+ ] })
6294
+ ] });
6295
+ }
6296
+ function ConfirmDialog2({
6297
+ title,
6298
+ message,
6299
+ confirmLabel = "Confirm",
6300
+ cancelLabel = "Cancel",
6301
+ variant = "default",
6302
+ onConfirm,
6303
+ onCancel
6304
+ }) {
6305
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fixed inset-0 z-50 flex items-center justify-center", children: [
6306
+ /* @__PURE__ */ jsxRuntime.jsx(
6307
+ "div",
6308
+ {
6309
+ className: "absolute inset-0 bg-black/50",
6310
+ onClick: onCancel
6311
+ }
6312
+ ),
6313
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative bg-background border border-border rounded-lg shadow-lg p-6 max-w-sm w-full mx-4", children: [
6314
+ /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-sm font-semibold text-foreground mb-2", children: title }),
6315
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-muted-foreground mb-4", children: message }),
6316
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex justify-end gap-2", children: [
6317
+ /* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.Button, { variant: "outline", size: "sm", onClick: onCancel, children: cancelLabel }),
6318
+ /* @__PURE__ */ jsxRuntime.jsx(
6319
+ fieldcraftReact.Button,
6320
+ {
6321
+ variant: variant === "destructive" ? "destructive" : "default",
6322
+ size: "sm",
6323
+ onClick: onConfirm,
6324
+ children: confirmLabel
6325
+ }
6326
+ )
6327
+ ] })
6328
+ ] })
6329
+ ] });
6330
+ }
4614
6331
 
4615
6332
  // src/response-viewer/export-utils.ts
4616
6333
  function buildCsvContent(schema, responses, options = {}) {
@@ -4745,6 +6462,84 @@ function paginate(items, currentPage, pageSize) {
4745
6462
  };
4746
6463
  }
4747
6464
 
6465
+ // src/response-viewer/search-utils.ts
6466
+ function searchResponses(responses, query, schema) {
6467
+ if (!query.trim()) return responses;
6468
+ const lowerQuery = query.toLowerCase().trim();
6469
+ const questions = getSearchableQuestions(schema);
6470
+ return responses.filter((response) => {
6471
+ if (new Date(response.submittedAt).toLocaleString().toLowerCase().includes(lowerQuery)) {
6472
+ return true;
6473
+ }
6474
+ if (response.sessionToken?.toLowerCase().includes(lowerQuery)) {
6475
+ return true;
6476
+ }
6477
+ for (const q of questions) {
6478
+ const value = response.values[q.id];
6479
+ if (value == null) continue;
6480
+ const stringified = stringifyValue(value, q.type);
6481
+ if (stringified.toLowerCase().includes(lowerQuery)) {
6482
+ return true;
6483
+ }
6484
+ }
6485
+ if (response.totalScore != null && String(response.totalScore).includes(lowerQuery)) {
6486
+ return true;
6487
+ }
6488
+ return false;
6489
+ });
6490
+ }
6491
+ function getSearchableQuestions(schema) {
6492
+ const questions = [];
6493
+ for (const section of schema.sections) {
6494
+ for (const q of section.questions) {
6495
+ if (q.type === "info-block" || q.type === "section-header" || q.type === "page-break") {
6496
+ continue;
6497
+ }
6498
+ questions.push(q);
6499
+ }
6500
+ }
6501
+ return questions;
6502
+ }
6503
+ function stringifyValue(value, type) {
6504
+ if (value == null) return "";
6505
+ switch (type) {
6506
+ case "vitals_entry": {
6507
+ if (typeof value !== "object" || value === null) break;
6508
+ const v = value;
6509
+ const parts = [];
6510
+ if (v.systolicBp && v.diastolicBp) parts.push(`BP ${v.systolicBp}/${v.diastolicBp}`);
6511
+ if (v.heartRate) parts.push(`HR ${v.heartRate}`);
6512
+ if (v.temperature) parts.push(`${v.temperature}`);
6513
+ if (v.oxygenSaturation) parts.push(`SpO2 ${v.oxygenSaturation}`);
6514
+ return parts.join(" ");
6515
+ }
6516
+ case "medication_list":
6517
+ if (Array.isArray(value)) {
6518
+ return value.map((m) => [m.name, m.dosage, m.frequency].filter(Boolean).join(" ")).join(" ");
6519
+ }
6520
+ break;
6521
+ case "allergy_list":
6522
+ if (Array.isArray(value)) {
6523
+ return value.map((a) => [a.allergen, a.severity].filter(Boolean).join(" ")).join(" ");
6524
+ }
6525
+ break;
6526
+ case "legal_name": {
6527
+ if (typeof value !== "object" || value === null) break;
6528
+ const n = value;
6529
+ return [n.first, n.middle, n.last].filter(Boolean).join(" ");
6530
+ }
6531
+ case "address": {
6532
+ if (typeof value !== "object" || value === null) break;
6533
+ const a = value;
6534
+ return [a.street, a.city, a.state, a.zip].filter(Boolean).join(" ");
6535
+ }
6536
+ }
6537
+ if (typeof value === "boolean") return value ? "Yes" : "No";
6538
+ if (Array.isArray(value)) return value.map(String).join(" ");
6539
+ if (typeof value === "object") return JSON.stringify(value);
6540
+ return String(value);
6541
+ }
6542
+
4748
6543
  // src/response-viewer/filter-utils.ts
4749
6544
  function createEmptyFilterState() {
4750
6545
  return { filters: [], dateRange: {} };
@@ -4843,7 +6638,11 @@ function ResponseViewerInner({
4843
6638
  dateFormat,
4844
6639
  columnLabels,
4845
6640
  onExport,
4846
- pageSize = 25
6641
+ pageSize = 25,
6642
+ onDelete,
6643
+ onBulkDelete,
6644
+ onBulkExport,
6645
+ selectable = false
4847
6646
  }) {
4848
6647
  const [viewMode, setViewMode] = react.useState("table");
4849
6648
  const [selectedResponse, setSelectedResponse] = react.useState(null);
@@ -4851,6 +6650,11 @@ function ResponseViewerInner({
4851
6650
  const [effectivePageSize, setEffectivePageSize] = react.useState(pageSize);
4852
6651
  const [filterState, setFilterState] = react.useState(createEmptyFilterState);
4853
6652
  const [showFilterPanel, setShowFilterPanel] = react.useState(false);
6653
+ const [searchQuery, setSearchQuery] = react.useState("");
6654
+ const [showStats, setShowStats] = react.useState(false);
6655
+ const [showCharts, setShowCharts] = react.useState(false);
6656
+ const [selectedIds, setSelectedIds] = react.useState(/* @__PURE__ */ new Set());
6657
+ const [confirmDialog, setConfirmDialog] = react.useState(null);
4854
6658
  const [draftFieldId, setDraftFieldId] = react.useState("");
4855
6659
  const [draftOperator, setDraftOperator] = react.useState("contains");
4856
6660
  const [draftValue, setDraftValue] = react.useState("");
@@ -4861,9 +6665,18 @@ function ResponseViewerInner({
4861
6665
  setEffectivePageSize(pageSize);
4862
6666
  setCurrentPage(1);
4863
6667
  }, [pageSize]);
6668
+ react.useEffect(() => {
6669
+ setSelectedIds(/* @__PURE__ */ new Set());
6670
+ }, [responses]);
6671
+ const searchedResponses = react.useMemo(
6672
+ () => searchResponses(responses, searchQuery, schema),
6673
+ [responses, searchQuery, schema]
6674
+ );
4864
6675
  const isFiltered = hasActiveFilters(filterState);
4865
- const filteredResponses = applyFilters(responses, filterState);
6676
+ const filteredResponses = applyFilters(searchedResponses, filterState);
4866
6677
  const pag = paginate(filteredResponses, currentPage, effectivePageSize);
6678
+ const questions = getAllQuestions2(schema);
6679
+ const isSelectable = selectable && (!!onBulkDelete || !!onBulkExport);
4867
6680
  function handleSelect(response) {
4868
6681
  setSelectedResponse(response);
4869
6682
  onResponseSelect?.(response);
@@ -4910,7 +6723,59 @@ function ResponseViewerInner({
4910
6723
  setFilterState(createEmptyFilterState());
4911
6724
  setCurrentPage(1);
4912
6725
  }
4913
- const questions = getAllQuestions2(schema);
6726
+ function handleToggleSelect(token) {
6727
+ setSelectedIds((prev) => {
6728
+ const next = new Set(prev);
6729
+ if (next.has(token)) {
6730
+ next.delete(token);
6731
+ } else {
6732
+ next.add(token);
6733
+ }
6734
+ return next;
6735
+ });
6736
+ }
6737
+ function handleSelectAll() {
6738
+ const currentTokens = pag.pageItems.map((r) => r.sessionToken).filter(Boolean);
6739
+ const allSelected = currentTokens.every((t) => selectedIds.has(t));
6740
+ setSelectedIds((prev) => {
6741
+ const next = new Set(prev);
6742
+ if (allSelected) {
6743
+ currentTokens.forEach((t) => next.delete(t));
6744
+ } else {
6745
+ currentTokens.forEach((t) => next.add(t));
6746
+ }
6747
+ return next;
6748
+ });
6749
+ }
6750
+ function handleBulkDelete() {
6751
+ if (!onBulkDelete || selectedIds.size === 0) return;
6752
+ setConfirmDialog({
6753
+ title: "Delete selected responses",
6754
+ message: `Are you sure you want to delete ${selectedIds.size} response${selectedIds.size !== 1 ? "s" : ""}? This cannot be undone.`,
6755
+ onConfirm: () => {
6756
+ onBulkDelete(Array.from(selectedIds));
6757
+ setSelectedIds(/* @__PURE__ */ new Set());
6758
+ setConfirmDialog(null);
6759
+ }
6760
+ });
6761
+ }
6762
+ function handleBulkExport() {
6763
+ if (!onBulkExport || selectedIds.size === 0) return;
6764
+ const selected = responses.filter((r) => r.sessionToken && selectedIds.has(r.sessionToken));
6765
+ onBulkExport(selected);
6766
+ }
6767
+ function handleDeleteSingle(token) {
6768
+ if (!onDelete) return;
6769
+ setConfirmDialog({
6770
+ title: "Delete response",
6771
+ message: "Are you sure you want to delete this response? This cannot be undone.",
6772
+ onConfirm: () => {
6773
+ onDelete(token);
6774
+ setSelectedResponse(null);
6775
+ setConfirmDialog(null);
6776
+ }
6777
+ });
6778
+ }
4914
6779
  function getFields(response) {
4915
6780
  return questions.map((q) => ({
4916
6781
  questionId: q.id,
@@ -4921,6 +6786,19 @@ function ResponseViewerInner({
4921
6786
  }
4922
6787
  const heightValue = typeof height === "number" ? `${height}px` : height;
4923
6788
  const widthValue = typeof width === "number" ? `${width}px` : width;
6789
+ const countText = (() => {
6790
+ const hasSearch = searchQuery.trim().length > 0;
6791
+ if (hasSearch && isFiltered) {
6792
+ return `${filteredResponses.length} of ${responses.length} responses (searched + filtered)`;
6793
+ }
6794
+ if (hasSearch) {
6795
+ return `${searchedResponses.length} result${searchedResponses.length !== 1 ? "s" : ""} for "${searchQuery}"`;
6796
+ }
6797
+ if (isFiltered) {
6798
+ return `Showing ${filteredResponses.length} of ${responses.length} responses (filtered)`;
6799
+ }
6800
+ return `${responses.length} response${responses.length !== 1 ? "s" : ""}`;
6801
+ })();
4924
6802
  return /* @__PURE__ */ jsxRuntime.jsxs(
4925
6803
  "div",
4926
6804
  {
@@ -4928,7 +6806,20 @@ function ResponseViewerInner({
4928
6806
  style: { height: heightValue, width: widthValue },
4929
6807
  children: [
4930
6808
  !selectedResponse && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-center gap-x-2 gap-y-1 px-3 py-2 border-b border-border", children: [
4931
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-xs text-muted-foreground mr-auto", children: isFiltered ? `Showing ${filteredResponses.length} of ${responses.length} responses (filtered)` : `${responses.length} response${responses.length !== 1 ? "s" : ""}` }),
6809
+ /* @__PURE__ */ jsxRuntime.jsx(
6810
+ fieldcraftReact.Input,
6811
+ {
6812
+ type: "text",
6813
+ placeholder: "Search responses...",
6814
+ className: "h-7 text-xs w-40",
6815
+ value: searchQuery,
6816
+ onChange: (e) => {
6817
+ setSearchQuery(e.target.value);
6818
+ setCurrentPage(1);
6819
+ }
6820
+ }
6821
+ ),
6822
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-xs text-muted-foreground mr-auto", children: countText }),
4932
6823
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1", children: [
4933
6824
  /* @__PURE__ */ jsxRuntime.jsx(
4934
6825
  fieldcraftReact.Button,
@@ -4955,6 +6846,48 @@ function ResponseViewerInner({
4955
6846
  onClick: () => setViewMode("card"),
4956
6847
  children: "Cards"
4957
6848
  }
6849
+ ),
6850
+ /* @__PURE__ */ jsxRuntime.jsx(
6851
+ fieldcraftReact.Button,
6852
+ {
6853
+ variant: viewMode === "timeline" ? "secondary" : "ghost",
6854
+ size: "sm",
6855
+ className: cn(
6856
+ "h-7 text-xs",
6857
+ viewMode === "timeline" && "font-semibold border border-ring"
6858
+ ),
6859
+ onClick: () => setViewMode("timeline"),
6860
+ children: "Timeline"
6861
+ }
6862
+ )
6863
+ ] }),
6864
+ /* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.Separator, { orientation: "vertical", className: "h-5 hidden sm:block" }),
6865
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1", children: [
6866
+ /* @__PURE__ */ jsxRuntime.jsx(
6867
+ fieldcraftReact.Button,
6868
+ {
6869
+ variant: showStats ? "secondary" : "ghost",
6870
+ size: "sm",
6871
+ className: cn(
6872
+ "h-7 text-xs",
6873
+ showStats && "font-semibold border border-ring"
6874
+ ),
6875
+ onClick: () => setShowStats((v) => !v),
6876
+ children: "Stats"
6877
+ }
6878
+ ),
6879
+ /* @__PURE__ */ jsxRuntime.jsx(
6880
+ fieldcraftReact.Button,
6881
+ {
6882
+ variant: showCharts ? "secondary" : "ghost",
6883
+ size: "sm",
6884
+ className: cn(
6885
+ "h-7 text-xs",
6886
+ showCharts && "font-semibold border border-ring"
6887
+ ),
6888
+ onClick: () => setShowCharts((v) => !v),
6889
+ children: "Charts"
6890
+ }
4958
6891
  )
4959
6892
  ] }),
4960
6893
  /* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.Separator, { orientation: "vertical", className: "h-5 hidden sm:block" }),
@@ -5020,6 +6953,26 @@ function ResponseViewerInner({
5020
6953
  )
5021
6954
  ] })
5022
6955
  ] }),
6956
+ !selectedResponse && isSelectable && selectedIds.size > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 px-3 py-1.5 border-b border-border bg-accent/50", children: [
6957
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-xs font-medium text-foreground", children: [
6958
+ selectedIds.size,
6959
+ " selected"
6960
+ ] }),
6961
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "ml-auto flex items-center gap-1", children: [
6962
+ onBulkExport && /* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.Button, { variant: "outline", size: "sm", className: "h-6 text-xs", onClick: handleBulkExport, children: "Export Selected" }),
6963
+ onBulkDelete && /* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.Button, { variant: "destructive", size: "sm", className: "h-6 text-xs", onClick: handleBulkDelete, children: "Delete Selected" }),
6964
+ /* @__PURE__ */ jsxRuntime.jsx(
6965
+ fieldcraftReact.Button,
6966
+ {
6967
+ variant: "ghost",
6968
+ size: "sm",
6969
+ className: "h-6 text-xs",
6970
+ onClick: () => setSelectedIds(/* @__PURE__ */ new Set()),
6971
+ children: "Clear"
6972
+ }
6973
+ )
6974
+ ] })
6975
+ ] }),
5023
6976
  !selectedResponse && showFilterPanel && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fc-rv-filter__panel px-3 py-2 border-b border-border bg-muted/50", children: [
5024
6977
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-center gap-2 mb-2", children: [
5025
6978
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-muted-foreground w-16 shrink-0", children: "Date:" }),
@@ -5148,19 +7101,47 @@ function ResponseViewerInner({
5148
7101
  );
5149
7102
  })
5150
7103
  ] }),
5151
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: cn("flex-1 min-w-0 overflow-auto", selectedResponse && "p-4"), children: selectedResponse ? /* @__PURE__ */ jsxRuntime.jsx(
7104
+ !selectedResponse && showStats && /* @__PURE__ */ jsxRuntime.jsx(
7105
+ StatsPanel,
7106
+ {
7107
+ schema,
7108
+ responses: filteredResponses,
7109
+ onClose: () => setShowStats(false)
7110
+ }
7111
+ ),
7112
+ !selectedResponse && showCharts && /* @__PURE__ */ jsxRuntime.jsx(
7113
+ ChartPanel,
7114
+ {
7115
+ schema,
7116
+ responses: filteredResponses,
7117
+ onClose: () => setShowCharts(false)
7118
+ }
7119
+ ),
7120
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: cn("flex-1 min-w-0 overflow-auto", selectedResponse && "p-4"), children: selectedResponse ? /* @__PURE__ */ jsxRuntime.jsx("div", { children: /* @__PURE__ */ jsxRuntime.jsx(
5152
7121
  ResponseDetail,
5153
7122
  {
5154
7123
  response: selectedResponse,
5155
7124
  fields: getFields(selectedResponse),
5156
- onBack: handleBack
7125
+ onBack: handleBack,
7126
+ onDelete: onDelete && selectedResponse.sessionToken ? () => handleDeleteSingle(selectedResponse.sessionToken) : void 0
7127
+ }
7128
+ ) }) : viewMode === "timeline" ? /* @__PURE__ */ jsxRuntime.jsx(
7129
+ TimelineView,
7130
+ {
7131
+ responses: filteredResponses,
7132
+ questions,
7133
+ onSelect: handleSelect
5157
7134
  }
5158
7135
  ) : viewMode === "table" ? /* @__PURE__ */ jsxRuntime.jsx(
5159
7136
  ResponseTable,
5160
7137
  {
5161
7138
  schema,
5162
7139
  responses: pag.pageItems,
5163
- onRowClick: handleSelect
7140
+ onRowClick: handleSelect,
7141
+ selectable: isSelectable,
7142
+ selectedIds,
7143
+ onToggleSelect: handleToggleSelect,
7144
+ onSelectAll: handleSelectAll
5164
7145
  }
5165
7146
  ) : /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid gap-3 p-3 grid-cols-[repeat(auto-fill,minmax(280px,1fr))]", children: [
5166
7147
  pag.pageItems.map((response, idx) => /* @__PURE__ */ jsxRuntime.jsx(
@@ -5168,13 +7149,16 @@ function ResponseViewerInner({
5168
7149
  {
5169
7150
  response,
5170
7151
  fields: getFields(response),
5171
- onClick: () => handleSelect(response)
7152
+ onClick: () => handleSelect(response),
7153
+ selectable: isSelectable,
7154
+ selected: !!(response.sessionToken && selectedIds.has(response.sessionToken)),
7155
+ onToggleSelect: response.sessionToken ? () => handleToggleSelect(response.sessionToken) : void 0
5172
7156
  },
5173
7157
  response.sessionToken || idx
5174
7158
  )),
5175
- filteredResponses.length === 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-8 text-center text-muted-foreground", children: isFiltered ? "No matching responses" : "No responses yet" })
7159
+ filteredResponses.length === 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-8 text-center text-muted-foreground", children: isFiltered || searchQuery.trim() ? "No matching responses" : "No responses yet" })
5176
7160
  ] }) }),
5177
- !selectedResponse && pag.showPagination && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fc-rv-pagination flex flex-wrap items-center gap-x-2 gap-y-1 px-3 py-2 border-t border-border", children: [
7161
+ !selectedResponse && viewMode !== "timeline" && pag.showPagination && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fc-rv-pagination flex flex-wrap items-center gap-x-2 gap-y-1 px-3 py-2 border-t border-border", children: [
5178
7162
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fc-rv-pagination__info text-xs text-muted-foreground mr-auto", children: [
5179
7163
  "Showing ",
5180
7164
  pag.startItem,
@@ -5228,7 +7212,18 @@ function ResponseViewerInner({
5228
7212
  }
5229
7213
  )
5230
7214
  ] })
5231
- ] })
7215
+ ] }),
7216
+ confirmDialog && /* @__PURE__ */ jsxRuntime.jsx(
7217
+ ConfirmDialog2,
7218
+ {
7219
+ title: confirmDialog.title,
7220
+ message: confirmDialog.message,
7221
+ confirmLabel: "Delete",
7222
+ variant: "destructive",
7223
+ onConfirm: confirmDialog.onConfirm,
7224
+ onCancel: () => setConfirmDialog(null)
7225
+ }
7226
+ )
5232
7227
  ]
5233
7228
  }
5234
7229
  );
@@ -5301,7 +7296,537 @@ var PREVIEW_SCHEMA = {
5301
7296
  ],
5302
7297
  submitAction: { type: "adapter" }
5303
7298
  };
7299
+
7300
+ // src/theme-editor/css-utils.ts
7301
+ var COLOR_MAP = {
7302
+ primary: "--fc-primary",
7303
+ primaryForeground: "--fc-primary-foreground",
7304
+ secondary: "--fc-secondary",
7305
+ secondaryForeground: "--fc-secondary-foreground",
7306
+ error: "--fc-error",
7307
+ errorForeground: "--fc-error-foreground",
7308
+ warning: "--fc-warning",
7309
+ success: "--fc-success",
7310
+ surface: "--fc-surface",
7311
+ background: "--fc-background",
7312
+ text: "--fc-text",
7313
+ textMuted: "--fc-text-muted",
7314
+ textDisabled: "--fc-text-disabled",
7315
+ border: "--fc-border",
7316
+ borderFocus: "--fc-border-focus",
7317
+ inputBackground: "--fc-input-background"
7318
+ };
7319
+ var TYPOGRAPHY_MAP = {
7320
+ fontFamily: "--fc-font-family",
7321
+ scale: "--fc-scale",
7322
+ questionSize: "--fc-question-size",
7323
+ labelSize: "--fc-label-size",
7324
+ helpTextSize: "--fc-help-text-size",
7325
+ bodySize: "--fc-body-size"
7326
+ };
7327
+ var SHAPE_MAP = {
7328
+ radius: "--fc-radius",
7329
+ inputRadius: "--fc-input-radius",
7330
+ buttonRadius: "--fc-button-radius",
7331
+ cardRadius: "--fc-card-radius"
7332
+ };
7333
+ var SPACING_MAP = {
7334
+ base: "--fc-spacing-base",
7335
+ sectionGap: "--fc-section-gap",
7336
+ fieldGap: "--fc-field-gap",
7337
+ inputPaddingX: "--fc-input-padding-x",
7338
+ inputPaddingY: "--fc-input-padding-y"
7339
+ };
7340
+ var LAYOUT_MAP = {
7341
+ maxWidth: "--fc-max-width",
7342
+ alignment: "--fc-alignment",
7343
+ progressPosition: "--fc-progress-position",
7344
+ sectionLayout: "--fc-section-layout"
7345
+ };
7346
+ function themeToCss(theme) {
7347
+ const lines = [];
7348
+ function addSection2(obj, map, comment) {
7349
+ if (!obj) return;
7350
+ const entries = [];
7351
+ for (const [key, cssVar] of Object.entries(map)) {
7352
+ const val = obj[key];
7353
+ if (val == null) continue;
7354
+ if (typeof val === "number") {
7355
+ entries.push(` ${cssVar}: ${val}px;`);
7356
+ } else {
7357
+ entries.push(` ${cssVar}: ${String(val)};`);
7358
+ }
7359
+ }
7360
+ if (entries.length > 0) {
7361
+ lines.push(` /* ${comment} */`);
7362
+ lines.push(...entries);
7363
+ lines.push("");
7364
+ }
7365
+ }
7366
+ lines.push(":root {");
7367
+ addSection2(theme.colors, COLOR_MAP, "Colors");
7368
+ addSection2(theme.typography, TYPOGRAPHY_MAP, "Typography");
7369
+ addSection2(theme.shape, SHAPE_MAP, "Shape");
7370
+ addSection2(theme.spacing, SPACING_MAP, "Spacing");
7371
+ addSection2(theme.layout, LAYOUT_MAP, "Layout");
7372
+ if (lines.length > 1 && lines[lines.length - 1] === "") {
7373
+ lines.pop();
7374
+ }
7375
+ lines.push("}");
7376
+ return lines.join("\n") + "\n";
7377
+ }
7378
+ var REVERSE_MAP = {};
7379
+ function buildReverseMap() {
7380
+ if (Object.keys(REVERSE_MAP).length > 0) return;
7381
+ const sections = [
7382
+ ["colors", COLOR_MAP],
7383
+ ["typography", TYPOGRAPHY_MAP],
7384
+ ["shape", SHAPE_MAP],
7385
+ ["spacing", SPACING_MAP],
7386
+ ["layout", LAYOUT_MAP]
7387
+ ];
7388
+ for (const [sectionKey, map] of sections) {
7389
+ for (const [propKey, cssVar] of Object.entries(map)) {
7390
+ REVERSE_MAP[cssVar] = [sectionKey, propKey];
7391
+ }
7392
+ }
7393
+ }
7394
+ var NUMERIC_PROPS = /* @__PURE__ */ new Set([
7395
+ "base",
7396
+ "sectionGap",
7397
+ "fieldGap",
7398
+ "inputPaddingX",
7399
+ "inputPaddingY"
7400
+ ]);
7401
+ function cssToTheme(css) {
7402
+ buildReverseMap();
7403
+ const theme = {};
7404
+ const varRegex = /(--fc-[\w-]+)\s*:\s*([^;]+);/g;
7405
+ let match;
7406
+ while ((match = varRegex.exec(css)) !== null) {
7407
+ const cssVar = match[1];
7408
+ let value = match[2].trim();
7409
+ const mapping = REVERSE_MAP[cssVar];
7410
+ if (!mapping) continue;
7411
+ const [sectionKey, propKey] = mapping;
7412
+ if (NUMERIC_PROPS.has(propKey)) {
7413
+ const num = parseFloat(value);
7414
+ if (!isNaN(num)) {
7415
+ value = num;
7416
+ }
7417
+ }
7418
+ if (!theme[sectionKey]) {
7419
+ theme[sectionKey] = {};
7420
+ }
7421
+ theme[sectionKey][propKey] = value;
7422
+ }
7423
+ return theme;
7424
+ }
7425
+ function exportCssFile(theme, filename = "fieldcraft-theme.css") {
7426
+ const css = themeToCss(theme);
7427
+ const blob = new Blob([css], { type: "text/css" });
7428
+ const url = URL.createObjectURL(blob);
7429
+ const a = document.createElement("a");
7430
+ a.href = url;
7431
+ a.download = filename;
7432
+ a.click();
7433
+ URL.revokeObjectURL(url);
7434
+ }
7435
+ function importCssFile(baseTheme, onImport) {
7436
+ const input = document.createElement("input");
7437
+ input.type = "file";
7438
+ input.accept = ".css";
7439
+ input.onchange = () => {
7440
+ const file = input.files?.[0];
7441
+ if (!file) return;
7442
+ const reader = new FileReader();
7443
+ reader.onload = () => {
7444
+ const cssText = reader.result;
7445
+ const partial = cssToTheme(cssText);
7446
+ const merged = deepMerge(baseTheme, partial);
7447
+ onImport(merged);
7448
+ };
7449
+ reader.readAsText(file);
7450
+ };
7451
+ input.click();
7452
+ }
7453
+ function deepMerge(base, partial) {
7454
+ const result = { ...base };
7455
+ for (const key of Object.keys(partial)) {
7456
+ const partialSection = partial[key];
7457
+ if (partialSection && typeof partialSection === "object") {
7458
+ result[key] = {
7459
+ ...base[key],
7460
+ ...partialSection
7461
+ };
7462
+ }
7463
+ }
7464
+ return result;
7465
+ }
7466
+
7467
+ // src/theme-editor/palette-generator.ts
7468
+ function generatePalette(baseHex) {
7469
+ const [h, s, l] = hexToHsl(baseHex);
7470
+ const secH = (h + 180) % 360;
7471
+ const isLightBase = l > 50;
7472
+ return {
7473
+ // Primary
7474
+ primary: hslToHex(h, s, clamp(l, 30, 60)),
7475
+ primaryForeground: isLightBase ? "#ffffff" : "#ffffff",
7476
+ // Secondary (complementary)
7477
+ secondary: hslToHex(secH, Math.max(s - 15, 10), clamp(l, 35, 55)),
7478
+ secondaryForeground: "#ffffff",
7479
+ // Surfaces
7480
+ surface: hslToHex(h, Math.max(s - 35, 3), 97),
7481
+ background: "#F4F7F8",
7482
+ inputBackground: hslToHex(h, Math.max(s - 40, 2), 99),
7483
+ // Text
7484
+ text: hslToHex(h, Math.max(s - 30, 5), 12),
7485
+ textMuted: hslToHex(h, Math.max(s - 30, 5), 45),
7486
+ textDisabled: hslToHex(h, Math.max(s - 35, 3), 65),
7487
+ // Borders
7488
+ border: hslToHex(h, Math.max(s - 30, 5), 85),
7489
+ borderFocus: hslToHex(h, s, clamp(l, 35, 55)),
7490
+ // Semantic — Drafting Teal palette
7491
+ error: "#B04A3C",
7492
+ errorForeground: "#FFFFFF",
7493
+ warning: "#C98A2E",
7494
+ success: "#2E7D5B"
7495
+ };
7496
+ }
7497
+ function applyPaletteToTheme(theme, palette) {
7498
+ return {
7499
+ ...theme,
7500
+ colors: {
7501
+ ...theme.colors,
7502
+ ...palette
7503
+ }
7504
+ };
7505
+ }
7506
+ function hexToHsl(hex) {
7507
+ const rgb = hexToRgb(hex);
7508
+ const r = rgb[0] / 255;
7509
+ const g = rgb[1] / 255;
7510
+ const b = rgb[2] / 255;
7511
+ const max = Math.max(r, g, b);
7512
+ const min = Math.min(r, g, b);
7513
+ const l = (max + min) / 2;
7514
+ if (max === min) {
7515
+ return [0, 0, Math.round(l * 100)];
7516
+ }
7517
+ const d = max - min;
7518
+ const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
7519
+ let h = 0;
7520
+ if (max === r) {
7521
+ h = ((g - b) / d + (g < b ? 6 : 0)) / 6;
7522
+ } else if (max === g) {
7523
+ h = ((b - r) / d + 2) / 6;
7524
+ } else {
7525
+ h = ((r - g) / d + 4) / 6;
7526
+ }
7527
+ return [Math.round(h * 360), Math.round(s * 100), Math.round(l * 100)];
7528
+ }
7529
+ function hslToHex(h, s, l) {
7530
+ const sNorm = s / 100;
7531
+ const lNorm = l / 100;
7532
+ const c = (1 - Math.abs(2 * lNorm - 1)) * sNorm;
7533
+ const x = c * (1 - Math.abs(h / 60 % 2 - 1));
7534
+ const m = lNorm - c / 2;
7535
+ let r = 0, g = 0, b = 0;
7536
+ if (h < 60) {
7537
+ r = c;
7538
+ g = x;
7539
+ b = 0;
7540
+ } else if (h < 120) {
7541
+ r = x;
7542
+ g = c;
7543
+ b = 0;
7544
+ } else if (h < 180) {
7545
+ r = 0;
7546
+ g = c;
7547
+ b = x;
7548
+ } else if (h < 240) {
7549
+ r = 0;
7550
+ g = x;
7551
+ b = c;
7552
+ } else if (h < 300) {
7553
+ r = x;
7554
+ g = 0;
7555
+ b = c;
7556
+ } else {
7557
+ r = c;
7558
+ g = 0;
7559
+ b = x;
7560
+ }
7561
+ return rgbToHex(
7562
+ Math.round((r + m) * 255),
7563
+ Math.round((g + m) * 255),
7564
+ Math.round((b + m) * 255)
7565
+ );
7566
+ }
7567
+ function hexToRgb(hex) {
7568
+ const cleaned = hex.replace("#", "");
7569
+ const full = cleaned.length === 3 ? cleaned[0] + cleaned[0] + cleaned[1] + cleaned[1] + cleaned[2] + cleaned[2] : cleaned;
7570
+ return [
7571
+ parseInt(full.substring(0, 2), 16),
7572
+ parseInt(full.substring(2, 4), 16),
7573
+ parseInt(full.substring(4, 6), 16)
7574
+ ];
7575
+ }
7576
+ function rgbToHex(r, g, b) {
7577
+ return "#" + [r, g, b].map((v) => v.toString(16).padStart(2, "0")).join("");
7578
+ }
7579
+ function clamp(value, min, max) {
7580
+ return Math.max(min, Math.min(max, value));
7581
+ }
7582
+ var SWATCH_KEYS = [
7583
+ { key: "primary", label: "Primary" },
7584
+ { key: "secondary", label: "Secondary" },
7585
+ { key: "surface", label: "Surface" },
7586
+ { key: "background", label: "Background" },
7587
+ { key: "text", label: "Text" },
7588
+ { key: "textMuted", label: "Muted" },
7589
+ { key: "border", label: "Border" },
7590
+ { key: "error", label: "Error" },
7591
+ { key: "warning", label: "Warning" },
7592
+ { key: "success", label: "Success" }
7593
+ ];
7594
+ function PaletteGenerator({ theme, onApply, onClose }) {
7595
+ const [baseColor, setBaseColor] = react.useState(theme.colors?.primary ?? "#3b82f6");
7596
+ const palette = react.useMemo(() => generatePalette(baseColor), [baseColor]);
7597
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fcte-palette", children: [
7598
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fcte-palette__header", children: [
7599
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "fcte-palette__title", children: "Generate Palette" }),
7600
+ /* @__PURE__ */ jsxRuntime.jsx("button", { className: "fcte-btn fcte-btn--secondary fcte-btn--sm", onClick: onClose, children: "Close" })
7601
+ ] }),
7602
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fcte-palette__body", children: [
7603
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fcte-palette__picker", children: [
7604
+ /* @__PURE__ */ jsxRuntime.jsx("label", { className: "fcte-palette__label", children: "Base Color" }),
7605
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fcte-palette__input-row", children: [
7606
+ /* @__PURE__ */ jsxRuntime.jsx(
7607
+ "input",
7608
+ {
7609
+ type: "color",
7610
+ value: baseColor,
7611
+ onChange: (e) => setBaseColor(e.target.value),
7612
+ className: "fcte-field__swatch"
7613
+ }
7614
+ ),
7615
+ /* @__PURE__ */ jsxRuntime.jsx(
7616
+ "input",
7617
+ {
7618
+ type: "text",
7619
+ value: baseColor,
7620
+ onChange: (e) => setBaseColor(e.target.value),
7621
+ className: "fcte-field__text",
7622
+ spellCheck: false
7623
+ }
7624
+ )
7625
+ ] })
7626
+ ] }),
7627
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "fcte-palette__swatches", children: SWATCH_KEYS.map(({ key, label }) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fcte-palette__swatch", children: [
7628
+ /* @__PURE__ */ jsxRuntime.jsx(
7629
+ "div",
7630
+ {
7631
+ className: "fcte-palette__swatch-color",
7632
+ style: { backgroundColor: palette[key] }
7633
+ }
7634
+ ),
7635
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "fcte-palette__swatch-label", children: label }),
7636
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "fcte-palette__swatch-hex", children: palette[key] })
7637
+ ] }, key)) }),
7638
+ /* @__PURE__ */ jsxRuntime.jsx(
7639
+ "button",
7640
+ {
7641
+ className: "fcte-btn fcte-btn--primary",
7642
+ onClick: () => onApply(applyPaletteToTheme(theme, palette)),
7643
+ children: "Apply to Theme"
7644
+ }
7645
+ )
7646
+ ] })
7647
+ ] });
7648
+ }
7649
+
7650
+ // src/theme-editor/presets.ts
7651
+ var draftingTealPreset = {
7652
+ colors: {
7653
+ primary: "#1F6B6E",
7654
+ primaryForeground: "#FFFFFF",
7655
+ secondary: "#B9D1CF",
7656
+ secondaryForeground: "#12222A",
7657
+ error: "#B04A3C",
7658
+ errorForeground: "#FFFFFF",
7659
+ warning: "#C98A2E",
7660
+ success: "#2E7D5B",
7661
+ surface: "#FFFFFF",
7662
+ background: "#F4F7F8",
7663
+ text: "#12222A",
7664
+ textMuted: "#6A7B85",
7665
+ textDisabled: "#B9D1CF",
7666
+ border: "#DCE4E8",
7667
+ borderFocus: "#1F6B6E",
7668
+ inputBackground: "#FFFFFF"
7669
+ },
7670
+ typography: {
7671
+ fontFamily: "'Space Grotesk', 'IBM Plex Sans', system-ui, sans-serif",
7672
+ scale: "comfortable",
7673
+ questionSize: "1.1875rem",
7674
+ labelSize: "0.8125rem",
7675
+ helpTextSize: "0.8125rem",
7676
+ bodySize: "0.9375rem"
7677
+ },
7678
+ shape: {
7679
+ radius: "none",
7680
+ inputRadius: "0px",
7681
+ buttonRadius: "0px",
7682
+ cardRadius: "0px"
7683
+ },
7684
+ spacing: {
7685
+ base: 16,
7686
+ sectionGap: 32,
7687
+ fieldGap: 24,
7688
+ inputPaddingX: 12,
7689
+ inputPaddingY: 10
7690
+ },
7691
+ layout: {
7692
+ maxWidth: "640px",
7693
+ alignment: "left",
7694
+ progressPosition: "top",
7695
+ sectionLayout: "flat"
7696
+ }
7697
+ };
7698
+ var COMPARISON_PRESETS = {
7699
+ "drafting-teal": { label: "Drafting Teal", theme: draftingTealPreset },
7700
+ clean: { label: "Clean", theme: fieldcraftReact.cleanPreset },
7701
+ dark: { label: "Dark", theme: fieldcraftReact.darkPreset },
7702
+ modern: { label: "Modern", theme: fieldcraftReact.modernPreset },
7703
+ "high-contrast": { label: "High Contrast", theme: fieldcraftReact.highContrastPreset },
7704
+ clinical: { label: "Clinical", theme: fieldcraftReact.clinicalPreset },
7705
+ playful: { label: "Playful", theme: fieldcraftReact.playfulPreset }
7706
+ };
7707
+ function ThemeComparison({ currentTheme, onClose }) {
7708
+ const [compareKey, setCompareKey] = react.useState("clean");
7709
+ const compareTheme = COMPARISON_PRESETS[compareKey]?.theme ?? fieldcraftReact.cleanPreset;
7710
+ const diffs = getColorDiffs(currentTheme, compareTheme);
7711
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fcte-compare", children: [
7712
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fcte-compare__header", children: [
7713
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "fcte-compare__title", children: "Theme Comparison" }),
7714
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fcte-compare__controls", children: [
7715
+ /* @__PURE__ */ jsxRuntime.jsx("label", { className: "fcte-compare__label", children: "Compare with:" }),
7716
+ /* @__PURE__ */ jsxRuntime.jsx(
7717
+ "select",
7718
+ {
7719
+ value: compareKey,
7720
+ onChange: (e) => setCompareKey(e.target.value),
7721
+ className: "fcte-toolbar__preset",
7722
+ children: Object.entries(COMPARISON_PRESETS).map(([key, { label }]) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: key, children: label }, key))
7723
+ }
7724
+ ),
7725
+ /* @__PURE__ */ jsxRuntime.jsx("button", { className: "fcte-btn fcte-btn--secondary fcte-btn--sm", onClick: onClose, children: "Close" })
7726
+ ] })
7727
+ ] }),
7728
+ diffs.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fcte-compare__diffs", children: [
7729
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "fcte-compare__diff-count", children: [
7730
+ diffs.length,
7731
+ " color",
7732
+ diffs.length !== 1 ? "s" : "",
7733
+ " differ"
7734
+ ] }),
7735
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fcte-compare__diff-list", children: [
7736
+ diffs.slice(0, 8).map((d) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fcte-compare__diff-item", children: [
7737
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "fcte-compare__diff-label", children: d.key }),
7738
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fcte-compare__diff-swatches", children: [
7739
+ /* @__PURE__ */ jsxRuntime.jsx(
7740
+ "div",
7741
+ {
7742
+ className: "fcte-compare__diff-swatch",
7743
+ style: { backgroundColor: d.current },
7744
+ title: `Current: ${d.current}`
7745
+ }
7746
+ ),
7747
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "fcte-compare__diff-arrow", children: "\u2192" }),
7748
+ /* @__PURE__ */ jsxRuntime.jsx(
7749
+ "div",
7750
+ {
7751
+ className: "fcte-compare__diff-swatch",
7752
+ style: { backgroundColor: d.compare },
7753
+ title: `${COMPARISON_PRESETS[compareKey]?.label}: ${d.compare}`
7754
+ }
7755
+ )
7756
+ ] })
7757
+ ] }, d.key)),
7758
+ diffs.length > 8 && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "fcte-compare__diff-more", children: [
7759
+ "+",
7760
+ diffs.length - 8,
7761
+ " more"
7762
+ ] })
7763
+ ] })
7764
+ ] }),
7765
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fcte-compare__panels", children: [
7766
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fcte-compare__panel", children: [
7767
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "fcte-compare__panel-label", children: "Current Theme" }),
7768
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "fcte-compare__panel-preview", children: /* @__PURE__ */ jsxRuntime.jsx(
7769
+ fieldcraftReact.FormEngineRenderer,
7770
+ {
7771
+ schema: PREVIEW_SCHEMA,
7772
+ theme: currentTheme,
7773
+ onSubmit: () => {
7774
+ }
7775
+ }
7776
+ ) })
7777
+ ] }),
7778
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fcte-compare__panel", children: [
7779
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "fcte-compare__panel-label", children: COMPARISON_PRESETS[compareKey]?.label ?? "Preset" }),
7780
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "fcte-compare__panel-preview", children: /* @__PURE__ */ jsxRuntime.jsx(
7781
+ fieldcraftReact.FormEngineRenderer,
7782
+ {
7783
+ schema: PREVIEW_SCHEMA,
7784
+ theme: compareTheme,
7785
+ onSubmit: () => {
7786
+ }
7787
+ }
7788
+ ) })
7789
+ ] })
7790
+ ] })
7791
+ ] });
7792
+ }
7793
+ function getColorDiffs(a, b) {
7794
+ const diffs = [];
7795
+ const aColors = a.colors ?? {};
7796
+ const bColors = b.colors ?? {};
7797
+ const allKeys = /* @__PURE__ */ new Set([...Object.keys(aColors), ...Object.keys(bColors)]);
7798
+ for (const key of allKeys) {
7799
+ const aVal = aColors[key] ?? "";
7800
+ const bVal = bColors[key] ?? "";
7801
+ if (aVal.toLowerCase() !== bVal.toLowerCase() && (aVal || bVal)) {
7802
+ diffs.push({ key, current: aVal || "(unset)", compare: bVal || "(unset)" });
7803
+ }
7804
+ }
7805
+ return diffs;
7806
+ }
7807
+ var ThemeCtx2 = react.createContext({});
7808
+ function themeToCssVars2(theme) {
7809
+ const vars = {};
7810
+ if (theme.background) vars["--fcte-bg"] = theme.background;
7811
+ if (theme.surface) vars["--fcte-surface"] = theme.surface;
7812
+ if (theme.surfaceHover) vars["--fcte-surface-hover"] = theme.surfaceHover;
7813
+ if (theme.text) vars["--fcte-text"] = theme.text;
7814
+ if (theme.textMuted) vars["--fcte-text-muted"] = theme.textMuted;
7815
+ if (theme.textDim) vars["--fcte-text-dim"] = theme.textDim;
7816
+ if (theme.border) vars["--fcte-border"] = theme.border;
7817
+ if (theme.borderStrong) vars["--fcte-border-strong"] = theme.borderStrong;
7818
+ if (theme.inputBackground) vars["--fcte-input-bg"] = theme.inputBackground;
7819
+ if (theme.accent) vars["--fcte-accent"] = theme.accent;
7820
+ if (theme.accentForeground) vars["--fcte-accent-text"] = theme.accentForeground;
7821
+ return vars;
7822
+ }
7823
+ function ThemeEditorThemeProvider({ theme, children }) {
7824
+ const resolved = theme ?? {};
7825
+ const cssVars = react.useMemo(() => themeToCssVars2(resolved), [resolved]);
7826
+ return /* @__PURE__ */ jsxRuntime.jsx(ThemeCtx2.Provider, { value: resolved, children: /* @__PURE__ */ jsxRuntime.jsx("div", { "data-fcte-root": "", style: cssVars, className: "fcte-provider-root", children }) });
7827
+ }
5304
7828
  var PRESETS = {
7829
+ "drafting-teal": draftingTealPreset,
5305
7830
  clean: fieldcraftReact.cleanPreset,
5306
7831
  dark: fieldcraftReact.darkPreset,
5307
7832
  modern: fieldcraftReact.modernPreset,
@@ -5431,15 +7956,18 @@ function ThemeEditorInner({
5431
7956
  initialTheme,
5432
7957
  onChange,
5433
7958
  onSave,
7959
+ theme: chromeTheme,
5434
7960
  height,
5435
7961
  width,
5436
7962
  className,
5437
7963
  toolbarExtra,
5438
7964
  showPreview = true
5439
7965
  }) {
5440
- const [theme, setTheme] = react.useState(initialTheme ?? fieldcraftReact.cleanPreset);
7966
+ const [theme, setTheme] = react.useState(initialTheme ?? draftingTealPreset);
5441
7967
  const [activeSection, setActiveSection] = react.useState("colors");
5442
- const [presetKey, setPresetKey] = react.useState("custom");
7968
+ const [presetKey, setPresetKey] = react.useState(initialTheme ? "custom" : "drafting-teal");
7969
+ const [showPalette, setShowPalette] = react.useState(false);
7970
+ const [showComparison, setShowComparison] = react.useState(false);
5443
7971
  const themeRef = react.useRef(theme);
5444
7972
  themeRef.current = theme;
5445
7973
  react.useEffect(() => {
@@ -5507,12 +8035,22 @@ function ThemeEditorInner({
5507
8035
  };
5508
8036
  input.click();
5509
8037
  }, [onChange]);
8038
+ const exportCss = react.useCallback(() => {
8039
+ exportCssFile(theme);
8040
+ }, [theme]);
8041
+ const importCss = react.useCallback(() => {
8042
+ importCssFile(theme, (merged) => {
8043
+ setTheme(merged);
8044
+ setPresetKey("custom");
8045
+ onChange?.(merged);
8046
+ });
8047
+ }, [theme, onChange]);
5510
8048
  const currentSection = react.useMemo(
5511
8049
  () => SECTIONS.find((s) => s.id === activeSection),
5512
8050
  [activeSection]
5513
8051
  );
5514
8052
  const sectionValues = theme[currentSection.themeKey] ?? {};
5515
- return /* @__PURE__ */ jsxRuntime.jsxs(
8053
+ return /* @__PURE__ */ jsxRuntime.jsx(ThemeEditorThemeProvider, { theme: chromeTheme, children: /* @__PURE__ */ jsxRuntime.jsxs(
5516
8054
  "div",
5517
8055
  {
5518
8056
  className: `fcte-root${className ? ` ${className}` : ""}`,
@@ -5539,22 +8077,65 @@ function ThemeEditorInner({
5539
8077
  ] }),
5540
8078
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fcte-toolbar__right", children: [
5541
8079
  toolbarExtra,
5542
- /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: importJson, className: "fcte-btn fcte-btn--secondary", children: "Import" }),
5543
- /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: exportJson, className: "fcte-btn fcte-btn--secondary", children: "Export" }),
8080
+ /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: importJson, className: "fcte-btn fcte-btn--secondary", children: "Import JSON" }),
8081
+ /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: exportJson, className: "fcte-btn fcte-btn--secondary", children: "Export JSON" }),
8082
+ /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: importCss, className: "fcte-btn fcte-btn--secondary", children: "Import CSS" }),
8083
+ /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: exportCss, className: "fcte-btn fcte-btn--secondary", children: "Export CSS" }),
8084
+ /* @__PURE__ */ jsxRuntime.jsx(
8085
+ "button",
8086
+ {
8087
+ onClick: () => {
8088
+ setShowPalette((v) => !v);
8089
+ setShowComparison(false);
8090
+ },
8091
+ className: `fcte-btn ${showPalette ? "fcte-btn--primary" : "fcte-btn--secondary"}`,
8092
+ children: "Palette"
8093
+ }
8094
+ ),
8095
+ /* @__PURE__ */ jsxRuntime.jsx(
8096
+ "button",
8097
+ {
8098
+ onClick: () => {
8099
+ setShowComparison((v) => !v);
8100
+ setShowPalette(false);
8101
+ },
8102
+ className: `fcte-btn ${showComparison ? "fcte-btn--primary" : "fcte-btn--secondary"}`,
8103
+ children: "Compare"
8104
+ }
8105
+ ),
5544
8106
  onSave && /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: () => onSave(theme), className: "fcte-btn fcte-btn--primary", children: "Save" })
5545
8107
  ] })
5546
8108
  ] }),
5547
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fcte-body", children: [
8109
+ showPalette && /* @__PURE__ */ jsxRuntime.jsx(
8110
+ PaletteGenerator,
8111
+ {
8112
+ theme,
8113
+ onApply: (newTheme) => {
8114
+ setTheme(newTheme);
8115
+ setPresetKey("custom");
8116
+ onChange?.(newTheme);
8117
+ setShowPalette(false);
8118
+ },
8119
+ onClose: () => setShowPalette(false)
8120
+ }
8121
+ ),
8122
+ showComparison ? /* @__PURE__ */ jsxRuntime.jsx(
8123
+ ThemeComparison,
8124
+ {
8125
+ currentTheme: theme,
8126
+ onClose: () => setShowComparison(false)
8127
+ }
8128
+ ) : /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fcte-body", children: [
5548
8129
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fcte-editor", children: [
5549
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "fcte-tabs", children: SECTIONS.map((s) => /* @__PURE__ */ jsxRuntime.jsx(
5550
- "button",
8130
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "fcte-section-select", children: /* @__PURE__ */ jsxRuntime.jsx(
8131
+ "select",
5551
8132
  {
5552
- className: `fcte-tab${activeSection === s.id ? " fcte-tab--active" : ""}`,
5553
- onClick: () => setActiveSection(s.id),
5554
- children: s.label
5555
- },
5556
- s.id
5557
- )) }),
8133
+ value: activeSection,
8134
+ onChange: (e) => setActiveSection(e.target.value),
8135
+ className: "fcte-field__select",
8136
+ children: SECTIONS.map((s) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: s.id, children: s.label }, s.id))
8137
+ }
8138
+ ) }),
5558
8139
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "fcte-fields", children: currentSection.fields.map((field) => {
5559
8140
  const val = sectionValues[field.key];
5560
8141
  if (field.kind === "color") {
@@ -5632,19 +8213,29 @@ function ThemeEditorInner({
5632
8213
  ] }, field.key);
5633
8214
  }) })
5634
8215
  ] }),
5635
- showPreview && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "fcte-preview", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "fcte-preview__inner", children: /* @__PURE__ */ jsxRuntime.jsx(
5636
- fieldcraftReact.FormEngineRenderer,
8216
+ showPreview && /* @__PURE__ */ jsxRuntime.jsx(
8217
+ "div",
5637
8218
  {
5638
- schema: PREVIEW_SCHEMA,
5639
- theme,
5640
- onSubmit: () => {
5641
- }
8219
+ className: "fcte-preview",
8220
+ style: {
8221
+ background: theme.colors?.background || void 0,
8222
+ color: theme.colors?.text || void 0
8223
+ },
8224
+ children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "fcte-preview__inner", children: /* @__PURE__ */ jsxRuntime.jsx(
8225
+ fieldcraftReact.FormEngineRenderer,
8226
+ {
8227
+ schema: PREVIEW_SCHEMA,
8228
+ theme,
8229
+ onSubmit: () => {
8230
+ }
8231
+ }
8232
+ ) })
5642
8233
  }
5643
- ) }) })
8234
+ )
5644
8235
  ] })
5645
8236
  ]
5646
8237
  }
5647
- );
8238
+ ) });
5648
8239
  }
5649
8240
 
5650
8241
  // src/theme-editor/ThemeEditor.tsx
@@ -5653,7 +8244,7 @@ var ThemeEditor = requireLicense(ThemeEditorInner, "ThemeEditor");
5653
8244
  // src/index.ts
5654
8245
  if (typeof globalThis !== "undefined" && typeof globalThis.process !== "undefined" && globalThis.process.env?.NODE_ENV !== "production") {
5655
8246
  const _fc_banner = `
5656
- %c FieldCraft Pro %c v1.3.0
8247
+ %c FieldCraft Pro %c v1.4.0
5657
8248
 
5658
8249
  %cForm Builder \xB7 Response Viewer \xB7 Theme Editor
5659
8250