@shipstatic/drop 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/index.d.cts CHANGED
@@ -52,6 +52,26 @@ interface ProcessedFile extends StaticFile {
52
52
  /** Upload progress (0-100) - only set during upload */
53
53
  progress?: number;
54
54
  }
55
+ /**
56
+ * State machine values for the drop hook
57
+ */
58
+ type DropStateValue = 'idle' | 'dragging' | 'processing' | 'ready' | 'error';
59
+ /**
60
+ * Status information with title and details
61
+ */
62
+ interface DropStatus {
63
+ title: string;
64
+ details: string;
65
+ }
66
+ /**
67
+ * State machine state for the drop hook
68
+ */
69
+ interface DropState {
70
+ value: DropStateValue;
71
+ files: ProcessedFile[];
72
+ sourceName: string;
73
+ status: DropStatus | null;
74
+ }
55
75
 
56
76
  interface DropOptions {
57
77
  /** Ship SDK instance (required for validation) */
@@ -64,17 +84,33 @@ interface DropOptions {
64
84
  stripPrefix?: boolean;
65
85
  }
66
86
  interface DropReturn {
67
- /** All processed files with their status */
68
- files: ProcessedFile[];
69
- /** Name of the source (file/folder/ZIP) that was dropped/selected */
70
- sourceName: string;
71
- /** Current status text */
72
- statusText: string;
87
+ /** Current state of the drop hook */
88
+ state: DropState;
73
89
  /** Whether currently processing files (ZIP extraction, etc.) */
74
90
  isProcessing: boolean;
75
- /** Last validation error if any */
76
- validationError: ClientError | null;
77
- /** Process files from drop (resets and replaces existing files) */
91
+ /** Whether user is currently dragging over the dropzone */
92
+ isDragging: boolean;
93
+ /** Get props to spread on dropzone element (handles drag & drop) */
94
+ getDropzoneProps: () => {
95
+ onDragOver: (e: React.DragEvent) => void;
96
+ onDragLeave: (e: React.DragEvent) => void;
97
+ onDrop: (e: React.DragEvent) => void;
98
+ onClick: () => void;
99
+ };
100
+ /** Get props to spread on hidden file input element */
101
+ getInputProps: () => {
102
+ ref: React.RefObject<HTMLInputElement | null>;
103
+ type: 'file';
104
+ style: {
105
+ display: string;
106
+ };
107
+ multiple: boolean;
108
+ webkitdirectory: string;
109
+ onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
110
+ };
111
+ /** Programmatically trigger file picker */
112
+ open: () => void;
113
+ /** Manually process files (for advanced usage) */
78
114
  processFiles: (files: File[]) => Promise<void>;
79
115
  /** Clear all files and reset state */
80
116
  clearAll: () => void;
@@ -88,9 +124,19 @@ interface DropReturn {
88
124
  }) => void;
89
125
  }
90
126
  /**
91
- * Headless drop hook
92
- * Handles file processing, ZIP extraction, and validation
93
- * Does NOT handle uploading - that's the consumer's responsibility
127
+ * Headless drop hook for file upload workflows
128
+ *
129
+ * @example
130
+ * ```tsx
131
+ * const drop = useDrop({ ship });
132
+ *
133
+ * return (
134
+ * <div {...drop.getDropzoneProps()} style={{...}}>
135
+ * <input {...drop.getInputProps()} />
136
+ * {drop.isDragging ? "📂 Drop" : "📁 Click"}
137
+ * </div>
138
+ * );
139
+ * ```
94
140
  */
95
141
  declare function useDrop(options: DropOptions): DropReturn;
96
142
 
@@ -157,4 +203,4 @@ declare function normalizePath(path: string): string;
157
203
  */
158
204
  declare function isZipFile(file: File): boolean;
159
205
 
160
- export { type ClientError, type DropOptions, type DropReturn, FILE_STATUSES, type FileStatus, type ProcessedFile, type ZipExtractionResult, createProcessedFile, extractZipToFiles, formatFileSize, getValidFiles, isZipFile, normalizePath, stripCommonPrefix, useDrop };
206
+ export { type ClientError, type DropOptions, type DropReturn, type DropState, type DropStateValue, type DropStatus, FILE_STATUSES, type FileStatus, type ProcessedFile, type ZipExtractionResult, createProcessedFile, extractZipToFiles, formatFileSize, getValidFiles, isZipFile, normalizePath, stripCommonPrefix, useDrop };
package/dist/index.d.ts CHANGED
@@ -52,6 +52,26 @@ interface ProcessedFile extends StaticFile {
52
52
  /** Upload progress (0-100) - only set during upload */
53
53
  progress?: number;
54
54
  }
55
+ /**
56
+ * State machine values for the drop hook
57
+ */
58
+ type DropStateValue = 'idle' | 'dragging' | 'processing' | 'ready' | 'error';
59
+ /**
60
+ * Status information with title and details
61
+ */
62
+ interface DropStatus {
63
+ title: string;
64
+ details: string;
65
+ }
66
+ /**
67
+ * State machine state for the drop hook
68
+ */
69
+ interface DropState {
70
+ value: DropStateValue;
71
+ files: ProcessedFile[];
72
+ sourceName: string;
73
+ status: DropStatus | null;
74
+ }
55
75
 
56
76
  interface DropOptions {
57
77
  /** Ship SDK instance (required for validation) */
@@ -64,17 +84,33 @@ interface DropOptions {
64
84
  stripPrefix?: boolean;
65
85
  }
66
86
  interface DropReturn {
67
- /** All processed files with their status */
68
- files: ProcessedFile[];
69
- /** Name of the source (file/folder/ZIP) that was dropped/selected */
70
- sourceName: string;
71
- /** Current status text */
72
- statusText: string;
87
+ /** Current state of the drop hook */
88
+ state: DropState;
73
89
  /** Whether currently processing files (ZIP extraction, etc.) */
74
90
  isProcessing: boolean;
75
- /** Last validation error if any */
76
- validationError: ClientError | null;
77
- /** Process files from drop (resets and replaces existing files) */
91
+ /** Whether user is currently dragging over the dropzone */
92
+ isDragging: boolean;
93
+ /** Get props to spread on dropzone element (handles drag & drop) */
94
+ getDropzoneProps: () => {
95
+ onDragOver: (e: React.DragEvent) => void;
96
+ onDragLeave: (e: React.DragEvent) => void;
97
+ onDrop: (e: React.DragEvent) => void;
98
+ onClick: () => void;
99
+ };
100
+ /** Get props to spread on hidden file input element */
101
+ getInputProps: () => {
102
+ ref: React.RefObject<HTMLInputElement | null>;
103
+ type: 'file';
104
+ style: {
105
+ display: string;
106
+ };
107
+ multiple: boolean;
108
+ webkitdirectory: string;
109
+ onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
110
+ };
111
+ /** Programmatically trigger file picker */
112
+ open: () => void;
113
+ /** Manually process files (for advanced usage) */
78
114
  processFiles: (files: File[]) => Promise<void>;
79
115
  /** Clear all files and reset state */
80
116
  clearAll: () => void;
@@ -88,9 +124,19 @@ interface DropReturn {
88
124
  }) => void;
89
125
  }
90
126
  /**
91
- * Headless drop hook
92
- * Handles file processing, ZIP extraction, and validation
93
- * Does NOT handle uploading - that's the consumer's responsibility
127
+ * Headless drop hook for file upload workflows
128
+ *
129
+ * @example
130
+ * ```tsx
131
+ * const drop = useDrop({ ship });
132
+ *
133
+ * return (
134
+ * <div {...drop.getDropzoneProps()} style={{...}}>
135
+ * <input {...drop.getInputProps()} />
136
+ * {drop.isDragging ? "📂 Drop" : "📁 Click"}
137
+ * </div>
138
+ * );
139
+ * ```
94
140
  */
95
141
  declare function useDrop(options: DropOptions): DropReturn;
96
142
 
@@ -157,4 +203,4 @@ declare function normalizePath(path: string): string;
157
203
  */
158
204
  declare function isZipFile(file: File): boolean;
159
205
 
160
- export { type ClientError, type DropOptions, type DropReturn, FILE_STATUSES, type FileStatus, type ProcessedFile, type ZipExtractionResult, createProcessedFile, extractZipToFiles, formatFileSize, getValidFiles, isZipFile, normalizePath, stripCommonPrefix, useDrop };
206
+ export { type ClientError, type DropOptions, type DropReturn, type DropState, type DropStateValue, type DropStatus, FILE_STATUSES, type FileStatus, type ProcessedFile, type ZipExtractionResult, createProcessedFile, extractZipToFiles, formatFileSize, getValidFiles, isZipFile, normalizePath, stripCommonPrefix, useDrop };
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { useState, useRef, useCallback } from 'react';
1
+ import { useState, useRef, useMemo, useCallback } from 'react';
2
2
  import { formatFileSize as formatFileSize$1, getValidFiles as getValidFiles$1, filterJunk, validateFiles } from '@shipstatic/ship';
3
3
 
4
4
  var __create = Object.create;
@@ -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,
@@ -11839,26 +11871,32 @@ function useDrop(options) {
11839
11871
  onFilesReady,
11840
11872
  stripPrefix = true
11841
11873
  } = options;
11842
- const [files, setFiles] = useState([]);
11843
- const [sourceName, setSourceName] = useState("");
11844
- const [statusText, setStatusText] = useState("");
11845
- const [isProcessing, setIsProcessing] = useState(false);
11846
- const [validationError, setValidationError] = useState(null);
11874
+ const initialState = {
11875
+ value: "idle",
11876
+ files: [],
11877
+ sourceName: "",
11878
+ status: null
11879
+ };
11880
+ const [state, setState] = useState(initialState);
11847
11881
  const isProcessingRef = useRef(false);
11882
+ const inputRef = useRef(null);
11883
+ const isProcessing = useMemo(() => state.value === "processing", [state.value]);
11884
+ const isDragging = useMemo(() => state.value === "dragging", [state.value]);
11848
11885
  const processFiles = useCallback(async (newFiles) => {
11849
11886
  if (isProcessingRef.current) {
11850
11887
  console.warn("File processing already in progress. Ignoring duplicate call.");
11851
11888
  return;
11852
11889
  }
11853
11890
  if (!newFiles || newFiles.length === 0) {
11854
- setStatusText("No files selected.");
11855
11891
  return;
11856
11892
  }
11857
11893
  isProcessingRef.current = true;
11858
- setIsProcessing(true);
11859
- setFiles([]);
11860
- setValidationError(null);
11861
- setStatusText("Processing files...");
11894
+ setState({
11895
+ value: "processing",
11896
+ files: [],
11897
+ sourceName: "",
11898
+ status: { title: "Processing...", details: "Validating and preparing files." }
11899
+ });
11862
11900
  try {
11863
11901
  let detectedSourceName = "";
11864
11902
  if (newFiles.length === 1 && isZipFile(newFiles[0])) {
@@ -11871,12 +11909,14 @@ function useDrop(options) {
11871
11909
  detectedSourceName = newFiles[0].name;
11872
11910
  }
11873
11911
  }
11874
- setSourceName(detectedSourceName);
11875
11912
  const allFiles = [];
11876
11913
  const shouldExtractZip = newFiles.length === 1 && isZipFile(newFiles[0]);
11877
11914
  if (shouldExtractZip) {
11878
11915
  const zipFile = newFiles[0];
11879
- setStatusText(`Extracting ${zipFile.name}...`);
11916
+ setState((prev) => ({
11917
+ ...prev,
11918
+ status: { title: "Extracting...", details: `Extracting ${zipFile.name}...` }
11919
+ }));
11880
11920
  const { files: extractedFiles, errors } = await extractZipToFiles(zipFile);
11881
11921
  if (errors.length > 0) {
11882
11922
  console.warn("ZIP extraction errors:", errors);
@@ -11892,29 +11932,44 @@ function useDrop(options) {
11892
11932
  const filePaths = allFiles.map(getFilePath);
11893
11933
  const validPaths = new Set(filterJunk(filePaths));
11894
11934
  const cleanFiles = allFiles.filter((f) => validPaths.has(getFilePath(f)));
11895
- setStatusText("Processing files...");
11935
+ setState((prev) => ({
11936
+ ...prev,
11937
+ status: { title: "Processing...", details: "Processing files..." }
11938
+ }));
11896
11939
  const processedFiles = await Promise.all(
11897
11940
  cleanFiles.map((file) => createProcessedFile(file))
11898
11941
  );
11899
11942
  const finalFiles = stripPrefix ? stripCommonPrefix(processedFiles) : processedFiles;
11900
11943
  const config = await ship.getConfig();
11901
11944
  const validation = validateFiles(finalFiles, config);
11902
- setFiles(validation.files);
11903
- setValidationError(validation.error);
11904
11945
  if (validation.error) {
11905
- setStatusText(validation.error.details);
11946
+ setState({
11947
+ value: "error",
11948
+ files: validation.files,
11949
+ sourceName: detectedSourceName,
11950
+ status: { title: validation.error.error, details: validation.error.details }
11951
+ });
11906
11952
  onValidationError?.(validation.error);
11907
11953
  } else if (validation.validFiles.length > 0) {
11908
- setStatusText(`${validation.validFiles.length} file(s) ready.`);
11954
+ setState({
11955
+ value: "ready",
11956
+ files: validation.files,
11957
+ sourceName: detectedSourceName,
11958
+ status: { title: "Ready", details: `${validation.validFiles.length} file(s) are ready.` }
11959
+ });
11909
11960
  onFilesReady?.(validation.validFiles);
11910
11961
  } else {
11911
11962
  const noValidError = {
11912
11963
  error: "No Valid Files",
11913
- details: "No files are valid for upload after processing.",
11964
+ details: "None of the provided files could be processed.",
11914
11965
  isClientError: true
11915
11966
  };
11916
- setStatusText(noValidError.details);
11917
- setValidationError(noValidError);
11967
+ setState({
11968
+ value: "error",
11969
+ files: validation.files,
11970
+ sourceName: detectedSourceName,
11971
+ status: { title: noValidError.error, details: noValidError.details }
11972
+ });
11918
11973
  onValidationError?.(noValidError);
11919
11974
  }
11920
11975
  } catch (error) {
@@ -11923,38 +11978,112 @@ function useDrop(options) {
11923
11978
  details: `Failed to process files: ${error instanceof Error ? error.message : String(error)}`,
11924
11979
  isClientError: true
11925
11980
  };
11926
- setStatusText(processingError.details);
11927
- setValidationError(processingError);
11981
+ setState((prev) => ({
11982
+ ...prev,
11983
+ value: "error",
11984
+ status: { title: processingError.error, details: processingError.details }
11985
+ }));
11928
11986
  onValidationError?.(processingError);
11929
11987
  } finally {
11930
11988
  isProcessingRef.current = false;
11931
- setIsProcessing(false);
11932
11989
  }
11933
11990
  }, [ship, onValidationError, onFilesReady, stripPrefix]);
11934
11991
  const clearAll = useCallback(() => {
11935
- setFiles([]);
11936
- setSourceName("");
11937
- setStatusText("");
11938
- setValidationError(null);
11992
+ setState(initialState);
11939
11993
  isProcessingRef.current = false;
11940
- setIsProcessing(false);
11941
11994
  }, []);
11942
11995
  const getValidFilesCallback = useCallback(() => {
11943
- return getValidFiles(files);
11944
- }, [files]);
11945
- const updateFileStatus = useCallback((fileId, state) => {
11946
- setFiles((prev) => prev.map(
11947
- (file) => file.id === fileId ? { ...file, ...state } : file
11948
- ));
11996
+ return getValidFiles(state.files);
11997
+ }, [state.files]);
11998
+ const updateFileStatus = useCallback((fileId, fileState) => {
11999
+ setState((prev) => ({
12000
+ ...prev,
12001
+ files: prev.files.map(
12002
+ (file) => file.id === fileId ? { ...file, ...fileState } : file
12003
+ )
12004
+ }));
12005
+ }, []);
12006
+ const handleDragOver = useCallback((e) => {
12007
+ e.preventDefault();
12008
+ setState((prev) => {
12009
+ if (prev.value === "idle" || prev.value === "ready" || prev.value === "error") {
12010
+ return { ...prev, value: "dragging" };
12011
+ }
12012
+ return prev;
12013
+ });
12014
+ }, []);
12015
+ const handleDragLeave = useCallback((e) => {
12016
+ e.preventDefault();
12017
+ setState((prev) => {
12018
+ if (prev.value !== "dragging") return prev;
12019
+ const nextValue = prev.files.length > 0 ? prev.status?.title === "Ready" ? "ready" : "error" : "idle";
12020
+ return { ...prev, value: nextValue };
12021
+ });
12022
+ }, []);
12023
+ const handleDrop = useCallback(async (e) => {
12024
+ e.preventDefault();
12025
+ const items = Array.from(e.dataTransfer.items);
12026
+ const files = [];
12027
+ let hasEntries = false;
12028
+ for (const item of items) {
12029
+ if (item.kind === "file") {
12030
+ const entry = item.webkitGetAsEntry?.();
12031
+ if (entry) {
12032
+ hasEntries = true;
12033
+ await traverseFileTree(
12034
+ entry,
12035
+ files,
12036
+ entry.isDirectory ? entry.name : ""
12037
+ );
12038
+ }
12039
+ }
12040
+ }
12041
+ if (!hasEntries && e.dataTransfer.files.length > 0) {
12042
+ files.push(...Array.from(e.dataTransfer.files));
12043
+ }
12044
+ if (files.length > 0) {
12045
+ await processFiles(files);
12046
+ } else if (state.value === "dragging") {
12047
+ setState((prev) => ({ ...prev, value: "idle" }));
12048
+ }
12049
+ }, [processFiles, state.value]);
12050
+ const handleInputChange = useCallback((e) => {
12051
+ const files = Array.from(e.target.files || []);
12052
+ if (files.length > 0) {
12053
+ processFiles(files);
12054
+ }
12055
+ }, [processFiles]);
12056
+ const open = useCallback(() => {
12057
+ inputRef.current?.click();
11949
12058
  }, []);
12059
+ const getDropzoneProps = useCallback(() => ({
12060
+ onDragOver: handleDragOver,
12061
+ onDragLeave: handleDragLeave,
12062
+ onDrop: handleDrop,
12063
+ onClick: open
12064
+ }), [handleDragOver, handleDragLeave, handleDrop, open]);
12065
+ const getInputProps = useCallback(() => ({
12066
+ ref: inputRef,
12067
+ type: "file",
12068
+ style: { display: "none" },
12069
+ multiple: true,
12070
+ webkitdirectory: "",
12071
+ onChange: handleInputChange
12072
+ }), [handleInputChange]);
11950
12073
  return {
11951
- files,
11952
- sourceName,
11953
- statusText,
12074
+ // State machine
12075
+ state,
12076
+ // Convenience getters (computed from state)
11954
12077
  isProcessing,
11955
- validationError,
12078
+ isDragging,
12079
+ // Primary API: Prop getters
12080
+ getDropzoneProps,
12081
+ getInputProps,
12082
+ // Actions
12083
+ open,
11956
12084
  processFiles,
11957
12085
  clearAll,
12086
+ // Helpers
11958
12087
  getValidFiles: getValidFilesCallback,
11959
12088
  updateFileStatus
11960
12089
  };