@squaredr/fieldcraft-pro 1.8.0 → 1.9.1

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
@@ -37,7 +37,7 @@ var React__namespace = /*#__PURE__*/_interopNamespace(React);
37
37
  var SelectPrimitive__namespace = /*#__PURE__*/_interopNamespace(SelectPrimitive);
38
38
 
39
39
  // package.json
40
- var version = "1.8.0";
40
+ var version = "1.9.1";
41
41
  var PRODUCT_IDS = {
42
42
  FIELDCRAFT_PRO: 1
43
43
  };
@@ -592,21 +592,20 @@ var MAX_HISTORY = 50;
592
592
  function useUndoRedo(currentSchema, setSchema) {
593
593
  const historyRef = React.useRef([currentSchema]);
594
594
  const [currentIndex, setCurrentIndex] = React.useState(0);
595
- const currentIndexRef = React.useRef(currentIndex);
596
- currentIndexRef.current = currentIndex;
597
595
  const canUndo = currentIndex > 0;
598
596
  const canRedo = currentIndex < historyRef.current.length - 1;
599
597
  const push = React.useCallback(
600
598
  (schema) => {
601
- const idx = currentIndexRef.current;
602
- historyRef.current = historyRef.current.slice(0, idx + 1);
603
- historyRef.current.push(schema);
604
- if (historyRef.current.length > MAX_HISTORY) {
605
- historyRef.current.shift();
606
- setCurrentIndex(historyRef.current.length - 1);
607
- } else {
608
- setCurrentIndex(idx + 1);
609
- }
599
+ setCurrentIndex((prevIndex) => {
600
+ historyRef.current = historyRef.current.slice(0, prevIndex + 1);
601
+ historyRef.current.push(schema);
602
+ let newIndex = prevIndex + 1;
603
+ if (historyRef.current.length > MAX_HISTORY) {
604
+ historyRef.current.shift();
605
+ newIndex = Math.max(0, newIndex - 1);
606
+ }
607
+ return newIndex;
608
+ });
610
609
  },
611
610
  []
612
611
  );
@@ -639,13 +638,9 @@ function useUndoRedo(currentSchema, setSchema) {
639
638
  }
640
639
 
641
640
  // src/form-builder/utils/id-generator.ts
642
- var counter = 0;
643
641
  function generateId(prefix) {
644
- const timestamp = Date.now().toString(36);
645
- const random = Math.random().toString(36).substring(2, 7);
646
- counter = (counter + 1) % 1e4;
647
- const count = counter.toString(36);
648
- return `${prefix}_${timestamp}${count}${random}`;
642
+ const uuid = crypto.randomUUID().replace(/-/g, "").substring(0, 12);
643
+ return `${prefix}_${uuid}`;
649
644
  }
650
645
  function generateSectionId() {
651
646
  return generateId("section");
@@ -732,18 +727,39 @@ function duplicateSection(schema, sectionId) {
732
727
  const sectionIndex = newSchema.sections.findIndex((s) => s.id === sectionId);
733
728
  if (sectionIndex === -1) return schema;
734
729
  const original = newSchema.sections[sectionIndex];
730
+ const idMap = /* @__PURE__ */ new Map();
731
+ for (const q of original.questions) {
732
+ idMap.set(q.id, generateQuestionId());
733
+ }
735
734
  const duplicate = {
736
735
  ...original,
737
736
  id: generateSectionId(),
738
737
  title: `${original.title} (Copy)`,
738
+ showIf: original.showIf ? remapConditionFieldIds(original.showIf, idMap) : void 0,
739
739
  questions: original.questions.map((q) => ({
740
740
  ...q,
741
- id: generateQuestionId()
741
+ id: idMap.get(q.id),
742
+ showIf: q.showIf ? remapConditionFieldIds(q.showIf, idMap) : void 0
742
743
  }))
743
744
  };
744
745
  newSchema.sections.splice(sectionIndex + 1, 0, duplicate);
745
746
  return newSchema;
746
747
  }
748
+ function remapConditionFieldIds(expr, idMap) {
749
+ if (expr.field) {
750
+ return {
751
+ ...expr,
752
+ field: idMap.get(expr.field) ?? expr.field
753
+ };
754
+ }
755
+ if (expr.conditions) {
756
+ return {
757
+ ...expr,
758
+ conditions: expr.conditions.map((c) => remapConditionFieldIds(c, idMap))
759
+ };
760
+ }
761
+ return expr;
762
+ }
747
763
  function addQuestion(schema, sectionId, question, index) {
748
764
  const newSchema = deepClone(schema);
749
765
  const section = newSchema.sections.find((s) => s.id === sectionId);
@@ -887,7 +903,7 @@ function useBuilderState(initialSchema) {
887
903
  (sectionId) => {
888
904
  applyMutation((s) => removeSection(s, sectionId));
889
905
  setSelectedItem((prev) => {
890
- if (prev?.type === "section" && prev.sectionId === sectionId) return null;
906
+ if (prev?.sectionId === sectionId) return null;
891
907
  return prev;
892
908
  });
893
909
  },
@@ -2020,6 +2036,14 @@ function SectionBlock({ section, builderState }) {
2020
2036
  }
2021
2037
  function FormCanvas({ builderState }) {
2022
2038
  const { schema } = builderState;
2039
+ const [localTitle, setLocalTitle] = React.useState(schema.title);
2040
+ const [localDesc, setLocalDesc] = React.useState(schema.description ?? "");
2041
+ React.useEffect(() => {
2042
+ setLocalTitle(schema.title);
2043
+ }, [schema.title]);
2044
+ React.useEffect(() => {
2045
+ setLocalDesc(schema.description ?? "");
2046
+ }, [schema.description]);
2023
2047
  const handleAddSection = () => {
2024
2048
  const newSection = {
2025
2049
  id: generateSectionId(),
@@ -2036,9 +2060,12 @@ function FormCanvas({ builderState }) {
2036
2060
  "input",
2037
2061
  {
2038
2062
  type: "text",
2039
- value: schema.title,
2040
- onChange: (e) => {
2041
- builderState.updateSchema({ ...schema, title: e.target.value });
2063
+ value: localTitle,
2064
+ onChange: (e) => setLocalTitle(e.target.value),
2065
+ onBlur: () => {
2066
+ if (localTitle !== schema.title) {
2067
+ builderState.updateSchema({ ...schema, title: localTitle });
2068
+ }
2042
2069
  },
2043
2070
  className: "w-full text-xl font-bold bg-transparent text-foreground border-0 outline-none p-1.5 rounded-md focus:bg-card transition-colors",
2044
2071
  placeholder: "Form Title"
@@ -2047,9 +2074,12 @@ function FormCanvas({ builderState }) {
2047
2074
  /* @__PURE__ */ jsxRuntime.jsx(
2048
2075
  "textarea",
2049
2076
  {
2050
- value: schema.description ?? "",
2051
- onChange: (e) => {
2052
- builderState.updateSchema({ ...schema, description: e.target.value });
2077
+ value: localDesc,
2078
+ onChange: (e) => setLocalDesc(e.target.value),
2079
+ onBlur: () => {
2080
+ if (localDesc !== (schema.description ?? "")) {
2081
+ builderState.updateSchema({ ...schema, description: localDesc || void 0 });
2082
+ }
2053
2083
  },
2054
2084
  className: "w-full text-sm text-muted-foreground bg-transparent border-0 outline-none p-1.5 resize-y min-h-12 rounded-md leading-relaxed focus:bg-card transition-colors",
2055
2085
  placeholder: "Form description (optional)"
@@ -2152,12 +2182,26 @@ SelectItem.displayName = "SelectItem";
2152
2182
  function useConfigUpdater(question, onUpdate) {
2153
2183
  return (updates) => {
2154
2184
  const current = question.config ?? {};
2155
- onUpdate({ config: { ...current, type: question.type, ...updates } });
2185
+ const merged = { ...current, type: question.type, ...updates };
2186
+ for (const key of Object.keys(merged)) {
2187
+ if (merged[key] === void 0) delete merged[key];
2188
+ }
2189
+ onUpdate({ config: merged });
2156
2190
  };
2157
2191
  }
2158
- function QuestionConfigEditor({ question, onUpdate }) {
2192
+ function QuestionConfigEditor({ question, schema, onUpdate }) {
2159
2193
  const updateConfig = useConfigUpdater(question, onUpdate);
2160
2194
  const config = question.config ?? {};
2195
+ const allQuestions = React.useMemo(() => {
2196
+ if (!schema?.sections) return [];
2197
+ return schema.sections.flatMap((s) => s.questions || []);
2198
+ }, [schema]);
2199
+ const candidateAmountFields = React.useMemo(() => {
2200
+ return allQuestions.filter((q) => q.id !== question.id && ["calculated", "number", "slider", "rating", "single_select"].includes(q.type)).map((q) => ({ label: `${q.label || q.id} (${q.id})`, value: q.id }));
2201
+ }, [allQuestions, question.id]);
2202
+ const candidateTimezoneFields = React.useMemo(() => {
2203
+ return allQuestions.filter((q) => q.id !== question.id && ["dropdown", "single_select", "short_text"].includes(q.type)).map((q) => ({ label: `${q.label || q.id} (${q.id})`, value: q.id }));
2204
+ }, [allQuestions, question.id]);
2161
2205
  switch (question.type) {
2162
2206
  // ── Text ──
2163
2207
  case "short_text":
@@ -2181,32 +2225,32 @@ function QuestionConfigEditor({ question, onUpdate }) {
2181
2225
  // ── Numeric ──
2182
2226
  case "number":
2183
2227
  return /* @__PURE__ */ jsxRuntime.jsxs(ConfigSection, { title: "Number Settings", children: [
2184
- /* @__PURE__ */ jsxRuntime.jsx(NumberField, { label: "Min", value: config.min, onChange: (v) => updateConfig({ min: v }) }),
2185
- /* @__PURE__ */ jsxRuntime.jsx(NumberField, { label: "Max", value: config.max, onChange: (v) => updateConfig({ max: v }) }),
2228
+ /* @__PURE__ */ jsxRuntime.jsx(NumberField, { label: "Min Value", value: config.min, onChange: (v) => updateConfig({ min: v }) }),
2229
+ /* @__PURE__ */ jsxRuntime.jsx(NumberField, { label: "Max Value", value: config.max, onChange: (v) => updateConfig({ max: v }) }),
2186
2230
  /* @__PURE__ */ jsxRuntime.jsx(NumberField, { label: "Step", value: config.step, placeholder: "1", onChange: (v) => updateConfig({ step: v }) }),
2187
- /* @__PURE__ */ jsxRuntime.jsx(NumberField, { label: "Decimal Places", value: config.decimalPlaces, placeholder: "0", onChange: (v) => updateConfig({ decimalPlaces: v }) }),
2188
2231
  /* @__PURE__ */ jsxRuntime.jsx(TextField, { label: "Prefix", value: config.prefix, placeholder: "e.g. $", onChange: (v) => updateConfig({ prefix: v }) }),
2189
2232
  /* @__PURE__ */ jsxRuntime.jsx(TextField, { label: "Suffix", value: config.suffix, placeholder: "e.g. kg", onChange: (v) => updateConfig({ suffix: v }) })
2190
2233
  ] });
2191
2234
  case "slider":
2192
2235
  return /* @__PURE__ */ jsxRuntime.jsxs(ConfigSection, { title: "Slider Settings", children: [
2193
- /* @__PURE__ */ jsxRuntime.jsx(NumberField, { label: "Min", value: config.min, placeholder: "0", onChange: (v) => updateConfig({ min: v }) }),
2194
- /* @__PURE__ */ jsxRuntime.jsx(NumberField, { label: "Max", value: config.max, placeholder: "100", onChange: (v) => updateConfig({ max: v }) }),
2195
- /* @__PURE__ */ jsxRuntime.jsx(NumberField, { label: "Step", value: config.step, placeholder: "1", onChange: (v) => updateConfig({ step: v }) }),
2196
- /* @__PURE__ */ jsxRuntime.jsx(ToggleField, { label: "Show Value", checked: !!config.showValue, onChange: (v) => updateConfig({ showValue: v }) }),
2197
- /* @__PURE__ */ jsxRuntime.jsx(TextField, { label: "Min Label", value: config.minLabel, onChange: (v) => updateConfig({ minLabel: v }) }),
2198
- /* @__PURE__ */ jsxRuntime.jsx(TextField, { label: "Max Label", value: config.maxLabel, onChange: (v) => updateConfig({ maxLabel: v }) })
2236
+ /* @__PURE__ */ jsxRuntime.jsx(NumberField, { label: "Min Value", value: config.min, placeholder: "0", onChange: (v) => updateConfig({ min: v ?? 0 }) }),
2237
+ /* @__PURE__ */ jsxRuntime.jsx(NumberField, { label: "Max Value", value: config.max, placeholder: "100", onChange: (v) => updateConfig({ max: v ?? 100 }) }),
2238
+ /* @__PURE__ */ jsxRuntime.jsx(NumberField, { label: "Step", value: config.step, placeholder: "1", onChange: (v) => updateConfig({ step: v ?? 1 }) }),
2239
+ /* @__PURE__ */ jsxRuntime.jsx(TextField, { label: "Min Label", value: config.minLabel, placeholder: "Low", onChange: (v) => updateConfig({ minLabel: v }) }),
2240
+ /* @__PURE__ */ jsxRuntime.jsx(TextField, { label: "Max Label", value: config.maxLabel, placeholder: "High", onChange: (v) => updateConfig({ maxLabel: v }) }),
2241
+ /* @__PURE__ */ jsxRuntime.jsx(ToggleField, { label: "Show Value", checked: config.showValue !== false, onChange: (v) => updateConfig({ showValue: v }) }),
2242
+ /* @__PURE__ */ jsxRuntime.jsx(TextField, { label: "Unit", value: config.unit, placeholder: "e.g. %", onChange: (v) => updateConfig({ unit: v }) })
2199
2243
  ] });
2200
2244
  case "rating":
2201
2245
  return /* @__PURE__ */ jsxRuntime.jsxs(ConfigSection, { title: "Rating Settings", children: [
2202
- /* @__PURE__ */ jsxRuntime.jsx(NumberField, { label: "Max Stars", value: config.max, placeholder: "5", onChange: (v) => updateConfig({ max: v }) }),
2203
- /* @__PURE__ */ jsxRuntime.jsx(SelectField, { label: "Icon", value: config.icon ?? "star", options: [{ label: "Star", value: "star" }, { label: "Heart", value: "heart" }, { label: "Circle", value: "circle" }], onChange: (v) => updateConfig({ icon: v }) }),
2204
- /* @__PURE__ */ jsxRuntime.jsx(ToggleField, { label: "Show Labels", checked: !!config.showLabels, onChange: (v) => updateConfig({ showLabels: v }) })
2246
+ /* @__PURE__ */ jsxRuntime.jsx(NumberField, { label: "Max Rating", value: config.maxRating, placeholder: "5", onChange: (v) => updateConfig({ maxRating: v ?? 5 }) }),
2247
+ /* @__PURE__ */ jsxRuntime.jsx(SelectField, { label: "Icon", value: config.icon ?? "star", options: [{ label: "Star", value: "star" }, { label: "Heart", value: "heart" }, { label: "Thumb", value: "thumb" }], onChange: (v) => updateConfig({ icon: v }) }),
2248
+ /* @__PURE__ */ jsxRuntime.jsx(ToggleField, { label: "Allow Half Ratings", checked: !!config.allowHalf, onChange: (v) => updateConfig({ allowHalf: v }) })
2205
2249
  ] });
2206
2250
  case "nps":
2207
2251
  return /* @__PURE__ */ jsxRuntime.jsxs(ConfigSection, { title: "NPS Settings", children: [
2208
- /* @__PURE__ */ jsxRuntime.jsx(TextField, { label: "Low Label", value: config.lowLabel ?? "Not likely", onChange: (v) => updateConfig({ lowLabel: v }) }),
2209
- /* @__PURE__ */ jsxRuntime.jsx(TextField, { label: "High Label", value: config.highLabel ?? "Very likely", onChange: (v) => updateConfig({ highLabel: v }) })
2252
+ /* @__PURE__ */ jsxRuntime.jsx(TextField, { label: "Left Label", value: config.leftLabel ?? "Not likely", onChange: (v) => updateConfig({ leftLabel: v }) }),
2253
+ /* @__PURE__ */ jsxRuntime.jsx(TextField, { label: "Right Label", value: config.rightLabel ?? "Extremely likely", onChange: (v) => updateConfig({ rightLabel: v }) })
2210
2254
  ] });
2211
2255
  case "opinion_scale":
2212
2256
  return /* @__PURE__ */ jsxRuntime.jsxs(ConfigSection, { title: "Scale Settings", children: [
@@ -2272,50 +2316,126 @@ function QuestionConfigEditor({ question, onUpdate }) {
2272
2316
  /* @__PURE__ */ jsxRuntime.jsx(NumberField, { label: "Max Range (days)", value: config.maxRangeDays, onChange: (v) => updateConfig({ maxRangeDays: v }) })
2273
2317
  ] });
2274
2318
  case "appointment": {
2275
- const appointmentMode = typeof config.embedUrl === "string" ? "embed" : typeof config.slotsUrl === "string" ? "url" : "static";
2319
+ const appointmentMode = typeof config.slotsUrl === "string" ? "url" : typeof config.embedUrl === "string" ? "embed" : "static";
2320
+ const timezoneFieldOptions = [
2321
+ { label: "None (Static Timezone)", value: "" },
2322
+ ...candidateTimezoneFields,
2323
+ { label: "Custom Field ID...", value: "__custom__" }
2324
+ ];
2325
+ const isCustomTzField = Boolean(config.timezoneField) && !candidateTimezoneFields.some((opt) => opt.value === config.timezoneField);
2276
2326
  return /* @__PURE__ */ jsxRuntime.jsxs(ConfigSection, { title: "Appointment Settings", children: [
2277
2327
  /* @__PURE__ */ jsxRuntime.jsx(
2278
2328
  SelectField,
2279
2329
  {
2280
- label: "Mode",
2330
+ label: "Scheduling Mode",
2281
2331
  value: appointmentMode,
2282
2332
  options: [
2283
- { label: "Static Slots", value: "static" },
2284
- { label: "API Endpoint", value: "url" },
2285
- { label: "Embed (Calendly / Cal.com)", value: "embed" }
2333
+ { label: "Manual Static Slots", value: "static" },
2334
+ { label: "Fetch from URL", value: "url" },
2335
+ { label: "Third-Party Embed (Calendly / Cal.com)", value: "embed" }
2286
2336
  ],
2287
2337
  onChange: (v) => {
2288
- if (v === "static") {
2289
- updateConfig({ slotsUrl: void 0, embedUrl: void 0, embedProvider: void 0 });
2290
- } else if (v === "url") {
2291
- updateConfig({ slotsUrl: "", embedUrl: void 0, embedProvider: void 0, slots: void 0 });
2338
+ if (v === "url") {
2339
+ updateConfig({
2340
+ slotsUrl: "",
2341
+ embedUrl: void 0,
2342
+ embedProvider: void 0,
2343
+ slots: void 0
2344
+ });
2292
2345
  } else if (v === "embed") {
2293
- updateConfig({ embedUrl: "", slotsUrl: void 0, slots: void 0 });
2346
+ updateConfig({
2347
+ embedUrl: "",
2348
+ embedProvider: config.embedProvider || "cal_com",
2349
+ slotsUrl: void 0,
2350
+ slots: void 0
2351
+ });
2352
+ } else {
2353
+ updateConfig({
2354
+ slots: config.slots ?? [],
2355
+ slotsUrl: void 0,
2356
+ embedUrl: void 0,
2357
+ embedProvider: void 0
2358
+ });
2294
2359
  }
2295
2360
  }
2296
2361
  }
2297
2362
  ),
2298
- /* @__PURE__ */ jsxRuntime.jsx(NumberField, { label: "Duration (min)", value: config.duration ?? 30, onChange: (v) => updateConfig({ duration: v }) }),
2299
- /* @__PURE__ */ jsxRuntime.jsx(TextField, { label: "Timezone", value: config.timezone, placeholder: "e.g. America/New_York", onChange: (v) => updateConfig({ timezone: v }) }),
2300
- /* @__PURE__ */ jsxRuntime.jsx(TextField, { label: "Timezone Field", value: config.timezoneField, placeholder: "Field ID for dynamic timezone", onChange: (v) => updateConfig({ timezoneField: v }) }),
2301
- /* @__PURE__ */ jsxRuntime.jsx(TextField, { label: "Date Format", value: config.dateFormat, placeholder: "Locale-aware (default)", onChange: (v) => updateConfig({ dateFormat: v }) }),
2302
- appointmentMode === "url" && /* @__PURE__ */ jsxRuntime.jsx(TextField, { label: "Slots URL", value: config.slotsUrl, placeholder: "https://api.example.com/slots", onChange: (v) => updateConfig({ slotsUrl: v }) }),
2363
+ appointmentMode === "url" && /* @__PURE__ */ jsxRuntime.jsx(
2364
+ TextField,
2365
+ {
2366
+ label: "Slots URL",
2367
+ value: config.slotsUrl,
2368
+ placeholder: "https://example.com/slots",
2369
+ onChange: (v) => updateConfig({ slotsUrl: v ?? "" })
2370
+ }
2371
+ ),
2303
2372
  appointmentMode === "embed" && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2304
- /* @__PURE__ */ jsxRuntime.jsx(TextField, { label: "Embed URL", value: config.embedUrl, placeholder: "https://calendly.com/your-name/30min", onChange: (v) => updateConfig({ embedUrl: v }) }),
2373
+ /* @__PURE__ */ jsxRuntime.jsx(
2374
+ TextField,
2375
+ {
2376
+ label: "Embed URL",
2377
+ value: config.embedUrl,
2378
+ placeholder: "https://calendly.com/... or https://cal.com/...",
2379
+ onChange: (v) => updateConfig({ embedUrl: v ?? "" })
2380
+ }
2381
+ ),
2305
2382
  /* @__PURE__ */ jsxRuntime.jsx(
2306
2383
  SelectField,
2307
2384
  {
2308
2385
  label: "Embed Provider",
2309
- value: config.embedProvider ?? "custom",
2386
+ value: config.embedProvider ?? "cal_com",
2310
2387
  options: [
2311
- { label: "Calendly", value: "calendly" },
2312
2388
  { label: "Cal.com", value: "cal_com" },
2389
+ { label: "Calendly", value: "calendly" },
2313
2390
  { label: "Custom", value: "custom" }
2314
2391
  ],
2315
2392
  onChange: (v) => updateConfig({ embedProvider: v })
2316
2393
  }
2317
2394
  )
2318
2395
  ] }),
2396
+ /* @__PURE__ */ jsxRuntime.jsx(NumberField, { label: "Duration (min)", value: config.duration ?? 30, onChange: (v) => updateConfig({ duration: v }) }),
2397
+ /* @__PURE__ */ jsxRuntime.jsx(TextField, { label: "Timezone", value: config.timezone, placeholder: "e.g. America/New_York", onChange: (v) => updateConfig({ timezone: v }) }),
2398
+ candidateTimezoneFields.length > 0 ? /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
2399
+ /* @__PURE__ */ jsxRuntime.jsx(
2400
+ SelectField,
2401
+ {
2402
+ label: "Dynamic Timezone Field",
2403
+ value: isCustomTzField ? "__custom__" : config.timezoneField ?? "",
2404
+ options: timezoneFieldOptions,
2405
+ onChange: (v) => {
2406
+ if (v !== "__custom__") {
2407
+ updateConfig({ timezoneField: v || void 0 });
2408
+ }
2409
+ }
2410
+ }
2411
+ ),
2412
+ isCustomTzField && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-2", children: /* @__PURE__ */ jsxRuntime.jsx(
2413
+ TextField,
2414
+ {
2415
+ label: "Custom Timezone Field ID",
2416
+ value: config.timezoneField,
2417
+ placeholder: "e.g. timezone",
2418
+ onChange: (v) => updateConfig({ timezoneField: v || void 0 })
2419
+ }
2420
+ ) })
2421
+ ] }) : /* @__PURE__ */ jsxRuntime.jsx(
2422
+ TextField,
2423
+ {
2424
+ label: "Timezone Field",
2425
+ value: config.timezoneField,
2426
+ placeholder: "Field ID for dynamic timezone",
2427
+ onChange: (v) => updateConfig({ timezoneField: v || void 0 })
2428
+ }
2429
+ ),
2430
+ /* @__PURE__ */ jsxRuntime.jsx(TextField, { label: "Date Format", value: config.dateFormat, placeholder: "Locale-aware (default)", onChange: (v) => updateConfig({ dateFormat: v }) }),
2431
+ /* @__PURE__ */ jsxRuntime.jsx(
2432
+ ToggleField,
2433
+ {
2434
+ label: "Auto-Advance on Booking Complete",
2435
+ checked: config.autoAdvance !== false,
2436
+ onChange: (v) => updateConfig({ autoAdvance: v })
2437
+ }
2438
+ ),
2319
2439
  appointmentMode === "static" && /* @__PURE__ */ jsxRuntime.jsx(
2320
2440
  AppointmentSlotsEditor,
2321
2441
  {
@@ -2328,10 +2448,41 @@ function QuestionConfigEditor({ question, onUpdate }) {
2328
2448
  // ── Media ──
2329
2449
  case "file_upload":
2330
2450
  return /* @__PURE__ */ jsxRuntime.jsxs(ConfigSection, { title: "Upload Settings", children: [
2451
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "p-2.5 bg-muted/40 border border-border rounded-md text-xs text-muted-foreground flex items-center gap-1.5", children: [
2452
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: "\u{1F4E6}" }),
2453
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: "Uploads stream to your cloud bucket using Form / Account Storage settings." })
2454
+ ] }),
2331
2455
  /* @__PURE__ */ jsxRuntime.jsx(NumberField, { label: "Max Files", value: config.maxFiles ?? 1, onChange: (v) => updateConfig({ maxFiles: v }) }),
2332
2456
  /* @__PURE__ */ jsxRuntime.jsx(NumberField, { label: "Max Size (MB)", value: config.maxSizeMb ?? 10, onChange: (v) => updateConfig({ maxSizeMb: v }) }),
2333
- /* @__PURE__ */ jsxRuntime.jsx(TextField, { label: "Accepted Types", value: config.accept?.join(", "), placeholder: "e.g. .pdf, .jpg, .png", onChange: (v) => updateConfig({ accept: v ? v.split(",").map((s) => s.trim()) : void 0 }) }),
2334
- /* @__PURE__ */ jsxRuntime.jsx(TextField, { label: "Upload URL", value: config.uploadUrl, placeholder: "Server upload endpoint", onChange: (v) => updateConfig({ uploadUrl: v }) })
2457
+ /* @__PURE__ */ jsxRuntime.jsx(
2458
+ TextField,
2459
+ {
2460
+ label: "Accepted Types (comma-separated)",
2461
+ value: config.accept?.join(", "),
2462
+ placeholder: "e.g. image/*, .pdf, .docx, .zip",
2463
+ onChange: (v) => updateConfig({
2464
+ accept: v ? v.split(",").map((s) => s.trim()).filter(Boolean) : void 0
2465
+ })
2466
+ }
2467
+ ),
2468
+ /* @__PURE__ */ jsxRuntime.jsx(
2469
+ TextField,
2470
+ {
2471
+ label: "Upload URL",
2472
+ value: config.uploadUrl,
2473
+ placeholder: "https://api.example.com/s3/presign",
2474
+ onChange: (v) => updateConfig({ uploadUrl: v || void 0 })
2475
+ }
2476
+ ),
2477
+ /* @__PURE__ */ jsxRuntime.jsx(
2478
+ TextField,
2479
+ {
2480
+ label: "Public Key",
2481
+ value: config.publicKey,
2482
+ placeholder: "pk_test_...",
2483
+ onChange: (v) => updateConfig({ publicKey: v || void 0 })
2484
+ }
2485
+ )
2335
2486
  ] });
2336
2487
  case "signature":
2337
2488
  return /* @__PURE__ */ jsxRuntime.jsxs(ConfigSection, { title: "Signature Settings", children: [
@@ -2448,32 +2599,120 @@ function QuestionConfigEditor({ question, onUpdate }) {
2448
2599
  /* @__PURE__ */ jsxRuntime.jsx(TextField, { label: "Default Country", value: config.defaultCountry, placeholder: "e.g. US", onChange: (v) => updateConfig({ defaultCountry: v }) }),
2449
2600
  /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
2450
2601
  /* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.Label, { className: "text-xs text-muted-foreground mb-2 block", children: "Fields" }),
2451
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "space-y-1.5", children: ADDRESS_FIELDS.map((f) => /* @__PURE__ */ jsxRuntime.jsx(
2452
- ToggleField,
2453
- {
2454
- label: f.label,
2455
- checked: activeFields.includes(f.value),
2456
- onChange: (checked) => {
2457
- const next = checked ? [...activeFields, f.value] : activeFields.filter((v) => v !== f.value);
2458
- updateConfig({ fields: next.length > 0 ? next : void 0 });
2459
- }
2460
- },
2461
- f.value
2462
- )) })
2602
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "space-y-1.5", children: ADDRESS_FIELDS.map((f) => {
2603
+ const isChecked = activeFields.includes(f.value);
2604
+ const isLastChecked = isChecked && activeFields.length === 1;
2605
+ return /* @__PURE__ */ jsxRuntime.jsx(
2606
+ ToggleField,
2607
+ {
2608
+ label: f.label,
2609
+ checked: isChecked,
2610
+ disabled: isLastChecked,
2611
+ onChange: (checked) => {
2612
+ const next = checked ? [...activeFields, f.value] : activeFields.filter((v) => v !== f.value);
2613
+ updateConfig({ fields: next });
2614
+ }
2615
+ },
2616
+ f.value
2617
+ );
2618
+ }) })
2463
2619
  ] })
2464
2620
  ] });
2465
2621
  }
2466
- case "payment":
2622
+ case "payment": {
2623
+ const amountFieldOptions = [
2624
+ { label: "None (Fixed Amount)", value: "" },
2625
+ ...candidateAmountFields,
2626
+ { label: "Custom Field ID...", value: "__custom__" }
2627
+ ];
2628
+ const isCustomAmountField = Boolean(config.amountField) && !candidateAmountFields.some((opt) => opt.value === config.amountField);
2467
2629
  return /* @__PURE__ */ jsxRuntime.jsxs(ConfigSection, { title: "Payment Settings", children: [
2468
- /* @__PURE__ */ jsxRuntime.jsx(SelectField, { label: "Provider", value: config.provider ?? "stripe", options: [{ label: "Stripe", value: "stripe" }, { label: "PayPal", value: "paypal" }], onChange: (v) => updateConfig({ provider: v }) }),
2469
- /* @__PURE__ */ jsxRuntime.jsx(TextField, { label: "Public Key", value: config.publicKey ?? "", placeholder: "Provider public/client key", onChange: (v) => updateConfig({ publicKey: v }) }),
2470
- /* @__PURE__ */ jsxRuntime.jsx(TextField, { label: "Server URL", value: config.serverUrl ?? "", placeholder: "https://api.example.com/create-intent", onChange: (v) => updateConfig({ serverUrl: v || void 0 }) }),
2471
- /* @__PURE__ */ jsxRuntime.jsx(TextField, { label: "Client Secret Path", value: config.responseMapping?.clientSecretPath ?? "", placeholder: "clientSecret", onChange: (v) => updateConfig({ responseMapping: v ? { clientSecretPath: v } : void 0 }) }),
2472
- /* @__PURE__ */ jsxRuntime.jsx(NumberField, { label: "Amount", value: config.amount, onChange: (v) => updateConfig({ amount: v }) }),
2473
- /* @__PURE__ */ jsxRuntime.jsx(TextField, { label: "Amount Field", value: config.amountField, placeholder: "Field ID for dynamic amount", onChange: (v) => updateConfig({ amountField: v }) }),
2474
- /* @__PURE__ */ jsxRuntime.jsx(TextField, { label: "Currency", value: config.currency, placeholder: "USD", onChange: (v) => updateConfig({ currency: v }) }),
2475
- /* @__PURE__ */ jsxRuntime.jsx(TextField, { label: "Description", value: config.description, onChange: (v) => updateConfig({ description: v }) })
2630
+ /* @__PURE__ */ jsxRuntime.jsx(
2631
+ SelectField,
2632
+ {
2633
+ label: "Provider",
2634
+ value: config.provider ?? "stripe",
2635
+ options: [
2636
+ { label: "Stripe", value: "stripe" },
2637
+ { label: "PayPal", value: "paypal" },
2638
+ { label: "Razorpay", value: "razorpay" }
2639
+ ],
2640
+ onChange: (v) => updateConfig({ provider: v })
2641
+ }
2642
+ ),
2643
+ /* @__PURE__ */ jsxRuntime.jsx(
2644
+ TextField,
2645
+ {
2646
+ label: "Public Key",
2647
+ value: config.publicKey,
2648
+ placeholder: "pk_test_...",
2649
+ onChange: (v) => updateConfig({ publicKey: v || void 0 })
2650
+ }
2651
+ ),
2652
+ /* @__PURE__ */ jsxRuntime.jsx(
2653
+ TextField,
2654
+ {
2655
+ label: "Server URL",
2656
+ value: config.serverUrl,
2657
+ placeholder: "https://api.example.com/create-intent",
2658
+ onChange: (v) => updateConfig({ serverUrl: v || void 0 })
2659
+ }
2660
+ ),
2661
+ candidateAmountFields.length > 0 ? /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
2662
+ /* @__PURE__ */ jsxRuntime.jsx(
2663
+ SelectField,
2664
+ {
2665
+ label: "Dynamic Amount Field",
2666
+ value: isCustomAmountField ? "__custom__" : config.amountField ?? "",
2667
+ options: amountFieldOptions,
2668
+ onChange: (v) => {
2669
+ if (v !== "__custom__") {
2670
+ updateConfig({ amountField: v || void 0 });
2671
+ }
2672
+ }
2673
+ }
2674
+ ),
2675
+ isCustomAmountField && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-2", children: /* @__PURE__ */ jsxRuntime.jsx(
2676
+ TextField,
2677
+ {
2678
+ label: "Custom Field ID",
2679
+ value: config.amountField,
2680
+ placeholder: "Field ID for dynamic amount",
2681
+ onChange: (v) => updateConfig({ amountField: v || void 0 })
2682
+ }
2683
+ ) })
2684
+ ] }) : /* @__PURE__ */ jsxRuntime.jsx(
2685
+ TextField,
2686
+ {
2687
+ label: "Amount Field",
2688
+ value: config.amountField,
2689
+ placeholder: "Field ID for dynamic amount (e.g. total_cost)",
2690
+ onChange: (v) => updateConfig({ amountField: v || void 0 })
2691
+ }
2692
+ ),
2693
+ !config.amountField && /* @__PURE__ */ jsxRuntime.jsx(
2694
+ NumberField,
2695
+ {
2696
+ label: "Fixed Amount",
2697
+ value: config.amount,
2698
+ onChange: (v) => updateConfig({ amount: v })
2699
+ }
2700
+ ),
2701
+ /* @__PURE__ */ jsxRuntime.jsx(TextField, { label: "Currency", value: config.currency ?? "USD", placeholder: "USD", onChange: (v) => updateConfig({ currency: v || "USD" }) }),
2702
+ /* @__PURE__ */ jsxRuntime.jsx(TextField, { label: "Description", value: config.description, onChange: (v) => updateConfig({ description: v }) }),
2703
+ /* @__PURE__ */ jsxRuntime.jsx(
2704
+ TextField,
2705
+ {
2706
+ label: "Client Secret Response Path",
2707
+ value: typeof config.responseMapping?.clientSecretPath === "string" ? config.responseMapping.clientSecretPath : config.responseMapping ?? "",
2708
+ placeholder: "clientSecret",
2709
+ onChange: (v) => updateConfig({
2710
+ responseMapping: v ? { clientSecretPath: v } : void 0
2711
+ })
2712
+ }
2713
+ )
2476
2714
  ] });
2715
+ }
2477
2716
  case "calculated":
2478
2717
  return /* @__PURE__ */ jsxRuntime.jsxs(ConfigSection, { title: "Calculated Field", children: [
2479
2718
  /* @__PURE__ */ jsxRuntime.jsx(TextField, { label: "Expression", value: config.expression ?? "", placeholder: "e.g. {q1} + {q2}", onChange: (v) => updateConfig({ expression: v }) }),
@@ -2582,11 +2821,12 @@ function NumberField({
2582
2821
  function ToggleField({
2583
2822
  label,
2584
2823
  checked,
2585
- onChange
2824
+ onChange,
2825
+ disabled
2586
2826
  }) {
2587
2827
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between", children: [
2588
2828
  /* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.Label, { className: "text-xs text-muted-foreground", children: label }),
2589
- /* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.Switch, { checked, onCheckedChange: onChange })
2829
+ /* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.Switch, { checked, onCheckedChange: onChange, disabled })
2590
2830
  ] });
2591
2831
  }
2592
2832
  function SelectField({
@@ -3558,7 +3798,14 @@ function QuestionProperties({ question, sectionId, builderState, onClose, onOpen
3558
3798
  }
3559
3799
  )
3560
3800
  ] }),
3561
- /* @__PURE__ */ jsxRuntime.jsx(QuestionConfigEditor, { question, onUpdate: updateQuestion2 }),
3801
+ /* @__PURE__ */ jsxRuntime.jsx(
3802
+ QuestionConfigEditor,
3803
+ {
3804
+ question,
3805
+ schema: builderState.schema,
3806
+ onUpdate: updateQuestion2
3807
+ }
3808
+ ),
3562
3809
  hasOptions && /* @__PURE__ */ jsxRuntime.jsx(
3563
3810
  OptionsEditor,
3564
3811
  {
@@ -3735,6 +3982,11 @@ function MonacoWrapper({ value, onChange, errors }) {
3735
3982
  editorRef.current = editor;
3736
3983
  monacoRef.current = monaco;
3737
3984
  };
3985
+ React.useEffect(() => {
3986
+ return () => {
3987
+ editorRef.current?.dispose();
3988
+ };
3989
+ }, []);
3738
3990
  React.useEffect(() => {
3739
3991
  const editor = editorRef.current;
3740
3992
  const monaco = monacoRef.current;
@@ -4539,6 +4791,11 @@ function TemplateGallery({ templates, onSelect, onClose }) {
4539
4791
  function useDebouncedValidation(delayMs = 500) {
4540
4792
  const [errors, setErrors] = React.useState([]);
4541
4793
  const timerRef = React.useRef(null);
4794
+ React.useEffect(() => {
4795
+ return () => {
4796
+ if (timerRef.current) clearTimeout(timerRef.current);
4797
+ };
4798
+ }, []);
4542
4799
  const validate = React.useCallback(
4543
4800
  (text) => {
4544
4801
  if (timerRef.current) {
@@ -5232,62 +5489,1143 @@ ${details}`);
5232
5489
  showTemplateGallery && templates && /* @__PURE__ */ jsxRuntime.jsx(
5233
5490
  TemplateGallery,
5234
5491
  {
5235
- templates,
5236
- onSelect: (template) => builderState.resetSchema(template.schema),
5237
- onClose: () => setShowTemplateGallery(false)
5492
+ templates,
5493
+ onSelect: (template) => builderState.resetSchema(template.schema),
5494
+ onClose: () => setShowTemplateGallery(false)
5495
+ }
5496
+ )
5497
+ ]
5498
+ }
5499
+ ) });
5500
+ }
5501
+ function DragOverlayContent({
5502
+ item,
5503
+ schema,
5504
+ questionTypes: types
5505
+ }) {
5506
+ if (item.type === "palette-item") {
5507
+ const typeInfo = types[item.questionType];
5508
+ if (!typeInfo) return null;
5509
+ const IconComponent = getIcon(typeInfo.icon);
5510
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3 px-3 py-2 rounded-md border border-primary bg-card text-foreground text-sm fcb-shadow-lg cursor-grabbing", children: [
5511
+ /* @__PURE__ */ jsxRuntime.jsx(IconComponent, { size: 14, className: "shrink-0 text-primary", strokeWidth: 1.75 }),
5512
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium", children: typeInfo.label })
5513
+ ] });
5514
+ }
5515
+ if (item.type === "question") {
5516
+ const found = findQuestion(schema, item.sectionId, item.questionId);
5517
+ if (!found) return null;
5518
+ const typeInfo = types[found.question.type];
5519
+ const IconComponent = typeInfo ? getIcon(typeInfo.icon) : null;
5520
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "p-3 rounded-md border border-primary bg-card text-foreground fcb-shadow-lg cursor-grabbing max-w-sm", children: [
5521
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center gap-2 mb-1", children: /* @__PURE__ */ jsxRuntime.jsxs(fieldcraftReact.Badge, { variant: "secondary", className: "gap-1.5 text-muted-foreground", children: [
5522
+ IconComponent && /* @__PURE__ */ jsxRuntime.jsx(IconComponent, { size: 11, strokeWidth: 1.75 }),
5523
+ typeInfo?.label ?? found.question.type
5524
+ ] }) }),
5525
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-sm font-medium", children: found.question.label })
5526
+ ] });
5527
+ }
5528
+ if (item.type === "section") {
5529
+ const section = schema.sections.find((s) => s.id === item.sectionId);
5530
+ if (!section) return null;
5531
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-4 py-3 rounded-md border border-primary bg-card text-foreground fcb-shadow-lg cursor-grabbing max-w-sm", children: [
5532
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-sm font-semibold", children: section.title }),
5533
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-xs text-muted-foreground mt-0.5", children: [
5534
+ section.questions.length,
5535
+ " field",
5536
+ section.questions.length !== 1 ? "s" : ""
5537
+ ] })
5538
+ ] });
5539
+ }
5540
+ return null;
5541
+ }
5542
+ function FormBuilderInner(props) {
5543
+ return /* @__PURE__ */ jsxRuntime.jsx(FormBuilderErrorBoundary, { children: /* @__PURE__ */ jsxRuntime.jsx(FormBuilderCore, { ...props }) });
5544
+ }
5545
+
5546
+ // src/form-builder/components/FormBuilderGated.tsx
5547
+ var FormBuilder = requireLicense(FormBuilderInner, "FormBuilder");
5548
+ function formatBytes(bytes, decimals = 1) {
5549
+ if (bytes === 0) return "0 B";
5550
+ const k = 1024;
5551
+ const dm = decimals < 0 ? 0 : decimals;
5552
+ const sizes = ["B", "KB", "MB", "GB", "TB"];
5553
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
5554
+ return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`;
5555
+ }
5556
+ function isFileAccepted(file, acceptList) {
5557
+ if (!acceptList || acceptList.length === 0) return true;
5558
+ const fileName = file.name.toLowerCase();
5559
+ const fileType = file.type.toLowerCase();
5560
+ return acceptList.some((pattern) => {
5561
+ const p = pattern.trim().toLowerCase();
5562
+ if (!p) return false;
5563
+ if (p === "*/*") return true;
5564
+ if (p.startsWith(".")) {
5565
+ return fileName.endsWith(p);
5566
+ }
5567
+ if (p.endsWith("/*")) {
5568
+ const baseType = p.slice(0, -2);
5569
+ return fileType.startsWith(baseType);
5570
+ }
5571
+ return fileType === p;
5572
+ });
5573
+ }
5574
+ async function decipherPresignPayload(encryptedHex, ivHex, secretKeyStr) {
5575
+ const enc = new TextEncoder();
5576
+ const rawKey = enc.encode(secretKeyStr.padEnd(32, "0").slice(0, 32));
5577
+ const cryptoObj = typeof window !== "undefined" ? window.crypto : globalThis.crypto;
5578
+ if (!cryptoObj || !cryptoObj.subtle) {
5579
+ throw new Error("Web Crypto API is not supported in this environment.");
5580
+ }
5581
+ const cryptoKey = await cryptoObj.subtle.importKey(
5582
+ "raw",
5583
+ rawKey,
5584
+ { name: "AES-GCM" },
5585
+ false,
5586
+ ["decrypt"]
5587
+ );
5588
+ let iv;
5589
+ if (/^[0-9a-fA-F]+$/.test(ivHex) && ivHex.length % 2 === 0) {
5590
+ iv = new Uint8Array(ivHex.match(/.{1,2}/g).map((b) => parseInt(b, 16)));
5591
+ } else {
5592
+ iv = Uint8Array.from(atob(ivHex), (c) => c.charCodeAt(0));
5593
+ }
5594
+ let ciphertext;
5595
+ if (/^[0-9a-fA-F]+$/.test(encryptedHex) && encryptedHex.length % 2 === 0) {
5596
+ ciphertext = new Uint8Array(encryptedHex.match(/.{1,2}/g).map((b) => parseInt(b, 16)));
5597
+ } else {
5598
+ ciphertext = Uint8Array.from(atob(encryptedHex), (c) => c.charCodeAt(0));
5599
+ }
5600
+ const decryptedBuffer = await cryptoObj.subtle.decrypt(
5601
+ { name: "AES-GCM", iv },
5602
+ cryptoKey,
5603
+ ciphertext
5604
+ );
5605
+ const plaintext = new TextDecoder().decode(decryptedBuffer);
5606
+ return JSON.parse(plaintext);
5607
+ }
5608
+ function bytesToHex(bytes) {
5609
+ let hex = "";
5610
+ for (let i = 0; i < bytes.length; i++) {
5611
+ hex += (bytes[i] ?? 0).toString(16).padStart(2, "0");
5612
+ }
5613
+ return hex;
5614
+ }
5615
+ async function generateEphemeralKeyPair() {
5616
+ const cryptoObj = typeof window !== "undefined" ? window.crypto : globalThis.crypto;
5617
+ if (!cryptoObj || !cryptoObj.subtle) {
5618
+ throw new Error("Web Crypto API is not supported in this environment.");
5619
+ }
5620
+ return await cryptoObj.subtle.generateKey(
5621
+ {
5622
+ name: "RSA-OAEP",
5623
+ modulusLength: 2048,
5624
+ publicExponent: new Uint8Array([1, 0, 1]),
5625
+ hash: "SHA-256"
5626
+ },
5627
+ true,
5628
+ ["encrypt", "decrypt"]
5629
+ );
5630
+ }
5631
+ async function exportPublicKeySpki(publicKey) {
5632
+ const cryptoObj = typeof window !== "undefined" ? window.crypto : globalThis.crypto;
5633
+ const exported = await cryptoObj.subtle.exportKey("spki", publicKey);
5634
+ const bytes = new Uint8Array(exported);
5635
+ let binary = "";
5636
+ for (let i = 0; i < bytes.byteLength; i++) {
5637
+ binary += String.fromCharCode(bytes[i] ?? 0);
5638
+ }
5639
+ return btoa(binary);
5640
+ }
5641
+ async function decryptEphemeralPayload(payload, privateKey) {
5642
+ const cryptoObj = typeof window !== "undefined" ? window.crypto : globalThis.crypto;
5643
+ const cipher = payload.encryptedPayload || payload.ciphertext;
5644
+ if (!cipher) throw new Error("Missing encryptedPayload in response");
5645
+ let ciphertext;
5646
+ if (/^[0-9a-fA-F]+$/.test(cipher) && cipher.length % 2 === 0) {
5647
+ ciphertext = new Uint8Array(cipher.match(/.{1,2}/g).map((b) => parseInt(b, 16)));
5648
+ } else {
5649
+ ciphertext = Uint8Array.from(atob(cipher), (c) => c.charCodeAt(0));
5650
+ }
5651
+ if (payload.encryptedKey && payload.iv) {
5652
+ let encKeyBytes;
5653
+ if (/^[0-9a-fA-F]+$/.test(payload.encryptedKey) && payload.encryptedKey.length % 2 === 0) {
5654
+ encKeyBytes = new Uint8Array(payload.encryptedKey.match(/.{1,2}/g).map((b) => parseInt(b, 16)));
5655
+ } else {
5656
+ encKeyBytes = Uint8Array.from(atob(payload.encryptedKey), (c) => c.charCodeAt(0));
5657
+ }
5658
+ const rawAesKey = await cryptoObj.subtle.decrypt(
5659
+ { name: "RSA-OAEP" },
5660
+ privateKey,
5661
+ encKeyBytes
5662
+ );
5663
+ const aesKey = await cryptoObj.subtle.importKey(
5664
+ "raw",
5665
+ rawAesKey,
5666
+ { name: "AES-GCM" },
5667
+ false,
5668
+ ["decrypt"]
5669
+ );
5670
+ let iv;
5671
+ if (/^[0-9a-fA-F]+$/.test(payload.iv) && payload.iv.length % 2 === 0) {
5672
+ iv = new Uint8Array(payload.iv.match(/.{1,2}/g).map((b) => parseInt(b, 16)));
5673
+ } else {
5674
+ iv = Uint8Array.from(atob(payload.iv), (c) => c.charCodeAt(0));
5675
+ }
5676
+ const decrypted2 = await cryptoObj.subtle.decrypt(
5677
+ { name: "AES-GCM", iv },
5678
+ aesKey,
5679
+ ciphertext
5680
+ );
5681
+ return JSON.parse(new TextDecoder().decode(decrypted2));
5682
+ }
5683
+ const decrypted = await cryptoObj.subtle.decrypt(
5684
+ { name: "RSA-OAEP" },
5685
+ privateKey,
5686
+ ciphertext
5687
+ );
5688
+ return JSON.parse(new TextDecoder().decode(decrypted));
5689
+ }
5690
+ async function encryptPresignPayload(data, secretKeyStr) {
5691
+ const enc = new TextEncoder();
5692
+ const rawKey = enc.encode(secretKeyStr.padEnd(32, "0").slice(0, 32));
5693
+ const cryptoObj = typeof window !== "undefined" ? window.crypto : globalThis.crypto;
5694
+ if (!cryptoObj || !cryptoObj.subtle) {
5695
+ throw new Error("Web Crypto API is not supported in this environment.");
5696
+ }
5697
+ const cryptoKey = await cryptoObj.subtle.importKey(
5698
+ "raw",
5699
+ rawKey,
5700
+ { name: "AES-GCM" },
5701
+ false,
5702
+ ["encrypt"]
5703
+ );
5704
+ const iv = cryptoObj.getRandomValues(new Uint8Array(12));
5705
+ const plaintextBytes = enc.encode(JSON.stringify(data));
5706
+ const encryptedBuffer = await cryptoObj.subtle.decrypt ? await cryptoObj.subtle.encrypt({ name: "AES-GCM", iv }, cryptoKey, plaintextBytes) : new ArrayBuffer(0);
5707
+ const encryptedPayload = bytesToHex(new Uint8Array(encryptedBuffer));
5708
+ const ivHex = bytesToHex(iv);
5709
+ return { encryptedPayload, iv: ivHex };
5710
+ }
5711
+ function ProFileUploadField(props) {
5712
+ const {
5713
+ value,
5714
+ onChange,
5715
+ onBlur,
5716
+ error,
5717
+ touched,
5718
+ disabled = false,
5719
+ customProps
5720
+ } = props;
5721
+ const field = props.field || props.question;
5722
+ const config = field?.config ?? {};
5723
+ const maxFiles = config.maxFiles ?? 1;
5724
+ const maxSizeMb = config.maxSizeMb ?? 10;
5725
+ const maxSizeBytes = maxSizeMb * 1024 * 1024;
5726
+ const acceptList = config.accept;
5727
+ const [items, setItems] = React.useState(() => {
5728
+ if (!value) return [];
5729
+ const arrayVal = Array.isArray(value) ? value : [value];
5730
+ return arrayVal.filter((v) => Boolean(v && typeof v === "object" && "url" in v)).map((v, idx) => ({
5731
+ id: `init-${idx}-${v.name}`,
5732
+ name: v.name,
5733
+ size: v.size || 0,
5734
+ type: v.type || "application/octet-stream",
5735
+ progress: 100,
5736
+ status: "succeeded",
5737
+ url: v.url,
5738
+ key: v.key
5739
+ }));
5740
+ });
5741
+ const [isDragOver, setIsDragOver] = React.useState(false);
5742
+ const [generalError, setGeneralError] = React.useState(null);
5743
+ const fileInputRef = React.useRef(null);
5744
+ const activeXhrsRef = React.useRef(/* @__PURE__ */ new Map());
5745
+ const notifyChange = React.useCallback(
5746
+ (newItems) => {
5747
+ const succeeded = newItems.filter((item) => item.status === "succeeded" && item.url).map((item) => ({
5748
+ name: item.name,
5749
+ size: item.size,
5750
+ type: item.type,
5751
+ url: item.url,
5752
+ key: item.key,
5753
+ uploadedAt: (/* @__PURE__ */ new Date()).toISOString()
5754
+ }));
5755
+ if (maxFiles === 1) {
5756
+ onChange(succeeded.length > 0 ? succeeded[0] : null);
5757
+ } else {
5758
+ onChange(succeeded);
5759
+ }
5760
+ },
5761
+ [maxFiles, onChange]
5762
+ );
5763
+ React.useEffect(() => {
5764
+ return () => {
5765
+ activeXhrsRef.current.forEach((xhr) => xhr.abort());
5766
+ activeXhrsRef.current.clear();
5767
+ };
5768
+ }, []);
5769
+ const uploadSingleFile = React.useCallback(
5770
+ async (fileState) => {
5771
+ const { file, id } = fileState;
5772
+ if (!file) return;
5773
+ const uploadEndpoint = config.uploadUrl || customProps?.uploadUrl || customProps?.presignUrl;
5774
+ if (!uploadEndpoint) {
5775
+ setItems((prev) => {
5776
+ const next = prev.map(
5777
+ (item) => item.id === id ? {
5778
+ ...item,
5779
+ progress: 0,
5780
+ status: "error",
5781
+ error: "No upload endpoint configured. Please configure storage settings or pass an uploadUrl."
5782
+ } : item
5783
+ );
5784
+ setTimeout(() => notifyChange(next), 0);
5785
+ return next;
5786
+ });
5787
+ return;
5788
+ }
5789
+ try {
5790
+ let ephemeralKeyPair = null;
5791
+ let ephemeralPublicKeyBase64 = void 0;
5792
+ try {
5793
+ const cryptoObj = typeof window !== "undefined" ? window.crypto : globalThis.crypto;
5794
+ if (cryptoObj?.subtle) {
5795
+ ephemeralKeyPair = await generateEphemeralKeyPair();
5796
+ ephemeralPublicKeyBase64 = await exportPublicKeySpki(ephemeralKeyPair.publicKey);
5797
+ }
5798
+ } catch {
5799
+ }
5800
+ const headers = {
5801
+ "Content-Type": "application/json",
5802
+ ...ephemeralPublicKeyBase64 ? { "X-Ephemeral-Public-Key": ephemeralPublicKeyBase64 } : {},
5803
+ ...config.publicKey ? { "X-Public-Key": config.publicKey } : {},
5804
+ ...config.headers || {}
5805
+ };
5806
+ const presignRes = await fetch(uploadEndpoint, {
5807
+ method: "POST",
5808
+ headers,
5809
+ body: JSON.stringify({
5810
+ filename: file.name,
5811
+ contentType: file.type || "application/octet-stream",
5812
+ size: file.size,
5813
+ ephemeralPublicKey: ephemeralPublicKeyBase64,
5814
+ publicKey: config.publicKey,
5815
+ storageProvider: config.storageProvider
5816
+ })
5817
+ });
5818
+ if (!presignRes.ok) {
5819
+ const errData = await presignRes.json().catch(() => null);
5820
+ throw new Error(
5821
+ errData?.message || `Presign request failed with status ${presignRes.status}`
5822
+ );
5823
+ }
5824
+ let presignData = await presignRes.json();
5825
+ if (ephemeralKeyPair && (presignData.encryptedPayload || presignData.ciphertext)) {
5826
+ presignData = await decryptEphemeralPayload(presignData, ephemeralKeyPair.privateKey);
5827
+ } else if (presignData.encryptedPayload || presignData.ciphertext) {
5828
+ const cipher = presignData.encryptedPayload || presignData.ciphertext;
5829
+ const iv = presignData.iv || presignData.nonce;
5830
+ const key2 = config.publicKey || customProps?.publicKey;
5831
+ if (key2) {
5832
+ presignData = await decipherPresignPayload(cipher, iv, key2);
5833
+ } else {
5834
+ throw new Error(
5835
+ "Cannot decipher encrypted upload payload: Missing public key in question configuration."
5836
+ );
5837
+ }
5838
+ }
5839
+ const uploadUrl = presignData.uploadUrl || presignData.url || presignData.signedUrl;
5840
+ const fileUrl = presignData.fileUrl || presignData.publicUrl || uploadUrl.split("?")[0];
5841
+ const key = presignData.key || presignData.fileKey;
5842
+ if (!uploadUrl) {
5843
+ throw new Error("Presign response did not contain an uploadUrl");
5844
+ }
5845
+ const xhr = new XMLHttpRequest();
5846
+ activeXhrsRef.current.set(id, xhr);
5847
+ xhr.upload.onprogress = (e) => {
5848
+ if (e.lengthComputable) {
5849
+ const pct = Math.round(e.loaded / e.total * 100);
5850
+ setItems(
5851
+ (prev) => prev.map((item) => item.id === id ? { ...item, progress: pct } : item)
5852
+ );
5853
+ }
5854
+ };
5855
+ xhr.onload = () => {
5856
+ activeXhrsRef.current.delete(id);
5857
+ if (xhr.status >= 200 && xhr.status < 300) {
5858
+ let finalUrl = fileUrl;
5859
+ let finalKey = key;
5860
+ try {
5861
+ if (xhr.responseText) {
5862
+ const resp = JSON.parse(xhr.responseText);
5863
+ if (resp.secure_url) finalUrl = resp.secure_url;
5864
+ else if (resp.url && !resp.url.includes("?")) finalUrl = resp.url;
5865
+ if (resp.public_id) finalKey = resp.public_id;
5866
+ else if (resp.key) finalKey = resp.key;
5867
+ }
5868
+ } catch {
5869
+ }
5870
+ setItems((prev) => {
5871
+ const next = prev.map(
5872
+ (item) => item.id === id ? {
5873
+ ...item,
5874
+ progress: 100,
5875
+ status: "succeeded",
5876
+ url: finalUrl,
5877
+ key: finalKey
5878
+ } : item
5879
+ );
5880
+ setTimeout(() => notifyChange(next), 0);
5881
+ return next;
5882
+ });
5883
+ } else {
5884
+ setItems(
5885
+ (prev) => prev.map(
5886
+ (item) => item.id === id ? {
5887
+ ...item,
5888
+ status: "error",
5889
+ error: `Upload failed (Status ${xhr.status})`
5890
+ } : item
5891
+ )
5892
+ );
5893
+ }
5894
+ };
5895
+ xhr.onerror = () => {
5896
+ activeXhrsRef.current.delete(id);
5897
+ setItems(
5898
+ (prev) => prev.map(
5899
+ (item) => item.id === id ? { ...item, status: "error", error: "Network error during upload" } : item
5900
+ )
5901
+ );
5902
+ };
5903
+ if (presignData.fields && typeof presignData.fields === "object") {
5904
+ const formData = new FormData();
5905
+ Object.entries(presignData.fields).forEach(([k, v]) => {
5906
+ formData.append(k, String(v));
5907
+ });
5908
+ formData.append("file", file);
5909
+ xhr.open("POST", uploadUrl);
5910
+ xhr.send(formData);
5911
+ } else {
5912
+ xhr.open("PUT", uploadUrl);
5913
+ xhr.setRequestHeader("Content-Type", file.type || "application/octet-stream");
5914
+ if (presignData.headers && typeof presignData.headers === "object") {
5915
+ Object.entries(presignData.headers).forEach(([k, v]) => {
5916
+ xhr.setRequestHeader(k, String(v));
5917
+ });
5918
+ }
5919
+ xhr.send(file);
5920
+ }
5921
+ } catch (err) {
5922
+ setItems(
5923
+ (prev) => prev.map(
5924
+ (item) => item.id === id ? {
5925
+ ...item,
5926
+ status: "error",
5927
+ error: err.message || "Failed to initialize upload"
5928
+ } : item
5929
+ )
5930
+ );
5931
+ }
5932
+ },
5933
+ [config, customProps]
5934
+ );
5935
+ const handleFilesAdded = React.useCallback(
5936
+ (files) => {
5937
+ setGeneralError(null);
5938
+ const incoming = Array.from(files);
5939
+ if (incoming.length === 0) return;
5940
+ const currentCount = items.filter((i) => i.status !== "error").length;
5941
+ if (currentCount + incoming.length > maxFiles) {
5942
+ setGeneralError(`You can upload a maximum of ${maxFiles} file${maxFiles === 1 ? "" : "s"}.`);
5943
+ return;
5944
+ }
5945
+ const validFiles = [];
5946
+ for (const file of incoming) {
5947
+ if (!isFileAccepted(file, acceptList)) {
5948
+ setGeneralError(`File "${file.name}" has an unsupported format.`);
5949
+ return;
5950
+ }
5951
+ if (file.size > maxSizeBytes) {
5952
+ setGeneralError(
5953
+ `File "${file.name}" exceeds the maximum size limit of ${maxSizeMb} MB.`
5954
+ );
5955
+ return;
5956
+ }
5957
+ const newId = `file-${Date.now()}-${Math.random().toString(36).substring(2, 8)}`;
5958
+ validFiles.push({
5959
+ id: newId,
5960
+ file,
5961
+ name: file.name,
5962
+ size: file.size,
5963
+ type: file.type,
5964
+ progress: 0,
5965
+ status: "uploading"
5966
+ });
5967
+ }
5968
+ setItems((prev) => {
5969
+ const next = maxFiles === 1 ? validFiles : [...prev, ...validFiles];
5970
+ return next;
5971
+ });
5972
+ validFiles.forEach((f) => uploadSingleFile(f));
5973
+ },
5974
+ [acceptList, items, maxFiles, maxSizeBytes, maxSizeMb, uploadSingleFile]
5975
+ );
5976
+ const removeItem = React.useCallback(
5977
+ (id) => {
5978
+ const activeXhr = activeXhrsRef.current.get(id);
5979
+ if (activeXhr) {
5980
+ activeXhr.abort();
5981
+ activeXhrsRef.current.delete(id);
5982
+ }
5983
+ setItems((prev) => {
5984
+ const next = prev.filter((item) => item.id !== id);
5985
+ setTimeout(() => notifyChange(next), 0);
5986
+ return next;
5987
+ });
5988
+ },
5989
+ [notifyChange]
5990
+ );
5991
+ const retryItem = React.useCallback(
5992
+ (id) => {
5993
+ const target = items.find((item) => item.id === id);
5994
+ if (!target || !target.file) return;
5995
+ setItems(
5996
+ (prev) => prev.map(
5997
+ (item) => item.id === id ? { ...item, progress: 0, status: "uploading", error: void 0 } : item
5998
+ )
5999
+ );
6000
+ uploadSingleFile({ ...target, progress: 0, status: "uploading", error: void 0 });
6001
+ },
6002
+ [items, uploadSingleFile]
6003
+ );
6004
+ const canAddMore = items.length < maxFiles && !disabled;
6005
+ const combinedError = error || generalError ? [
6006
+ ...Array.isArray(error) ? error : error ? [String(error)] : [],
6007
+ ...generalError ? [generalError] : []
6008
+ ] : void 0;
6009
+ const c = props.theme?.colors;
6010
+ const inputRadius = props.theme?.shape?.inputRadius || "8px";
6011
+ const dropzoneStyle = {
6012
+ borderRadius: inputRadius,
6013
+ ...c?.border && !isDragOver ? { borderColor: c.border } : {},
6014
+ ...c?.surface ? { backgroundColor: isDragOver ? `${c.primary || "#3b82f6"}15` : c.surface } : {},
6015
+ ...c?.text ? { color: c.text } : {}
6016
+ };
6017
+ const cardStyle = {
6018
+ borderRadius: inputRadius,
6019
+ ...c?.border ? { borderColor: c.border } : {},
6020
+ ...c?.surface ? { backgroundColor: c.surface } : {},
6021
+ ...c?.text ? { color: c.text } : {}
6022
+ };
6023
+ return /* @__PURE__ */ jsxRuntime.jsx(
6024
+ fieldcraftReact.FieldWrapper,
6025
+ {
6026
+ field,
6027
+ error: combinedError,
6028
+ touched: touched || Boolean(generalError),
6029
+ children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-3 font-sans", children: [
6030
+ /* @__PURE__ */ jsxRuntime.jsx(
6031
+ "input",
6032
+ {
6033
+ ref: fileInputRef,
6034
+ type: "file",
6035
+ multiple: maxFiles > 1,
6036
+ accept: acceptList?.join(","),
6037
+ className: "hidden",
6038
+ disabled,
6039
+ onChange: (e) => {
6040
+ if (e.target.files) {
6041
+ handleFilesAdded(e.target.files);
6042
+ e.target.value = "";
6043
+ }
6044
+ },
6045
+ onBlur
6046
+ }
6047
+ ),
6048
+ canAddMore && /* @__PURE__ */ jsxRuntime.jsx(
6049
+ "div",
6050
+ {
6051
+ style: dropzoneStyle,
6052
+ onDragOver: (e) => {
6053
+ e.preventDefault();
6054
+ if (!disabled) setIsDragOver(true);
6055
+ },
6056
+ onDragLeave: () => setIsDragOver(false),
6057
+ onDrop: (e) => {
6058
+ e.preventDefault();
6059
+ setIsDragOver(false);
6060
+ if (!disabled && e.dataTransfer.files) {
6061
+ handleFilesAdded(e.dataTransfer.files);
6062
+ }
6063
+ },
6064
+ onClick: () => {
6065
+ if (!disabled) fileInputRef.current?.click();
6066
+ },
6067
+ className: `
6068
+ relative flex flex-col items-center justify-center p-6 border-2 border-dashed rounded-xl cursor-pointer transition-all duration-200
6069
+ ${isDragOver ? "border-primary bg-primary/5 scale-[0.99]" : "border-muted-foreground/25 hover:border-primary/60 hover:bg-muted/30 bg-muted/10"}
6070
+ ${disabled ? "opacity-50 cursor-not-allowed" : ""}
6071
+ `,
6072
+ children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col items-center text-center space-y-2", children: [
6073
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-3 bg-primary/10 text-primary rounded-full", children: /* @__PURE__ */ jsxRuntime.jsx(
6074
+ "svg",
6075
+ {
6076
+ xmlns: "http://www.w3.org/2000/svg",
6077
+ className: "w-6 h-6",
6078
+ fill: "none",
6079
+ viewBox: "0 0 24 24",
6080
+ stroke: "currentColor",
6081
+ children: /* @__PURE__ */ jsxRuntime.jsx(
6082
+ "path",
6083
+ {
6084
+ strokeLinecap: "round",
6085
+ strokeLinejoin: "round",
6086
+ strokeWidth: 2,
6087
+ d: "M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"
6088
+ }
6089
+ )
6090
+ }
6091
+ ) }),
6092
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-sm", children: [
6093
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-semibold text-foreground", children: "Click to upload" }),
6094
+ " ",
6095
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground", children: "or drag and drop" })
6096
+ ] }),
6097
+ /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-xs text-muted-foreground", children: [
6098
+ acceptList && acceptList.length > 0 ? `Supported: ${acceptList.join(", ")}` : "All file types supported",
6099
+ " ",
6100
+ "(Max ",
6101
+ maxSizeMb,
6102
+ " MB)"
6103
+ ] })
6104
+ ] })
6105
+ }
6106
+ ),
6107
+ items.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "space-y-2", children: items.map((item) => {
6108
+ const isImage = item.type.startsWith("image/") && item.url;
6109
+ return /* @__PURE__ */ jsxRuntime.jsxs(
6110
+ "div",
6111
+ {
6112
+ style: cardStyle,
6113
+ className: "flex items-center justify-between p-3 bg-card border rounded-lg shadow-sm text-sm",
6114
+ children: [
6115
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center space-x-3 min-w-0 flex-1 mr-3", children: [
6116
+ isImage ? /* @__PURE__ */ jsxRuntime.jsx(
6117
+ "img",
6118
+ {
6119
+ src: item.url,
6120
+ alt: item.name,
6121
+ className: "w-10 h-10 rounded object-cover shrink-0 border"
6122
+ }
6123
+ ) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-10 h-10 rounded bg-muted flex items-center justify-center shrink-0 text-muted-foreground font-mono text-xs uppercase font-bold", children: item.name.split(".").pop()?.slice(0, 4) || "FILE" }),
6124
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "min-w-0 flex-1", children: [
6125
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between", children: [
6126
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm font-medium text-foreground truncate", children: item.name }),
6127
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-muted-foreground shrink-0 ml-2", children: formatBytes(item.size) })
6128
+ ] }),
6129
+ item.status === "uploading" && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-1.5 w-full bg-muted rounded-full h-1.5 overflow-hidden", children: /* @__PURE__ */ jsxRuntime.jsx(
6130
+ "div",
6131
+ {
6132
+ className: "bg-primary h-1.5 rounded-full transition-all duration-150",
6133
+ style: { width: `${item.progress}%` }
6134
+ }
6135
+ ) }),
6136
+ item.status === "error" && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-destructive mt-0.5", children: item.error || "Upload failed" }),
6137
+ item.status === "succeeded" && /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-xs text-emerald-600 dark:text-emerald-400 mt-0.5 flex items-center gap-1", children: [
6138
+ /* @__PURE__ */ jsxRuntime.jsx("svg", { className: "w-3.5 h-3.5", viewBox: "0 0 20 20", fill: "currentColor", children: /* @__PURE__ */ jsxRuntime.jsx(
6139
+ "path",
6140
+ {
6141
+ fillRule: "evenodd",
6142
+ d: "M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z",
6143
+ clipRule: "evenodd"
6144
+ }
6145
+ ) }),
6146
+ "Uploaded"
6147
+ ] })
6148
+ ] })
6149
+ ] }),
6150
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center space-x-1.5 shrink-0", children: [
6151
+ item.status === "error" && /* @__PURE__ */ jsxRuntime.jsx(
6152
+ "button",
6153
+ {
6154
+ type: "button",
6155
+ onClick: () => retryItem(item.id),
6156
+ className: "p-1 text-xs text-primary hover:underline",
6157
+ title: "Retry upload",
6158
+ children: "Retry"
6159
+ }
6160
+ ),
6161
+ item.status === "succeeded" && item.url && /* @__PURE__ */ jsxRuntime.jsx(
6162
+ "a",
6163
+ {
6164
+ href: item.url,
6165
+ target: "_blank",
6166
+ rel: "noreferrer",
6167
+ className: "p-1.5 text-muted-foreground hover:text-foreground rounded transition-colors",
6168
+ title: "View file",
6169
+ children: /* @__PURE__ */ jsxRuntime.jsx("svg", { className: "w-4 h-4", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", children: /* @__PURE__ */ jsxRuntime.jsx(
6170
+ "path",
6171
+ {
6172
+ strokeLinecap: "round",
6173
+ strokeLinejoin: "round",
6174
+ strokeWidth: 2,
6175
+ d: "M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
6176
+ }
6177
+ ) })
6178
+ }
6179
+ ),
6180
+ !disabled && /* @__PURE__ */ jsxRuntime.jsx(
6181
+ "button",
6182
+ {
6183
+ type: "button",
6184
+ onClick: () => removeItem(item.id),
6185
+ className: "p-1.5 text-muted-foreground hover:text-destructive rounded transition-colors",
6186
+ title: "Remove file",
6187
+ children: /* @__PURE__ */ jsxRuntime.jsx("svg", { className: "w-4 h-4", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", children: /* @__PURE__ */ jsxRuntime.jsx(
6188
+ "path",
6189
+ {
6190
+ strokeLinecap: "round",
6191
+ strokeLinejoin: "round",
6192
+ strokeWidth: 2,
6193
+ d: "M6 18L18 6M6 6l12 12"
6194
+ }
6195
+ ) })
6196
+ }
6197
+ )
6198
+ ] })
6199
+ ]
6200
+ },
6201
+ item.id
6202
+ );
6203
+ }) })
6204
+ ] })
6205
+ }
6206
+ );
6207
+ }
6208
+ var ProFileUploadField_default = ProFileUploadField;
6209
+ function extractAttendeeInfo(fieldValues) {
6210
+ if (!fieldValues) return {};
6211
+ let name;
6212
+ let email;
6213
+ for (const [key, val] of Object.entries(fieldValues)) {
6214
+ const k = key.toLowerCase();
6215
+ if (!name && (k.includes("name") || k === "fullname" || k === "first_name")) {
6216
+ if (typeof val === "string" && val.trim()) {
6217
+ name = val.trim();
6218
+ } else if (val && typeof val === "object" && "first" in val) {
6219
+ const obj = val;
6220
+ name = [obj.first, obj.middle, obj.last].filter(Boolean).join(" ").trim();
6221
+ }
6222
+ }
6223
+ if (!email && (k.includes("email") || k.includes("mail"))) {
6224
+ if (typeof val === "string" && val.includes("@")) {
6225
+ email = val.trim();
6226
+ }
6227
+ }
6228
+ }
6229
+ return { name, email };
6230
+ }
6231
+ function formatTime12h(timeStr) {
6232
+ const [hStr, mStr] = timeStr.split(":");
6233
+ const h = parseInt(hStr, 10);
6234
+ if (isNaN(h)) return timeStr;
6235
+ const ampm = h >= 12 ? "PM" : "AM";
6236
+ const h12 = h % 12 || 12;
6237
+ return `${h12}:${mStr || "00"} ${ampm}`;
6238
+ }
6239
+ function ProAppointmentField(props) {
6240
+ const {
6241
+ value,
6242
+ onChange,
6243
+ onBlur,
6244
+ error,
6245
+ touched,
6246
+ disabled = false,
6247
+ fieldValues,
6248
+ customProps
6249
+ } = props;
6250
+ const onNext = customProps?.onNext || props.onNext;
6251
+ const field = props.field || props.question;
6252
+ const config = field?.config ?? {};
6253
+ const provider = React.useMemo(() => {
6254
+ if (config.embedUrl) {
6255
+ if (config.embedProvider === "calendly" || config.embedUrl.includes("calendly.com")) {
6256
+ return "calendly";
6257
+ }
6258
+ return "cal_com";
6259
+ }
6260
+ return "slots";
6261
+ }, [config.embedUrl, config.embedProvider]);
6262
+ const booking = value ?? null;
6263
+ const isConfirmed = booking?.status === "confirmed";
6264
+ const resolvedTimezone = React.useMemo(() => {
6265
+ if (config.timezoneField && fieldValues?.[config.timezoneField]) {
6266
+ const dynamicVal = String(fieldValues[config.timezoneField]);
6267
+ if (dynamicVal.trim()) return dynamicVal.trim();
6268
+ }
6269
+ if (config.timezone) return config.timezone;
6270
+ try {
6271
+ return Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
6272
+ } catch {
6273
+ return "UTC";
6274
+ }
6275
+ }, [config.timezone, config.timezoneField, fieldValues]);
6276
+ const [selectedDate, setSelectedDate] = React.useState(() => {
6277
+ if (booking?.date) return booking.date;
6278
+ if (config.slots && config.slots.length > 0) return config.slots[0].date;
6279
+ return (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
6280
+ });
6281
+ const [selectedTime, setSelectedTime] = React.useState(() => booking?.time || null);
6282
+ const [remoteSlots, setRemoteSlots] = React.useState(config.slots || []);
6283
+ const [isLoadingSlots, setIsLoadingSlots] = React.useState(false);
6284
+ const [slotsFetchError, setSlotsFetchError] = React.useState(null);
6285
+ React.useEffect(() => {
6286
+ if (provider !== "slots" || !config.slotsUrl) return;
6287
+ let isMounted = true;
6288
+ setIsLoadingSlots(true);
6289
+ setSlotsFetchError(null);
6290
+ fetch(config.slotsUrl).then((res) => {
6291
+ if (!res.ok) throw new Error(`Slots API returned status ${res.status}`);
6292
+ return res.json();
6293
+ }).then((data) => {
6294
+ if (isMounted) {
6295
+ const slotsList = Array.isArray(data) ? data : data.slots || [];
6296
+ setRemoteSlots(slotsList);
6297
+ if (slotsList.length > 0 && !selectedDate) {
6298
+ setSelectedDate(slotsList[0].date);
6299
+ }
6300
+ setIsLoadingSlots(false);
6301
+ }
6302
+ }).catch((err) => {
6303
+ if (isMounted) {
6304
+ setSlotsFetchError(err.message || "Failed to load available slots");
6305
+ setIsLoadingSlots(false);
6306
+ }
6307
+ });
6308
+ return () => {
6309
+ isMounted = false;
6310
+ };
6311
+ }, [config.slotsUrl, provider, selectedDate]);
6312
+ const embedSrc = React.useMemo(() => {
6313
+ if (!config.embedUrl) return "";
6314
+ try {
6315
+ const url = new URL(config.embedUrl);
6316
+ const { name, email } = extractAttendeeInfo(fieldValues);
6317
+ if (name && !url.searchParams.has("name")) url.searchParams.set("name", name);
6318
+ if (email && !url.searchParams.has("email")) url.searchParams.set("email", email);
6319
+ if (resolvedTimezone && !url.searchParams.has("timezone")) {
6320
+ url.searchParams.set("timezone", resolvedTimezone);
6321
+ }
6322
+ if (provider === "cal_com") {
6323
+ if (!url.searchParams.has("embed")) url.searchParams.set("embed", "true");
6324
+ if (!url.searchParams.has("layout")) url.searchParams.set("layout", "month_view");
6325
+ } else if (provider === "calendly") {
6326
+ if (!url.searchParams.has("embed_type")) url.searchParams.set("embed_type", "Inline");
6327
+ }
6328
+ return url.toString();
6329
+ } catch {
6330
+ return config.embedUrl;
6331
+ }
6332
+ }, [config.embedUrl, fieldValues, provider, resolvedTimezone]);
6333
+ React.useEffect(() => {
6334
+ const handleMessage = (event) => {
6335
+ let data = event.data;
6336
+ if (!data) return;
6337
+ if (typeof data === "string") {
6338
+ try {
6339
+ data = JSON.parse(data);
6340
+ } catch {
6341
+ }
6342
+ }
6343
+ if (typeof data !== "object" || data === null) return;
6344
+ if (data.type === "cal:bookingSuccessful" || data.origin === "CAL" && data.action === "bookingSuccessful" || data.event === "cal:bookingSuccessful") {
6345
+ const payload = data.data || data.payload || {};
6346
+ const newBooking = {
6347
+ provider: "cal_com",
6348
+ status: "confirmed",
6349
+ bookingId: String(payload.bookingId || payload.id || `cal-${Date.now()}`),
6350
+ date: payload.date || payload.startTime?.split("T")[0],
6351
+ time: payload.time || payload.startTime?.split("T")[1]?.slice(0, 5),
6352
+ duration: payload.duration || config.duration || 30,
6353
+ timezone: payload.timezone || resolvedTimezone,
6354
+ eventTitle: payload.eventTitle || payload.title,
6355
+ attendeeName: payload.name || payload.attendeeName,
6356
+ attendeeEmail: payload.email || payload.attendeeEmail,
6357
+ confirmedAt: (/* @__PURE__ */ new Date()).toISOString()
6358
+ };
6359
+ onChange(newBooking);
6360
+ if (config.autoAdvance !== false && typeof onNext === "function") {
6361
+ setTimeout(() => onNext(), 400);
6362
+ }
6363
+ }
6364
+ if (data.event === "calendly.event_scheduled" || data.action === "calendly.event_scheduled" || data.type === "calendly.event_scheduled" || data.event === "event_scheduled") {
6365
+ const payload = data.payload || data.data || {};
6366
+ const newBooking = {
6367
+ provider: "calendly",
6368
+ status: "confirmed",
6369
+ eventUri: payload.event?.uri,
6370
+ inviteeUri: payload.invitee?.uri,
6371
+ bookingId: payload.event?.uri ? String(payload.event.uri).split("/").pop() : `cal-${Date.now()}`,
6372
+ duration: config.duration || 30,
6373
+ timezone: resolvedTimezone,
6374
+ confirmedAt: (/* @__PURE__ */ new Date()).toISOString()
6375
+ };
6376
+ onChange(newBooking);
6377
+ if (config.autoAdvance !== false && typeof onNext === "function") {
6378
+ setTimeout(() => onNext(), 400);
6379
+ }
6380
+ }
6381
+ };
6382
+ window.addEventListener("message", handleMessage);
6383
+ return () => window.removeEventListener("message", handleMessage);
6384
+ }, [config.autoAdvance, config.duration, onChange, onNext, resolvedTimezone]);
6385
+ const handleSlotSelect = React.useCallback(
6386
+ (time) => {
6387
+ setSelectedTime(time);
6388
+ const newBooking = {
6389
+ provider: "slots",
6390
+ status: "confirmed",
6391
+ date: selectedDate,
6392
+ time,
6393
+ duration: config.duration || 30,
6394
+ timezone: resolvedTimezone,
6395
+ confirmedAt: (/* @__PURE__ */ new Date()).toISOString()
6396
+ };
6397
+ onChange(newBooking);
6398
+ },
6399
+ [config.duration, onChange, resolvedTimezone, selectedDate]
6400
+ );
6401
+ const handleResetBooking = React.useCallback(() => {
6402
+ onChange(null);
6403
+ setSelectedTime(null);
6404
+ }, [onChange]);
6405
+ const availableTimes = React.useMemo(() => {
6406
+ const dayMatch = remoteSlots.find((s) => s.date === selectedDate);
6407
+ if (dayMatch && Array.isArray(dayMatch.times) && dayMatch.times.length > 0) {
6408
+ return dayMatch.times;
6409
+ }
6410
+ return [
6411
+ "09:00",
6412
+ "09:30",
6413
+ "10:00",
6414
+ "10:30",
6415
+ "11:00",
6416
+ "11:30",
6417
+ "13:00",
6418
+ "13:30",
6419
+ "14:00",
6420
+ "14:30",
6421
+ "15:00",
6422
+ "15:30",
6423
+ "16:00"
6424
+ ];
6425
+ }, [remoteSlots, selectedDate]);
6426
+ const combinedError = error || slotsFetchError ? [
6427
+ ...Array.isArray(error) ? error : error ? [String(error)] : [],
6428
+ ...slotsFetchError ? [slotsFetchError] : []
6429
+ ] : void 0;
6430
+ const c = props.theme?.colors;
6431
+ const inputRadius = props.theme?.shape?.inputRadius || "8px";
6432
+ const cardStyle = {
6433
+ borderRadius: inputRadius,
6434
+ ...c?.border ? { borderColor: c.border } : {},
6435
+ ...c?.surface ? { backgroundColor: c.surface } : {},
6436
+ ...c?.text ? { color: c.text } : {}
6437
+ };
6438
+ const activeSlotStyle = {
6439
+ borderRadius: inputRadius,
6440
+ ...c?.primary ? { backgroundColor: c.primary, borderColor: c.primary } : {},
6441
+ ...c?.primaryForeground ? { color: c.primaryForeground } : {}
6442
+ };
6443
+ const inactiveSlotStyle = {
6444
+ borderRadius: inputRadius,
6445
+ ...c?.border ? { borderColor: c.border } : {},
6446
+ ...c?.surface ? { backgroundColor: c.surface } : {},
6447
+ ...c?.text ? { color: c.text } : {}
6448
+ };
6449
+ return /* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.FieldWrapper, { field, error: combinedError, touched: touched || Boolean(slotsFetchError), children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-4 font-sans", children: [
6450
+ isConfirmed && booking && /* @__PURE__ */ jsxRuntime.jsxs(
6451
+ "div",
6452
+ {
6453
+ style: cardStyle,
6454
+ className: "p-5 border border-emerald-500/30 bg-emerald-500/5 rounded-xl text-foreground space-y-3",
6455
+ children: [
6456
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between", children: [
6457
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center space-x-2.5", children: [
6458
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-2 bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 rounded-full", children: /* @__PURE__ */ jsxRuntime.jsx("svg", { className: "w-5 h-5", viewBox: "0 0 20 20", fill: "currentColor", children: /* @__PURE__ */ jsxRuntime.jsx(
6459
+ "path",
6460
+ {
6461
+ fillRule: "evenodd",
6462
+ d: "M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z",
6463
+ clipRule: "evenodd"
6464
+ }
6465
+ ) }) }),
6466
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
6467
+ /* @__PURE__ */ jsxRuntime.jsx("h4", { className: "text-sm font-semibold text-foreground", children: "Appointment Confirmed" }),
6468
+ /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-xs text-muted-foreground capitalize", children: [
6469
+ "via ",
6470
+ booking.provider.replace("_", ".")
6471
+ ] })
6472
+ ] })
6473
+ ] }),
6474
+ !disabled && /* @__PURE__ */ jsxRuntime.jsx(
6475
+ "button",
6476
+ {
6477
+ type: "button",
6478
+ onClick: handleResetBooking,
6479
+ className: "text-xs text-muted-foreground hover:text-foreground underline transition-colors",
6480
+ children: "Reschedule"
6481
+ }
6482
+ )
6483
+ ] }),
6484
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-2 gap-2 text-xs pt-1 border-t border-emerald-500/20", children: [
6485
+ booking.date && /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
6486
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground", children: "Date:" }),
6487
+ " ",
6488
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium text-foreground", children: booking.date })
6489
+ ] }),
6490
+ booking.time && /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
6491
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground", children: "Time:" }),
6492
+ " ",
6493
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium text-foreground", children: formatTime12h(booking.time) })
6494
+ ] }),
6495
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
6496
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground", children: "Timezone:" }),
6497
+ " ",
6498
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium text-foreground", children: booking.timezone || resolvedTimezone })
6499
+ ] }),
6500
+ booking.duration && /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
6501
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground", children: "Duration:" }),
6502
+ " ",
6503
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "font-medium text-foreground", children: [
6504
+ booking.duration,
6505
+ " mins"
6506
+ ] })
6507
+ ] })
6508
+ ] }),
6509
+ typeof onNext === "function" && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "pt-2 border-t border-emerald-500/20 flex justify-end", children: /* @__PURE__ */ jsxRuntime.jsxs(
6510
+ "button",
6511
+ {
6512
+ type: "button",
6513
+ onClick: onNext,
6514
+ className: "px-3.5 py-1.5 text-xs font-semibold rounded-lg bg-emerald-600 hover:bg-emerald-500 text-white transition-colors flex items-center gap-1",
6515
+ children: [
6516
+ "Continue",
6517
+ /* @__PURE__ */ jsxRuntime.jsx("svg", { className: "w-3.5 h-3.5", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", children: /* @__PURE__ */ jsxRuntime.jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M9 5l7 7-7 7" }) })
6518
+ ]
6519
+ }
6520
+ ) })
6521
+ ]
6522
+ }
6523
+ ),
6524
+ !isConfirmed && (provider === "cal_com" || provider === "calendly") && embedSrc && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-3", children: [
6525
+ /* @__PURE__ */ jsxRuntime.jsx(
6526
+ "div",
6527
+ {
6528
+ style: cardStyle,
6529
+ className: "w-full border rounded-xl overflow-hidden shadow-sm bg-card",
6530
+ children: /* @__PURE__ */ jsxRuntime.jsx(
6531
+ "iframe",
6532
+ {
6533
+ src: embedSrc,
6534
+ title: "Schedule Appointment",
6535
+ className: "w-full h-[580px] border-0",
6536
+ allow: "camera; microphone; autoplay; fullscreen; payment",
6537
+ loading: "lazy"
6538
+ }
6539
+ )
6540
+ }
6541
+ ),
6542
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col sm:flex-row sm:items-center justify-between gap-2 text-xs text-muted-foreground px-1", children: [
6543
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: "Finished scheduling in the calendar above?" }),
6544
+ /* @__PURE__ */ jsxRuntime.jsx(
6545
+ "button",
6546
+ {
6547
+ type: "button",
6548
+ onClick: () => {
6549
+ const manualBooking = {
6550
+ provider,
6551
+ status: "confirmed",
6552
+ bookingId: `manual-${Date.now()}`,
6553
+ timezone: resolvedTimezone,
6554
+ confirmedAt: (/* @__PURE__ */ new Date()).toISOString()
6555
+ };
6556
+ onChange(manualBooking);
6557
+ if (typeof onNext === "function") {
6558
+ setTimeout(() => onNext(), 300);
6559
+ }
6560
+ },
6561
+ className: "px-3 py-1.5 font-medium rounded-lg border border-border bg-background hover:bg-muted text-foreground transition-colors flex items-center justify-center gap-1 shadow-sm self-end sm:self-auto",
6562
+ children: "\u2713 I've Completed My Booking"
5238
6563
  }
5239
6564
  )
5240
- ]
5241
- }
5242
- ) });
5243
- }
5244
- function DragOverlayContent({
5245
- item,
5246
- schema,
5247
- questionTypes: types
5248
- }) {
5249
- if (item.type === "palette-item") {
5250
- const typeInfo = types[item.questionType];
5251
- if (!typeInfo) return null;
5252
- const IconComponent = getIcon(typeInfo.icon);
5253
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3 px-3 py-2 rounded-md border border-primary bg-card text-foreground text-sm fcb-shadow-lg cursor-grabbing", children: [
5254
- /* @__PURE__ */ jsxRuntime.jsx(IconComponent, { size: 14, className: "shrink-0 text-primary", strokeWidth: 1.75 }),
5255
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium", children: typeInfo.label })
5256
- ] });
5257
- }
5258
- if (item.type === "question") {
5259
- const found = findQuestion(schema, item.sectionId, item.questionId);
5260
- if (!found) return null;
5261
- const typeInfo = types[found.question.type];
5262
- const IconComponent = typeInfo ? getIcon(typeInfo.icon) : null;
5263
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "p-3 rounded-md border border-primary bg-card text-foreground fcb-shadow-lg cursor-grabbing max-w-sm", children: [
5264
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center gap-2 mb-1", children: /* @__PURE__ */ jsxRuntime.jsxs(fieldcraftReact.Badge, { variant: "secondary", className: "gap-1.5 text-muted-foreground", children: [
5265
- IconComponent && /* @__PURE__ */ jsxRuntime.jsx(IconComponent, { size: 11, strokeWidth: 1.75 }),
5266
- typeInfo?.label ?? found.question.type
5267
- ] }) }),
5268
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-sm font-medium", children: found.question.label })
5269
- ] });
5270
- }
5271
- if (item.type === "section") {
5272
- const section = schema.sections.find((s) => s.id === item.sectionId);
5273
- if (!section) return null;
5274
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-4 py-3 rounded-md border border-primary bg-card text-foreground fcb-shadow-lg cursor-grabbing max-w-sm", children: [
5275
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-sm font-semibold", children: section.title }),
5276
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-xs text-muted-foreground mt-0.5", children: [
5277
- section.questions.length,
5278
- " field",
5279
- section.questions.length !== 1 ? "s" : ""
5280
6565
  ] })
5281
- ] });
5282
- }
5283
- return null;
5284
- }
5285
- function FormBuilderInner(props) {
5286
- return /* @__PURE__ */ jsxRuntime.jsx(FormBuilderErrorBoundary, { children: /* @__PURE__ */ jsxRuntime.jsx(FormBuilderCore, { ...props }) });
6566
+ ] }),
6567
+ !isConfirmed && provider === "slots" && /* @__PURE__ */ jsxRuntime.jsxs(
6568
+ "div",
6569
+ {
6570
+ style: cardStyle,
6571
+ className: "space-y-4 p-4 border rounded-xl bg-card shadow-sm",
6572
+ children: [
6573
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col sm:flex-row sm:items-center justify-between gap-3 pb-3 border-b", children: [
6574
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
6575
+ /* @__PURE__ */ jsxRuntime.jsx("label", { className: "text-xs font-semibold text-foreground block mb-1", children: "Select Date" }),
6576
+ /* @__PURE__ */ jsxRuntime.jsx(
6577
+ "input",
6578
+ {
6579
+ type: "date",
6580
+ value: selectedDate,
6581
+ min: (/* @__PURE__ */ new Date()).toISOString().split("T")[0],
6582
+ disabled,
6583
+ onChange: (e) => setSelectedDate(e.target.value),
6584
+ onBlur,
6585
+ className: "px-3 py-1.5 text-sm border rounded-lg bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-primary/40"
6586
+ }
6587
+ )
6588
+ ] }),
6589
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-xs text-muted-foreground flex items-center gap-1.5 self-start sm:self-auto", children: [
6590
+ /* @__PURE__ */ jsxRuntime.jsx("svg", { className: "w-4 h-4 text-muted-foreground/70", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", children: /* @__PURE__ */ jsxRuntime.jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" }) }),
6591
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
6592
+ "Timezone: ",
6593
+ /* @__PURE__ */ jsxRuntime.jsx("strong", { className: "text-foreground", children: resolvedTimezone })
6594
+ ] })
6595
+ ] })
6596
+ ] }),
6597
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2", children: [
6598
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-xs font-semibold text-foreground block", children: [
6599
+ "Available Times (",
6600
+ config.duration || 30,
6601
+ " mins)"
6602
+ ] }),
6603
+ isLoadingSlots ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "py-8 text-center text-xs text-muted-foreground", children: "Loading available slots..." }) : availableTimes.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-3 sm:grid-cols-4 md:grid-cols-6 gap-2", children: availableTimes.map((timeStr) => {
6604
+ const isSelected = selectedTime === timeStr;
6605
+ return /* @__PURE__ */ jsxRuntime.jsx(
6606
+ "button",
6607
+ {
6608
+ type: "button",
6609
+ style: isSelected ? activeSlotStyle : inactiveSlotStyle,
6610
+ disabled,
6611
+ onClick: () => handleSlotSelect(timeStr),
6612
+ className: `
6613
+ px-2.5 py-2 text-xs font-medium rounded-lg border transition-all duration-150 text-center
6614
+ ${isSelected ? "bg-primary text-primary-foreground border-primary shadow-sm ring-2 ring-primary/20 scale-[1.02]" : "bg-background text-foreground border-border hover:border-primary/60 hover:bg-muted/50"}
6615
+ ${disabled ? "opacity-50 cursor-not-allowed" : ""}
6616
+ `,
6617
+ children: formatTime12h(timeStr)
6618
+ },
6619
+ timeStr
6620
+ );
6621
+ }) }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "py-6 text-center text-xs text-muted-foreground", children: "No slots available on this date. Please select another date." })
6622
+ ] })
6623
+ ]
6624
+ }
6625
+ )
6626
+ ] }) });
5287
6627
  }
5288
-
5289
- // src/form-builder/components/FormBuilderGated.tsx
5290
- var FormBuilder = requireLicense(FormBuilderInner, "FormBuilder");
6628
+ var ProAppointmentField_default = ProAppointmentField;
5291
6629
  function resolvePath(obj, path) {
5292
6630
  let current = obj;
5293
6631
  for (const key of path.split(".")) {
@@ -5384,13 +6722,16 @@ function ProPaymentField(props) {
5384
6722
  onChange,
5385
6723
  onBlur,
5386
6724
  customProps,
5387
- theme: formTheme
6725
+ theme: formTheme,
6726
+ fieldValues
5388
6727
  } = props;
5389
6728
  const config = field.config;
5390
6729
  const current = value ?? { status: "pending" };
5391
6730
  const provider = config?.provider ?? "stripe";
5392
6731
  const publicKey = config?.publicKey;
5393
- const amount = customProps?.amount ?? config?.amount;
6732
+ const rawDynamicAmount = config?.amountField && fieldValues ? fieldValues[config.amountField] : void 0;
6733
+ const dynamicAmountCents = typeof rawDynamicAmount === "number" ? Math.round(rawDynamicAmount * 100) : typeof rawDynamicAmount === "string" && !isNaN(Number(rawDynamicAmount)) && rawDynamicAmount !== "" ? Math.round(Number(rawDynamicAmount) * 100) : void 0;
6734
+ const amount = customProps?.amount ?? dynamicAmountCents ?? config?.amount;
5394
6735
  const currency = config?.currency ?? "USD";
5395
6736
  const directSecret = customProps?.clientSecret;
5396
6737
  const onCreateIntent = customProps?.onCreatePaymentIntent;
@@ -5399,6 +6740,15 @@ function ProPaymentField(props) {
5399
6740
  const [clientSecret, setClientSecret] = React.useState(directSecret);
5400
6741
  const [intentLoading, setIntentLoading] = React.useState(false);
5401
6742
  const [intentError, setIntentError] = React.useState(null);
6743
+ const prevAmountRef = React.useRef(amount);
6744
+ React.useEffect(() => {
6745
+ if (prevAmountRef.current !== amount) {
6746
+ prevAmountRef.current = amount;
6747
+ if (!directSecret) {
6748
+ setClientSecret(void 0);
6749
+ }
6750
+ }
6751
+ }, [amount, directSecret]);
5402
6752
  const mode = directSecret ? "direct" : onCreateIntent ? "callback" : serverUrl ? "url" : "setup";
5403
6753
  React.useEffect(() => {
5404
6754
  if (directSecret) {
@@ -5465,7 +6815,7 @@ function ProPaymentField(props) {
5465
6815
  const c = formTheme.colors;
5466
6816
  const cardStyle = {
5467
6817
  borderRadius: formTheme.shape?.inputRadius || "8px",
5468
- border: `1px solid ${c?.border || "#e2e8f0"}`,
6818
+ border: `1px solid ${c?.border || "var(--rule)"}`,
5469
6819
  padding: "16px",
5470
6820
  background: c?.surface || c?.background,
5471
6821
  color: c?.text
@@ -5753,10 +7103,29 @@ function PayKitCheckout({
5753
7103
  ) });
5754
7104
  }
5755
7105
  var PRO_FIELD_OVERRIDES = {
5756
- payment: ProPaymentField
7106
+ payment: ProPaymentField,
7107
+ file_upload: ProFileUploadField_default,
7108
+ appointment: ProAppointmentField_default
5757
7109
  };
7110
+
7111
+ // src/response-viewer/constants.ts
7112
+ var DISPLAY_ONLY_TYPES = /* @__PURE__ */ new Set([
7113
+ "info-block",
7114
+ "info_block",
7115
+ "section-header",
7116
+ "page-break",
7117
+ "image",
7118
+ "divider",
7119
+ "spacer",
7120
+ "video",
7121
+ "rich-text",
7122
+ "welcome-screen",
7123
+ "thank-you-screen",
7124
+ "hidden",
7125
+ "calculated"
7126
+ ]);
5758
7127
  function formatDuration(ms) {
5759
- if (ms == null) return "\u2014";
7128
+ if (ms == null || ms < 0) return "\u2014";
5760
7129
  const totalSeconds = Math.round(ms / 1e3);
5761
7130
  if (totalSeconds < 60) return `${totalSeconds}s`;
5762
7131
  const minutes = Math.floor(totalSeconds / 60);
@@ -5833,11 +7202,7 @@ function ResponseTable({
5833
7202
  ) }),
5834
7203
  /* @__PURE__ */ jsxRuntime.jsx("td", { className: "px-3 py-2 text-foreground whitespace-nowrap", children: new Date(response.submittedAt).toLocaleString() }),
5835
7204
  /* @__PURE__ */ jsxRuntime.jsx("td", { className: "px-3 py-2 text-foreground whitespace-nowrap", children: formatDuration(response.completionTimeMs) }),
5836
- /* @__PURE__ */ jsxRuntime.jsxs("td", { className: "px-3 py-2 text-foreground whitespace-nowrap", children: [
5837
- answered,
5838
- "/",
5839
- totalFields
5840
- ] }),
7205
+ /* @__PURE__ */ jsxRuntime.jsx("td", { className: "px-3 py-2 text-foreground whitespace-nowrap", children: totalFields > 0 ? `${answered}/${totalFields}` : "\u2014" }),
5841
7206
  /* @__PURE__ */ jsxRuntime.jsx("td", { className: "px-3 py-2 text-foreground whitespace-nowrap", children: response.totalScore ?? "\u2014" })
5842
7207
  ]
5843
7208
  },
@@ -5855,21 +7220,6 @@ function ResponseTable({
5855
7220
  ] })
5856
7221
  ] }) });
5857
7222
  }
5858
- var DISPLAY_ONLY_TYPES = /* @__PURE__ */ new Set([
5859
- "info-block",
5860
- "info_block",
5861
- "section-header",
5862
- "page-break",
5863
- "image",
5864
- "divider",
5865
- "spacer",
5866
- "video",
5867
- "rich-text",
5868
- "welcome-screen",
5869
- "thank-you-screen",
5870
- "hidden",
5871
- "calculated"
5872
- ]);
5873
7223
 
5874
7224
  // src/response-viewer/clinical-display-data.ts
5875
7225
  var BODY_REGION_LABELS = {
@@ -6490,7 +7840,13 @@ function formatFallbackValue(value) {
6490
7840
  if (value == null) return "\u2014";
6491
7841
  if (typeof value === "boolean") return value ? "Yes" : "No";
6492
7842
  if (Array.isArray(value)) return value.map(formatFallbackValue).join(", ");
6493
- if (typeof value === "object") return JSON.stringify(value, null, 2);
7843
+ if (typeof value === "object") {
7844
+ try {
7845
+ return JSON.stringify(value, null, 2);
7846
+ } catch {
7847
+ return "[Object]";
7848
+ }
7849
+ }
6494
7850
  return String(value);
6495
7851
  }
6496
7852
  function TimelineView({ responses, questions, onSelect }) {
@@ -7004,26 +8360,11 @@ function exportToJson(responses, filename = "responses.json") {
7004
8360
  const content = JSON.stringify(responses, null, 2);
7005
8361
  downloadBlob(content, filename, "application/json;charset=utf-8;");
7006
8362
  }
7007
- var DISPLAY_ONLY_TYPES2 = /* @__PURE__ */ new Set([
7008
- "info-block",
7009
- "info_block",
7010
- "section-header",
7011
- "page-break",
7012
- "image",
7013
- "divider",
7014
- "spacer",
7015
- "video",
7016
- "rich-text",
7017
- "welcome-screen",
7018
- "thank-you-screen",
7019
- "hidden",
7020
- "calculated"
7021
- ]);
7022
8363
  function getExportableQuestions(schema) {
7023
8364
  const questions = [];
7024
8365
  for (const section of schema.sections) {
7025
8366
  for (const q of section.questions) {
7026
- if (DISPLAY_ONLY_TYPES2.has(q.type)) continue;
8367
+ if (DISPLAY_ONLY_TYPES.has(q.type)) continue;
7027
8368
  questions.push(q);
7028
8369
  }
7029
8370
  }
@@ -7107,7 +8448,7 @@ function downloadBlob(content, filename, mimeType) {
7107
8448
  document.body.appendChild(link);
7108
8449
  link.click();
7109
8450
  document.body.removeChild(link);
7110
- setTimeout(() => URL.revokeObjectURL(url), 1e4);
8451
+ setTimeout(() => URL.revokeObjectURL(url), 100);
7111
8452
  }
7112
8453
 
7113
8454
  // src/response-viewer/pagination-utils.ts
@@ -7227,14 +8568,15 @@ function applyFilters(responses, state) {
7227
8568
  function matchesDateRange(submittedAt, range) {
7228
8569
  if (!range.from && !range.to) return true;
7229
8570
  const submitted = new Date(submittedAt);
8571
+ if (isNaN(submitted.getTime())) return false;
7230
8572
  if (range.from) {
7231
- const from = new Date(range.from);
7232
- from.setHours(0, 0, 0, 0);
8573
+ const from = /* @__PURE__ */ new Date(range.from + "T00:00:00");
8574
+ if (isNaN(from.getTime())) return false;
7233
8575
  if (submitted < from) return false;
7234
8576
  }
7235
8577
  if (range.to) {
7236
- const to = new Date(range.to);
7237
- to.setHours(23, 59, 59, 999);
8578
+ const to = /* @__PURE__ */ new Date(range.to + "T23:59:59.999");
8579
+ if (isNaN(to.getTime())) return false;
7238
8580
  if (submitted > to) return false;
7239
8581
  }
7240
8582
  return true;
@@ -7345,7 +8687,7 @@ function ResponseViewerInner({
7345
8687
  [searchedResponses, filterState]
7346
8688
  );
7347
8689
  const pag = paginate(filteredResponses, currentPage, effectivePageSize);
7348
- const questions = getAllQuestions(schema);
8690
+ const questions = React.useMemo(() => getAllQuestions(schema), [schema]);
7349
8691
  const isSelectable = selectable && (!!onBulkDelete || !!onBulkExport);
7350
8692
  function handleSelect(response) {
7351
8693
  setSelectedResponse(response);
@@ -7794,7 +9136,7 @@ function ResponseViewerInner({
7794
9136
  response: selectedResponse,
7795
9137
  fields: getFields(selectedResponse),
7796
9138
  onBack: handleBack,
7797
- onDelete: onDelete && selectedResponse.sessionToken ? () => handleDeleteSingle(selectedResponse.sessionToken) : void 0
9139
+ onDelete: onDelete && selectedResponse.sessionToken != null ? () => handleDeleteSingle(selectedResponse.sessionToken) : void 0
7798
9140
  }
7799
9141
  ) }) : viewMode === "timeline" ? /* @__PURE__ */ jsxRuntime.jsx(
7800
9142
  TimelineView,
@@ -7899,26 +9241,11 @@ function ResponseViewerInner({
7899
9241
  }
7900
9242
  );
7901
9243
  }
7902
- var DISPLAY_ONLY_TYPES3 = /* @__PURE__ */ new Set([
7903
- "info-block",
7904
- "info_block",
7905
- "section-header",
7906
- "page-break",
7907
- "image",
7908
- "divider",
7909
- "spacer",
7910
- "video",
7911
- "rich-text",
7912
- "welcome-screen",
7913
- "thank-you-screen",
7914
- "hidden",
7915
- "calculated"
7916
- ]);
7917
9244
  function getAllQuestions(schema) {
7918
9245
  const questions = [];
7919
9246
  for (const section of schema.sections) {
7920
9247
  for (const q of section.questions) {
7921
- if (DISPLAY_ONLY_TYPES3.has(q.type)) continue;
9248
+ if (DISPLAY_ONLY_TYPES.has(q.type)) continue;
7922
9249
  questions.push(q);
7923
9250
  }
7924
9251
  }
@@ -8344,16 +9671,20 @@ function deepMerge(base, partial) {
8344
9671
 
8345
9672
  // src/theme-editor/palette-generator.ts
8346
9673
  function generatePalette(baseHex) {
9674
+ const cleaned = baseHex.replace("#", "");
9675
+ if (!/^[0-9a-fA-F]{3}$|^[0-9a-fA-F]{6}$/.test(cleaned)) {
9676
+ return generatePalette("#6B7280");
9677
+ }
8347
9678
  const [h, s, l] = hexToHsl(baseHex);
8348
9679
  const secH = (h + 180) % 360;
8349
9680
  const isLightBase = l > 50;
8350
9681
  return {
8351
9682
  // Primary
8352
9683
  primary: hslToHex(h, s, clamp(l, 30, 60)),
8353
- primaryForeground: isLightBase ? "#ffffff" : "#ffffff",
9684
+ primaryForeground: isLightBase ? "#ffffff" : "#000000",
8354
9685
  // Secondary (complementary)
8355
9686
  secondary: hslToHex(secH, Math.max(s - 15, 10), clamp(l, 35, 55)),
8356
- secondaryForeground: "#ffffff",
9687
+ secondaryForeground: isLightBase ? "#ffffff" : "#000000",
8357
9688
  // Surfaces
8358
9689
  surface: hslToHex(h, Math.max(s - 35, 3), 97),
8359
9690
  background: "#F4F7F8",
@@ -9399,6 +10730,12 @@ function resolveThemeFromDOM() {
9399
10730
  const val = styles.getPropertyValue(prop).trim();
9400
10731
  return val || void 0;
9401
10732
  };
10733
+ const getNumber = (prop) => {
10734
+ const val = get(prop);
10735
+ if (val == null) return void 0;
10736
+ const num = parseFloat(val);
10737
+ return isNaN(num) ? void 0 : num;
10738
+ };
9402
10739
  return {
9403
10740
  colors: {
9404
10741
  primary: get("--primary"),
@@ -9431,6 +10768,17 @@ function resolveThemeFromDOM() {
9431
10768
  inputRadius: get("--fc-radius-input"),
9432
10769
  buttonRadius: get("--fc-radius-button"),
9433
10770
  cardRadius: get("--fc-radius-card")
10771
+ },
10772
+ spacing: {
10773
+ base: getNumber("--fc-spacing-base"),
10774
+ sectionGap: getNumber("--fc-section-gap"),
10775
+ fieldGap: getNumber("--fc-field-gap"),
10776
+ inputPaddingX: getNumber("--fc-input-padding-x"),
10777
+ inputPaddingY: getNumber("--fc-input-padding-y")
10778
+ },
10779
+ layout: {
10780
+ maxWidth: get("--fc-max-width")
10781
+ // alignment, progressPosition, sectionLayout are enums — not resolvable from CSS
9434
10782
  }
9435
10783
  };
9436
10784
  }
@@ -9485,6 +10833,9 @@ function detectPresetFamily(theme) {
9485
10833
  }
9486
10834
  return null;
9487
10835
  }
10836
+ function isValidHex(value) {
10837
+ return /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(value);
10838
+ }
9488
10839
  var SECTIONS = [
9489
10840
  {
9490
10841
  id: "colors",
@@ -9735,6 +11086,7 @@ function ThemeEditorInner({
9735
11086
  a.click();
9736
11087
  URL.revokeObjectURL(url);
9737
11088
  }, [theme]);
11089
+ const [importError, setImportError] = React.useState(null);
9738
11090
  const importJson = React.useCallback(() => {
9739
11091
  const input = document.createElement("input");
9740
11092
  input.type = "file";
@@ -9746,10 +11098,16 @@ function ThemeEditorInner({
9746
11098
  reader.onload = () => {
9747
11099
  try {
9748
11100
  const parsed = JSON.parse(reader.result);
11101
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed) || !("colors" in parsed || "typography" in parsed || "shape" in parsed || "spacing" in parsed || "layout" in parsed)) {
11102
+ setImportError("Invalid theme file: expected an object with colors, typography, shape, spacing, or layout.");
11103
+ return;
11104
+ }
11105
+ setImportError(null);
9749
11106
  setTheme(parsed);
9750
11107
  setPresetKey("custom");
9751
11108
  onChange?.(parsed, "custom");
9752
11109
  } catch {
11110
+ setImportError("Failed to parse JSON file. Please check the file format.");
9753
11111
  }
9754
11112
  };
9755
11113
  reader.readAsText(file);
@@ -9827,6 +11185,10 @@ function ThemeEditorInner({
9827
11185
  onSave && /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: () => onSave(theme, presetKey), className: "fcte-btn fcte-btn--primary", children: "Save" })
9828
11186
  ] })
9829
11187
  ] }),
11188
+ importError && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fcte-import-error", role: "alert", children: [
11189
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: importError }),
11190
+ /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: () => setImportError(null), className: "fcte-import-error__close", children: "\xD7" })
11191
+ ] }),
9830
11192
  showPalette && /* @__PURE__ */ jsxRuntime.jsx(
9831
11193
  PaletteGenerator,
9832
11194
  {
@@ -9860,6 +11222,8 @@ function ThemeEditorInner({
9860
11222
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "fcte-fields", children: currentSection.fields.map((field) => {
9861
11223
  const val = sectionValues[field.key];
9862
11224
  if (field.kind === "color") {
11225
+ const colorVal = typeof val === "string" ? val : "#000000";
11226
+ const isInvalid = colorVal.length > 0 && !isValidHex(colorVal);
9863
11227
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fcte-field", children: [
9864
11228
  /* @__PURE__ */ jsxRuntime.jsx("label", { className: "fcte-field__label", children: field.label }),
9865
11229
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fcte-field__color-row", children: [
@@ -9867,7 +11231,7 @@ function ThemeEditorInner({
9867
11231
  "input",
9868
11232
  {
9869
11233
  type: "color",
9870
- value: typeof val === "string" ? val : "#000000",
11234
+ value: isValidHex(colorVal) ? colorVal : "#000000",
9871
11235
  onChange: (e) => updateField(currentSection.themeKey, field.key, e.target.value),
9872
11236
  className: "fcte-field__swatch"
9873
11237
  }
@@ -9876,10 +11240,24 @@ function ThemeEditorInner({
9876
11240
  "input",
9877
11241
  {
9878
11242
  type: "text",
9879
- value: typeof val === "string" ? val : "",
9880
- onChange: (e) => updateField(currentSection.themeKey, field.key, e.target.value),
9881
- className: "fcte-field__text",
9882
- spellCheck: false
11243
+ value: colorVal,
11244
+ onChange: (e) => {
11245
+ const v = e.target.value;
11246
+ if (isValidHex(v)) {
11247
+ updateField(currentSection.themeKey, field.key, v);
11248
+ } else {
11249
+ setTheme((prev) => ({
11250
+ ...prev,
11251
+ [currentSection.themeKey]: {
11252
+ ...prev[currentSection.themeKey],
11253
+ [field.key]: v
11254
+ }
11255
+ }));
11256
+ }
11257
+ },
11258
+ className: `fcte-field__text${isInvalid ? " fcte-field__text--invalid" : ""}`,
11259
+ spellCheck: false,
11260
+ placeholder: "#RRGGBB"
9883
11261
  }
9884
11262
  )
9885
11263
  ] })
@@ -9908,9 +11286,16 @@ function ThemeEditorInner({
9908
11286
  {
9909
11287
  type: "number",
9910
11288
  value: typeof val === "number" ? val : 0,
9911
- onChange: (e) => updateField(currentSection.themeKey, field.key, Number(e.target.value)),
11289
+ onChange: (e) => {
11290
+ let n = Math.round(Number(e.target.value));
11291
+ if (isNaN(n)) n = 0;
11292
+ if (field.min != null) n = Math.max(field.min, n);
11293
+ if (field.max != null) n = Math.min(field.max, n);
11294
+ updateField(currentSection.themeKey, field.key, n);
11295
+ },
9912
11296
  min: field.min,
9913
11297
  max: field.max,
11298
+ step: 1,
9914
11299
  className: "fcte-field__number"
9915
11300
  }
9916
11301
  ),
@@ -9928,7 +11313,8 @@ function ThemeEditorInner({
9928
11313
  onChange: (e) => updateField(currentSection.themeKey, field.key, e.target.value),
9929
11314
  placeholder: field.placeholder,
9930
11315
  className: "fcte-field__text",
9931
- spellCheck: false
11316
+ spellCheck: false,
11317
+ maxLength: 200
9932
11318
  }
9933
11319
  )
9934
11320
  ] }, field.key);
@@ -10366,6 +11752,7 @@ var consultationBookingSchema = {
10366
11752
  required: true,
10367
11753
  config: {
10368
11754
  type: "appointment",
11755
+ // Example slots — replace with dynamic data or a slots URL in production
10369
11756
  slots: [
10370
11757
  {
10371
11758
  date: "2026-09-01",
@@ -10466,6 +11853,55 @@ var consultationBookingSchema = {
10466
11853
  type: "divider",
10467
11854
  label: ""
10468
11855
  },
11856
+ {
11857
+ id: "hourly_rate",
11858
+ type: "calculated",
11859
+ label: "Hourly Rate",
11860
+ config: {
11861
+ type: "calculated",
11862
+ expression: 'IF({service_type} = "legal", 150, IF({service_type} = "business", 200, IF({service_type} = "technical", 175, 0)))',
11863
+ format: "currency",
11864
+ decimalPlaces: 2,
11865
+ prefix: "$"
11866
+ }
11867
+ },
11868
+ {
11869
+ id: "session_cost",
11870
+ type: "calculated",
11871
+ label: "Session Cost",
11872
+ helpText: "Based on service type and session duration",
11873
+ config: {
11874
+ type: "calculated",
11875
+ expression: "{hourly_rate} * {session_duration} / 60",
11876
+ format: "currency",
11877
+ decimalPlaces: 2,
11878
+ prefix: "$"
11879
+ }
11880
+ },
11881
+ {
11882
+ id: "rush_fee",
11883
+ type: "calculated",
11884
+ label: "Rush Fee",
11885
+ config: {
11886
+ type: "calculated",
11887
+ expression: 'IF({urgency} = "rush", 50, 0)',
11888
+ format: "currency",
11889
+ decimalPlaces: 2,
11890
+ prefix: "$"
11891
+ }
11892
+ },
11893
+ {
11894
+ id: "total_cost",
11895
+ type: "calculated",
11896
+ label: "Total",
11897
+ config: {
11898
+ type: "calculated",
11899
+ expression: "{session_cost} + {rush_fee}",
11900
+ format: "currency",
11901
+ decimalPlaces: 2,
11902
+ prefix: "$"
11903
+ }
11904
+ },
10469
11905
  {
10470
11906
  id: "promo_code",
10471
11907
  type: "short_text",
@@ -10485,10 +11921,10 @@ var consultationBookingSchema = {
10485
11921
  provider: "stripe",
10486
11922
  publicKey: "",
10487
11923
  // Set your Stripe publishable key (pk_test_... or pk_live_...)
10488
- amount: 2e4,
11924
+ amountField: "total_cost",
10489
11925
  currency: "USD",
10490
11926
  description: "Consultation Session"
10491
- // No serverUrl — developer must configure their own endpoint via customProps or serverUrl
11927
+ // serverUrl: "" — developer must configure their own payment intent endpoint
10492
11928
  }
10493
11929
  }
10494
11930
  ]
@@ -10985,13 +12421,50 @@ var ecommerceCheckoutSchema = {
10985
12421
  prefix: "$"
10986
12422
  }
10987
12423
  },
12424
+ {
12425
+ id: "shipping_cost",
12426
+ type: "calculated",
12427
+ label: "Shipping",
12428
+ helpText: "Based on your selected shipping method",
12429
+ config: {
12430
+ type: "calculated",
12431
+ expression: 'IF({shipping_method} = "overnight", 29.99, IF({shipping_method} = "express", 14.99, IF({shipping_method} = "standard", 7.99, 0)))',
12432
+ format: "currency",
12433
+ decimalPlaces: 2,
12434
+ prefix: "$"
12435
+ }
12436
+ },
12437
+ {
12438
+ id: "insurance_cost",
12439
+ type: "calculated",
12440
+ label: "Shipping Insurance",
12441
+ config: {
12442
+ type: "calculated",
12443
+ expression: "IF({shipping_insurance} = true, 4.99, 0)",
12444
+ format: "currency",
12445
+ decimalPlaces: 2,
12446
+ prefix: "$"
12447
+ }
12448
+ },
12449
+ {
12450
+ id: "gift_wrap_cost",
12451
+ type: "calculated",
12452
+ label: "Gift Wrapping",
12453
+ config: {
12454
+ type: "calculated",
12455
+ expression: "IF({gift_wrap} = true, 5.99, 0)",
12456
+ format: "currency",
12457
+ decimalPlaces: 2,
12458
+ prefix: "$"
12459
+ }
12460
+ },
10988
12461
  {
10989
12462
  id: "order_total",
10990
12463
  type: "calculated",
10991
12464
  label: "Order Total",
10992
12465
  config: {
10993
12466
  type: "calculated",
10994
- expression: "{order_subtotal} + {order_tax}",
12467
+ expression: "{order_subtotal} + {order_tax} + {shipping_cost} + {insurance_cost} + {gift_wrap_cost}",
10995
12468
  format: "currency",
10996
12469
  decimalPlaces: 2,
10997
12470
  prefix: "$"
@@ -11013,10 +12486,10 @@ var ecommerceCheckoutSchema = {
11013
12486
  provider: "stripe",
11014
12487
  publicKey: "",
11015
12488
  // Set your Stripe publishable key (pk_test_... or pk_live_...)
11016
- amount: 1e4,
12489
+ amountField: "order_total",
11017
12490
  currency: "USD",
11018
12491
  description: "E-commerce Order"
11019
- // No serverUrl — developer must configure their own endpoint via customProps or serverUrl
12492
+ // serverUrl: "" — developer must configure their own payment intent endpoint
11020
12493
  }
11021
12494
  }
11022
12495
  ]
@@ -11122,6 +12595,8 @@ exports.FormBuilderThemeProvider = FormBuilderThemeProvider;
11122
12595
  exports.PRESET_FAMILIES = PRESET_FAMILIES;
11123
12596
  exports.PREVIEW_SCHEMA = PREVIEW_SCHEMA;
11124
12597
  exports.PRO_FIELD_OVERRIDES = PRO_FIELD_OVERRIDES;
12598
+ exports.ProAppointmentField = ProAppointmentField;
12599
+ exports.ProFileUploadField = ProFileUploadField;
11125
12600
  exports.ProPaymentField = ProPaymentField;
11126
12601
  exports.QUESTION_TYPE_INFO = QUESTION_TYPE_INFO;
11127
12602
  exports.ResponseViewer = ResponseViewer;
@@ -11135,13 +12610,18 @@ exports.cn = cn;
11135
12610
  exports.consultationBooking = consultationBooking;
11136
12611
  exports.consultationBookingMeta = consultationBookingMeta;
11137
12612
  exports.consultationBookingSchema = consultationBookingSchema;
12613
+ exports.decipherPresignPayload = decipherPresignPayload;
12614
+ exports.decryptEphemeralPayload = decryptEphemeralPayload;
11138
12615
  exports.duplicateQuestion = duplicateQuestion;
11139
12616
  exports.duplicateSection = duplicateSection;
11140
12617
  exports.ecommerceCheckout = ecommerceCheckout;
11141
12618
  exports.ecommerceCheckoutMeta = ecommerceCheckoutMeta;
11142
12619
  exports.ecommerceCheckoutSchema = ecommerceCheckoutSchema;
12620
+ exports.encryptPresignPayload = encryptPresignPayload;
12621
+ exports.exportPublicKeySpki = exportPublicKeySpki;
11143
12622
  exports.findQuestion = findQuestion;
11144
12623
  exports.findSection = findSection;
12624
+ exports.generateEphemeralKeyPair = generateEphemeralKeyPair;
11145
12625
  exports.generateId = generateId;
11146
12626
  exports.generateOptionId = generateOptionId;
11147
12627
  exports.generateQuestionId = generateQuestionId;