@shipstatic/drop 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.
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
 
@@ -152,26 +184,9 @@ declare function extractZipToFiles(zipFile: File): Promise<ZipExtractionResult>;
152
184
  * normalizePath('/absolute/path.txt') → 'absolute/path.txt'
153
185
  */
154
186
  declare function normalizePath(path: string): string;
155
- /**
156
- * Check if a file path is a junk file that should be filtered out
157
- * Filters common system files like .DS_Store, Thumbs.db, desktop.ini,
158
- * and macOS resource fork metadata in __MACOSX directories
159
- *
160
- * Case-insensitive matching to handle files from different operating systems
161
- * (Windows file systems are case-insensitive, so Thumbs.db === THUMBS.DB)
162
- *
163
- * @example
164
- * isJunkFile('.DS_Store') → true
165
- * isJunkFile('.ds_store') → true (case-insensitive)
166
- * isJunkFile('THUMBS.DB') → true (case-insensitive)
167
- * isJunkFile('folder/.DS_Store') → true
168
- * isJunkFile('__MACOSX/file.txt') → true
169
- * isJunkFile('mydsstore.txt') → false
170
- */
171
- declare function isJunkFile(path: string): boolean;
172
187
  /**
173
188
  * Check if a file is a ZIP file based on MIME type or extension
174
189
  */
175
190
  declare function isZipFile(file: File): boolean;
176
191
 
177
- export { type ClientError, type DropOptions, type DropReturn, FILE_STATUSES, type FileStatus, type ProcessedFile, type ZipExtractionResult, createProcessedFile, extractZipToFiles, formatFileSize, getValidFiles, isJunkFile, isZipFile, normalizePath, stripCommonPrefix, useDrop };
192
+ export { type ClientError, type DropOptions, type DropReturn, FILE_STATUSES, type FileStatus, type ProcessedFile, type ZipExtractionResult, createProcessedFile, extractZipToFiles, formatFileSize, getValidFiles, isZipFile, normalizePath, stripCommonPrefix, useDrop };
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
 
@@ -152,26 +184,9 @@ declare function extractZipToFiles(zipFile: File): Promise<ZipExtractionResult>;
152
184
  * normalizePath('/absolute/path.txt') → 'absolute/path.txt'
153
185
  */
154
186
  declare function normalizePath(path: string): string;
155
- /**
156
- * Check if a file path is a junk file that should be filtered out
157
- * Filters common system files like .DS_Store, Thumbs.db, desktop.ini,
158
- * and macOS resource fork metadata in __MACOSX directories
159
- *
160
- * Case-insensitive matching to handle files from different operating systems
161
- * (Windows file systems are case-insensitive, so Thumbs.db === THUMBS.DB)
162
- *
163
- * @example
164
- * isJunkFile('.DS_Store') → true
165
- * isJunkFile('.ds_store') → true (case-insensitive)
166
- * isJunkFile('THUMBS.DB') → true (case-insensitive)
167
- * isJunkFile('folder/.DS_Store') → true
168
- * isJunkFile('__MACOSX/file.txt') → true
169
- * isJunkFile('mydsstore.txt') → false
170
- */
171
- declare function isJunkFile(path: string): boolean;
172
187
  /**
173
188
  * Check if a file is a ZIP file based on MIME type or extension
174
189
  */
175
190
  declare function isZipFile(file: File): boolean;
176
191
 
177
- export { type ClientError, type DropOptions, type DropReturn, FILE_STATUSES, type FileStatus, type ProcessedFile, type ZipExtractionResult, createProcessedFile, extractZipToFiles, formatFileSize, getValidFiles, isJunkFile, isZipFile, normalizePath, stripCommonPrefix, useDrop };
192
+ export { type ClientError, type DropOptions, type DropReturn, FILE_STATUSES, type FileStatus, type ProcessedFile, type ZipExtractionResult, createProcessedFile, extractZipToFiles, formatFileSize, getValidFiles, isZipFile, normalizePath, stripCommonPrefix, useDrop };
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { useState, useRef, useCallback } from 'react';
2
- import { formatFileSize as formatFileSize$1, getValidFiles as getValidFiles$1, validateFiles } from '@shipstatic/ship';
2
+ import { formatFileSize as formatFileSize$1, getValidFiles as getValidFiles$1, filterJunk, validateFiles } from '@shipstatic/ship';
3
3
 
4
4
  var __create = Object.create;
5
5
  var __defProp = Object.defineProperty;
@@ -11735,7 +11735,6 @@ async function extractZipToFiles(zipFile) {
11735
11735
  errors.push(`Skipped invalid path: ${path}`);
11736
11736
  continue;
11737
11737
  }
11738
- if (isJunkFile(sanitizedPath)) continue;
11739
11738
  try {
11740
11739
  const content = await entry.async("blob");
11741
11740
  const mimeType = getMimeType(sanitizedPath);
@@ -11773,11 +11772,6 @@ function normalizePath(path) {
11773
11772
  }
11774
11773
  return normalized.join("/");
11775
11774
  }
11776
- function isJunkFile(path) {
11777
- const basename = (path.split("/").pop() || "").toLowerCase();
11778
- const junkFiles = [".ds_store", "thumbs.db", "desktop.ini", "._.ds_store"];
11779
- return path.toLowerCase().startsWith("__macosx/") || junkFiles.includes(basename);
11780
- }
11781
11775
  function isZipFile(file) {
11782
11776
  return file.type === "application/zip" || file.type === "application/x-zip-compressed" || file.name.toLowerCase().endsWith(".zip");
11783
11777
  }
@@ -11838,6 +11832,38 @@ function stripCommonPrefix(files) {
11838
11832
  path: f.path.startsWith(prefix) ? f.path.slice(prefix.length) : f.path
11839
11833
  }));
11840
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
+ }
11841
11867
  function useDrop(options) {
11842
11868
  const {
11843
11869
  ship,
@@ -11849,8 +11875,10 @@ function useDrop(options) {
11849
11875
  const [sourceName, setSourceName] = useState("");
11850
11876
  const [statusText, setStatusText] = useState("");
11851
11877
  const [isProcessing, setIsProcessing] = useState(false);
11878
+ const [isDragging, setIsDragging] = useState(false);
11852
11879
  const [validationError, setValidationError] = useState(null);
11853
11880
  const isProcessingRef = useRef(false);
11881
+ const inputRef = useRef(null);
11854
11882
  const processFiles = useCallback(async (newFiles) => {
11855
11883
  if (isProcessingRef.current) {
11856
11884
  console.warn("File processing already in progress. Ignoring duplicate call.");
@@ -11891,9 +11919,16 @@ function useDrop(options) {
11891
11919
  } else {
11892
11920
  allFiles.push(...newFiles);
11893
11921
  }
11922
+ const getFilePath = (f) => {
11923
+ const webkitPath = f.webkitRelativePath;
11924
+ return webkitPath && webkitPath.trim() ? webkitPath : f.name;
11925
+ };
11926
+ const filePaths = allFiles.map(getFilePath);
11927
+ const validPaths = new Set(filterJunk(filePaths));
11928
+ const cleanFiles = allFiles.filter((f) => validPaths.has(getFilePath(f)));
11894
11929
  setStatusText("Processing files...");
11895
11930
  const processedFiles = await Promise.all(
11896
- allFiles.map((file) => createProcessedFile(file))
11931
+ cleanFiles.map((file) => createProcessedFile(file))
11897
11932
  );
11898
11933
  const finalFiles = stripPrefix ? stripCommonPrefix(processedFiles) : processedFiles;
11899
11934
  const config = await ship.getConfig();
@@ -11935,6 +11970,7 @@ function useDrop(options) {
11935
11970
  setSourceName("");
11936
11971
  setStatusText("");
11937
11972
  setValidationError(null);
11973
+ setIsDragging(false);
11938
11974
  isProcessingRef.current = false;
11939
11975
  setIsProcessing(false);
11940
11976
  }, []);
@@ -11946,14 +11982,79 @@ function useDrop(options) {
11946
11982
  (file) => file.id === fileId ? { ...file, ...state } : file
11947
11983
  ));
11948
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]);
11949
12042
  return {
12043
+ // State
11950
12044
  files,
11951
12045
  sourceName,
11952
12046
  statusText,
11953
12047
  isProcessing,
12048
+ isDragging,
11954
12049
  validationError,
12050
+ // Primary API: Prop getters
12051
+ getDropzoneProps,
12052
+ getInputProps,
12053
+ // Actions
12054
+ open,
11955
12055
  processFiles,
11956
12056
  clearAll,
12057
+ // Helpers
11957
12058
  getValidFiles: getValidFilesCallback,
11958
12059
  updateFileStatus
11959
12060
  };
@@ -11982,6 +12083,6 @@ mime-db/index.js:
11982
12083
  *)
11983
12084
  */
11984
12085
 
11985
- export { FILE_STATUSES, createProcessedFile, extractZipToFiles, formatFileSize, getValidFiles, isJunkFile, isZipFile, normalizePath, stripCommonPrefix, useDrop };
12086
+ export { FILE_STATUSES, createProcessedFile, extractZipToFiles, formatFileSize, getValidFiles, isZipFile, normalizePath, stripCommonPrefix, useDrop };
11986
12087
  //# sourceMappingURL=index.js.map
11987
12088
  //# sourceMappingURL=index.js.map