@shipstatic/drop 0.1.5 → 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.
package/dist/index.d.cts CHANGED
@@ -72,9 +72,31 @@ interface DropReturn {
72
72
  statusText: string;
73
73
  /** Whether currently processing files (ZIP extraction, etc.) */
74
74
  isProcessing: boolean;
75
+ /** Whether user is currently dragging over the dropzone */
76
+ isDragging: boolean;
75
77
  /** Last validation error if any */
76
78
  validationError: ClientError | null;
77
- /** Process files from drop (resets and replaces existing files) */
79
+ /** Get props to spread on dropzone element (handles drag & drop) */
80
+ getDropzoneProps: () => {
81
+ onDragOver: (e: React.DragEvent) => void;
82
+ onDragLeave: (e: React.DragEvent) => void;
83
+ onDrop: (e: React.DragEvent) => void;
84
+ onClick: () => void;
85
+ };
86
+ /** Get props to spread on hidden file input element */
87
+ getInputProps: () => {
88
+ ref: React.RefObject<HTMLInputElement | null>;
89
+ type: 'file';
90
+ style: {
91
+ display: string;
92
+ };
93
+ multiple: boolean;
94
+ webkitdirectory: string;
95
+ onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
96
+ };
97
+ /** Programmatically trigger file picker */
98
+ open: () => void;
99
+ /** Manually process files (for advanced usage) */
78
100
  processFiles: (files: File[]) => Promise<void>;
79
101
  /** Clear all files and reset state */
80
102
  clearAll: () => void;
@@ -88,9 +110,19 @@ interface DropReturn {
88
110
  }) => void;
89
111
  }
90
112
  /**
91
- * Headless drop hook
92
- * Handles file processing, ZIP extraction, and validation
93
- * Does NOT handle uploading - that's the consumer's responsibility
113
+ * Headless drop hook for file upload workflows
114
+ *
115
+ * @example
116
+ * ```tsx
117
+ * const drop = useDrop({ ship });
118
+ *
119
+ * return (
120
+ * <div {...drop.getDropzoneProps()} style={{...}}>
121
+ * <input {...drop.getInputProps()} />
122
+ * {drop.isDragging ? "📂 Drop" : "📁 Click"}
123
+ * </div>
124
+ * );
125
+ * ```
94
126
  */
95
127
  declare function useDrop(options: DropOptions): DropReturn;
96
128
 
package/dist/index.d.ts CHANGED
@@ -72,9 +72,31 @@ interface DropReturn {
72
72
  statusText: string;
73
73
  /** Whether currently processing files (ZIP extraction, etc.) */
74
74
  isProcessing: boolean;
75
+ /** Whether user is currently dragging over the dropzone */
76
+ isDragging: boolean;
75
77
  /** Last validation error if any */
76
78
  validationError: ClientError | null;
77
- /** Process files from drop (resets and replaces existing files) */
79
+ /** Get props to spread on dropzone element (handles drag & drop) */
80
+ getDropzoneProps: () => {
81
+ onDragOver: (e: React.DragEvent) => void;
82
+ onDragLeave: (e: React.DragEvent) => void;
83
+ onDrop: (e: React.DragEvent) => void;
84
+ onClick: () => void;
85
+ };
86
+ /** Get props to spread on hidden file input element */
87
+ getInputProps: () => {
88
+ ref: React.RefObject<HTMLInputElement | null>;
89
+ type: 'file';
90
+ style: {
91
+ display: string;
92
+ };
93
+ multiple: boolean;
94
+ webkitdirectory: string;
95
+ onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
96
+ };
97
+ /** Programmatically trigger file picker */
98
+ open: () => void;
99
+ /** Manually process files (for advanced usage) */
78
100
  processFiles: (files: File[]) => Promise<void>;
79
101
  /** Clear all files and reset state */
80
102
  clearAll: () => void;
@@ -88,9 +110,19 @@ interface DropReturn {
88
110
  }) => void;
89
111
  }
90
112
  /**
91
- * Headless drop hook
92
- * Handles file processing, ZIP extraction, and validation
93
- * Does NOT handle uploading - that's the consumer's responsibility
113
+ * Headless drop hook for file upload workflows
114
+ *
115
+ * @example
116
+ * ```tsx
117
+ * const drop = useDrop({ ship });
118
+ *
119
+ * return (
120
+ * <div {...drop.getDropzoneProps()} style={{...}}>
121
+ * <input {...drop.getInputProps()} />
122
+ * {drop.isDragging ? "📂 Drop" : "📁 Click"}
123
+ * </div>
124
+ * );
125
+ * ```
94
126
  */
95
127
  declare function useDrop(options: DropOptions): DropReturn;
96
128
 
package/dist/index.js CHANGED
@@ -11832,6 +11832,38 @@ function stripCommonPrefix(files) {
11832
11832
  path: f.path.startsWith(prefix) ? f.path.slice(prefix.length) : f.path
11833
11833
  }));
11834
11834
  }
11835
+ async function traverseFileTree(entry, files, currentPath = "") {
11836
+ if (entry.isFile) {
11837
+ const file = await new Promise((resolve, reject) => {
11838
+ entry.file(resolve, reject);
11839
+ });
11840
+ const relativePath = currentPath ? `${currentPath}/${file.name}` : file.name;
11841
+ Object.defineProperty(file, "webkitRelativePath", {
11842
+ value: relativePath,
11843
+ writable: false
11844
+ });
11845
+ files.push(file);
11846
+ } else if (entry.isDirectory) {
11847
+ const dirReader = entry.createReader();
11848
+ let allEntries = [];
11849
+ const readEntriesBatch = async () => {
11850
+ const batch = await new Promise(
11851
+ (resolve, reject) => {
11852
+ dirReader.readEntries(resolve, reject);
11853
+ }
11854
+ );
11855
+ if (batch.length > 0) {
11856
+ allEntries = allEntries.concat(batch);
11857
+ await readEntriesBatch();
11858
+ }
11859
+ };
11860
+ await readEntriesBatch();
11861
+ for (const childEntry of allEntries) {
11862
+ const entryPath = childEntry.isDirectory ? currentPath ? `${currentPath}/${childEntry.name}` : childEntry.name : currentPath;
11863
+ await traverseFileTree(childEntry, files, entryPath);
11864
+ }
11865
+ }
11866
+ }
11835
11867
  function useDrop(options) {
11836
11868
  const {
11837
11869
  ship,
@@ -11843,8 +11875,10 @@ function useDrop(options) {
11843
11875
  const [sourceName, setSourceName] = useState("");
11844
11876
  const [statusText, setStatusText] = useState("");
11845
11877
  const [isProcessing, setIsProcessing] = useState(false);
11878
+ const [isDragging, setIsDragging] = useState(false);
11846
11879
  const [validationError, setValidationError] = useState(null);
11847
11880
  const isProcessingRef = useRef(false);
11881
+ const inputRef = useRef(null);
11848
11882
  const processFiles = useCallback(async (newFiles) => {
11849
11883
  if (isProcessingRef.current) {
11850
11884
  console.warn("File processing already in progress. Ignoring duplicate call.");
@@ -11936,6 +11970,7 @@ function useDrop(options) {
11936
11970
  setSourceName("");
11937
11971
  setStatusText("");
11938
11972
  setValidationError(null);
11973
+ setIsDragging(false);
11939
11974
  isProcessingRef.current = false;
11940
11975
  setIsProcessing(false);
11941
11976
  }, []);
@@ -11947,14 +11982,79 @@ function useDrop(options) {
11947
11982
  (file) => file.id === fileId ? { ...file, ...state } : file
11948
11983
  ));
11949
11984
  }, []);
11985
+ const handleDragOver = useCallback((e) => {
11986
+ e.preventDefault();
11987
+ setIsDragging(true);
11988
+ }, []);
11989
+ const handleDragLeave = useCallback((e) => {
11990
+ e.preventDefault();
11991
+ setIsDragging(false);
11992
+ }, []);
11993
+ const handleDrop = useCallback(async (e) => {
11994
+ e.preventDefault();
11995
+ setIsDragging(false);
11996
+ const items = Array.from(e.dataTransfer.items);
11997
+ const files2 = [];
11998
+ let hasEntries = false;
11999
+ for (const item of items) {
12000
+ if (item.kind === "file") {
12001
+ const entry = item.webkitGetAsEntry?.();
12002
+ if (entry) {
12003
+ hasEntries = true;
12004
+ await traverseFileTree(
12005
+ entry,
12006
+ files2,
12007
+ entry.isDirectory ? entry.name : ""
12008
+ );
12009
+ }
12010
+ }
12011
+ }
12012
+ if (!hasEntries && e.dataTransfer.files.length > 0) {
12013
+ files2.push(...Array.from(e.dataTransfer.files));
12014
+ }
12015
+ if (files2.length > 0) {
12016
+ await processFiles(files2);
12017
+ }
12018
+ }, [processFiles]);
12019
+ const handleInputChange = useCallback((e) => {
12020
+ const files2 = Array.from(e.target.files || []);
12021
+ if (files2.length > 0) {
12022
+ processFiles(files2);
12023
+ }
12024
+ }, [processFiles]);
12025
+ const open = useCallback(() => {
12026
+ inputRef.current?.click();
12027
+ }, []);
12028
+ const getDropzoneProps = useCallback(() => ({
12029
+ onDragOver: handleDragOver,
12030
+ onDragLeave: handleDragLeave,
12031
+ onDrop: handleDrop,
12032
+ onClick: open
12033
+ }), [handleDragOver, handleDragLeave, handleDrop, open]);
12034
+ const getInputProps = useCallback(() => ({
12035
+ ref: inputRef,
12036
+ type: "file",
12037
+ style: { display: "none" },
12038
+ multiple: true,
12039
+ webkitdirectory: "",
12040
+ onChange: handleInputChange
12041
+ }), [handleInputChange]);
11950
12042
  return {
12043
+ // State
11951
12044
  files,
11952
12045
  sourceName,
11953
12046
  statusText,
11954
12047
  isProcessing,
12048
+ isDragging,
11955
12049
  validationError,
12050
+ // Primary API: Prop getters
12051
+ getDropzoneProps,
12052
+ getInputProps,
12053
+ // Actions
12054
+ open,
11956
12055
  processFiles,
11957
12056
  clearAll,
12057
+ // Helpers
11958
12058
  getValidFiles: getValidFilesCallback,
11959
12059
  updateFileStatus
11960
12060
  };