@page-speed/forms 0.1.5 → 0.1.7
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/inputs.cjs +252 -0
- package/dist/inputs.cjs.map +1 -1
- package/dist/inputs.d.cts +77 -1
- package/dist/inputs.d.ts +77 -1
- package/dist/inputs.js +252 -1
- package/dist/inputs.js.map +1 -1
- package/dist/integration.cjs +243 -0
- package/dist/integration.cjs.map +1 -0
- package/dist/integration.d.cts +381 -0
- package/dist/integration.d.ts +381 -0
- package/dist/integration.js +217 -0
- package/dist/integration.js.map +1 -0
- package/dist/upload.cjs +348 -0
- package/dist/upload.cjs.map +1 -0
- package/dist/upload.d.cts +174 -0
- package/dist/upload.d.ts +174 -0
- package/dist/upload.js +326 -0
- package/dist/upload.js.map +1 -0
- package/package.json +11 -1
package/dist/inputs.d.cts
CHANGED
|
@@ -572,4 +572,80 @@ declare namespace Select {
|
|
|
572
572
|
var displayName: string;
|
|
573
573
|
}
|
|
574
574
|
|
|
575
|
-
|
|
575
|
+
/**
|
|
576
|
+
* File validation error details
|
|
577
|
+
*/
|
|
578
|
+
interface FileValidationError {
|
|
579
|
+
file: File;
|
|
580
|
+
error: "type" | "size" | "count";
|
|
581
|
+
message: string;
|
|
582
|
+
}
|
|
583
|
+
/**
|
|
584
|
+
* FileInput component props
|
|
585
|
+
*/
|
|
586
|
+
interface FileInputProps extends Omit<InputProps<File[]>, "value" | "onChange"> {
|
|
587
|
+
/**
|
|
588
|
+
* Current file value(s)
|
|
589
|
+
*/
|
|
590
|
+
value?: File[];
|
|
591
|
+
/**
|
|
592
|
+
* Change handler receives array of files
|
|
593
|
+
*/
|
|
594
|
+
onChange: (files: File[]) => void;
|
|
595
|
+
/**
|
|
596
|
+
* Accepted file types (MIME types or extensions)
|
|
597
|
+
* @example ".pdf,.doc,.docx"
|
|
598
|
+
* @example "image/*,application/pdf"
|
|
599
|
+
*/
|
|
600
|
+
accept?: string;
|
|
601
|
+
/**
|
|
602
|
+
* Maximum file size in bytes
|
|
603
|
+
* @default 5MB (5 * 1024 * 1024)
|
|
604
|
+
*/
|
|
605
|
+
maxSize?: number;
|
|
606
|
+
/**
|
|
607
|
+
* Maximum number of files
|
|
608
|
+
* @default 1
|
|
609
|
+
*/
|
|
610
|
+
maxFiles?: number;
|
|
611
|
+
/**
|
|
612
|
+
* Allow multiple file selection
|
|
613
|
+
* @default false
|
|
614
|
+
*/
|
|
615
|
+
multiple?: boolean;
|
|
616
|
+
/**
|
|
617
|
+
* Show file preview thumbnails
|
|
618
|
+
* @default true
|
|
619
|
+
*/
|
|
620
|
+
showPreview?: boolean;
|
|
621
|
+
/**
|
|
622
|
+
* Validation error handler
|
|
623
|
+
*/
|
|
624
|
+
onValidationError?: (errors: FileValidationError[]) => void;
|
|
625
|
+
/**
|
|
626
|
+
* File removed handler
|
|
627
|
+
*/
|
|
628
|
+
onFileRemove?: (file: File, index: number) => void;
|
|
629
|
+
}
|
|
630
|
+
/**
|
|
631
|
+
* FileInput component for file selection with validation
|
|
632
|
+
*
|
|
633
|
+
* @example
|
|
634
|
+
* ```tsx
|
|
635
|
+
* <FileInput
|
|
636
|
+
* name="resume"
|
|
637
|
+
* accept=".pdf,.doc,.docx"
|
|
638
|
+
* maxSize={5 * 1024 * 1024}
|
|
639
|
+
* value={files}
|
|
640
|
+
* onChange={(files) => setFiles(files)}
|
|
641
|
+
* error={hasError}
|
|
642
|
+
* />
|
|
643
|
+
* ```
|
|
644
|
+
*/
|
|
645
|
+
declare function FileInput({ name, value, onChange, onBlur, placeholder, disabled, required, error, className, accept, maxSize, // 5MB default
|
|
646
|
+
maxFiles, multiple, showPreview, onValidationError, onFileRemove, ...props }: FileInputProps): React.JSX.Element;
|
|
647
|
+
declare namespace FileInput {
|
|
648
|
+
var displayName: string;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
export { Checkbox, CheckboxGroup, type CheckboxGroupOption, type CheckboxGroupProps, type CheckboxProps, FileInput, type FileInputProps, type FileValidationError, Radio, type RadioOption, type RadioProps, Select, type SelectOption, type SelectOptionGroup, type SelectProps, TextArea, type TextAreaProps, TextInput };
|
package/dist/inputs.d.ts
CHANGED
|
@@ -572,4 +572,80 @@ declare namespace Select {
|
|
|
572
572
|
var displayName: string;
|
|
573
573
|
}
|
|
574
574
|
|
|
575
|
-
|
|
575
|
+
/**
|
|
576
|
+
* File validation error details
|
|
577
|
+
*/
|
|
578
|
+
interface FileValidationError {
|
|
579
|
+
file: File;
|
|
580
|
+
error: "type" | "size" | "count";
|
|
581
|
+
message: string;
|
|
582
|
+
}
|
|
583
|
+
/**
|
|
584
|
+
* FileInput component props
|
|
585
|
+
*/
|
|
586
|
+
interface FileInputProps extends Omit<InputProps<File[]>, "value" | "onChange"> {
|
|
587
|
+
/**
|
|
588
|
+
* Current file value(s)
|
|
589
|
+
*/
|
|
590
|
+
value?: File[];
|
|
591
|
+
/**
|
|
592
|
+
* Change handler receives array of files
|
|
593
|
+
*/
|
|
594
|
+
onChange: (files: File[]) => void;
|
|
595
|
+
/**
|
|
596
|
+
* Accepted file types (MIME types or extensions)
|
|
597
|
+
* @example ".pdf,.doc,.docx"
|
|
598
|
+
* @example "image/*,application/pdf"
|
|
599
|
+
*/
|
|
600
|
+
accept?: string;
|
|
601
|
+
/**
|
|
602
|
+
* Maximum file size in bytes
|
|
603
|
+
* @default 5MB (5 * 1024 * 1024)
|
|
604
|
+
*/
|
|
605
|
+
maxSize?: number;
|
|
606
|
+
/**
|
|
607
|
+
* Maximum number of files
|
|
608
|
+
* @default 1
|
|
609
|
+
*/
|
|
610
|
+
maxFiles?: number;
|
|
611
|
+
/**
|
|
612
|
+
* Allow multiple file selection
|
|
613
|
+
* @default false
|
|
614
|
+
*/
|
|
615
|
+
multiple?: boolean;
|
|
616
|
+
/**
|
|
617
|
+
* Show file preview thumbnails
|
|
618
|
+
* @default true
|
|
619
|
+
*/
|
|
620
|
+
showPreview?: boolean;
|
|
621
|
+
/**
|
|
622
|
+
* Validation error handler
|
|
623
|
+
*/
|
|
624
|
+
onValidationError?: (errors: FileValidationError[]) => void;
|
|
625
|
+
/**
|
|
626
|
+
* File removed handler
|
|
627
|
+
*/
|
|
628
|
+
onFileRemove?: (file: File, index: number) => void;
|
|
629
|
+
}
|
|
630
|
+
/**
|
|
631
|
+
* FileInput component for file selection with validation
|
|
632
|
+
*
|
|
633
|
+
* @example
|
|
634
|
+
* ```tsx
|
|
635
|
+
* <FileInput
|
|
636
|
+
* name="resume"
|
|
637
|
+
* accept=".pdf,.doc,.docx"
|
|
638
|
+
* maxSize={5 * 1024 * 1024}
|
|
639
|
+
* value={files}
|
|
640
|
+
* onChange={(files) => setFiles(files)}
|
|
641
|
+
* error={hasError}
|
|
642
|
+
* />
|
|
643
|
+
* ```
|
|
644
|
+
*/
|
|
645
|
+
declare function FileInput({ name, value, onChange, onBlur, placeholder, disabled, required, error, className, accept, maxSize, // 5MB default
|
|
646
|
+
maxFiles, multiple, showPreview, onValidationError, onFileRemove, ...props }: FileInputProps): React.JSX.Element;
|
|
647
|
+
declare namespace FileInput {
|
|
648
|
+
var displayName: string;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
export { Checkbox, CheckboxGroup, type CheckboxGroupOption, type CheckboxGroupProps, type CheckboxProps, FileInput, type FileInputProps, type FileValidationError, Radio, type RadioOption, type RadioProps, Select, type SelectOption, type SelectOptionGroup, type SelectProps, TextArea, type TextAreaProps, TextInput };
|
package/dist/inputs.js
CHANGED
|
@@ -647,7 +647,258 @@ function Select({
|
|
|
647
647
|
);
|
|
648
648
|
}
|
|
649
649
|
Select.displayName = "Select";
|
|
650
|
+
function FileInput({
|
|
651
|
+
name,
|
|
652
|
+
value = [],
|
|
653
|
+
onChange,
|
|
654
|
+
onBlur,
|
|
655
|
+
placeholder = "Choose file(s)...",
|
|
656
|
+
disabled = false,
|
|
657
|
+
required = false,
|
|
658
|
+
error = false,
|
|
659
|
+
className = "",
|
|
660
|
+
accept,
|
|
661
|
+
maxSize = 5 * 1024 * 1024,
|
|
662
|
+
// 5MB default
|
|
663
|
+
maxFiles = 1,
|
|
664
|
+
multiple = false,
|
|
665
|
+
showPreview = true,
|
|
666
|
+
onValidationError,
|
|
667
|
+
onFileRemove,
|
|
668
|
+
...props
|
|
669
|
+
}) {
|
|
670
|
+
const inputRef = React6.useRef(null);
|
|
671
|
+
const [dragActive, setDragActive] = React6.useState(false);
|
|
672
|
+
const validateFile = React6.useCallback(
|
|
673
|
+
(file) => {
|
|
674
|
+
if (accept) {
|
|
675
|
+
const acceptedTypes = accept.split(",").map((t) => t.trim());
|
|
676
|
+
const isValidType = acceptedTypes.some((type) => {
|
|
677
|
+
if (type.startsWith(".")) {
|
|
678
|
+
return file.name.toLowerCase().endsWith(type.toLowerCase());
|
|
679
|
+
} else if (type.endsWith("/*")) {
|
|
680
|
+
const baseType = type.split("/")[0];
|
|
681
|
+
return file.type.startsWith(baseType + "/");
|
|
682
|
+
} else {
|
|
683
|
+
return file.type === type;
|
|
684
|
+
}
|
|
685
|
+
});
|
|
686
|
+
if (!isValidType) {
|
|
687
|
+
return {
|
|
688
|
+
file,
|
|
689
|
+
error: "type",
|
|
690
|
+
message: `File type "${file.type}" is not accepted. Accepted types: ${accept}`
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
if (file.size > maxSize) {
|
|
695
|
+
const maxSizeMB = (maxSize / (1024 * 1024)).toFixed(2);
|
|
696
|
+
const fileSizeMB = (file.size / (1024 * 1024)).toFixed(2);
|
|
697
|
+
return {
|
|
698
|
+
file,
|
|
699
|
+
error: "size",
|
|
700
|
+
message: `File size ${fileSizeMB}MB exceeds maximum ${maxSizeMB}MB`
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
return null;
|
|
704
|
+
},
|
|
705
|
+
[accept, maxSize]
|
|
706
|
+
);
|
|
707
|
+
const handleFiles = React6.useCallback(
|
|
708
|
+
(fileList) => {
|
|
709
|
+
if (!fileList || fileList.length === 0) return;
|
|
710
|
+
const newFiles = Array.from(fileList);
|
|
711
|
+
const validationErrors = [];
|
|
712
|
+
const validFiles = [];
|
|
713
|
+
for (const file of newFiles) {
|
|
714
|
+
const validationError = validateFile(file);
|
|
715
|
+
if (validationError) {
|
|
716
|
+
validationErrors.push(validationError);
|
|
717
|
+
} else {
|
|
718
|
+
validFiles.push(file);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
const totalFiles = value.length + validFiles.length;
|
|
722
|
+
if (totalFiles > maxFiles) {
|
|
723
|
+
validationErrors.push({
|
|
724
|
+
file: validFiles[0],
|
|
725
|
+
// Use first file as reference
|
|
726
|
+
error: "count",
|
|
727
|
+
message: `Maximum ${maxFiles} file(s) allowed. Attempting to add ${validFiles.length} to existing ${value.length}.`
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
if (validationErrors.length > 0 && onValidationError) {
|
|
731
|
+
onValidationError(validationErrors);
|
|
732
|
+
}
|
|
733
|
+
if (validFiles.length > 0 && totalFiles <= maxFiles) {
|
|
734
|
+
const updatedFiles = multiple ? [...value, ...validFiles] : validFiles;
|
|
735
|
+
onChange(updatedFiles.slice(0, maxFiles));
|
|
736
|
+
}
|
|
737
|
+
if (inputRef.current) {
|
|
738
|
+
inputRef.current.value = "";
|
|
739
|
+
}
|
|
740
|
+
},
|
|
741
|
+
[value, onChange, validateFile, maxFiles, multiple, onValidationError]
|
|
742
|
+
);
|
|
743
|
+
const handleChange = (e) => {
|
|
744
|
+
handleFiles(e.target.files);
|
|
745
|
+
};
|
|
746
|
+
const handleRemove = (index) => {
|
|
747
|
+
const fileToRemove = value[index];
|
|
748
|
+
const updatedFiles = value.filter((_, i) => i !== index);
|
|
749
|
+
onChange(updatedFiles);
|
|
750
|
+
if (onFileRemove && fileToRemove) {
|
|
751
|
+
onFileRemove(fileToRemove, index);
|
|
752
|
+
}
|
|
753
|
+
};
|
|
754
|
+
const handleDrag = (e) => {
|
|
755
|
+
e.preventDefault();
|
|
756
|
+
e.stopPropagation();
|
|
757
|
+
if (e.type === "dragenter" || e.type === "dragover") {
|
|
758
|
+
setDragActive(true);
|
|
759
|
+
} else if (e.type === "dragleave") {
|
|
760
|
+
setDragActive(false);
|
|
761
|
+
}
|
|
762
|
+
};
|
|
763
|
+
const handleDrop = (e) => {
|
|
764
|
+
e.preventDefault();
|
|
765
|
+
e.stopPropagation();
|
|
766
|
+
setDragActive(false);
|
|
767
|
+
if (disabled) return;
|
|
768
|
+
handleFiles(e.dataTransfer.files);
|
|
769
|
+
};
|
|
770
|
+
const handleClick = () => {
|
|
771
|
+
inputRef.current?.click();
|
|
772
|
+
};
|
|
773
|
+
const handleKeyDown = (e) => {
|
|
774
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
775
|
+
e.preventDefault();
|
|
776
|
+
handleClick();
|
|
777
|
+
}
|
|
778
|
+
};
|
|
779
|
+
const formatFileSize = (bytes) => {
|
|
780
|
+
if (bytes === 0) return "0 Bytes";
|
|
781
|
+
const k = 1024;
|
|
782
|
+
const sizes = ["Bytes", "KB", "MB", "GB"];
|
|
783
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
784
|
+
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + " " + sizes[i];
|
|
785
|
+
};
|
|
786
|
+
const getPreviewUrl = (file) => {
|
|
787
|
+
if (file.type.startsWith("image/")) {
|
|
788
|
+
return URL.createObjectURL(file);
|
|
789
|
+
}
|
|
790
|
+
return null;
|
|
791
|
+
};
|
|
792
|
+
React6.useEffect(() => {
|
|
793
|
+
return () => {
|
|
794
|
+
value.forEach((file) => {
|
|
795
|
+
const previewUrl = getPreviewUrl(file);
|
|
796
|
+
if (previewUrl) {
|
|
797
|
+
URL.revokeObjectURL(previewUrl);
|
|
798
|
+
}
|
|
799
|
+
});
|
|
800
|
+
};
|
|
801
|
+
}, [value]);
|
|
802
|
+
const baseClassName = "file-input";
|
|
803
|
+
const errorClassName = error ? "file-input--error" : "";
|
|
804
|
+
const dragClassName = dragActive ? "file-input--drag-active" : "";
|
|
805
|
+
const disabledClassName = disabled ? "file-input--disabled" : "";
|
|
806
|
+
const combinedClassName = `${baseClassName} ${errorClassName} ${dragClassName} ${disabledClassName} ${className}`.trim();
|
|
807
|
+
return /* @__PURE__ */ React6.createElement("div", { className: combinedClassName }, /* @__PURE__ */ React6.createElement(
|
|
808
|
+
"input",
|
|
809
|
+
{
|
|
810
|
+
ref: inputRef,
|
|
811
|
+
type: "file",
|
|
812
|
+
name,
|
|
813
|
+
onChange: handleChange,
|
|
814
|
+
onBlur,
|
|
815
|
+
accept,
|
|
816
|
+
multiple,
|
|
817
|
+
disabled,
|
|
818
|
+
required: required && value.length === 0,
|
|
819
|
+
className: "file-input__native",
|
|
820
|
+
"aria-invalid": error || props["aria-invalid"],
|
|
821
|
+
"aria-describedby": props["aria-describedby"],
|
|
822
|
+
"aria-required": required || props["aria-required"],
|
|
823
|
+
style: { display: "none" }
|
|
824
|
+
}
|
|
825
|
+
), /* @__PURE__ */ React6.createElement(
|
|
826
|
+
"div",
|
|
827
|
+
{
|
|
828
|
+
className: "file-input__dropzone",
|
|
829
|
+
onDragEnter: handleDrag,
|
|
830
|
+
onDragLeave: handleDrag,
|
|
831
|
+
onDragOver: handleDrag,
|
|
832
|
+
onDrop: handleDrop,
|
|
833
|
+
onClick: handleClick,
|
|
834
|
+
onKeyDown: handleKeyDown,
|
|
835
|
+
role: "button",
|
|
836
|
+
tabIndex: disabled ? -1 : 0,
|
|
837
|
+
"aria-label": placeholder,
|
|
838
|
+
"aria-disabled": disabled
|
|
839
|
+
},
|
|
840
|
+
/* @__PURE__ */ React6.createElement("div", { className: "file-input__dropzone-content" }, /* @__PURE__ */ React6.createElement(
|
|
841
|
+
"svg",
|
|
842
|
+
{
|
|
843
|
+
className: "file-input__icon",
|
|
844
|
+
width: "48",
|
|
845
|
+
height: "48",
|
|
846
|
+
viewBox: "0 0 24 24",
|
|
847
|
+
fill: "none",
|
|
848
|
+
stroke: "currentColor",
|
|
849
|
+
strokeWidth: "2",
|
|
850
|
+
strokeLinecap: "round",
|
|
851
|
+
strokeLinejoin: "round",
|
|
852
|
+
"aria-hidden": "true"
|
|
853
|
+
},
|
|
854
|
+
/* @__PURE__ */ React6.createElement("path", { d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" }),
|
|
855
|
+
/* @__PURE__ */ React6.createElement("polyline", { points: "17 8 12 3 7 8" }),
|
|
856
|
+
/* @__PURE__ */ React6.createElement("line", { x1: "12", y1: "3", x2: "12", y2: "15" })
|
|
857
|
+
), /* @__PURE__ */ React6.createElement("p", { className: "file-input__placeholder" }, value.length > 0 ? `${value.length} file(s) selected` : placeholder), accept && /* @__PURE__ */ React6.createElement("p", { className: "file-input__hint" }, "Accepted: ", accept), maxSize && /* @__PURE__ */ React6.createElement("p", { className: "file-input__hint" }, "Max size: ", formatFileSize(maxSize)))
|
|
858
|
+
), value.length > 0 && /* @__PURE__ */ React6.createElement("ul", { className: "file-input__list", role: "list" }, value.map((file, index) => {
|
|
859
|
+
const previewUrl = showPreview ? getPreviewUrl(file) : null;
|
|
860
|
+
return /* @__PURE__ */ React6.createElement("li", { key: `${file.name}-${index}`, className: "file-input__item" }, previewUrl && /* @__PURE__ */ React6.createElement(
|
|
861
|
+
"img",
|
|
862
|
+
{
|
|
863
|
+
src: previewUrl,
|
|
864
|
+
alt: file.name,
|
|
865
|
+
className: "file-input__preview",
|
|
866
|
+
width: "48",
|
|
867
|
+
height: "48"
|
|
868
|
+
}
|
|
869
|
+
), /* @__PURE__ */ React6.createElement("div", { className: "file-input__details" }, /* @__PURE__ */ React6.createElement("span", { className: "file-input__filename" }, file.name), /* @__PURE__ */ React6.createElement("span", { className: "file-input__filesize" }, formatFileSize(file.size))), /* @__PURE__ */ React6.createElement(
|
|
870
|
+
"button",
|
|
871
|
+
{
|
|
872
|
+
type: "button",
|
|
873
|
+
onClick: (e) => {
|
|
874
|
+
e.stopPropagation();
|
|
875
|
+
handleRemove(index);
|
|
876
|
+
},
|
|
877
|
+
disabled,
|
|
878
|
+
className: "file-input__remove",
|
|
879
|
+
"aria-label": `Remove ${file.name}`
|
|
880
|
+
},
|
|
881
|
+
/* @__PURE__ */ React6.createElement(
|
|
882
|
+
"svg",
|
|
883
|
+
{
|
|
884
|
+
width: "20",
|
|
885
|
+
height: "20",
|
|
886
|
+
viewBox: "0 0 24 24",
|
|
887
|
+
fill: "none",
|
|
888
|
+
stroke: "currentColor",
|
|
889
|
+
strokeWidth: "2",
|
|
890
|
+
strokeLinecap: "round",
|
|
891
|
+
strokeLinejoin: "round",
|
|
892
|
+
"aria-hidden": "true"
|
|
893
|
+
},
|
|
894
|
+
/* @__PURE__ */ React6.createElement("line", { x1: "18", y1: "6", x2: "6", y2: "18" }),
|
|
895
|
+
/* @__PURE__ */ React6.createElement("line", { x1: "6", y1: "6", x2: "18", y2: "18" })
|
|
896
|
+
)
|
|
897
|
+
));
|
|
898
|
+
})));
|
|
899
|
+
}
|
|
900
|
+
FileInput.displayName = "FileInput";
|
|
650
901
|
|
|
651
|
-
export { Checkbox, CheckboxGroup, Radio, Select, TextArea, TextInput };
|
|
902
|
+
export { Checkbox, CheckboxGroup, FileInput, Radio, Select, TextArea, TextInput };
|
|
652
903
|
//# sourceMappingURL=inputs.js.map
|
|
653
904
|
//# sourceMappingURL=inputs.js.map
|