@mehdashti/forms 0.3.1 → 0.4.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
@@ -1,7 +1,7 @@
1
- import { useForm, Controller, useFieldArray } from 'react-hook-form';
1
+ import { useForm, Controller, useFormContext, useFieldArray } from 'react-hook-form';
2
2
  import { zodResolver } from '@hookform/resolvers/zod';
3
3
  import { jsx, jsxs } from 'react/jsx-runtime';
4
- import * as React2 from 'react';
4
+ import * as React4 from 'react';
5
5
  import { z } from 'zod';
6
6
  export { z } from 'zod';
7
7
 
@@ -273,7 +273,7 @@ function FormFileUpload({
273
273
  showPreview = true,
274
274
  onFileChange
275
275
  }) {
276
- const [previews, setPreviews] = React2.useState([]);
276
+ const [previews, setPreviews] = React4.useState([]);
277
277
  const handleFileChange = (files, onChange) => {
278
278
  if (!files || files.length === 0) {
279
279
  setPreviews([]);
@@ -655,6 +655,425 @@ function FormMultiSelect({
655
655
  }
656
656
  );
657
657
  }
658
+ function FormRadioGroup({
659
+ name,
660
+ label,
661
+ options,
662
+ orientation = "vertical",
663
+ helperText,
664
+ required,
665
+ disabled,
666
+ className
667
+ }) {
668
+ const {
669
+ register,
670
+ formState: { errors }
671
+ } = useFormContext();
672
+ const error = errors[name]?.message;
673
+ const fieldId = `field-${name}`;
674
+ return /* @__PURE__ */ jsxs("div", { className, children: [
675
+ label && /* @__PURE__ */ jsx("div", { className: "mb-2", children: /* @__PURE__ */ jsxs("label", { id: fieldId, className: "text-sm font-medium text-gray-700 dark:text-gray-300", children: [
676
+ label,
677
+ required && /* @__PURE__ */ jsx("span", { className: "text-red-500 ml-1", children: "*" })
678
+ ] }) }),
679
+ /* @__PURE__ */ jsx(
680
+ "div",
681
+ {
682
+ role: "radiogroup",
683
+ "aria-labelledby": label ? fieldId : void 0,
684
+ className: orientation === "horizontal" ? "flex flex-wrap gap-4" : "space-y-3",
685
+ children: options.map((option) => {
686
+ const optionId = `${fieldId}-${option.value}`;
687
+ const isDisabled = disabled || option.disabled;
688
+ return /* @__PURE__ */ jsxs("div", { className: "flex items-start", children: [
689
+ /* @__PURE__ */ jsx("div", { className: "flex items-center h-5", children: /* @__PURE__ */ jsx(
690
+ "input",
691
+ {
692
+ ...register(name),
693
+ type: "radio",
694
+ id: optionId,
695
+ value: option.value,
696
+ disabled: isDisabled,
697
+ className: `
698
+ h-4 w-4 border-gray-300 text-blue-600
699
+ focus:ring-2 focus:ring-blue-500 focus:ring-offset-2
700
+ disabled:opacity-50 disabled:cursor-not-allowed
701
+ dark:border-gray-600 dark:bg-gray-800 dark:focus:ring-offset-gray-900
702
+ `
703
+ }
704
+ ) }),
705
+ /* @__PURE__ */ jsxs("div", { className: "ml-3", children: [
706
+ /* @__PURE__ */ jsx(
707
+ "label",
708
+ {
709
+ htmlFor: optionId,
710
+ className: `
711
+ text-sm font-medium
712
+ ${isDisabled ? "text-gray-400 cursor-not-allowed" : "text-gray-700 dark:text-gray-300 cursor-pointer"}
713
+ `,
714
+ children: option.label
715
+ }
716
+ ),
717
+ option.description && /* @__PURE__ */ jsx("p", { className: "text-xs text-gray-500 dark:text-gray-400 mt-1", children: option.description })
718
+ ] })
719
+ ] }, option.value);
720
+ })
721
+ }
722
+ ),
723
+ helperText && !error && /* @__PURE__ */ jsx("p", { className: "mt-2 text-sm text-gray-500 dark:text-gray-400", children: helperText }),
724
+ error && /* @__PURE__ */ jsx("p", { className: "mt-2 text-sm text-red-600 dark:text-red-400", role: "alert", children: error })
725
+ ] });
726
+ }
727
+ function FormSwitch({
728
+ name,
729
+ label,
730
+ description,
731
+ helperText,
732
+ required,
733
+ disabled,
734
+ className,
735
+ onChange
736
+ }) {
737
+ const {
738
+ register,
739
+ watch,
740
+ setValue,
741
+ formState: { errors }
742
+ } = useFormContext();
743
+ const value = watch(name);
744
+ const checked = Boolean(value);
745
+ const error = errors[name]?.message;
746
+ const fieldId = `field-${name}`;
747
+ const handleChange = (e) => {
748
+ const newValue = e.target.checked;
749
+ setValue(name, newValue, { shouldValidate: true, shouldDirty: true });
750
+ onChange?.(newValue);
751
+ };
752
+ return /* @__PURE__ */ jsxs("div", { className, children: [
753
+ /* @__PURE__ */ jsxs("div", { className: "flex items-start", children: [
754
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center h-5", children: [
755
+ /* @__PURE__ */ jsxs(
756
+ "button",
757
+ {
758
+ type: "button",
759
+ role: "switch",
760
+ "aria-checked": checked,
761
+ "aria-labelledby": label ? `${fieldId}-label` : void 0,
762
+ "aria-describedby": description ? `${fieldId}-description` : void 0,
763
+ disabled,
764
+ onClick: () => {
765
+ if (!disabled) {
766
+ const newValue = !checked;
767
+ setValue(name, newValue, { shouldValidate: true, shouldDirty: true });
768
+ onChange?.(newValue);
769
+ }
770
+ },
771
+ className: `
772
+ relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent
773
+ transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2
774
+ disabled:opacity-50 disabled:cursor-not-allowed
775
+ dark:focus:ring-offset-gray-900
776
+ ${checked ? "bg-blue-600" : "bg-gray-200 dark:bg-gray-700"}
777
+ `,
778
+ children: [
779
+ /* @__PURE__ */ jsx("span", { className: "sr-only", children: label }),
780
+ /* @__PURE__ */ jsx(
781
+ "span",
782
+ {
783
+ className: `
784
+ pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0
785
+ transition duration-200 ease-in-out
786
+ ${checked ? "translate-x-5" : "translate-x-0"}
787
+ `
788
+ }
789
+ )
790
+ ]
791
+ }
792
+ ),
793
+ /* @__PURE__ */ jsx(
794
+ "input",
795
+ {
796
+ ...register(name),
797
+ type: "checkbox",
798
+ id: fieldId,
799
+ checked,
800
+ onChange: handleChange,
801
+ className: "sr-only",
802
+ disabled
803
+ }
804
+ )
805
+ ] }),
806
+ (label || description) && /* @__PURE__ */ jsxs("div", { className: "ml-3", children: [
807
+ label && /* @__PURE__ */ jsxs(
808
+ "label",
809
+ {
810
+ id: `${fieldId}-label`,
811
+ htmlFor: fieldId,
812
+ className: `
813
+ text-sm font-medium
814
+ ${disabled ? "text-gray-400" : "text-gray-700 dark:text-gray-300"}
815
+ `,
816
+ children: [
817
+ label,
818
+ required && /* @__PURE__ */ jsx("span", { className: "text-red-500 ml-1", children: "*" })
819
+ ]
820
+ }
821
+ ),
822
+ description && /* @__PURE__ */ jsx(
823
+ "p",
824
+ {
825
+ id: `${fieldId}-description`,
826
+ className: "text-xs text-gray-500 dark:text-gray-400",
827
+ children: description
828
+ }
829
+ )
830
+ ] })
831
+ ] }),
832
+ helperText && !error && /* @__PURE__ */ jsx("p", { className: "mt-2 text-sm text-gray-500 dark:text-gray-400", children: helperText }),
833
+ error && /* @__PURE__ */ jsx("p", { className: "mt-2 text-sm text-red-600 dark:text-red-400", role: "alert", children: error })
834
+ ] });
835
+ }
836
+ function FormTagInput({
837
+ name,
838
+ label,
839
+ placeholder = "Type and press Enter...",
840
+ helperText,
841
+ required,
842
+ disabled,
843
+ maxTags,
844
+ separators = ["Enter", ","],
845
+ validateTag,
846
+ className
847
+ }) {
848
+ const {
849
+ watch,
850
+ setValue,
851
+ formState: { errors }
852
+ } = useFormContext();
853
+ const [inputValue, setInputValue] = React4.useState("");
854
+ const [tagError, setTagError] = React4.useState("");
855
+ const tags = watch(name) || [];
856
+ const error = errors[name]?.message;
857
+ const fieldId = `field-${name}`;
858
+ const addTag = (tag) => {
859
+ const trimmed = tag.trim();
860
+ if (!trimmed) return;
861
+ if (tags.includes(trimmed)) {
862
+ setTagError("Tag already exists");
863
+ return;
864
+ }
865
+ if (maxTags && tags.length >= maxTags) {
866
+ setTagError(`Maximum ${maxTags} tags allowed`);
867
+ return;
868
+ }
869
+ if (validateTag) {
870
+ const validation = validateTag(trimmed);
871
+ if (validation === false) {
872
+ setTagError("Invalid tag");
873
+ return;
874
+ }
875
+ if (typeof validation === "string") {
876
+ setTagError(validation);
877
+ return;
878
+ }
879
+ }
880
+ setValue(name, [...tags, trimmed], {
881
+ shouldValidate: true,
882
+ shouldDirty: true
883
+ });
884
+ setInputValue("");
885
+ setTagError("");
886
+ };
887
+ const removeTag = (index) => {
888
+ const newTags = tags.filter((_, i) => i !== index);
889
+ setValue(name, newTags, {
890
+ shouldValidate: true,
891
+ shouldDirty: true
892
+ });
893
+ };
894
+ const handleKeyDown = (e) => {
895
+ if (separators.includes(e.key)) {
896
+ e.preventDefault();
897
+ addTag(inputValue);
898
+ } else if (e.key === "Backspace" && !inputValue && tags.length > 0) {
899
+ removeTag(tags.length - 1);
900
+ }
901
+ };
902
+ const handleBlur = () => {
903
+ if (inputValue.trim()) {
904
+ addTag(inputValue);
905
+ }
906
+ };
907
+ return /* @__PURE__ */ jsxs("div", { className, children: [
908
+ label && /* @__PURE__ */ jsxs(
909
+ "label",
910
+ {
911
+ htmlFor: fieldId,
912
+ className: "block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1",
913
+ children: [
914
+ label,
915
+ required && /* @__PURE__ */ jsx("span", { className: "text-red-500 ml-1", children: "*" })
916
+ ]
917
+ }
918
+ ),
919
+ /* @__PURE__ */ jsxs(
920
+ "div",
921
+ {
922
+ className: `
923
+ flex flex-wrap gap-2 p-2 border rounded-md min-h-[42px]
924
+ ${disabled ? "bg-gray-50 dark:bg-gray-900 cursor-not-allowed" : "bg-white dark:bg-gray-800"}
925
+ ${error || tagError ? "border-red-300 dark:border-red-700" : "border-gray-300 dark:border-gray-600"}
926
+ focus-within:ring-2 focus-within:ring-blue-500 focus-within:border-blue-500
927
+ dark:focus-within:ring-blue-400 dark:focus-within:border-blue-400
928
+ `,
929
+ children: [
930
+ tags.map((tag, index) => /* @__PURE__ */ jsxs(
931
+ "span",
932
+ {
933
+ className: "inline-flex items-center gap-1 px-2 py-1 text-sm bg-blue-100 text-blue-800 rounded dark:bg-blue-900 dark:text-blue-200",
934
+ children: [
935
+ tag,
936
+ !disabled && /* @__PURE__ */ jsx(
937
+ "button",
938
+ {
939
+ type: "button",
940
+ onClick: () => removeTag(index),
941
+ className: "ml-1 hover:text-blue-600 dark:hover:text-blue-300",
942
+ "aria-label": `Remove ${tag}`,
943
+ children: "\xD7"
944
+ }
945
+ )
946
+ ]
947
+ },
948
+ index
949
+ )),
950
+ /* @__PURE__ */ jsx(
951
+ "input",
952
+ {
953
+ id: fieldId,
954
+ type: "text",
955
+ value: inputValue,
956
+ onChange: (e) => {
957
+ setInputValue(e.target.value);
958
+ setTagError("");
959
+ },
960
+ onKeyDown: handleKeyDown,
961
+ onBlur: handleBlur,
962
+ placeholder: tags.length === 0 ? placeholder : "",
963
+ disabled: disabled || maxTags !== void 0 && tags.length >= maxTags,
964
+ className: "flex-1 min-w-[120px] outline-none bg-transparent text-sm text-gray-900 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-500 disabled:cursor-not-allowed"
965
+ }
966
+ )
967
+ ]
968
+ }
969
+ ),
970
+ helperText && !error && !tagError && /* @__PURE__ */ jsx("p", { className: "mt-1 text-sm text-gray-500 dark:text-gray-400", children: helperText }),
971
+ (error || tagError) && /* @__PURE__ */ jsx("p", { className: "mt-1 text-sm text-red-600 dark:text-red-400", role: "alert", children: error || tagError }),
972
+ maxTags && /* @__PURE__ */ jsxs("p", { className: "mt-1 text-xs text-gray-500 dark:text-gray-400", children: [
973
+ tags.length,
974
+ " / ",
975
+ maxTags,
976
+ " tags"
977
+ ] })
978
+ ] });
979
+ }
980
+ function FormErrorSummary({
981
+ title = "Please fix the following errors:",
982
+ fieldLabels = {},
983
+ show,
984
+ className,
985
+ onErrorClick
986
+ }) {
987
+ const {
988
+ formState: { errors }
989
+ } = useFormContext();
990
+ const errorList = React4.useMemo(() => {
991
+ return flattenErrors(errors, fieldLabels);
992
+ }, [errors, fieldLabels]);
993
+ const shouldShow = show !== void 0 ? show : errorList.length > 0;
994
+ if (!shouldShow) {
995
+ return null;
996
+ }
997
+ const handleErrorClick = (fieldName) => {
998
+ const field = document.getElementById(`field-${fieldName}`) || document.querySelector(`[name="${fieldName}"]`);
999
+ if (field) {
1000
+ field.focus();
1001
+ field.scrollIntoView({ behavior: "smooth", block: "center" });
1002
+ }
1003
+ onErrorClick?.(fieldName);
1004
+ };
1005
+ return /* @__PURE__ */ jsx(
1006
+ "div",
1007
+ {
1008
+ role: "alert",
1009
+ "aria-live": "polite",
1010
+ className: `
1011
+ rounded-md bg-red-50 dark:bg-red-900/20 p-4 border border-red-200 dark:border-red-800
1012
+ ${className}
1013
+ `,
1014
+ children: /* @__PURE__ */ jsxs("div", { className: "flex", children: [
1015
+ /* @__PURE__ */ jsx("div", { className: "flex-shrink-0", children: /* @__PURE__ */ jsx(
1016
+ "svg",
1017
+ {
1018
+ className: "h-5 w-5 text-red-400 dark:text-red-600",
1019
+ xmlns: "http://www.w3.org/2000/svg",
1020
+ viewBox: "0 0 20 20",
1021
+ fill: "currentColor",
1022
+ "aria-hidden": "true",
1023
+ children: /* @__PURE__ */ jsx(
1024
+ "path",
1025
+ {
1026
+ fillRule: "evenodd",
1027
+ d: "M10 18a8 8 0 100-16 8 8 0 000 16zM8.28 7.22a.75.75 0 00-1.06 1.06L8.94 10l-1.72 1.72a.75.75 0 101.06 1.06L10 11.06l1.72 1.72a.75.75 0 101.06-1.06L11.06 10l1.72-1.72a.75.75 0 00-1.06-1.06L10 8.94 8.28 7.22z",
1028
+ clipRule: "evenodd"
1029
+ }
1030
+ )
1031
+ }
1032
+ ) }),
1033
+ /* @__PURE__ */ jsxs("div", { className: "ml-3 flex-1", children: [
1034
+ /* @__PURE__ */ jsx("h3", { className: "text-sm font-medium text-red-800 dark:text-red-300", children: title }),
1035
+ /* @__PURE__ */ jsx("div", { className: "mt-2 text-sm text-red-700 dark:text-red-400", children: /* @__PURE__ */ jsx("ul", { className: "list-disc space-y-1 pl-5", children: errorList.map(({ fieldName, fieldLabel, message }) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs(
1036
+ "button",
1037
+ {
1038
+ type: "button",
1039
+ onClick: () => handleErrorClick(fieldName),
1040
+ className: "hover:underline focus:outline-none focus:underline text-left",
1041
+ children: [
1042
+ /* @__PURE__ */ jsxs("strong", { children: [
1043
+ fieldLabel,
1044
+ ":"
1045
+ ] }),
1046
+ " ",
1047
+ message
1048
+ ]
1049
+ }
1050
+ ) }, fieldName)) }) })
1051
+ ] })
1052
+ ] })
1053
+ }
1054
+ );
1055
+ }
1056
+ function flattenErrors(errors, fieldLabels, prefix = "") {
1057
+ const result = [];
1058
+ for (const [key, value] of Object.entries(errors)) {
1059
+ const fieldName = prefix ? `${prefix}.${key}` : key;
1060
+ if (value && typeof value === "object") {
1061
+ if ("message" in value && typeof value.message === "string") {
1062
+ result.push({
1063
+ fieldName,
1064
+ fieldLabel: fieldLabels[fieldName] || formatFieldName(fieldName),
1065
+ message: value.message
1066
+ });
1067
+ } else {
1068
+ result.push(...flattenErrors(value, fieldLabels, fieldName));
1069
+ }
1070
+ }
1071
+ }
1072
+ return result;
1073
+ }
1074
+ function formatFieldName(name) {
1075
+ return name.split(".").pop().replace(/([A-Z])/g, " $1").replace(/_/g, " ").replace(/^\w/, (c) => c.toUpperCase()).trim();
1076
+ }
658
1077
  var spacingClasses = {
659
1078
  sm: "gap-3",
660
1079
  md: "gap-4",
@@ -766,9 +1185,9 @@ function FormWizard({
766
1185
  className,
767
1186
  showStepNumbers = true
768
1187
  }) {
769
- const [currentStep, setCurrentStep] = React2.useState(0);
770
- const [isValidating, setIsValidating] = React2.useState(false);
771
- const [isCompleting, setIsCompleting] = React2.useState(false);
1188
+ const [currentStep, setCurrentStep] = React4.useState(0);
1189
+ const [isValidating, setIsValidating] = React4.useState(false);
1190
+ const [isCompleting, setIsCompleting] = React4.useState(false);
772
1191
  const isFirstStep = currentStep === 0;
773
1192
  const isLastStep = currentStep === steps.length - 1;
774
1193
  const handleNext = async () => {
@@ -928,6 +1347,47 @@ function useSmartFieldArray({
928
1347
  append
929
1348
  };
930
1349
  }
1350
+ function useUnsavedChanges(options = {}) {
1351
+ const {
1352
+ message = "You have unsaved changes. Are you sure you want to leave?",
1353
+ enabled = true,
1354
+ onBeforeUnload
1355
+ } = options;
1356
+ const {
1357
+ formState: { isDirty, isSubmitting, isSubmitSuccessful }
1358
+ } = useFormContext();
1359
+ const hasUnsavedChanges = isDirty && !isSubmitting && !isSubmitSuccessful;
1360
+ React4.useEffect(() => {
1361
+ if (!enabled) return;
1362
+ const handleBeforeUnload = (event) => {
1363
+ if (hasUnsavedChanges) {
1364
+ event.preventDefault();
1365
+ event.returnValue = message;
1366
+ onBeforeUnload?.(hasUnsavedChanges);
1367
+ return message;
1368
+ }
1369
+ };
1370
+ window.addEventListener("beforeunload", handleBeforeUnload);
1371
+ return () => {
1372
+ window.removeEventListener("beforeunload", handleBeforeUnload);
1373
+ };
1374
+ }, [hasUnsavedChanges, message, enabled, onBeforeUnload]);
1375
+ return {
1376
+ hasUnsavedChanges,
1377
+ isDirty
1378
+ };
1379
+ }
1380
+ function useUnsavedChangesBlocker() {
1381
+ const {
1382
+ formState: { isDirty, isSubmitting, isSubmitSuccessful }
1383
+ } = useFormContext();
1384
+ const hasUnsavedChanges = isDirty && !isSubmitting && !isSubmitSuccessful;
1385
+ return {
1386
+ hasUnsavedChanges,
1387
+ isDirty,
1388
+ shouldBlock: hasUnsavedChanges
1389
+ };
1390
+ }
931
1391
  var emailSchema = z.string().min(1, "Email is required").email("Invalid email address");
932
1392
  var passwordSchema = z.string().min(8, "Password must be at least 8 characters").regex(/[A-Z]/, "Password must contain at least one uppercase letter").regex(/[a-z]/, "Password must contain at least one lowercase letter").regex(/[0-9]/, "Password must contain at least one number");
933
1393
  var strongPasswordSchema = z.string().min(12, "Password must be at least 12 characters").regex(/[A-Z]/, "Password must contain at least one uppercase letter").regex(/[a-z]/, "Password must contain at least one lowercase letter").regex(/[0-9]/, "Password must contain at least one number").regex(/[^A-Za-z0-9]/, "Password must contain at least one special character");
@@ -1098,6 +1558,6 @@ function formatValidationError(error) {
1098
1558
  return formatted;
1099
1559
  }
1100
1560
 
1101
- export { FormCheckbox, FormDatePicker, FormField, FormFileUpload, FormGrid, FormGridItem, FormLayout, FormMultiSelect, FormSection, FormSelect, FormTextarea, FormWizard, conditionalSchema, createConfirmPasswordSchema, createFileSizeSchema, createFileTypeSchema, dateStringSchema, emailSchema, formatValidationError, futureDateSchema, getAllErrors, getErrorMessage, hasError, imageFileSchema, invalidFormatMessage, lengthMessage, optionalEmailSchema, optionalUrlSchema, passwordSchema, pastDateSchema, percentageSchema, phoneSchema, positiveIntegerSchema, positiveNumberSchema, priceSchema, requiredMessage, sanitizeString, slugSchema, strongPasswordSchema, urlSchema, useSmartFieldArray, useSmartForm, usernameSchema, validateCreditCard, validateFileExtension, validateIBAN, validatePasswordStrength };
1561
+ export { FormCheckbox, FormDatePicker, FormErrorSummary, FormField, FormFileUpload, FormGrid, FormGridItem, FormLayout, FormMultiSelect, FormRadioGroup, FormSection, FormSelect, FormSwitch, FormTagInput, FormTextarea, FormWizard, conditionalSchema, createConfirmPasswordSchema, createFileSizeSchema, createFileTypeSchema, dateStringSchema, emailSchema, formatValidationError, futureDateSchema, getAllErrors, getErrorMessage, hasError, imageFileSchema, invalidFormatMessage, lengthMessage, optionalEmailSchema, optionalUrlSchema, passwordSchema, pastDateSchema, percentageSchema, phoneSchema, positiveIntegerSchema, positiveNumberSchema, priceSchema, requiredMessage, sanitizeString, slugSchema, strongPasswordSchema, urlSchema, useSmartFieldArray, useSmartForm, useUnsavedChanges, useUnsavedChangesBlocker, usernameSchema, validateCreditCard, validateFileExtension, validateIBAN, validatePasswordStrength };
1102
1562
  //# sourceMappingURL=index.js.map
1103
1563
  //# sourceMappingURL=index.js.map