@page-speed/forms 0.1.4 → 0.1.6

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.
Files changed (52) hide show
  1. package/README.md +1 -1
  2. package/dist/core.cjs +376 -21
  3. package/dist/core.cjs.map +1 -1
  4. package/dist/core.js +356 -1
  5. package/dist/core.js.map +1 -1
  6. package/dist/index.cjs +376 -21
  7. package/dist/index.cjs.map +1 -1
  8. package/dist/index.js +356 -1
  9. package/dist/index.js.map +1 -1
  10. package/dist/inputs.cjs +253 -0
  11. package/dist/inputs.cjs.map +1 -1
  12. package/dist/inputs.d.cts +77 -1
  13. package/dist/inputs.d.ts +77 -1
  14. package/dist/inputs.js +253 -1
  15. package/dist/inputs.js.map +1 -1
  16. package/dist/integration.cjs +243 -0
  17. package/dist/integration.cjs.map +1 -0
  18. package/dist/integration.d.cts +381 -0
  19. package/dist/integration.d.ts +381 -0
  20. package/dist/integration.js +217 -0
  21. package/dist/integration.js.map +1 -0
  22. package/dist/upload.cjs +348 -0
  23. package/dist/upload.cjs.map +1 -0
  24. package/dist/upload.d.cts +174 -0
  25. package/dist/upload.d.ts +174 -0
  26. package/dist/upload.js +326 -0
  27. package/dist/upload.js.map +1 -0
  28. package/dist/validation-rules.cjs +231 -75
  29. package/dist/validation-rules.cjs.map +1 -1
  30. package/dist/validation-rules.js +215 -1
  31. package/dist/validation-rules.js.map +1 -1
  32. package/dist/validation-utils.cjs +133 -43
  33. package/dist/validation-utils.cjs.map +1 -1
  34. package/dist/validation-utils.js +125 -1
  35. package/dist/validation-utils.js.map +1 -1
  36. package/dist/validation.cjs +364 -115
  37. package/dist/validation.cjs.map +1 -1
  38. package/dist/validation.js +339 -2
  39. package/dist/validation.js.map +1 -1
  40. package/package.json +14 -4
  41. package/dist/chunk-2FXAQT7S.cjs +0 -236
  42. package/dist/chunk-2FXAQT7S.cjs.map +0 -1
  43. package/dist/chunk-A3UV7BIN.js +0 -357
  44. package/dist/chunk-A3UV7BIN.js.map +0 -1
  45. package/dist/chunk-P37YLBFA.cjs +0 -138
  46. package/dist/chunk-P37YLBFA.cjs.map +0 -1
  47. package/dist/chunk-WHQMBQNI.js +0 -127
  48. package/dist/chunk-WHQMBQNI.js.map +0 -1
  49. package/dist/chunk-YTTOWHBZ.js +0 -217
  50. package/dist/chunk-YTTOWHBZ.js.map +0 -1
  51. package/dist/chunk-ZQCPEOB6.cjs +0 -382
  52. package/dist/chunk-ZQCPEOB6.cjs.map +0 -1
package/dist/inputs.cjs CHANGED
@@ -22,6 +22,7 @@ function _interopNamespace(e) {
22
22
 
23
23
  var React6__namespace = /*#__PURE__*/_interopNamespace(React6);
24
24
 
25
+ // src/inputs/TextInput.tsx
25
26
  function TextInput({
26
27
  name,
27
28
  value,
@@ -668,9 +669,261 @@ function Select({
668
669
  );
669
670
  }
670
671
  Select.displayName = "Select";
672
+ function FileInput({
673
+ name,
674
+ value = [],
675
+ onChange,
676
+ onBlur,
677
+ placeholder = "Choose file(s)...",
678
+ disabled = false,
679
+ required = false,
680
+ error = false,
681
+ className = "",
682
+ accept,
683
+ maxSize = 5 * 1024 * 1024,
684
+ // 5MB default
685
+ maxFiles = 1,
686
+ multiple = false,
687
+ showPreview = true,
688
+ onValidationError,
689
+ onFileRemove,
690
+ ...props
691
+ }) {
692
+ const inputRef = React6__namespace.useRef(null);
693
+ const [dragActive, setDragActive] = React6__namespace.useState(false);
694
+ const validateFile = React6__namespace.useCallback(
695
+ (file) => {
696
+ if (accept) {
697
+ const acceptedTypes = accept.split(",").map((t) => t.trim());
698
+ const isValidType = acceptedTypes.some((type) => {
699
+ if (type.startsWith(".")) {
700
+ return file.name.toLowerCase().endsWith(type.toLowerCase());
701
+ } else if (type.endsWith("/*")) {
702
+ const baseType = type.split("/")[0];
703
+ return file.type.startsWith(baseType + "/");
704
+ } else {
705
+ return file.type === type;
706
+ }
707
+ });
708
+ if (!isValidType) {
709
+ return {
710
+ file,
711
+ error: "type",
712
+ message: `File type "${file.type}" is not accepted. Accepted types: ${accept}`
713
+ };
714
+ }
715
+ }
716
+ if (file.size > maxSize) {
717
+ const maxSizeMB = (maxSize / (1024 * 1024)).toFixed(2);
718
+ const fileSizeMB = (file.size / (1024 * 1024)).toFixed(2);
719
+ return {
720
+ file,
721
+ error: "size",
722
+ message: `File size ${fileSizeMB}MB exceeds maximum ${maxSizeMB}MB`
723
+ };
724
+ }
725
+ return null;
726
+ },
727
+ [accept, maxSize]
728
+ );
729
+ const handleFiles = React6__namespace.useCallback(
730
+ (fileList) => {
731
+ if (!fileList || fileList.length === 0) return;
732
+ const newFiles = Array.from(fileList);
733
+ const validationErrors = [];
734
+ const validFiles = [];
735
+ for (const file of newFiles) {
736
+ const validationError = validateFile(file);
737
+ if (validationError) {
738
+ validationErrors.push(validationError);
739
+ } else {
740
+ validFiles.push(file);
741
+ }
742
+ }
743
+ const totalFiles = value.length + validFiles.length;
744
+ if (totalFiles > maxFiles) {
745
+ validationErrors.push({
746
+ file: validFiles[0],
747
+ // Use first file as reference
748
+ error: "count",
749
+ message: `Maximum ${maxFiles} file(s) allowed. Attempting to add ${validFiles.length} to existing ${value.length}.`
750
+ });
751
+ }
752
+ if (validationErrors.length > 0 && onValidationError) {
753
+ onValidationError(validationErrors);
754
+ }
755
+ if (validFiles.length > 0 && totalFiles <= maxFiles) {
756
+ const updatedFiles = multiple ? [...value, ...validFiles] : validFiles;
757
+ onChange(updatedFiles.slice(0, maxFiles));
758
+ }
759
+ if (inputRef.current) {
760
+ inputRef.current.value = "";
761
+ }
762
+ },
763
+ [value, onChange, validateFile, maxFiles, multiple, onValidationError]
764
+ );
765
+ const handleChange = (e) => {
766
+ handleFiles(e.target.files);
767
+ };
768
+ const handleRemove = (index) => {
769
+ const fileToRemove = value[index];
770
+ const updatedFiles = value.filter((_, i) => i !== index);
771
+ onChange(updatedFiles);
772
+ if (onFileRemove && fileToRemove) {
773
+ onFileRemove(fileToRemove, index);
774
+ }
775
+ };
776
+ const handleDrag = (e) => {
777
+ e.preventDefault();
778
+ e.stopPropagation();
779
+ if (e.type === "dragenter" || e.type === "dragover") {
780
+ setDragActive(true);
781
+ } else if (e.type === "dragleave") {
782
+ setDragActive(false);
783
+ }
784
+ };
785
+ const handleDrop = (e) => {
786
+ e.preventDefault();
787
+ e.stopPropagation();
788
+ setDragActive(false);
789
+ if (disabled) return;
790
+ handleFiles(e.dataTransfer.files);
791
+ };
792
+ const handleClick = () => {
793
+ inputRef.current?.click();
794
+ };
795
+ const handleKeyDown = (e) => {
796
+ if (e.key === "Enter" || e.key === " ") {
797
+ e.preventDefault();
798
+ handleClick();
799
+ }
800
+ };
801
+ const formatFileSize = (bytes) => {
802
+ if (bytes === 0) return "0 Bytes";
803
+ const k = 1024;
804
+ const sizes = ["Bytes", "KB", "MB", "GB"];
805
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
806
+ return Math.round(bytes / Math.pow(k, i) * 100) / 100 + " " + sizes[i];
807
+ };
808
+ const getPreviewUrl = (file) => {
809
+ if (file.type.startsWith("image/")) {
810
+ return URL.createObjectURL(file);
811
+ }
812
+ return null;
813
+ };
814
+ React6__namespace.useEffect(() => {
815
+ return () => {
816
+ value.forEach((file) => {
817
+ const previewUrl = getPreviewUrl(file);
818
+ if (previewUrl) {
819
+ URL.revokeObjectURL(previewUrl);
820
+ }
821
+ });
822
+ };
823
+ }, [value]);
824
+ const baseClassName = "file-input";
825
+ const errorClassName = error ? "file-input--error" : "";
826
+ const dragClassName = dragActive ? "file-input--drag-active" : "";
827
+ const disabledClassName = disabled ? "file-input--disabled" : "";
828
+ const combinedClassName = `${baseClassName} ${errorClassName} ${dragClassName} ${disabledClassName} ${className}`.trim();
829
+ return /* @__PURE__ */ React6__namespace.createElement("div", { className: combinedClassName }, /* @__PURE__ */ React6__namespace.createElement(
830
+ "input",
831
+ {
832
+ ref: inputRef,
833
+ type: "file",
834
+ name,
835
+ onChange: handleChange,
836
+ onBlur,
837
+ accept,
838
+ multiple,
839
+ disabled,
840
+ required: required && value.length === 0,
841
+ className: "file-input__native",
842
+ "aria-invalid": error || props["aria-invalid"],
843
+ "aria-describedby": props["aria-describedby"],
844
+ "aria-required": required || props["aria-required"],
845
+ style: { display: "none" }
846
+ }
847
+ ), /* @__PURE__ */ React6__namespace.createElement(
848
+ "div",
849
+ {
850
+ className: "file-input__dropzone",
851
+ onDragEnter: handleDrag,
852
+ onDragLeave: handleDrag,
853
+ onDragOver: handleDrag,
854
+ onDrop: handleDrop,
855
+ onClick: handleClick,
856
+ onKeyDown: handleKeyDown,
857
+ role: "button",
858
+ tabIndex: disabled ? -1 : 0,
859
+ "aria-label": placeholder,
860
+ "aria-disabled": disabled
861
+ },
862
+ /* @__PURE__ */ React6__namespace.createElement("div", { className: "file-input__dropzone-content" }, /* @__PURE__ */ React6__namespace.createElement(
863
+ "svg",
864
+ {
865
+ className: "file-input__icon",
866
+ width: "48",
867
+ height: "48",
868
+ viewBox: "0 0 24 24",
869
+ fill: "none",
870
+ stroke: "currentColor",
871
+ strokeWidth: "2",
872
+ strokeLinecap: "round",
873
+ strokeLinejoin: "round",
874
+ "aria-hidden": "true"
875
+ },
876
+ /* @__PURE__ */ React6__namespace.createElement("path", { d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" }),
877
+ /* @__PURE__ */ React6__namespace.createElement("polyline", { points: "17 8 12 3 7 8" }),
878
+ /* @__PURE__ */ React6__namespace.createElement("line", { x1: "12", y1: "3", x2: "12", y2: "15" })
879
+ ), /* @__PURE__ */ React6__namespace.createElement("p", { className: "file-input__placeholder" }, value.length > 0 ? `${value.length} file(s) selected` : placeholder), accept && /* @__PURE__ */ React6__namespace.createElement("p", { className: "file-input__hint" }, "Accepted: ", accept), maxSize && /* @__PURE__ */ React6__namespace.createElement("p", { className: "file-input__hint" }, "Max size: ", formatFileSize(maxSize)))
880
+ ), value.length > 0 && /* @__PURE__ */ React6__namespace.createElement("ul", { className: "file-input__list", role: "list" }, value.map((file, index) => {
881
+ const previewUrl = showPreview ? getPreviewUrl(file) : null;
882
+ return /* @__PURE__ */ React6__namespace.createElement("li", { key: `${file.name}-${index}`, className: "file-input__item" }, previewUrl && /* @__PURE__ */ React6__namespace.createElement(
883
+ "img",
884
+ {
885
+ src: previewUrl,
886
+ alt: file.name,
887
+ className: "file-input__preview",
888
+ width: "48",
889
+ height: "48"
890
+ }
891
+ ), /* @__PURE__ */ React6__namespace.createElement("div", { className: "file-input__details" }, /* @__PURE__ */ React6__namespace.createElement("span", { className: "file-input__filename" }, file.name), /* @__PURE__ */ React6__namespace.createElement("span", { className: "file-input__filesize" }, formatFileSize(file.size))), /* @__PURE__ */ React6__namespace.createElement(
892
+ "button",
893
+ {
894
+ type: "button",
895
+ onClick: (e) => {
896
+ e.stopPropagation();
897
+ handleRemove(index);
898
+ },
899
+ disabled,
900
+ className: "file-input__remove",
901
+ "aria-label": `Remove ${file.name}`
902
+ },
903
+ /* @__PURE__ */ React6__namespace.createElement(
904
+ "svg",
905
+ {
906
+ width: "20",
907
+ height: "20",
908
+ viewBox: "0 0 24 24",
909
+ fill: "none",
910
+ stroke: "currentColor",
911
+ strokeWidth: "2",
912
+ strokeLinecap: "round",
913
+ strokeLinejoin: "round",
914
+ "aria-hidden": "true"
915
+ },
916
+ /* @__PURE__ */ React6__namespace.createElement("line", { x1: "18", y1: "6", x2: "6", y2: "18" }),
917
+ /* @__PURE__ */ React6__namespace.createElement("line", { x1: "6", y1: "6", x2: "18", y2: "18" })
918
+ )
919
+ ));
920
+ })));
921
+ }
922
+ FileInput.displayName = "FileInput";
671
923
 
672
924
  exports.Checkbox = Checkbox;
673
925
  exports.CheckboxGroup = CheckboxGroup;
926
+ exports.FileInput = FileInput;
674
927
  exports.Radio = Radio;
675
928
  exports.Select = Select;
676
929
  exports.TextArea = TextArea;