@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.d.cts CHANGED
@@ -572,4 +572,80 @@ declare namespace Select {
572
572
  var displayName: string;
573
573
  }
574
574
 
575
- export { Checkbox, CheckboxGroup, type CheckboxGroupOption, type CheckboxGroupProps, type CheckboxProps, Radio, type RadioOption, type RadioProps, Select, type SelectOption, type SelectOptionGroup, type SelectProps, TextArea, type TextAreaProps, TextInput };
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
- export { Checkbox, CheckboxGroup, type CheckboxGroupOption, type CheckboxGroupProps, type CheckboxProps, Radio, type RadioOption, type RadioProps, Select, type SelectOption, type SelectOptionGroup, type SelectProps, TextArea, type TextAreaProps, TextInput };
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