@putkoff/abstract-securefiles 0.0.45 → 0.0.48

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.
@@ -1,4 +1,4 @@
1
1
  export { FileRow } from './../FileRow';
2
2
  export { ShareModal } from './../ShareModal';
3
3
  export type { FileItem, FileTableProps } from './../imports';
4
- export { useState, useFiles, downloadFile, Spinner, fetchSharePatch, Input, currentUsername, Button } from './../imports';
4
+ export { useState, useFiles, downloadFile, Spinner, Input, currentUsername, Button } from './../imports';
@@ -1,4 +1,4 @@
1
1
  export type { QueuedFile, FileItem, FileTableProps, UploadProps, ShareModalProps, FileRowProps } from './../imports';
2
- export { CloudArrowUpIcon, currentUsername, Button, Checkbox, Input, Spinner, useEffect, useRef, useState, useCallback, ClipboardIcon, createPortal, fetchSharePatch } from './../imports';
2
+ export { CloudArrowUpIcon, currentUsername, Button, Checkbox, Input, Spinner, useEffect, useRef, useState, useCallback, ClipboardIcon, createPortal, } from './../imports';
3
3
  export { useFiles } from './../hooks';
4
4
  export { downloadFile, uploadFiles } from './../file_utils';
@@ -1,2 +1,2 @@
1
- export { fetchSharePatch, requireToken, alertit, fetchIt, stripPrefixes, make_path, secureFetchIt } from "./../imports";
1
+ export { requireToken, alertit, fetchIt, stripPrefixes, make_path } from "./../imports";
2
2
  export type { FileItem, ApiRow } from "./../types";
@@ -1 +1 @@
1
- export { secureFetchIt, requireToken, alertit } from "./../imports";
1
+ export { requireToken, alertit } from "./../imports";
@@ -1,2 +1,2 @@
1
- export { secureFetchIt, requireToken } from './../imports';
1
+ export { requireToken } from './../imports';
2
2
  export type { FileItem } from './../imports';
@@ -1,2 +1,2 @@
1
- import { FileItem } from './../imports';
1
+ import type { FileItem } from './../imports';
2
2
  export declare function downloadSelect(item: FileItem): Promise<void>;
@@ -1,2 +1,2 @@
1
1
  export type { FileItem, ApiRow } from './../../types/src/types';
2
- export { alertit, requireToken, secureFetchIt, fetchSharePatch } from './../imports';
2
+ export { alertit, requireToken } from './../imports';
@@ -1,2 +1 @@
1
- export { secureFetchIt, fetchSharePatch } from './../imports';
2
1
  export type { FileItem, ApiRow } from './../imports';
@@ -1,2 +1 @@
1
1
  export type { FileItem } from './../../../types/src/types';
2
- export { fetchSharePatch } from './../imports';
@@ -1,3 +1,3 @@
1
1
  export type { FileItem, ApiRow } from './../imports';
2
- export { secureFetchIt, fetchSharePatch, alertit, useCallback, isLoggedIn } from './../imports';
2
+ export { alertit, useCallback, isLoggedIn } from './../imports';
3
3
  export { toggleShare, removeFile, listFiles, downloadFile } from './../file_utils';
@@ -1,2 +1,2 @@
1
1
  export type { FileItem, ApiRow } from './../imports';
2
- export { secureFetchIt, fetchSharePatch, alertit, toggleShare, removeFile, listFiles, useCallback, downloadFile, isLoggedIn } from './../imports';
2
+ export { alertit, toggleShare, removeFile, listFiles, useCallback, downloadFile, isLoggedIn } from './../imports';
package/dist/imports.d.ts CHANGED
@@ -4,4 +4,3 @@ export { useEffect, useRef, useState, useCallback } from 'react';
4
4
  export type { QueuedFile, FileItem, FileTableProps, UploadProps, ShareModalProps, FileRowProps, ApiRow, EnhancedInputProps } from './types';
5
5
  export { currentUsername, alertit, requireToken, isLoggedIn, fetchIt, stripPrefixes, make_path } from "@putkoff/abstract-utilities";
6
6
  export { Button, Checkbox, Input, Spinner } from './UI';
7
- export { secureFetchIt, fetchSharePatch } from "@putkoff/abstract-apis";
package/dist/index.cjs CHANGED
@@ -4,7 +4,6 @@ var jsxRuntime = require('react/jsx-runtime');
4
4
  var reactDom = require('react-dom');
5
5
  var React = require('react');
6
6
  var abstractUtilities = require('@putkoff/abstract-utilities');
7
- var abstractApis = require('@putkoff/abstract-apis');
8
7
 
9
8
  function _interopNamespaceDefault(e) {
10
9
  var n = Object.create(null);
@@ -184,110 +183,6 @@ async function downloadFile(item) {
184
183
  triggerDownload(blob, item.filename || "download");
185
184
  }
186
185
 
187
- async function downloadSelect(item) {
188
- const token = abstractUtilities.requireToken();
189
- const body = item;
190
- const headers = {
191
- Authorization: `Bearer ${token}`,
192
- Accept: "application/octet-stream",
193
- "Content-Type": "application/json"
194
- };
195
- const blob = await abstractApis.secureFetchIt("download", body, "POST", headers, true, true);
196
- if (!blob || blob.size === 0) {
197
- throw new Error(`Received empty file data for “${item.filename}”`);
198
- }
199
- triggerDownload(blob, item.filename);
200
- }
201
-
202
- /** Given a checkbox inside a table-row → download that row’s file. */
203
- async function downloadRow(cb) {
204
- const row = cb.closest("tr");
205
- const filepath = cb.value;
206
- const filename = row?.children[1]?.textContent?.trim() || "downloaded_file";
207
- if (filepath)
208
- await downloadSelect({ filepath, filename });
209
- }
210
- /** Bulk download: pass a NodeList or array of checkboxes. */
211
- async function downloadSelected(checkboxes) {
212
- const list = Array.from(checkboxes);
213
- if (!list.length) {
214
- alert("No files selected.");
215
- return;
216
- }
217
- for (const cb of list) {
218
- await downloadRow(cb);
219
- }
220
- }
221
-
222
- /**
223
- * Classic “select all rows” logic (still handy in non-React views
224
- * such as the admin dashboard).
225
- */
226
- function syncHeaderCheckbox() {
227
- const all = document.querySelectorAll(".file-checkbox");
228
- const checked = document.querySelectorAll(".file-checkbox:checked");
229
- const header = document.getElementById("select-all-checkbox");
230
- header.indeterminate =
231
- checked.length > 0 && checked.length < all.length;
232
- header.checked = checked.length === all.length;
233
- }
234
-
235
- /** Wire up header + row checkboxes once the table is in the DOM. */
236
- function initSelectAll() {
237
- const header = document.getElementById("select-all-checkbox");
238
- const btn = document.getElementById("select-all");
239
- // 1) header → toggle all
240
- header.addEventListener("change", (e) => {
241
- const on = e.target.checked;
242
- document
243
- .querySelectorAll(".file-checkbox")
244
- .forEach((cb) => (cb.checked = on));
245
- });
246
- // 2) button → toggle all
247
- btn.addEventListener("click", () => {
248
- const cbs = Array.from(document.querySelectorAll(".file-checkbox"));
249
- if (!cbs.length) {
250
- alert("No files to select.");
251
- return;
252
- }
253
- const anyUnchecked = cbs.some((cb) => !cb.checked);
254
- cbs.forEach((cb) => (cb.checked = anyUnchecked));
255
- syncHeaderCheckbox();
256
- });
257
- // 3) row boxes → keep header state
258
- document
259
- .querySelectorAll(".file-checkbox")
260
- .forEach((cb) => cb.addEventListener("change", syncHeaderCheckbox));
261
- syncHeaderCheckbox();
262
- }
263
-
264
- /** Utility for other screens: toggle a list of checkboxes on/off. */
265
- function toggleCheckboxes(cbs) {
266
- if (!cbs.length) {
267
- alert("No files to select.");
268
- return;
269
- }
270
- const anyUnchecked = cbs.some((cb) => !cb.checked);
271
- cbs.forEach((cb) => (cb.checked = anyUnchecked));
272
- syncHeaderCheckbox();
273
- }
274
-
275
- const API_BASE$1 = '/api/secure-files';
276
- /** Generic PATCH helper for file-level admin settings. */
277
- async function patchFileSetting(fileId, field, value) {
278
- const res = await fetch(`${API_BASE$1}/files/${fileId}/settings/${field}`, {
279
- method: 'PATCH',
280
- credentials: 'include',
281
- headers: { 'Content-Type': 'application/json' },
282
- body: JSON.stringify({ value }),
283
- });
284
- if (!res.ok) {
285
- const err = await res.json().catch(() => ({}));
286
- throw new Error(err.error || res.statusText);
287
- }
288
- return (await res.json());
289
- }
290
-
291
186
  /**
292
187
  * Cookie-authenticated API helpers.
293
188
  * All requests include credentials: 'include' so the browser sends the
@@ -324,10 +219,10 @@ function resolveApiBase() {
324
219
  // 4. Same-origin default
325
220
  return "/api/secure-files";
326
221
  }
327
- const API_BASE = resolveApiBase();
222
+ const API_BASE$1 = resolveApiBase();
328
223
  function apiUrl(path) {
329
224
  const p = path.startsWith("/") ? path : `/${path}`;
330
- return `${API_BASE}${p}`;
225
+ return `${API_BASE$1}${p}`;
331
226
  }
332
227
  /**
333
228
  * Generic JSON fetch with session-cookie auth.
@@ -388,6 +283,118 @@ async function cookieUpload(file, relPath = "") {
388
283
  throw new Error(`Upload failed: HTTP ${res.status}`);
389
284
  return res.json();
390
285
  }
286
+ /** The resolved API base URL, exported for debugging / display purposes. */
287
+ const SECURE_FILES_API_BASE = API_BASE$1;
288
+
289
+ async function downloadSelect(item) {
290
+ const res = await fetch(`${SECURE_FILES_API_BASE}/download`, {
291
+ method: "POST",
292
+ credentials: "include",
293
+ headers: {
294
+ "Content-Type": "application/json",
295
+ Accept: "application/octet-stream",
296
+ },
297
+ body: JSON.stringify(item),
298
+ });
299
+ if (res.status === 401)
300
+ throw new Error("Not authenticated – please log in.");
301
+ if (!res.ok)
302
+ throw new Error(`Download failed: HTTP ${res.status}`);
303
+ const blob = await res.blob();
304
+ if (!blob || blob.size === 0) {
305
+ throw new Error(`Received empty file data for "${item.filename}"`);
306
+ }
307
+ triggerDownload(blob, item.filename);
308
+ }
309
+
310
+ /** Given a checkbox inside a table-row → download that row’s file. */
311
+ async function downloadRow(cb) {
312
+ const row = cb.closest("tr");
313
+ const filepath = cb.value;
314
+ const filename = row?.children[1]?.textContent?.trim() || "downloaded_file";
315
+ if (filepath)
316
+ await downloadSelect({ filepath, filename });
317
+ }
318
+ /** Bulk download: pass a NodeList or array of checkboxes. */
319
+ async function downloadSelected(checkboxes) {
320
+ const list = Array.from(checkboxes);
321
+ if (!list.length) {
322
+ alert("No files selected.");
323
+ return;
324
+ }
325
+ for (const cb of list) {
326
+ await downloadRow(cb);
327
+ }
328
+ }
329
+
330
+ /**
331
+ * Classic “select all rows” logic (still handy in non-React views
332
+ * such as the admin dashboard).
333
+ */
334
+ function syncHeaderCheckbox() {
335
+ const all = document.querySelectorAll(".file-checkbox");
336
+ const checked = document.querySelectorAll(".file-checkbox:checked");
337
+ const header = document.getElementById("select-all-checkbox");
338
+ header.indeterminate =
339
+ checked.length > 0 && checked.length < all.length;
340
+ header.checked = checked.length === all.length;
341
+ }
342
+
343
+ /** Wire up header + row checkboxes once the table is in the DOM. */
344
+ function initSelectAll() {
345
+ const header = document.getElementById("select-all-checkbox");
346
+ const btn = document.getElementById("select-all");
347
+ // 1) header → toggle all
348
+ header.addEventListener("change", (e) => {
349
+ const on = e.target.checked;
350
+ document
351
+ .querySelectorAll(".file-checkbox")
352
+ .forEach((cb) => (cb.checked = on));
353
+ });
354
+ // 2) button → toggle all
355
+ btn.addEventListener("click", () => {
356
+ const cbs = Array.from(document.querySelectorAll(".file-checkbox"));
357
+ if (!cbs.length) {
358
+ alert("No files to select.");
359
+ return;
360
+ }
361
+ const anyUnchecked = cbs.some((cb) => !cb.checked);
362
+ cbs.forEach((cb) => (cb.checked = anyUnchecked));
363
+ syncHeaderCheckbox();
364
+ });
365
+ // 3) row boxes → keep header state
366
+ document
367
+ .querySelectorAll(".file-checkbox")
368
+ .forEach((cb) => cb.addEventListener("change", syncHeaderCheckbox));
369
+ syncHeaderCheckbox();
370
+ }
371
+
372
+ /** Utility for other screens: toggle a list of checkboxes on/off. */
373
+ function toggleCheckboxes(cbs) {
374
+ if (!cbs.length) {
375
+ alert("No files to select.");
376
+ return;
377
+ }
378
+ const anyUnchecked = cbs.some((cb) => !cb.checked);
379
+ cbs.forEach((cb) => (cb.checked = anyUnchecked));
380
+ syncHeaderCheckbox();
381
+ }
382
+
383
+ const API_BASE = '/api/secure-files';
384
+ /** Generic PATCH helper for file-level admin settings. */
385
+ async function patchFileSetting(fileId, field, value) {
386
+ const res = await fetch(`${API_BASE}/files/${fileId}/settings/${field}`, {
387
+ method: 'PATCH',
388
+ credentials: 'include',
389
+ headers: { 'Content-Type': 'application/json' },
390
+ body: JSON.stringify({ value }),
391
+ });
392
+ if (!res.ok) {
393
+ const err = await res.json().catch(() => ({}));
394
+ throw new Error(err.error || res.statusText);
395
+ }
396
+ return (await res.json());
397
+ }
391
398
 
392
399
  /** File-upload helper (used by non-React fallback pages). */
393
400
  async function uploadFile(file, subdir) {
@@ -857,12 +864,45 @@ function FileTable({ files, loading, error, onToggleShare, onPatchShare, onDelet
857
864
  } })] }));
858
865
  }
859
866
 
867
+ // ── helpers ─────────────────────────────────────────────────────────────────
868
+ /** Strip filename from a webkitRelativePath, keep the trailing slash. */
869
+ function deriveRelPath(f) {
870
+ const wrp = f.webkitRelativePath;
871
+ if (!wrp)
872
+ return '';
873
+ const idx = wrp.lastIndexOf('/');
874
+ return idx >= 0 ? wrp.slice(0, idx + 1) : '';
875
+ }
876
+ /**
877
+ * Drain a DirectoryReader completely. The browser only returns ~100 entries
878
+ * per readEntries() call; you must call it repeatedly until it returns [].
879
+ */
880
+ function readAllEntries(reader) {
881
+ return new Promise((resolve, reject) => {
882
+ const all = [];
883
+ const pump = () => {
884
+ reader.readEntries((batch) => {
885
+ if (batch.length === 0)
886
+ resolve(all);
887
+ else {
888
+ all.push(...batch);
889
+ pump();
890
+ }
891
+ }, reject);
892
+ };
893
+ pump();
894
+ });
895
+ }
896
+ // ── component ───────────────────────────────────────────────────────────────
860
897
  function Upload({ onUploaded }) {
861
898
  const [queue, setQueue] = React.useState([]);
862
899
  const [busy, setBusy] = React.useState(false);
863
900
  const [message, setMessage] = React.useState(null);
864
901
  const [drag, setDrag] = React.useState(false);
865
- const inputRef = React.useRef(null);
902
+ // Two refs: one for plain file picking, one for folder picking.
903
+ const fileInputRef = React.useRef(null);
904
+ const dirInputRef = React.useRef(null);
905
+ const [pickMode, setPickMode] = React.useState('files');
866
906
  const [dirs, setDirs] = React.useState([]);
867
907
  // — existing files → directory list —
868
908
  const { files, load } = useFiles();
@@ -882,56 +922,76 @@ function Upload({ onUploaded }) {
882
922
  const [creatingNew, setCreatingNew] = React.useState(false);
883
923
  const [newDirName, setNewDirName] = React.useState('');
884
924
  // — queue logic —
885
- const enqueue = React.useCallback((files) => {
925
+ const enqueue = React.useCallback((incoming) => {
886
926
  setQueue(prev => {
887
927
  const map = new Map(prev.map(q => [`${q.file.name}-${q.file.size}`, q]));
888
- files.forEach(q => map.set(`${q.file.name}-${q.file.size}`, q));
928
+ incoming.forEach(q => map.set(`${q.file.name}-${q.file.size}`, q));
889
929
  return Array.from(map.values());
890
930
  });
891
931
  }, []);
892
932
  const extractFromDataTransfer = React.useCallback(async (dt) => {
933
+ const items = Array.from(dt.items ?? []);
934
+ const hasEntryApi = items.length > 0 &&
935
+ typeof items[0].webkitGetAsEntry === 'function';
936
+ // Fallback: no entry API → flat files only, no folder traversal.
937
+ if (!hasEntryApi) {
938
+ return Array.from(dt.files ?? []).map(file => ({
939
+ file,
940
+ relPath: '',
941
+ progress: 0,
942
+ }));
943
+ }
893
944
  const out = [];
894
- const walk = async (entry, path = "") => {
945
+ const walk = async (entry, path = '') => {
895
946
  if (!entry)
896
947
  return;
897
948
  if (entry.isFile) {
898
- const file = await new Promise(res => entry.file(res));
949
+ const file = await new Promise((res, rej) => entry.file(res, rej));
899
950
  out.push({ file, relPath: path, progress: 0 });
951
+ return;
900
952
  }
901
- else if (entry.isDirectory) {
953
+ if (entry.isDirectory) {
902
954
  const reader = entry.createReader();
903
- const entries = await new Promise(r => reader.readEntries(r));
955
+ const entries = await readAllEntries(reader);
904
956
  await Promise.all(entries.map(e => walk(e, `${path}${entry.name}/`)));
905
957
  }
906
958
  };
907
- await Promise.all(Array.from(dt.items).map(i => walk(i.webkitGetAsEntry())));
959
+ await Promise.all(items.map(i => walk(i.webkitGetAsEntry?.())));
908
960
  return out;
909
961
  }, []);
910
962
  const handleSelect = (e) => {
911
- const files = Array.from(e.target.files || []);
912
- const queued = files.map(f => ({
963
+ const picked = Array.from(e.target.files ?? []);
964
+ const queued = picked.map(f => ({
913
965
  file: f,
914
- relPath: f.webkitRelativePath.replace(/[^/]*$/, ""),
966
+ relPath: deriveRelPath(f),
915
967
  progress: 0,
916
968
  }));
917
969
  enqueue(queued);
918
- e.target.value = "";
970
+ e.target.value = '';
919
971
  };
920
972
  const handleDrop = async (e) => {
921
973
  e.preventDefault();
922
- const files = await extractFromDataTransfer(e.dataTransfer);
923
- enqueue(files.length ? files : []);
974
+ e.stopPropagation();
975
+ const dropped = await extractFromDataTransfer(e.dataTransfer);
976
+ if (dropped.length)
977
+ enqueue(dropped);
924
978
  setDrag(false);
925
979
  };
980
+ const openPicker = (e) => {
981
+ // Only fire when the dropzone background itself is clicked, not children.
982
+ if (e.target !== e.currentTarget && !e.target.closest('[data-dz-passthrough]')) ;
983
+ const ref = pickMode === 'folder' ? dirInputRef : fileInputRef;
984
+ ref.current?.click();
985
+ };
926
986
  const handleUploadAll = async () => {
927
987
  if (!queue.length) {
928
- setMessage("No files queued.");
988
+ setMessage('No files queued.');
929
989
  return;
930
990
  }
931
991
  setBusy(true);
932
992
  setMessage(null);
933
993
  const targetDir = creatingNew
934
- ? (newDirName.trim().replace(/^\/+|\/+$/g, "") + "/")
994
+ ? (newDirName.trim().replace(/^\/+|\/+$/g, '') + '/')
935
995
  : uploadDir;
936
996
  try {
937
997
  for (const { file, relPath } of queue) {
@@ -939,12 +999,12 @@ function Upload({ onUploaded }) {
939
999
  setQueue(cur => cur.map(item => item.file === file ? { ...item, progress: 100 } : item));
940
1000
  }
941
1001
  setQueue([]);
942
- await onUploaded();
943
- setMessage("Upload complete");
944
- load();
1002
+ await onUploaded(); // SecureFiles passes filesHook.load here
1003
+ setMessage('Upload complete');
1004
+ // (removed the duplicate load() — onUploaded already does it)
945
1005
  }
946
1006
  catch {
947
- setMessage("Upload failed");
1007
+ setMessage('Upload failed');
948
1008
  }
949
1009
  finally {
950
1010
  setBusy(false);
@@ -953,9 +1013,9 @@ function Upload({ onUploaded }) {
953
1013
  // — directory selector handlers —
954
1014
  const handleDirChange = (e) => {
955
1015
  const v = e.target.value;
956
- if (v === "__new__") {
1016
+ if (v === '__new__') {
957
1017
  setCreatingNew(true);
958
- setNewDirName("");
1018
+ setNewDirName('');
959
1019
  }
960
1020
  else {
961
1021
  setCreatingNew(false);
@@ -963,22 +1023,19 @@ function Upload({ onUploaded }) {
963
1023
  }
964
1024
  };
965
1025
  const acceptNewDir = () => {
966
- // normalize like before
967
1026
  const cleanName = newDirName.trim().replace(/^\/+|\/+$/g, '');
1027
+ if (!cleanName) {
1028
+ setCreatingNew(false);
1029
+ return;
1030
+ }
968
1031
  const normalized = `${uploadDir}${cleanName}/`;
969
- // ① add it to our dirs list if it isn't there already
970
- setDirs(prev => {
971
- if (prev.includes(normalized))
972
- return prev;
973
- return [...prev, normalized].sort();
974
- });
975
- // ② select it & flip back to the <select>
1032
+ setDirs(prev => prev.includes(normalized) ? prev : [...prev, normalized].sort());
976
1033
  setUploadDir(normalized);
977
1034
  setCreatingNew(false);
978
1035
  setNewDirName('');
979
1036
  };
980
- return (jsxRuntime.jsxs("div", { className: "form-container max-w-2xl mx-auto", children: [jsxRuntime.jsxs("div", { className: `flex items-center border-2 border-dashed rounded-lg p-6 bg-white shadow-md space-x-4 transition-shadow duration-150
981
- ${drag ? 'border-indigo-500 bg-indigo-50 shadow-lg' : 'border-gray-300 hover:shadow-lg'}`, onClick: () => inputRef.current?.click(), onDragEnter: e => { e.preventDefault(); setDrag(true); }, onDragOver: e => e.preventDefault(), onDragLeave: () => setDrag(false), onDrop: handleDrop, role: "region", "aria-label": "File upload area", children: [jsxRuntime.jsx(ForwardRef, { style: { width: 300, height: 300 }, className: "text-gray-500 mb-3", "aria-hidden": "true" }), jsxRuntime.jsxs("div", { className: "flex-1", children: [jsxRuntime.jsx("p", { className: "text-base font-semibold text-gray-700", children: "Drag & drop files or folders here" }), queue.length > 0 && (jsxRuntime.jsxs("p", { className: "mt-1 text-sm text-gray-600", children: [queue.length, " item", queue.length > 1 && 's', " queued"] }))] })] }), jsxRuntime.jsx("input", { ref: inputRef, type: "file", multiple: true, className: "hidden", ...{ webkitdirectory: true, directory: '' }, onChange: handleSelect }), jsxRuntime.jsxs("div", { className: "flex items-center justify-between mt-4", children: [jsxRuntime.jsx("div", { className: "flex-1 mr-4", children: !creatingNew ? (jsxRuntime.jsxs("select", { value: uploadDir, onChange: handleDirChange, className: "form-select w-full", children: [dirs.map(d => (jsxRuntime.jsx("option", { value: d, children: d }, d))), jsxRuntime.jsx("option", { value: "__new__", children: "+ Create new directory\u2026" })] })) : (jsxRuntime.jsxs("div", { className: "flex", children: [jsxRuntime.jsx("input", { type: "text", value: newDirName, placeholder: "new-folder-name", onChange: e => setNewDirName(e.target.value), onBlur: acceptNewDir, onKeyDown: e => e.key === 'Enter' && acceptNewDir(), className: "form-input flex-1" }), jsxRuntime.jsx(Button, { onClick: acceptNewDir, className: "ml-2", children: "OK" })] })) }), jsxRuntime.jsx(Button, { color: "blue", variant: "primary", onClick: handleUploadAll, disabled: busy || !queue.length, children: busy ? 'Uploading…' : `Upload ${queue.length}` })] }), message && (jsxRuntime.jsx("p", { className: `text-center text-sm ${message.includes('failed') ? 'text-red-600' : 'text-gray-600'}`, role: "alert", children: message })), queue.length > 0 && (jsxRuntime.jsxs("div", { className: "wm-container mt-4", children: [jsxRuntime.jsx("h3", { className: "mb-2 text-sm font-semibold text-gray-700", children: "Queued Files" }), jsxRuntime.jsx("ul", { className: "max-h-64 space-y-4 overflow-y-auto", children: queue.map(({ file, relPath, progress }) => (jsxRuntime.jsxs("li", { className: "space-y-1", children: [jsxRuntime.jsxs("div", { className: "flex justify-between items-center", children: [jsxRuntime.jsxs("span", { className: "truncate text-sm text-gray-800", children: [relPath, file.name] }), jsxRuntime.jsxs("span", { className: "text-xs text-gray-500", children: [progress, "%"] })] }), jsxRuntime.jsx("div", { className: "w-full bg-gray-200 rounded-full h-2 overflow-hidden", children: jsxRuntime.jsx("div", { className: "bg-indigo-600 h-2", style: { width: `${progress}%` } }) })] }, `${file.name}-${file.size}`))) })] }))] }));
1037
+ return (jsxRuntime.jsxs("div", { className: "form-container max-w-2xl mx-auto", children: [jsxRuntime.jsxs("div", { className: "flex gap-4 text-xs text-gray-700 mb-1", children: [jsxRuntime.jsxs("label", { className: "flex items-center gap-1", children: [jsxRuntime.jsx("input", { type: "radio", name: "pick-mode", checked: pickMode === 'files', onChange: () => setPickMode('files') }), "Files"] }), jsxRuntime.jsxs("label", { className: "flex items-center gap-1", children: [jsxRuntime.jsx("input", { type: "radio", name: "pick-mode", checked: pickMode === 'folder', onChange: () => setPickMode('folder') }), "Folder"] }), jsxRuntime.jsx("span", { className: "text-gray-400", children: "(drag-and-drop accepts either)" })] }), jsxRuntime.jsxs("div", { className: `flex items-center border-2 border-dashed rounded-lg p-6 bg-white shadow-md space-x-4 transition-shadow duration-150
1038
+ ${drag ? 'border-indigo-500 bg-indigo-50 shadow-lg' : 'border-gray-300 hover:shadow-lg'}`, onClick: openPicker, onDragEnter: e => { e.preventDefault(); e.stopPropagation(); setDrag(true); }, onDragOver: e => { e.preventDefault(); e.stopPropagation(); }, onDragLeave: e => { e.preventDefault(); e.stopPropagation(); setDrag(false); }, onDrop: handleDrop, role: "region", "aria-label": "File upload area", children: [jsxRuntime.jsx(ForwardRef, { style: { width: 300, height: 300 }, className: "text-gray-500 mb-3", "aria-hidden": "true" }), jsxRuntime.jsxs("div", { className: "flex-1", children: [jsxRuntime.jsx("p", { className: "text-base font-semibold text-gray-700", children: "Drag & drop files or folders here" }), jsxRuntime.jsxs("p", { className: "text-xs text-gray-500 mt-1", children: ["Click to ", pickMode === 'folder' ? 'choose a folder' : 'choose files'] }), queue.length > 0 && (jsxRuntime.jsxs("p", { className: "mt-1 text-sm text-gray-600", children: [queue.length, " item", queue.length > 1 && 's', " queued"] }))] })] }), jsxRuntime.jsx("input", { ref: fileInputRef, type: "file", multiple: true, className: "hidden", onChange: handleSelect }), jsxRuntime.jsx("input", { ref: dirInputRef, type: "file", multiple: true, className: "hidden", ...{ webkitdirectory: '', directory: '' }, onChange: handleSelect }), jsxRuntime.jsxs("div", { className: "flex items-center justify-between mt-4", children: [jsxRuntime.jsx("div", { className: "flex-1 mr-4", children: !creatingNew ? (jsxRuntime.jsxs("select", { value: uploadDir, onChange: handleDirChange, className: "form-select w-full", children: [dirs.map(d => (jsxRuntime.jsx("option", { value: d, children: d }, d))), jsxRuntime.jsx("option", { value: "__new__", children: "+ Create new directory\u2026" })] })) : (jsxRuntime.jsxs("div", { className: "flex", children: [jsxRuntime.jsx("input", { type: "text", value: newDirName, placeholder: "new-folder-name", onChange: e => setNewDirName(e.target.value), onBlur: acceptNewDir, onKeyDown: e => e.key === 'Enter' && acceptNewDir(), className: "form-input flex-1" }), jsxRuntime.jsx(Button, { onClick: acceptNewDir, className: "ml-2", children: "OK" })] })) }), jsxRuntime.jsx(Button, { color: "blue", variant: "primary", onClick: handleUploadAll, disabled: busy || !queue.length, children: busy ? 'Uploading…' : `Upload ${queue.length}` })] }), message && (jsxRuntime.jsx("p", { className: `text-center text-sm ${message.includes('failed') ? 'text-red-600' : 'text-gray-600'}`, role: "alert", children: message })), queue.length > 0 && (jsxRuntime.jsxs("div", { className: "wm-container mt-4", children: [jsxRuntime.jsx("h3", { className: "mb-2 text-sm font-semibold text-gray-700", children: "Queued Files" }), jsxRuntime.jsx("ul", { className: "max-h-64 space-y-4 overflow-y-auto", children: queue.map(({ file, relPath, progress }) => (jsxRuntime.jsxs("li", { className: "space-y-1", children: [jsxRuntime.jsxs("div", { className: "flex justify-between items-center", children: [jsxRuntime.jsxs("span", { className: "truncate text-sm text-gray-800", children: [relPath, file.name] }), jsxRuntime.jsxs("span", { className: "text-xs text-gray-500", children: [progress, "%"] })] }), jsxRuntime.jsx("div", { className: "w-full bg-gray-200 rounded-full h-2 overflow-hidden", children: jsxRuntime.jsx("div", { className: "bg-indigo-600 h-2", style: { width: `${progress}%` } }) })] }, `${file.name}-${file.size}`))) })] }))] }));
982
1039
  }
983
1040
 
984
1041
  const SecureFiles = () => {