@shipstatic/drop 2.0.0-beta.1 → 2.0.0-beta.11

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/README.md CHANGED
@@ -94,10 +94,10 @@ interface DropReturn {
94
94
 
95
95
  // Prop getters
96
96
  getDropzoneProps: (options?: { clickable?: boolean }) => { ... };
97
- getInputProps: () => { ... };
97
+ getInputProps: (mode?: PickerMode) => { ... }; // 'folder' (default) | 'files'
98
98
 
99
99
  // Actions
100
- open: () => void; // trigger the file picker
100
+ open: (mode?: PickerMode) => void; // trigger a picker (default: folder)
101
101
  processFiles: (files: File[]) => Promise<void>; // advanced — see below
102
102
  reset: () => void;
103
103
 
@@ -141,18 +141,28 @@ useEffect(() => {
141
141
  </div>
142
142
  ```
143
143
 
144
- Drag-only, with your own trigger:
144
+ Drag-only, with your own triggers:
145
145
 
146
146
  ```tsx
147
147
  <div {...drop.getDropzoneProps({ clickable: false })}>
148
- <input {...drop.getInputProps()} />
149
- <button onClick={drop.open}>Select folder</button>
148
+ <input {...drop.getInputProps('folder')} />
149
+ <input {...drop.getInputProps('files')} />
150
+ <button onClick={() => drop.open('folder')}>Select folder</button>
151
+ <button onClick={() => drop.open('files')}>Select files</button>
150
152
  </div>
151
153
  ```
152
154
 
153
155
  `getDropzoneProps()` handles `webkitGetAsEntry` internally, which is what preserves folder structure. Calling `processFiles()` yourself loses it — the browser invalidates `dataTransfer.items` at the first `await`, so entries must be captured synchronously.
154
156
 
155
- **The hidden input is a folder picker.** It always carries `webkitdirectory`, so clicking opens a directory chooser. Individual files arrive by drag & drop.
157
+ ### Two pickers
158
+
159
+ `PickerMode` is `'folder' | 'files'`, and **folder is the default** — a bare `getInputProps()` / `open()`, and the dropzone's own click, open the folder picker.
160
+
161
+ An `<input>` is either a folder picker or a file picker, so each mode owns its own element and its own ref: a UI offering both renders **both inputs**, and `open(mode)` clicks whichever is mounted. Exactly one attribute differs — `webkitdirectory` in folder mode, `accept` in files mode.
162
+
163
+ Wrap `open` in a handler rather than passing it by reference (`onClick={() => drop.open('files')}`): React hands a click handler a `MouseEvent`, which would otherwise arrive as the mode.
164
+
165
+ **Selecting is not a second code path.** A picked file set — loose files or a ZIP — runs the identical pipeline as a dropped one, with the same paths, the same source name and the same verdict. The `accept` list is a *hint* that biases what the file dialog shows first; it decides nothing, since every dialog offers an all-files escape and drag & drop ignores `accept` outright. What files may be deployed is one rule, applied downstream of both entry points.
156
166
 
157
167
  ## Validation
158
168
 
@@ -191,7 +201,7 @@ await ship.deployments.upload(drop.getFilesForUpload(), {
191
201
 
192
202
  ## ZIP handling
193
203
 
194
- A **single** dropped ZIP is extracted and its contents deployed. ZIPs among several files are treated as ordinary files. Archive paths are sanitized against directory traversal (`../../etc/passwd` → `etc/passwd`).
204
+ A **single** dropped ZIP is extracted and its contents deployed. ZIPs among several files are treated as ordinary files. Archive paths are sanitized against directory traversal (`../../config.json` → `config.json`).
195
205
 
196
206
  ## Without React
197
207
 
package/dist/index.cjs CHANGED
@@ -4,10 +4,16 @@ var ship = require('@shipstatic/ship');
4
4
  var react = require('react');
5
5
 
6
6
  // src/process.ts
7
-
8
- // node_modules/.pnpm/@shipstatic+types@2.2.1-beta.0/node_modules/@shipstatic/types/dist/index.js
9
7
  var ErrorType = {
10
- /** Validation failed (400). Input shape is wrong. */
8
+ /**
9
+ * Validation failed. Input shape is wrong.
10
+ *
11
+ * Carries 400 when an API judged it — including a client-side pre-check of a
12
+ * rule the server enforces too, which keeps the error identical wherever it
13
+ * was caught. **Statusless** when a client rejects something no API judges,
14
+ * such as a CLI's own command grammar: `status` is documented "(API
15
+ * contexts)" on `ErrorResponse`, so there is none to report.
16
+ */
11
17
  Validation: "validation_failed",
12
18
  /** Resource not found (404). */
13
19
  NotFound: "not_found",
@@ -21,6 +27,17 @@ var ErrorType = {
21
27
  Business: "business_logic_error",
22
28
  /** API server error (500). Generic server-side fault. */
23
29
  Api: "internal_server_error",
30
+ /**
31
+ * The platform is closed for maintenance (503). A deliberate operator
32
+ * state, not a fault — nothing errored; the API is refusing work on
33
+ * purpose, and deployed sites keep serving throughout.
34
+ *
35
+ * Distinct from `Api` at 503, which the platform already uses for a
36
+ * dependency that failed (moderation unavailable). A consumer has to tell
37
+ * "we closed the door" from "something broke": the two get opposite words
38
+ * and opposite retry behaviour.
39
+ */
40
+ Maintenance: "maintenance",
24
41
  /** Network/connection error. Client-side only — set by HTTP clients on fetch failure; never produced server-side. */
25
42
  Network: "network_error",
26
43
  /** Operation was cancelled. Client-side only — set on `AbortSignal` abort; never produced server-side. */
@@ -40,6 +57,90 @@ new Set(Object.values(ErrorType).filter((t) => !CLIENT_ONLY_ERROR_TYPES.has(t)))
40
57
  function isShipError(error) {
41
58
  return error !== null && typeof error === "object" && "name" in error && error.name === "ShipError" && "status" in error;
42
59
  }
60
+ var WEB_FILE_EXTENSIONS = [
61
+ // Markup & documents
62
+ "html",
63
+ "htm",
64
+ "xhtml",
65
+ "xml",
66
+ "txt",
67
+ "md",
68
+ "markdown",
69
+ "pdf",
70
+ "csv",
71
+ // Data & config
72
+ "json",
73
+ "jsonc",
74
+ "webmanifest",
75
+ "map",
76
+ "toml",
77
+ "yaml",
78
+ "yml",
79
+ "rss",
80
+ "atom",
81
+ // Styles
82
+ "css",
83
+ "scss",
84
+ "sass",
85
+ "less",
86
+ // Scripts & modules
87
+ "js",
88
+ "mjs",
89
+ "cjs",
90
+ "jsx",
91
+ "ts",
92
+ "tsx",
93
+ "wasm",
94
+ "vue",
95
+ "svelte",
96
+ // Images
97
+ "png",
98
+ "jpg",
99
+ "jpeg",
100
+ "gif",
101
+ "webp",
102
+ "avif",
103
+ "svg",
104
+ "ico",
105
+ "bmp",
106
+ "tif",
107
+ "tiff",
108
+ "heic",
109
+ "heif",
110
+ // Fonts
111
+ "woff",
112
+ "woff2",
113
+ "ttf",
114
+ "otf",
115
+ "eot",
116
+ // Audio
117
+ "mp3",
118
+ "wav",
119
+ "ogg",
120
+ "oga",
121
+ "opus",
122
+ "m4a",
123
+ "aac",
124
+ "flac",
125
+ "weba",
126
+ // Video
127
+ "mp4",
128
+ "webm",
129
+ "ogv",
130
+ "mov",
131
+ "m4v",
132
+ "avi",
133
+ // 3D models
134
+ "glb",
135
+ "gltf",
136
+ "usdz",
137
+ // Text tracks
138
+ "vtt",
139
+ "srt",
140
+ // Archive — a whole site in one file
141
+ "zip"
142
+ ];
143
+ var WEB_FILE_ACCEPT = WEB_FILE_EXTENSIONS.map((ext) => `.${ext}`).join(",");
43
144
  var UNBUILT_PROJECT_MARKERS = /* @__PURE__ */ new Set([
44
145
  "node_modules",
45
146
  "package.json"
@@ -948,7 +1049,8 @@ var initialState = {
948
1049
  function useDrop({ ship }) {
949
1050
  const [state, setState] = react.useState(initialState);
950
1051
  const isProcessingRef = react.useRef(false);
951
- const inputRef = react.useRef(null);
1052
+ const folderInputRef = react.useRef(null);
1053
+ const filesInputRef = react.useRef(null);
952
1054
  const isProcessing = state.phase === "processing";
953
1055
  const hasError = state.phase === "error";
954
1056
  const isInteractive = state.phase === "idle" || state.phase === "ready";
@@ -1042,8 +1144,15 @@ function useDrop({ ship }) {
1042
1144
  },
1043
1145
  [processFiles2]
1044
1146
  );
1045
- const open = react.useCallback(() => {
1046
- inputRef.current?.click();
1147
+ const open = react.useCallback((mode) => {
1148
+ const input = mode === "files" ? filesInputRef.current : folderInputRef.current;
1149
+ if (!input) {
1150
+ console.warn(
1151
+ `No ${mode === "files" ? "files" : "folder"} input is mounted. Spread getInputProps('${mode === "files" ? "files" : "folder"}') onto an <input> to open this picker.`
1152
+ );
1153
+ return;
1154
+ }
1155
+ input.click();
1047
1156
  }, []);
1048
1157
  const getDropzoneProps = react.useCallback(
1049
1158
  (options) => {
@@ -1052,18 +1161,20 @@ function useDrop({ ship }) {
1052
1161
  onDragOver: handleDragOver,
1053
1162
  onDragLeave: handleDragLeave,
1054
1163
  onDrop: handleDrop,
1055
- ...clickable && { onClick: open }
1164
+ // Wrapped rather than passed by reference: `open` takes a mode, and a
1165
+ // click handler would hand it a MouseEvent.
1166
+ ...clickable && { onClick: () => open() }
1056
1167
  };
1057
1168
  },
1058
1169
  [handleDragOver, handleDragLeave, handleDrop, open]
1059
1170
  );
1060
1171
  const getInputProps = react.useCallback(
1061
- () => ({
1062
- ref: inputRef,
1172
+ (mode) => ({
1173
+ ref: mode === "files" ? filesInputRef : folderInputRef,
1063
1174
  type: "file",
1064
1175
  style: { display: "none" },
1065
1176
  multiple: true,
1066
- webkitdirectory: "",
1177
+ ...mode === "files" ? { accept: WEB_FILE_ACCEPT } : { webkitdirectory: "" },
1067
1178
  onChange: handleInputChange
1068
1179
  }),
1069
1180
  [handleInputChange]