formanitor 0.0.48 → 0.0.49

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.mjs CHANGED
@@ -32,8 +32,25 @@ function fieldMetaRequestsMarMedicationOrdersButton(meta) {
32
32
 
33
33
  // src/core/validate.ts
34
34
  var EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
35
- function validateField(value, field) {
35
+ function validateField(value, field, allValues) {
36
36
  if (field.hidden || field.disabled) return null;
37
+ if (field.type === "otp_input") {
38
+ if (field.required) {
39
+ if (!value || typeof value !== "object" || value.verified !== true) {
40
+ return "Please verify your mobile number with OTP";
41
+ }
42
+ const otpField = field;
43
+ const phoneFieldId = otpField.meta?.phoneFieldId ?? "phone";
44
+ if (allValues) {
45
+ const phoneRaw = allValues[phoneFieldId] ?? "";
46
+ const e164 = phoneRaw.startsWith("+") ? phoneRaw : `+91${phoneRaw}`;
47
+ if (value.phone && value.phone !== e164) {
48
+ return "Mobile number changed \u2014 please re-verify";
49
+ }
50
+ }
51
+ }
52
+ return null;
53
+ }
37
54
  if (field.required) {
38
55
  if (value === null || value === void 0 || value === "") {
39
56
  return "This field is required";
@@ -94,7 +111,7 @@ function validateForm(values, fields) {
94
111
  const errors = {};
95
112
  for (const [key, field] of Object.entries(fields)) {
96
113
  if (field.type === "static_text") continue;
97
- const error = validateField(values[key], field);
114
+ const error = validateField(values[key], field, values);
98
115
  if (error) {
99
116
  errors[key] = error;
100
117
  }
@@ -1171,6 +1188,9 @@ var FormStore = class {
1171
1188
  persistence;
1172
1189
  prefillData = {};
1173
1190
  config;
1191
+ // Widget-owned errors that schema validation cannot express (e.g. "OTP not sent yet").
1192
+ // Merged on top of validateForm output in evaluate().
1193
+ externalErrors = {};
1174
1194
  // Cache for debounce
1175
1195
  saveTimeout = null;
1176
1196
  draftCallbackTimeout = null;
@@ -1290,6 +1310,9 @@ var FormStore = class {
1290
1310
  getSchema() {
1291
1311
  return this.schema;
1292
1312
  }
1313
+ getConfig() {
1314
+ return this.config;
1315
+ }
1293
1316
  getFieldDef(fieldId) {
1294
1317
  return this.fieldMap[fieldId];
1295
1318
  }
@@ -1337,6 +1360,17 @@ var FormStore = class {
1337
1360
  this.notify();
1338
1361
  this.scheduleSave();
1339
1362
  }
1363
+ /**
1364
+ * Let a widget report an error that schema validation cannot express
1365
+ * (e.g. "Send OTP to continue" until the API call succeeds).
1366
+ * Pass `null` to clear a previously set external error.
1367
+ */
1368
+ setExternalError(fieldId, message) {
1369
+ if ((this.externalErrors[fieldId] ?? null) === message) return;
1370
+ this.externalErrors = { ...this.externalErrors, [fieldId]: message };
1371
+ this.evaluate();
1372
+ this.notify();
1373
+ }
1340
1374
  setTouched(fieldId) {
1341
1375
  if (this.state.touched[fieldId]) return;
1342
1376
  this.state.touched = { ...this.state.touched, [fieldId]: true };
@@ -1465,6 +1499,20 @@ var FormStore = class {
1465
1499
  result[field.id] = parts.length > 0 ? parts.join("\n\n") : null;
1466
1500
  return;
1467
1501
  }
1502
+ if (field.type === "otp_input") {
1503
+ const raw = values[field.id];
1504
+ if (typeof raw === "string") {
1505
+ result[field.id] = raw;
1506
+ return;
1507
+ }
1508
+ if (raw && typeof raw === "object" && !Array.isArray(raw) && raw.verified === true) {
1509
+ const code = raw.code;
1510
+ result[field.id] = typeof code === "string" ? code : null;
1511
+ return;
1512
+ }
1513
+ result[field.id] = null;
1514
+ return;
1515
+ }
1468
1516
  if (field.type === "image_upload" || field.type === "media_upload" || field.type === "signature") {
1469
1517
  const raw = values[field.id];
1470
1518
  if (raw !== void 0 && raw !== null && raw !== "") {
@@ -1581,8 +1629,12 @@ var FormStore = class {
1581
1629
  };
1582
1630
  });
1583
1631
  const errors = validateForm(this.state.values, effectiveFields);
1584
- this.state.errors = errors;
1585
- this.state.isValid = Object.keys(errors).length === 0;
1632
+ const merged = { ...errors };
1633
+ for (const [id, msg] of Object.entries(this.externalErrors)) {
1634
+ if (msg) merged[id] = msg;
1635
+ }
1636
+ this.state.errors = merged;
1637
+ this.state.isValid = Object.keys(merged).length === 0;
1586
1638
  }
1587
1639
  // --- Workflow ---
1588
1640
  getAvailableTransitions() {
@@ -13775,6 +13827,347 @@ var OBGPathwayWidget = ({ fieldId }) => {
13775
13827
  ] })
13776
13828
  ] });
13777
13829
  };
13830
+ var RESEND_COOLDOWN_SECONDS = 30;
13831
+ var PhoneInputWidget = ({ fieldId }) => {
13832
+ const { fieldDef, error, touched, disabled, value: fieldValue } = useField(fieldId);
13833
+ const { state } = useForm();
13834
+ const store = useFormStore();
13835
+ const rawPhone = fieldValue ?? "";
13836
+ const wasAlreadySent = rawPhone.length === 10 && !error;
13837
+ const [status, setStatus] = useState(wasAlreadySent ? "sent" : "idle");
13838
+ const [resendCooldown, setResendCooldown] = useState(0);
13839
+ const [resendError, setResendError] = useState("");
13840
+ const hasSentRef = useRef(wasAlreadySent);
13841
+ const sentPhoneRef = useRef(wasAlreadySent ? rawPhone : "");
13842
+ const isSendingRef = useRef(false);
13843
+ const isResendingRef = useRef(false);
13844
+ const showError = !!error && (touched || state.submitAttempted) && status !== "sending" && status !== "sent";
13845
+ useEffect(() => {
13846
+ if (resendCooldown <= 0) return;
13847
+ const t = setTimeout(() => setResendCooldown((c) => Math.max(0, c - 1)), 1e3);
13848
+ return () => clearTimeout(t);
13849
+ }, [resendCooldown]);
13850
+ if (!fieldDef) return null;
13851
+ const placeholder = typeof fieldDef.placeholder === "string" && fieldDef.placeholder.trim() ? fieldDef.placeholder : "Enter 10-digit mobile number";
13852
+ const handleChange = (e) => {
13853
+ const digits = e.target.value.replace(/\D/g, "").slice(0, 10);
13854
+ if (hasSentRef.current && digits !== sentPhoneRef.current) {
13855
+ hasSentRef.current = false;
13856
+ sentPhoneRef.current = "";
13857
+ setStatus("idle");
13858
+ setResendCooldown(0);
13859
+ setResendError("");
13860
+ store.setExternalError(fieldId, "Send OTP to continue");
13861
+ const otpFieldId = fieldDef.meta?.otpFieldId;
13862
+ if (otpFieldId) {
13863
+ store.setValue(otpFieldId, "");
13864
+ store.setExternalError(otpFieldId, "Verify OTP to continue");
13865
+ }
13866
+ }
13867
+ store.setValue(fieldId, digits);
13868
+ if (digits.length === 10 && !hasSentRef.current && !isSendingRef.current) {
13869
+ sendOtp(digits);
13870
+ }
13871
+ };
13872
+ const sendOtp = async (phone) => {
13873
+ if (isSendingRef.current) return;
13874
+ isSendingRef.current = true;
13875
+ setStatus("sending");
13876
+ store.setExternalError(fieldId, "Sending OTP...");
13877
+ try {
13878
+ const authCfg = store.getConfig().auth;
13879
+ const base = authCfg?.baseUrl ?? ((typeof process !== "undefined" && process.env?.NEXT_PUBLIC_API_URL) ?? "");
13880
+ const sendPath = authCfg?.sendPath ?? "/api/auth/otp/send/";
13881
+ const countryCode = authCfg?.defaultCountryCode ?? "+91";
13882
+ const res = await fetch(`${base}${sendPath}`, {
13883
+ method: "POST",
13884
+ headers: { "Content-Type": "application/json" },
13885
+ body: JSON.stringify({ mobile_number: `${countryCode}${phone}` })
13886
+ });
13887
+ const data = await res.json().catch(() => ({}));
13888
+ if (res.ok) {
13889
+ hasSentRef.current = true;
13890
+ sentPhoneRef.current = phone;
13891
+ setStatus("sent");
13892
+ setResendCooldown(RESEND_COOLDOWN_SECONDS);
13893
+ store.setExternalError(fieldId, null);
13894
+ } else {
13895
+ setStatus("error");
13896
+ store.setExternalError(fieldId, data.error ?? "Failed to send OTP. Please try again.");
13897
+ }
13898
+ } catch {
13899
+ setStatus("error");
13900
+ store.setExternalError(fieldId, "Network error. Please try again.");
13901
+ } finally {
13902
+ isSendingRef.current = false;
13903
+ }
13904
+ };
13905
+ const handleResend = async () => {
13906
+ if (isResendingRef.current || resendCooldown > 0) return;
13907
+ isResendingRef.current = true;
13908
+ setResendError("");
13909
+ try {
13910
+ const authCfg = store.getConfig().auth;
13911
+ const base = authCfg?.baseUrl ?? ((typeof process !== "undefined" && process.env?.NEXT_PUBLIC_API_URL) ?? "");
13912
+ const resendPath = authCfg?.resendPath ?? "/api/auth/otp/resend/";
13913
+ const countryCode = authCfg?.defaultCountryCode ?? "+91";
13914
+ const res = await fetch(`${base}${resendPath}`, {
13915
+ method: "POST",
13916
+ headers: { "Content-Type": "application/json" },
13917
+ body: JSON.stringify({ mobile_number: `${countryCode}${rawPhone}`, retrytype: "text" })
13918
+ });
13919
+ const data = await res.json().catch(() => ({}));
13920
+ if (res.ok) {
13921
+ setResendCooldown(RESEND_COOLDOWN_SECONDS);
13922
+ setResendError("");
13923
+ } else {
13924
+ setResendError(data.error ?? "Failed to resend OTP. Please try again.");
13925
+ }
13926
+ } catch {
13927
+ setResendError("Network error. Please try again.");
13928
+ } finally {
13929
+ isResendingRef.current = false;
13930
+ }
13931
+ };
13932
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
13933
+ /* @__PURE__ */ jsxs(Label, { htmlFor: fieldId, children: [
13934
+ fieldDef.label,
13935
+ " ",
13936
+ fieldDef.required && /* @__PURE__ */ jsx("span", { className: "text-red-500", children: "*" })
13937
+ ] }),
13938
+ /* @__PURE__ */ jsxs("div", { className: "relative", children: [
13939
+ /* @__PURE__ */ jsx(
13940
+ Input,
13941
+ {
13942
+ id: fieldId,
13943
+ type: "tel",
13944
+ inputMode: "numeric",
13945
+ value: rawPhone,
13946
+ onChange: handleChange,
13947
+ onBlur: () => store.setTouched(fieldId),
13948
+ disabled: disabled || status === "sending",
13949
+ placeholder,
13950
+ maxLength: 10,
13951
+ className: cn(
13952
+ showError && "border-red-500",
13953
+ status === "sent" && "border-green-500 pr-9",
13954
+ status === "sending" && "pr-9"
13955
+ )
13956
+ }
13957
+ ),
13958
+ status === "sending" && /* @__PURE__ */ jsx("div", { className: "absolute inset-y-0 right-3 flex items-center pointer-events-none", children: /* @__PURE__ */ jsxs(
13959
+ "svg",
13960
+ {
13961
+ className: "animate-spin h-4 w-4 text-muted-foreground",
13962
+ viewBox: "0 0 24 24",
13963
+ fill: "none",
13964
+ children: [
13965
+ /* @__PURE__ */ jsx(
13966
+ "circle",
13967
+ {
13968
+ className: "opacity-25",
13969
+ cx: "12",
13970
+ cy: "12",
13971
+ r: "10",
13972
+ stroke: "currentColor",
13973
+ strokeWidth: "4"
13974
+ }
13975
+ ),
13976
+ /* @__PURE__ */ jsx(
13977
+ "path",
13978
+ {
13979
+ className: "opacity-75",
13980
+ fill: "currentColor",
13981
+ d: "M4 12a8 8 0 018-8v8H4z"
13982
+ }
13983
+ )
13984
+ ]
13985
+ }
13986
+ ) }),
13987
+ status === "sent" && /* @__PURE__ */ jsx("div", { className: "absolute inset-y-0 right-3 flex items-center pointer-events-none", children: /* @__PURE__ */ jsx(
13988
+ "svg",
13989
+ {
13990
+ className: "h-4 w-4 text-green-500",
13991
+ viewBox: "0 0 20 20",
13992
+ fill: "currentColor",
13993
+ children: /* @__PURE__ */ jsx(
13994
+ "path",
13995
+ {
13996
+ fillRule: "evenodd",
13997
+ 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",
13998
+ clipRule: "evenodd"
13999
+ }
14000
+ )
14001
+ }
14002
+ ) })
14003
+ ] }),
14004
+ status === "sent" && /* @__PURE__ */ jsxs("p", { className: "text-xs text-green-600", children: [
14005
+ "OTP sent to +91",
14006
+ rawPhone
14007
+ ] }),
14008
+ showError && /* @__PURE__ */ jsx("p", { className: "text-xs text-red-500", children: error }),
14009
+ status === "sent" && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1 text-xs text-muted-foreground", children: [
14010
+ /* @__PURE__ */ jsx("span", { children: "Didn't receive it?" }),
14011
+ /* @__PURE__ */ jsx(
14012
+ "button",
14013
+ {
14014
+ type: "button",
14015
+ onClick: handleResend,
14016
+ disabled: resendCooldown > 0,
14017
+ className: cn(
14018
+ "underline transition-colors",
14019
+ resendCooldown > 0 ? "opacity-40 cursor-not-allowed" : "text-primary hover:text-primary/80 cursor-pointer"
14020
+ ),
14021
+ children: resendCooldown > 0 ? `Resend in ${resendCooldown}s` : "Resend OTP"
14022
+ }
14023
+ )
14024
+ ] }),
14025
+ resendError && /* @__PURE__ */ jsx("p", { className: "text-xs text-red-500", children: resendError })
14026
+ ] });
14027
+ };
14028
+ var OtpInputWidget = ({ fieldId }) => {
14029
+ const { fieldDef, error, touched, disabled } = useField(fieldId);
14030
+ const { state } = useForm();
14031
+ const store = useFormStore();
14032
+ const phoneFieldId = fieldDef?.meta?.phoneFieldId ?? "phone";
14033
+ const { value: phoneValue } = useField(phoneFieldId);
14034
+ const phone = phoneValue ?? "";
14035
+ const e164Phone = phone.startsWith("+") ? phone : `+91${phone}`;
14036
+ const [otp, setOtp] = useState("");
14037
+ const [status, setStatus] = useState("idle");
14038
+ const isVerifyingRef = useRef(false);
14039
+ const showError = !!error && (touched || state.submitAttempted) && status !== "verifying" && status !== "verified";
14040
+ if (!fieldDef) return null;
14041
+ const placeholder = typeof fieldDef.placeholder === "string" && fieldDef.placeholder.trim() ? fieldDef.placeholder : "Enter 4-digit OTP";
14042
+ const handleChange = (e) => {
14043
+ const digits = e.target.value.replace(/\D/g, "").slice(0, 4);
14044
+ if (status === "failed") {
14045
+ setStatus("idle");
14046
+ store.setExternalError(fieldId, null);
14047
+ }
14048
+ setOtp(digits);
14049
+ store.setValue(fieldId, digits);
14050
+ if (digits.length === 4 && !isVerifyingRef.current) {
14051
+ verifyOtp(digits);
14052
+ }
14053
+ };
14054
+ const verifyOtp = async (code) => {
14055
+ if (isVerifyingRef.current) return;
14056
+ isVerifyingRef.current = true;
14057
+ setStatus("verifying");
14058
+ store.setExternalError(fieldId, "Verifying...");
14059
+ try {
14060
+ const authCfg = store.getConfig().auth;
14061
+ const base = authCfg?.baseUrl ?? ((typeof process !== "undefined" && process.env?.NEXT_PUBLIC_API_URL) ?? "");
14062
+ const verifyPath = authCfg?.verifyPath ?? "/api/auth/otp/verify/";
14063
+ const res = await fetch(`${base}${verifyPath}`, {
14064
+ method: "POST",
14065
+ headers: { "Content-Type": "application/json" },
14066
+ body: JSON.stringify({ mobile_number: e164Phone, otp_code: code })
14067
+ });
14068
+ const data = await res.json().catch(() => ({}));
14069
+ if (res.ok) {
14070
+ setStatus("verified");
14071
+ store.setExternalError(fieldId, null);
14072
+ store.setValue(fieldId, {
14073
+ verified: true,
14074
+ phone: e164Phone,
14075
+ code,
14076
+ ...data?.verification_token ? { token: data.verification_token } : {}
14077
+ });
14078
+ } else {
14079
+ setStatus("failed");
14080
+ const msg = data.error ?? "Invalid OTP. Please try again.";
14081
+ store.setExternalError(fieldId, msg);
14082
+ setOtp("");
14083
+ store.setValue(fieldId, "");
14084
+ }
14085
+ } catch {
14086
+ setStatus("failed");
14087
+ store.setExternalError(fieldId, "Network error. Please try again.");
14088
+ setOtp("");
14089
+ store.setValue(fieldId, "");
14090
+ } finally {
14091
+ isVerifyingRef.current = false;
14092
+ }
14093
+ };
14094
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
14095
+ /* @__PURE__ */ jsxs(Label, { htmlFor: fieldId, children: [
14096
+ fieldDef.label,
14097
+ " ",
14098
+ fieldDef.required && /* @__PURE__ */ jsx("span", { className: "text-red-500", children: "*" })
14099
+ ] }),
14100
+ /* @__PURE__ */ jsxs("div", { className: "relative", children: [
14101
+ /* @__PURE__ */ jsx(
14102
+ Input,
14103
+ {
14104
+ id: fieldId,
14105
+ type: "tel",
14106
+ inputMode: "numeric",
14107
+ value: otp,
14108
+ onChange: handleChange,
14109
+ onBlur: () => store.setTouched(fieldId),
14110
+ disabled: disabled || status === "verifying" || status === "verified",
14111
+ placeholder,
14112
+ maxLength: 4,
14113
+ className: cn(
14114
+ "tracking-widest text-center text-lg",
14115
+ showError && "border-red-500",
14116
+ status === "verified" && "border-green-500 pr-9",
14117
+ status === "verifying" && "pr-9"
14118
+ )
14119
+ }
14120
+ ),
14121
+ status === "verifying" && /* @__PURE__ */ jsx("div", { className: "absolute inset-y-0 right-3 flex items-center pointer-events-none", children: /* @__PURE__ */ jsxs(
14122
+ "svg",
14123
+ {
14124
+ className: "animate-spin h-4 w-4 text-muted-foreground",
14125
+ viewBox: "0 0 24 24",
14126
+ fill: "none",
14127
+ children: [
14128
+ /* @__PURE__ */ jsx(
14129
+ "circle",
14130
+ {
14131
+ className: "opacity-25",
14132
+ cx: "12",
14133
+ cy: "12",
14134
+ r: "10",
14135
+ stroke: "currentColor",
14136
+ strokeWidth: "4"
14137
+ }
14138
+ ),
14139
+ /* @__PURE__ */ jsx(
14140
+ "path",
14141
+ {
14142
+ className: "opacity-75",
14143
+ fill: "currentColor",
14144
+ d: "M4 12a8 8 0 018-8v8H4z"
14145
+ }
14146
+ )
14147
+ ]
14148
+ }
14149
+ ) }),
14150
+ status === "verified" && /* @__PURE__ */ jsx("div", { className: "absolute inset-y-0 right-3 flex items-center pointer-events-none", children: /* @__PURE__ */ jsx(
14151
+ "svg",
14152
+ {
14153
+ className: "h-4 w-4 text-green-500",
14154
+ viewBox: "0 0 20 20",
14155
+ fill: "currentColor",
14156
+ children: /* @__PURE__ */ jsx(
14157
+ "path",
14158
+ {
14159
+ fillRule: "evenodd",
14160
+ 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",
14161
+ clipRule: "evenodd"
14162
+ }
14163
+ )
14164
+ }
14165
+ ) })
14166
+ ] }),
14167
+ status === "verified" && /* @__PURE__ */ jsx("p", { className: "text-xs text-green-600", children: "Mobile number verified successfully" }),
14168
+ showError && /* @__PURE__ */ jsx("p", { className: "text-xs text-red-500", children: error })
14169
+ ] });
14170
+ };
13778
14171
  var FieldRenderer = ({ fieldId }) => {
13779
14172
  const { fieldDef, visible } = useField(fieldId);
13780
14173
  if (!visible || !fieldDef) {
@@ -13866,6 +14259,10 @@ function renderWidget(fieldId, fieldDef) {
13866
14259
  return /* @__PURE__ */ jsx(OBGExaminationWidget, { fieldId });
13867
14260
  case "obg_pathway":
13868
14261
  return /* @__PURE__ */ jsx(OBGPathwayWidget, { fieldId });
14262
+ case "phone_input":
14263
+ return /* @__PURE__ */ jsx(PhoneInputWidget, { fieldId });
14264
+ case "otp_input":
14265
+ return /* @__PURE__ */ jsx(OtpInputWidget, { fieldId });
13869
14266
  case "toggle": {
13870
14267
  const { value, setValue, setTouched, disabled } = useField(fieldId);
13871
14268
  const store = useFormStore();
@@ -13986,16 +14383,6 @@ var DynamicForm = () => {
13986
14383
  })
13987
14384
  ] });
13988
14385
  };
13989
- function PlainSection({
13990
- title,
13991
- showHeading,
13992
- children
13993
- }) {
13994
- return /* @__PURE__ */ jsxs("div", { className: "mb-6 space-y-4", children: [
13995
- showHeading ? /* @__PURE__ */ jsx("h2", { className: "text-base font-medium text-foreground", children: title }) : null,
13996
- children
13997
- ] });
13998
- }
13999
14386
  function renderSectionChildren(children) {
14000
14387
  return children.map((child, index) => {
14001
14388
  if (typeof child === "string") {
@@ -14015,6 +14402,16 @@ function renderSectionChildren(children) {
14015
14402
  return /* @__PURE__ */ jsx(React15__default.Fragment, {}, index);
14016
14403
  });
14017
14404
  }
14405
+ function PlainSection({
14406
+ title,
14407
+ showHeading,
14408
+ children
14409
+ }) {
14410
+ return /* @__PURE__ */ jsxs("div", { className: "mb-6 space-y-4", children: [
14411
+ showHeading ? /* @__PURE__ */ jsx("h2", { className: "text-base font-medium text-foreground", children: title }) : null,
14412
+ children
14413
+ ] });
14414
+ }
14018
14415
  var DynamicFormV2 = ({
14019
14416
  showTitle = true,
14020
14417
  sectionMode = "plain",
@@ -14055,6 +14452,247 @@ var DynamicFormV2 = ({
14055
14452
  })
14056
14453
  ] });
14057
14454
  };
14455
+ function flattenStepFieldIds(step) {
14456
+ const ids = [];
14457
+ for (const child of step.children) {
14458
+ if (typeof child === "string") {
14459
+ ids.push(child);
14460
+ } else if (child.type === "column_layout") {
14461
+ ids.push(...child.children);
14462
+ }
14463
+ }
14464
+ return ids;
14465
+ }
14466
+ function isFieldComplete(id, state, fieldMap) {
14467
+ if (state.visibility[id] === false) return true;
14468
+ if (state.errors[id]) return false;
14469
+ const def = fieldMap[id];
14470
+ if (def?.required) {
14471
+ const v = state.values[id];
14472
+ if (v === null || v === void 0 || v === "") return false;
14473
+ if (Array.isArray(v) && v.length === 0) return false;
14474
+ }
14475
+ return true;
14476
+ }
14477
+ function isStepComplete(step, state, fieldMap) {
14478
+ return flattenStepFieldIds(step).every(
14479
+ (id) => isFieldComplete(id, state, fieldMap)
14480
+ );
14481
+ }
14482
+ var Stepper2 = ({ steps, statuses, variant }) => {
14483
+ if (variant === "dots") {
14484
+ return /* @__PURE__ */ jsx("div", { className: "flex items-center gap-2 mb-6", children: steps.map((step, i) => {
14485
+ const status = statuses[i];
14486
+ return /* @__PURE__ */ jsxs(React15__default.Fragment, { children: [
14487
+ /* @__PURE__ */ jsx(
14488
+ "div",
14489
+ {
14490
+ className: cn(
14491
+ "h-2.5 w-2.5 rounded-full transition-colors",
14492
+ status === "complete" && "bg-primary",
14493
+ status === "active" && "bg-primary ring-2 ring-primary/30",
14494
+ status === "locked" && "bg-muted"
14495
+ ),
14496
+ title: step.title
14497
+ }
14498
+ ),
14499
+ i < steps.length - 1 && /* @__PURE__ */ jsx(
14500
+ "div",
14501
+ {
14502
+ className: cn(
14503
+ "flex-1 h-px transition-colors",
14504
+ status === "complete" ? "bg-primary" : "bg-border"
14505
+ )
14506
+ }
14507
+ )
14508
+ ] }, step.id);
14509
+ }) });
14510
+ }
14511
+ if (variant === "labels") {
14512
+ return /* @__PURE__ */ jsx("div", { className: "flex items-center gap-0 mb-6", children: steps.map((step, i) => {
14513
+ const status = statuses[i];
14514
+ return /* @__PURE__ */ jsxs(React15__default.Fragment, { children: [
14515
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center gap-1", children: [
14516
+ /* @__PURE__ */ jsx(
14517
+ "div",
14518
+ {
14519
+ className: cn(
14520
+ "h-2 w-2 rounded-full",
14521
+ status === "complete" && "bg-primary",
14522
+ status === "active" && "bg-primary",
14523
+ status === "locked" && "bg-muted"
14524
+ )
14525
+ }
14526
+ ),
14527
+ /* @__PURE__ */ jsx(
14528
+ "span",
14529
+ {
14530
+ className: cn(
14531
+ "text-xs whitespace-nowrap",
14532
+ status === "locked" ? "text-muted-foreground" : "text-foreground"
14533
+ ),
14534
+ children: step.title
14535
+ }
14536
+ )
14537
+ ] }),
14538
+ i < steps.length - 1 && /* @__PURE__ */ jsx(
14539
+ "div",
14540
+ {
14541
+ className: cn(
14542
+ "flex-1 h-px mb-4 transition-colors",
14543
+ status === "complete" ? "bg-primary" : "bg-border"
14544
+ )
14545
+ }
14546
+ )
14547
+ ] }, step.id);
14548
+ }) });
14549
+ }
14550
+ return /* @__PURE__ */ jsx("div", { className: "flex items-center gap-0 mb-6", children: steps.map((step, i) => {
14551
+ const status = statuses[i];
14552
+ return /* @__PURE__ */ jsxs(React15__default.Fragment, { children: [
14553
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
14554
+ /* @__PURE__ */ jsx(
14555
+ "div",
14556
+ {
14557
+ className: cn(
14558
+ "flex h-7 w-7 items-center justify-center rounded-full text-xs font-semibold border-2 transition-colors",
14559
+ status === "complete" && "border-primary bg-primary text-primary-foreground",
14560
+ status === "active" && "border-primary bg-background text-primary",
14561
+ status === "locked" && "border-border bg-background text-muted-foreground"
14562
+ ),
14563
+ children: status === "complete" ? /* @__PURE__ */ jsx(Check, { className: "h-3.5 w-3.5" }) : i + 1
14564
+ }
14565
+ ),
14566
+ /* @__PURE__ */ jsx(
14567
+ "span",
14568
+ {
14569
+ className: cn(
14570
+ "text-sm font-medium hidden sm:block",
14571
+ status === "locked" ? "text-muted-foreground" : "text-foreground"
14572
+ ),
14573
+ children: step.title
14574
+ }
14575
+ )
14576
+ ] }),
14577
+ i < steps.length - 1 && /* @__PURE__ */ jsx(
14578
+ "div",
14579
+ {
14580
+ className: cn(
14581
+ "flex-1 h-px mx-3 transition-colors",
14582
+ status === "complete" ? "bg-primary" : "bg-border"
14583
+ )
14584
+ }
14585
+ )
14586
+ ] }, step.id);
14587
+ }) });
14588
+ };
14589
+ var ActiveStep = ({
14590
+ title,
14591
+ children
14592
+ }) => /* @__PURE__ */ jsxs("div", { className: "mb-4 space-y-4", children: [
14593
+ /* @__PURE__ */ jsx("h2", { className: "text-base font-medium text-foreground", children: title }),
14594
+ children
14595
+ ] });
14596
+ var Wizard = ({ node }) => {
14597
+ const store = useFormStore();
14598
+ const [formState, setFormState] = useState(() => ({
14599
+ ...store.getState()
14600
+ }));
14601
+ useEffect(() => {
14602
+ return store.subscribe((state) => {
14603
+ console.log("[MultiStepForm] store update \u2192", {
14604
+ values: state.values,
14605
+ errors: state.errors,
14606
+ visibility: state.visibility
14607
+ });
14608
+ setFormState({ ...state });
14609
+ });
14610
+ }, [store]);
14611
+ const schema = store.getSchema();
14612
+ const fieldMap = {};
14613
+ schema.fields.forEach((f) => {
14614
+ fieldMap[f.id] = f;
14615
+ });
14616
+ const { steps, meta } = node;
14617
+ const showStepper = meta?.showStepper !== false;
14618
+ const variant = meta?.stepperVariant ?? "numbered";
14619
+ const collapseCompleted = meta?.collapseCompleted !== false;
14620
+ const stepComplete = steps.map((s) => {
14621
+ const fieldIds = flattenStepFieldIds(s);
14622
+ const complete = isStepComplete(s, formState, fieldMap);
14623
+ console.log(`[MultiStepForm] step "${s.id}" fieldIds=${JSON.stringify(fieldIds)} complete=${complete}`, {
14624
+ errors: fieldIds.map((id) => ({ id, error: formState.errors[id] })),
14625
+ values: fieldIds.map((id) => ({ id, value: formState.values[id] }))
14626
+ });
14627
+ return complete;
14628
+ });
14629
+ const activeIndex = stepComplete.findIndex((c) => !c) === -1 ? steps.length : stepComplete.findIndex((c) => !c);
14630
+ console.log(`[MultiStepForm] activeIndex=${activeIndex} stepComplete=${JSON.stringify(stepComplete)}`);
14631
+ const statuses = steps.map((_, i) => {
14632
+ if (i < activeIndex) return "complete";
14633
+ if (i === activeIndex) return "active";
14634
+ return "locked";
14635
+ });
14636
+ return /* @__PURE__ */ jsxs("div", { children: [
14637
+ node.title && /* @__PURE__ */ jsx("h2", { className: "text-lg font-semibold mb-4", children: node.title }),
14638
+ showStepper && /* @__PURE__ */ jsx(Stepper2, { steps, statuses, variant }),
14639
+ /* @__PURE__ */ jsx("div", { className: "space-y-2", children: steps.map((step, i) => {
14640
+ const status = statuses[i];
14641
+ if (status === "locked") return null;
14642
+ const children = renderSectionChildren(step.children);
14643
+ if (status === "complete") {
14644
+ if (collapseCompleted) {
14645
+ return /* @__PURE__ */ jsx(Section, { title: step.title, children }, step.id);
14646
+ }
14647
+ return /* @__PURE__ */ jsxs(
14648
+ "div",
14649
+ {
14650
+ className: "mb-4 opacity-60 pointer-events-none select-none space-y-4",
14651
+ children: [
14652
+ /* @__PURE__ */ jsx("h2", { className: "text-base font-medium text-foreground", children: step.title }),
14653
+ children
14654
+ ]
14655
+ },
14656
+ step.id
14657
+ );
14658
+ }
14659
+ return /* @__PURE__ */ jsx(ActiveStep, { title: step.title, children }, step.id);
14660
+ }) })
14661
+ ] });
14662
+ };
14663
+ var MultiStepForm = ({
14664
+ showTitle = true,
14665
+ className
14666
+ }) => {
14667
+ const store = useFormStore();
14668
+ const schema = store.getSchema();
14669
+ return /* @__PURE__ */ jsxs("div", { className: cn("space-y-6", className), children: [
14670
+ showTitle && /* @__PURE__ */ jsx("div", { className: "mb-4", children: /* @__PURE__ */ jsx("h1", { className: "text-2xl font-bold", children: schema.title }) }),
14671
+ schema.layout.map((node) => {
14672
+ if (node.type === "multi_step") {
14673
+ return /* @__PURE__ */ jsx(Wizard, { node }, node.id);
14674
+ }
14675
+ if (node.type === "section") {
14676
+ return /* @__PURE__ */ jsxs("div", { className: "mb-6 space-y-4", children: [
14677
+ /* @__PURE__ */ jsx("h2", { className: "text-base font-medium text-foreground", children: node.title }),
14678
+ renderSectionChildren(node.children)
14679
+ ] }, node.id);
14680
+ }
14681
+ if (node.type === "column_layout") {
14682
+ return /* @__PURE__ */ jsx(
14683
+ ColumnLayout,
14684
+ {
14685
+ columns: node.columns,
14686
+ label: node.label,
14687
+ children: node.children.map((fieldId) => /* @__PURE__ */ jsx(FieldRenderer, { fieldId }, fieldId))
14688
+ },
14689
+ node.id
14690
+ );
14691
+ }
14692
+ return null;
14693
+ })
14694
+ ] });
14695
+ };
14058
14696
  var SUGGESTION_KEYS = /* @__PURE__ */ new Set(["medications", "investigations", "procedures"]);
14059
14697
  function shallowEqualKeys(a, b) {
14060
14698
  const keysA = Object.keys(a);
@@ -15017,7 +15655,8 @@ var FormControls = ({
15017
15655
  actionsFullWidth = false,
15018
15656
  buttonClassName,
15019
15657
  onSaveDraft,
15020
- onSubmit
15658
+ onSubmit,
15659
+ submitLabel
15021
15660
  }) => {
15022
15661
  const {
15023
15662
  state,
@@ -15106,7 +15745,7 @@ var FormControls = ({
15106
15745
  disabled: !state.isValid || hasUploadsInFlight,
15107
15746
  size: "sm",
15108
15747
  className: buttonCn,
15109
- children: hasUploadsInFlight ? "Upload in progress\u2026" : availableTransitions.length > 1 ? `Submit to ${t.to}` : "Submit"
15748
+ children: hasUploadsInFlight ? "Upload in progress\u2026" : availableTransitions.length > 1 ? `Submit to ${t.to}` : submitLabel ?? "Submit"
15110
15749
  },
15111
15750
  t.to
15112
15751
  )) : onSubmit ? /* @__PURE__ */ jsx(
@@ -15119,7 +15758,7 @@ var FormControls = ({
15119
15758
  disabled: !state.isValid || hasUploadsInFlight,
15120
15759
  size: "sm",
15121
15760
  className: buttonCn,
15122
- children: hasUploadsInFlight ? "Upload in progress\u2026" : "Submit"
15761
+ children: hasUploadsInFlight ? "Upload in progress\u2026" : submitLabel ?? "Submit"
15123
15762
  }
15124
15763
  ) : null })
15125
15764
  ]
@@ -15160,6 +15799,6 @@ function createUploadHandler(options) {
15160
15799
  };
15161
15800
  }
15162
15801
 
15163
- export { AddInvestigationField, AddMedicationField, DifferentialDiagnosis, DynamicForm, DynamicFormV2, EMAIL_REGEX, FieldRenderer, FormControls, FormProvider, FormStore, ImageUploadWidget, MAR_MEDICATION_ORDERS_META_KEY, MediaUploadWidget, ReadOnlyForm, ReadOnlyImageUpload, ReadOnlyMediaUpload, ReadOnlySignature, ReadOnlyTable, RichTextWidget, SignatureUploadWidget, SmartForm, SmartTextareaWidget, createUploadHandler, evaluateRules, fieldMetaRequestsMarMedicationOrdersButton, useField, useFieldHandlers, useForm, useFormStore, useFrequentItems, usePackages, useSmartKeywords, validateField, validateForm };
15802
+ export { AddInvestigationField, AddMedicationField, DifferentialDiagnosis, DynamicForm, DynamicFormV2, EMAIL_REGEX, FieldRenderer, FormControls, FormProvider, FormStore, ImageUploadWidget, MAR_MEDICATION_ORDERS_META_KEY, MediaUploadWidget, MultiStepForm, ReadOnlyForm, ReadOnlyImageUpload, ReadOnlyMediaUpload, ReadOnlySignature, ReadOnlyTable, RichTextWidget, SignatureUploadWidget, SmartForm, SmartTextareaWidget, createUploadHandler, evaluateRules, fieldMetaRequestsMarMedicationOrdersButton, useField, useFieldHandlers, useForm, useFormStore, useFrequentItems, usePackages, useSmartKeywords, validateField, validateForm };
15164
15803
  //# sourceMappingURL=index.mjs.map
15165
15804
  //# sourceMappingURL=index.mjs.map