@putkoff/abstract-securefiles 0.0.53 → 0.0.54

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.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  export { default as SecureFiles } from "./SecureFiles";
2
2
  export { createApi, DEFAULT_API_BASE, ApiError } from "./api";
3
3
  export type { SecureFilesApi } from "./api";
4
- export type { SecureFile, ShareUpdate, SecureFilesProps, LinkRoot, RootChallenge, BrowseResult, Device } from "./types";
4
+ export type { SecureFile, ShareUpdate, SecureFilesProps, LinkRoot, RootChallenge, BrowseResult, Device, Person, SharedItem, SharedOwner } from "./types";
package/dist/index.js CHANGED
@@ -38,6 +38,27 @@ function createApi(apiBase = DEFAULT_API_BASE) {
38
38
  devices: () => json("/devices"),
39
39
  pairDevice: () => json("/devices/pair", { method: "POST", body: "{}" }),
40
40
  removeDevice: (id) => json(`/devices/${id}`, { method: "DELETE" }),
41
+ people: () => json("/people"),
42
+ invitePerson: () => json("/people/invite", { method: "POST", body: "{}" }),
43
+ acceptPerson: (code) => json("/people/accept", {
44
+ method: "POST",
45
+ body: JSON.stringify({ code }),
46
+ }),
47
+ setGrants: (username, grant) => json(`/people/${encodeURIComponent(username)}/grants`, {
48
+ method: "PUT",
49
+ body: JSON.stringify(grant),
50
+ }),
51
+ removePerson: (username) => json(`/people/${encodeURIComponent(username)}`, { method: "DELETE" }),
52
+ sharedWithMe: () => json("/shared-with-me"),
53
+ /** Browser-native download of an item someone shared with me (streams to disk, resumable). */
54
+ downloadShared(id) {
55
+ const a = document.createElement("a");
56
+ a.href = `${root}/shared-with-me/${id}`;
57
+ a.rel = "noopener";
58
+ document.body.appendChild(a);
59
+ a.click();
60
+ a.remove();
61
+ },
41
62
  roots: () => json("/admin/roots"),
42
63
  requestRoot: (path) => json("/admin/roots", { method: "POST", body: JSON.stringify({ path }) }),
43
64
  confirmRoot: (nonce, signature, label) => json("/admin/roots/confirm", {
@@ -320,7 +341,7 @@ function ShareDialog({ file, onClose, onSave, }) {
320
341
  return (jsx("div", { className: "sf-overlay", onMouseDown: (e) => e.target === e.currentTarget && onClose(), children: jsxs("div", { className: "sf-dialog", role: "dialog", "aria-modal": "true", "aria-label": `Share settings for ${file.filename}`, children: [jsxs("header", { className: "sf-dialog__head", children: [jsx("h3", { children: "Share settings" }), jsx("button", { type: "button", className: "sf-icon-btn", "aria-label": "Close", onClick: onClose, children: "\u2715" })] }), jsx("p", { className: "sf-muted", children: file.filepath }), jsxs("label", { className: "sf-check", children: [jsx("input", { type: "checkbox", checked: shareable, onChange: (e) => setShareable(e.target.checked) }), "Anyone with the link can download"] }), jsxs("fieldset", { disabled: !shareable, className: "sf-fieldset", children: [jsxs("label", { className: "sf-field", children: [jsx("span", { children: "Download limit (blank = unlimited)" }), jsx("input", { className: "sf-input", inputMode: "numeric", value: limit, onChange: (e) => setLimit(e.target.value.trim()), placeholder: "unlimited" }), !limitValid && jsx("span", { className: "sf-error", children: "Enter a whole number" })] }), jsxs("p", { className: "sf-muted", children: ["Downloaded ", file.download_count, " time", file.download_count === 1 ? "" : "s", file.download_limit ? ` of ${file.download_limit}` : "", ".", " ", jsxs("label", { className: "sf-check sf-check--inline", children: [jsx("input", { type: "checkbox", checked: resetCount, onChange: (e) => setResetCount(e.target.checked) }), "reset count"] })] }), jsxs("div", { className: "sf-field", children: [jsx("span", { children: "Password" }), jsxs("div", { className: "sf-row", children: [file.has_password && (jsxs("label", { className: "sf-check sf-check--inline", children: [jsx("input", { type: "radio", checked: pwMode === "keep", onChange: () => setPwMode("keep") }), "keep current"] })), jsxs("label", { className: "sf-check sf-check--inline", children: [jsx("input", { type: "radio", checked: pwMode === "set", onChange: () => setPwMode("set") }), file.has_password ? "change" : "require password"] }), jsxs("label", { className: "sf-check sf-check--inline", children: [jsx("input", { type: "radio", checked: pwMode === "remove", onChange: () => setPwMode("remove") }), file.has_password ? "remove" : "none"] })] }), pwMode === "set" && (jsx("input", { className: "sf-input", type: "password", autoComplete: "new-password", value: password, onChange: (e) => setPassword(e.target.value), placeholder: "New password" }))] }), jsxs("div", { className: "sf-field", children: [jsx("span", { children: "Share link" }), file.share_url ? (jsxs("div", { className: "sf-row", children: [jsx("input", { className: "sf-input", readOnly: true, value: file.share_url, onFocus: (e) => e.target.select() }), jsx(CopyButton, { text: file.share_url }), jsx(ConfirmButton, { label: "New link", disabled: saving, onConfirm: newLink })] })) : (jsx("span", { className: "sf-muted", children: "A private link is created when you save with sharing on." })), file.share_url && !file.shareable && (jsx("span", { className: "sf-muted", children: "The link works once you save with sharing on." })), jsx("span", { className: "sf-muted", children: "\"New link\" invalidates the current one immediately." })] })] }), error && jsx("p", { className: "sf-error", children: error }), jsxs("footer", { className: "sf-row sf-dialog__foot", children: [jsx("button", { type: "button", className: "sf-btn", onClick: onClose, children: "Cancel" }), jsx("button", { type: "button", className: "sf-btn sf-btn--primary", disabled: saving || !limitValid || !passwordValid, onClick: save, children: saving ? "Saving…" : "Save" })] })] }) }));
321
342
  }
322
343
 
323
- function formatSize(bytes) {
344
+ function formatSize$1(bytes) {
324
345
  const units = ["B", "KB", "MB", "GB", "TB"];
325
346
  let n = bytes;
326
347
  let i = 0;
@@ -330,7 +351,7 @@ function formatSize(bytes) {
330
351
  }
331
352
  return `${n.toFixed(i ? 1 : 0)} ${units[i]}`;
332
353
  }
333
- const errText$1 = (err) => (err instanceof Error ? err.message : String(err));
354
+ const errText$2 = (err) => (err instanceof Error ? err.message : String(err));
334
355
  function AddRoot({ api, onAdded }) {
335
356
  const [path, setPath] = useState("");
336
357
  const [label, setLabel] = useState("");
@@ -346,7 +367,7 @@ function AddRoot({ api, onAdded }) {
346
367
  setSignature("");
347
368
  }
348
369
  catch (err) {
349
- setError(errText$1(err));
370
+ setError(errText$2(err));
350
371
  }
351
372
  finally {
352
373
  setBusy(false);
@@ -365,7 +386,7 @@ function AddRoot({ api, onAdded }) {
365
386
  setSignature("");
366
387
  }
367
388
  catch (err) {
368
- setError(errText$1(err));
389
+ setError(errText$2(err));
369
390
  }
370
391
  finally {
371
392
  setBusy(false);
@@ -388,7 +409,7 @@ function Browser({ api, roots, folders, onLinked, }) {
388
409
  setPicked(new Set());
389
410
  }
390
411
  catch (err) {
391
- setError(errText$1(err));
412
+ setError(errText$2(err));
392
413
  }
393
414
  }, [api]);
394
415
  useEffect(() => {
@@ -416,7 +437,7 @@ function Browser({ api, roots, folders, onLinked, }) {
416
437
  await open(listing.path);
417
438
  }
418
439
  catch (err) {
419
- setResult(errText$1(err));
440
+ setResult(errText$2(err));
420
441
  }
421
442
  finally {
422
443
  setBusy(false);
@@ -429,7 +450,7 @@ function Browser({ api, roots, folders, onLinked, }) {
429
450
  ...(listing?.files.filter((f) => !f.linked_id) ?? []),
430
451
  ];
431
452
  const relative = listing ? listing.path.slice(listing.root.length) || "/" : "";
432
- return (jsxs("div", { className: "sf-card", children: [jsx("h4", { children: "Link files or folders" }), jsxs("div", { className: "sf-row", children: [jsx("select", { className: "sf-input sf-root-select", value: listing?.root ?? "", onChange: (e) => open(e.target.value), children: roots.map((r) => (jsx("option", { value: r.path, children: r.label ? `${r.label} (${r.path})` : r.path }, r.id))) }), listing && jsx("code", { className: "sf-muted", children: relative })] }), error && jsx("p", { className: "sf-error", children: error }), listing && (jsxs("div", { className: "sf-browser", children: [listing.parent && (jsx("button", { type: "button", className: "sf-browser__dir", onClick: () => open(listing.parent), children: "\u21A9 .." })), listing.dirs.map((d) => (jsxs("div", { className: `sf-browser__row${d.linked_id ? " sf-muted" : ""}`, children: [jsx("input", { type: "checkbox", "aria-label": `Select folder ${d.name}`, disabled: !!d.linked_id, checked: picked.has(d.path), onChange: (e) => togglePick(d.path, e.target.checked) }), jsxs("button", { type: "button", className: "sf-browser__dir", onClick: () => open(d.path), title: "Open folder", children: ["\uD83D\uDCC1 ", d.name] }), jsx("span", { className: "sf-muted", children: d.linked_id ? "already linked" : "folder → .tar" })] }, d.path))), linkable.length > 0 && (jsxs("label", { className: "sf-check sf-browser__all", children: [jsx("input", { type: "checkbox", checked: linkable.length > 0 && linkable.every((f) => picked.has(f.path)), disabled: !linkable.length, onChange: (e) => setPicked(e.target.checked ? new Set(linkable.map((f) => f.path)) : new Set()) }), "Select everything here"] })), listing.files.map((f) => (jsxs("label", { className: `sf-check sf-browser__file${f.linked_id ? " sf-muted" : ""}`, children: [jsx("input", { type: "checkbox", disabled: !!f.linked_id, checked: picked.has(f.path), onChange: (e) => togglePick(f.path, e.target.checked) }), jsx("span", { className: "sf-browser__name", children: f.name }), jsx("span", { className: "sf-muted", children: f.linked_id ? "already linked" : formatSize(f.size) })] }, f.path))), !listing.dirs.length && !listing.files.length && jsx("p", { className: "sf-muted", children: "Empty directory" }), listing.truncated && jsx("p", { className: "sf-muted", children: "Showing the first 5000 entries." })] })), jsxs("div", { className: "sf-row sf-row--bottom", children: [jsx(FolderSelect, { label: "Show in Secure Files under", folders: folders, value: target, onChange: setTarget }), jsx("button", { type: "button", className: "sf-btn sf-btn--primary", disabled: busy || !picked.size, onClick: link, children: busy ? "Linking…" : `Link ${picked.size || ""}`.trim() })] }), result && jsx("p", { className: result.includes("skipped") ? "sf-error" : "sf-muted", children: result })] }));
453
+ return (jsxs("div", { className: "sf-card", children: [jsx("h4", { children: "Link files or folders" }), jsxs("div", { className: "sf-row", children: [jsx("select", { className: "sf-input sf-root-select", value: listing?.root ?? "", onChange: (e) => open(e.target.value), children: roots.map((r) => (jsx("option", { value: r.path, children: r.label ? `${r.label} (${r.path})` : r.path }, r.id))) }), listing && jsx("code", { className: "sf-muted", children: relative })] }), error && jsx("p", { className: "sf-error", children: error }), listing && (jsxs("div", { className: "sf-browser", children: [listing.parent && (jsx("button", { type: "button", className: "sf-browser__dir", onClick: () => open(listing.parent), children: "\u21A9 .." })), listing.dirs.map((d) => (jsxs("div", { className: `sf-browser__row${d.linked_id ? " sf-muted" : ""}`, children: [jsx("input", { type: "checkbox", "aria-label": `Select folder ${d.name}`, disabled: !!d.linked_id, checked: picked.has(d.path), onChange: (e) => togglePick(d.path, e.target.checked) }), jsxs("button", { type: "button", className: "sf-browser__dir", onClick: () => open(d.path), title: "Open folder", children: ["\uD83D\uDCC1 ", d.name] }), jsx("span", { className: "sf-muted", children: d.linked_id ? "already linked" : "folder → .tar" })] }, d.path))), linkable.length > 0 && (jsxs("label", { className: "sf-check sf-browser__all", children: [jsx("input", { type: "checkbox", checked: linkable.length > 0 && linkable.every((f) => picked.has(f.path)), disabled: !linkable.length, onChange: (e) => setPicked(e.target.checked ? new Set(linkable.map((f) => f.path)) : new Set()) }), "Select everything here"] })), listing.files.map((f) => (jsxs("label", { className: `sf-check sf-browser__file${f.linked_id ? " sf-muted" : ""}`, children: [jsx("input", { type: "checkbox", disabled: !!f.linked_id, checked: picked.has(f.path), onChange: (e) => togglePick(f.path, e.target.checked) }), jsx("span", { className: "sf-browser__name", children: f.name }), jsx("span", { className: "sf-muted", children: f.linked_id ? "already linked" : formatSize$1(f.size) })] }, f.path))), !listing.dirs.length && !listing.files.length && jsx("p", { className: "sf-muted", children: "Empty directory" }), listing.truncated && jsx("p", { className: "sf-muted", children: "Showing the first 5000 entries." })] })), jsxs("div", { className: "sf-row sf-row--bottom", children: [jsx(FolderSelect, { label: "Show in Secure Files under", folders: folders, value: target, onChange: setTarget }), jsx("button", { type: "button", className: "sf-btn sf-btn--primary", disabled: busy || !picked.size, onClick: link, children: busy ? "Linking…" : `Link ${picked.size || ""}`.trim() })] }), result && jsx("p", { className: result.includes("skipped") ? "sf-error" : "sf-muted", children: result })] }));
433
454
  }
434
455
  function ServerFiles({ api, folders, onLinked, }) {
435
456
  const [roots, setRoots] = useState(null);
@@ -440,7 +461,7 @@ function ServerFiles({ api, folders, onLinked, }) {
440
461
  setError(null);
441
462
  }
442
463
  catch (err) {
443
- setError(errText$1(err));
464
+ setError(errText$2(err));
444
465
  }
445
466
  }, [api]);
446
467
  return (jsxs("details", { className: "sf-admin", onToggle: (e) => e.currentTarget.open && !roots && load(), children: [jsx("summary", { children: "Server files (admin)" }), jsxs("div", { className: "sf-stack", children: [jsx("p", { className: "sf-muted", children: "Link files or whole folders that already live on the server instead of uploading copies. A linked folder downloads as one uncompressed .tar with an exact size, and interrupted downloads can be resumed. Only items inside allowed directories can be linked; removing a directory immediately disables its links. Deleting a linked entry never touches the original. Available from the local network only." }), error && jsx("p", { className: "sf-error", children: error }), roots && (jsxs(Fragment, { children: [jsxs("div", { className: "sf-card", children: [jsx("h4", { children: "Allowed directories" }), roots.length === 0 ? (jsx("p", { className: "sf-muted", children: "None yet." })) : (jsx("ul", { className: "sf-roots", children: roots.map((r) => (jsxs("li", { children: [jsxs("span", { children: [jsx("code", { children: r.path }), " ", r.label && jsxs("span", { className: "sf-muted", children: ["\u2014 ", r.label] }), !r.exists && jsx("span", { className: "sf-error", children: " (missing)" })] }), jsx(ConfirmButton, { label: "Remove", onConfirm: async () => {
@@ -449,12 +470,12 @@ function ServerFiles({ api, folders, onLinked, }) {
449
470
  onLinked();
450
471
  }
451
472
  catch (err) {
452
- setError(errText$1(err));
473
+ setError(errText$2(err));
453
474
  }
454
475
  } })] }, r.id))) }))] }), jsx(AddRoot, { api: api, onAdded: setRoots }), jsx(Browser, { api: api, roots: roots, folders: folders, onLinked: onLinked })] }))] })] }));
455
476
  }
456
477
 
457
- const errText = (err) => (err instanceof Error ? err.message : String(err));
478
+ const errText$1 = (err) => (err instanceof Error ? err.message : String(err));
458
479
  const UNIX_CMD = "pipx install abstract-securefiles && pipx ensurepath && ~/.local/bin/securefiles setup";
459
480
  const WIN_CMD = "py -m pip install --user pipx; py -m pipx install abstract-securefiles; py -m pipx ensurepath";
460
481
  function ago(iso) {
@@ -493,7 +514,7 @@ function AddComputer({ api, onPaired }) {
493
514
  setExpiresAt(Date.now() + res.expires_in * 1000);
494
515
  }
495
516
  catch (err) {
496
- setError(errText(err));
517
+ setError(errText$1(err));
497
518
  }
498
519
  };
499
520
  const minutesLeft = Math.max(1, Math.round((expiresAt - Date.now()) / 60000));
@@ -510,11 +531,139 @@ function MyComputers({ api, devices, onChange, }) {
510
531
  onChange();
511
532
  }
512
533
  catch (err) {
513
- setError(errText(err));
534
+ setError(errText$1(err));
514
535
  }
515
536
  } })] }, d.id))) })), error && jsx("p", { className: "sf-error", children: error }), jsx(AddComputer, { api: api, onPaired: onChange })] })] }));
516
537
  }
517
538
 
539
+ const errText = (err) => (err instanceof Error ? err.message : String(err));
540
+ function formatSize(bytes) {
541
+ if (bytes == null)
542
+ return "";
543
+ const units = ["B", "KB", "MB", "GB", "TB"];
544
+ let n = bytes;
545
+ let i = 0;
546
+ while (n >= 1024 && i < units.length - 1) {
547
+ n /= 1024;
548
+ i++;
549
+ }
550
+ return `${n.toFixed(i ? 1 : 0)} ${units[i]}`;
551
+ }
552
+ function GrantEditor({ person, files, onSave, onClose, }) {
553
+ const [all, setAll] = useState(person.my_grant.all);
554
+ const [ids, setIds] = useState(new Set(person.my_grant.ids));
555
+ const [search, setSearch] = useState("");
556
+ const [saving, setSaving] = useState(false);
557
+ const [error, setError] = useState(null);
558
+ const visible = useMemo(() => {
559
+ const q = search.trim().toLowerCase();
560
+ return [...files]
561
+ .sort((a, b) => a.filepath.localeCompare(b.filepath))
562
+ .filter((f) => !q || f.filepath.toLowerCase().includes(q));
563
+ }, [files, search]);
564
+ const save = async () => {
565
+ setSaving(true);
566
+ setError(null);
567
+ try {
568
+ await onSave({ all, ids: all ? [] : [...ids] });
569
+ onClose();
570
+ }
571
+ catch (err) {
572
+ setError(errText(err));
573
+ }
574
+ finally {
575
+ setSaving(false);
576
+ }
577
+ };
578
+ return (jsxs("div", { className: "sf-card", children: [jsxs("h4", { children: ["What ", person.username, " can access"] }), jsxs("label", { className: "sf-check", children: [jsx("input", { type: "radio", checked: all, onChange: () => setAll(true) }), jsxs("span", { children: [jsx("b", { children: "All my files" }), " ", jsx("span", { className: "sf-muted", children: "\u2014 including anything you add later" })] })] }), jsxs("label", { className: "sf-check", children: [jsx("input", { type: "radio", checked: !all, onChange: () => setAll(false) }), jsx("b", { children: "Only the items I choose" })] }), !all && (jsxs(Fragment, { children: [jsx("input", { className: "sf-input", type: "search", placeholder: "Filter your files\u2026", value: search, onChange: (e) => setSearch(e.target.value) }), jsxs("div", { className: "sf-browser", children: [visible.length === 0 && jsx("p", { className: "sf-muted", children: "No files." }), visible.map((f) => (jsxs("label", { className: "sf-check sf-browser__file", children: [jsx("input", { type: "checkbox", checked: ids.has(f.id), onChange: (e) => setIds((s) => {
579
+ const next = new Set(s);
580
+ e.target.checked ? next.add(f.id) : next.delete(f.id);
581
+ return next;
582
+ }) }), jsx("span", { className: "sf-browser__name", children: f.filepath }), jsx("span", { className: "sf-muted", children: f.kind === "folder" ? "folder · .tar" : formatSize(f.size) })] }, f.id)))] }), jsxs("span", { className: "sf-muted", children: [ids.size, " selected"] })] })), error && jsx("p", { className: "sf-error", children: error }), jsxs("div", { className: "sf-row sf-row--end", children: [jsx("button", { type: "button", className: "sf-btn", onClick: onClose, children: "Cancel" }), jsx("button", { type: "button", className: "sf-btn sf-btn--primary", disabled: saving, onClick: save, children: saving ? "Saving…" : "Save" })] })] }));
583
+ }
584
+ function People({ api, files }) {
585
+ const [people, setPeople] = useState([]);
586
+ const [code, setCode] = useState(null);
587
+ const [entered, setEntered] = useState("");
588
+ const [editing, setEditing] = useState(null);
589
+ const [error, setError] = useState(null);
590
+ const [notice, setNotice] = useState(null);
591
+ const load = useCallback(async () => {
592
+ try {
593
+ setPeople((await api.people()).people);
594
+ }
595
+ catch (err) {
596
+ setError(errText(err));
597
+ }
598
+ }, [api]);
599
+ useEffect(() => {
600
+ load();
601
+ }, [load]);
602
+ useEffect(() => {
603
+ if (!code)
604
+ return;
605
+ const timer = setInterval(load, 5000);
606
+ const expire = setTimeout(() => setCode(null), 15 * 60 * 1000);
607
+ return () => {
608
+ clearInterval(timer);
609
+ clearTimeout(expire);
610
+ };
611
+ }, [code, load]);
612
+ const invite = async () => {
613
+ setError(null);
614
+ try {
615
+ setCode((await api.invitePerson()).code);
616
+ }
617
+ catch (err) {
618
+ setError(errText(err));
619
+ }
620
+ };
621
+ const accept = async () => {
622
+ setError(null);
623
+ setNotice(null);
624
+ try {
625
+ const res = await api.acceptPerson(entered);
626
+ setPeople(res.people);
627
+ setEntered("");
628
+ setNotice(`Connected with ${res.connected_with}.`);
629
+ }
630
+ catch (err) {
631
+ setError(errText(err));
632
+ }
633
+ };
634
+ const describe = (p) => p.my_grant.all ? "can access all your files" : `can access ${p.my_grant.ids.length} of your items`;
635
+ return (jsxs("details", { className: "sf-admin", children: [jsxs("summary", { children: ["People ", jsxs("span", { className: "sf-muted", children: ["(", people.length, ")"] })] }), jsxs("div", { className: "sf-stack", children: [jsx("p", { className: "sf-muted", children: "Connect with other abstractendeavors.com accounts, then choose what each person can access: all your files or specific ones. They download while signed in; nothing is public." }), people.length > 0 && (jsx("ul", { className: "sf-roots", children: people.map((p) => (jsxs("li", { children: [jsxs("span", { children: [jsx("b", { children: p.username }), " ", jsx("span", { className: p.my_grant.all ? "sf-tag sf-tag--warn" : "sf-muted", children: describe(p) }), p.shared_with_me > 0 && (jsxs("span", { className: "sf-muted", children: [" \u00B7 shares ", p.shared_with_me, " with you"] }))] }), jsxs("span", { className: "sf-row", children: [jsx("button", { type: "button", className: "sf-btn", onClick: () => setEditing(p.username), children: "Choose access" }), jsx(ConfirmButton, { label: "Disconnect", onConfirm: async () => {
636
+ try {
637
+ setPeople((await api.removePerson(p.username)).people);
638
+ }
639
+ catch (err) {
640
+ setError(errText(err));
641
+ }
642
+ } })] })] }, p.username))) })), editing && (jsx(GrantEditor, { person: people.find((p) => p.username === editing), files: files, onClose: () => setEditing(null), onSave: async (grant) => setPeople((await api.setGrants(editing, grant)).people) }, editing)), jsxs("div", { className: "sf-row sf-row--bottom", children: [code ? (jsxs("div", { className: "sf-field", children: [jsx("span", { children: "Send this code to the person (valid 15 minutes, once):" }), jsxs("div", { className: "sf-row", children: [jsx("span", { className: "sf-paircode", children: code }), jsx(CopyButton, { text: code, label: "Copy" }), jsx("button", { type: "button", className: "sf-btn", onClick: () => setCode(null), children: "Done" })] }), jsx("span", { className: "sf-muted", children: "They enter it under People \u2192 \"Have a code?\" on their Secure Files page." })] })) : (jsx("button", { type: "button", className: "sf-btn sf-btn--primary", onClick: invite, children: "Invite someone" })), jsxs("label", { className: "sf-field sf-field--narrow", children: [jsx("span", { children: "Have a code?" }), jsxs("div", { className: "sf-row", children: [jsx("input", { className: "sf-input", value: entered, placeholder: "ABCD-1234", onChange: (e) => setEntered(e.target.value), onKeyDown: (e) => e.key === "Enter" && entered.trim() && accept() }), jsx("button", { type: "button", className: "sf-btn", disabled: !entered.trim(), onClick: accept, children: "Connect" })] })] })] }), notice && jsx("p", { className: "sf-muted", children: notice }), error && jsx("p", { className: "sf-error", children: error })] })] }));
643
+ }
644
+ function SharedWithMe({ api }) {
645
+ const [owners, setOwners] = useState(null);
646
+ const [error, setError] = useState(null);
647
+ const load = useCallback(async () => {
648
+ try {
649
+ setOwners((await api.sharedWithMe()).owners);
650
+ setError(null);
651
+ }
652
+ catch (err) {
653
+ setError(errText(err));
654
+ }
655
+ }, [api]);
656
+ useEffect(() => {
657
+ load();
658
+ const onVisible = () => document.visibilityState === "visible" && load();
659
+ document.addEventListener("visibilitychange", onVisible);
660
+ return () => document.removeEventListener("visibilitychange", onVisible);
661
+ }, [load]);
662
+ if (!owners || owners.length === 0)
663
+ return error ? jsx("p", { className: "sf-error", children: error }) : null;
664
+ return (jsxs("section", { className: "sf-stack", children: [jsx("h3", { className: "sf-subhead", children: "Shared with me" }), owners.map((o) => (jsx("div", { className: "sf-table-wrap", children: jsxs("table", { className: "sf-table", children: [jsx("thead", { children: jsxs("tr", { children: [jsxs("th", { children: ["From ", jsx("b", { children: o.owner }), " ", o.all && jsx("span", { className: "sf-muted", children: "(all their files)" })] }), jsx("th", { children: "Size" }), jsx("th", { className: "sf-col-actions" })] }) }), jsx("tbody", { children: o.files.map((f) => (jsxs("tr", { children: [jsxs("td", { children: [f.filepath, f.kind === "folder" && jsx("span", { className: "sf-tag", children: "folder \u00B7 .tar" }), f.device && (jsxs("span", { className: `sf-tag${f.device.online ? "" : " sf-tag--warn"}`, children: ["on ", f.device.name, " \u00B7 ", f.device.online ? "online" : "offline"] }))] }), jsx("td", { className: "sf-muted", children: formatSize(f.size) }), jsx("td", { className: "sf-col-actions", children: jsx("button", { type: "button", className: "sf-btn", disabled: !f.available, onClick: () => api.downloadShared(f.id), children: "Download" }) })] }, f.id))) })] }) }, o.owner)))] }));
665
+ }
666
+
518
667
  function styleInject(css, ref) {
519
668
  if ( ref === void 0 ) ref = {};
520
669
  var insertAt = ref.insertAt;
@@ -542,7 +691,7 @@ function styleInject(css, ref) {
542
691
  }
543
692
  }
544
693
 
545
- var css_248z = ".sf-root{--sf-border:color-mix(in srgb,currentColor 22%,transparent);--sf-subtle:color-mix(in srgb,currentColor 6%,transparent);--sf-accent:#2563eb;--sf-danger:#dc2626;display:flex;flex-direction:column;font-size:14px;gap:16px;margin:0 auto;max-width:1100px;padding:16px;text-align:left}.sf-root *,.sf-root :after,.sf-root :before{box-sizing:border-box}.sf-head{align-items:baseline;display:flex;gap:12px;justify-content:space-between}.sf-head h2{font-size:1.4em;margin:0}.sf-muted{opacity:.7}.sf-error{color:var(--sf-danger)}.sf-row{align-items:center;display:flex;flex-wrap:wrap;gap:8px}.sf-row--end{justify-content:flex-end}.sf-field{display:flex;flex:1;flex-direction:column;gap:4px;min-width:200px}.sf-input{background:var(--sf-subtle);border:1px solid var(--sf-border);border-radius:6px;color:inherit;font:inherit;min-width:0;padding:6px 10px;width:100%}.sf-row .sf-input{flex:1;width:auto}.sf-btn{background:var(--sf-subtle);border:1px solid var(--sf-border);border-radius:6px;color:inherit;cursor:pointer;font:inherit;padding:5px 12px;white-space:nowrap}.sf-btn:hover:not(:disabled){border-color:var(--sf-accent)}.sf-btn:disabled{cursor:default;opacity:.45}.sf-btn--primary{background:var(--sf-accent);border-color:var(--sf-accent);color:#fff}.sf-btn--armed,.sf-btn--danger:hover:not(:disabled){border-color:var(--sf-danger);color:var(--sf-danger)}.sf-btn--armed{background:color-mix(in srgb,var(--sf-danger) 12%,transparent)}.sf-icon-btn{background:none;border:1px solid transparent;border-radius:6px;color:inherit;cursor:pointer;font:inherit;padding:3px 6px}.sf-icon-btn:hover{border-color:var(--sf-border)}.sf-upload{display:flex;flex-direction:column;gap:10px}.sf-drop{align-items:center;border:2px dashed var(--sf-border);border-radius:12px;cursor:pointer;display:flex;gap:20px;padding:28px 24px;transition:border-color .15s,background .15s,box-shadow .15s;-webkit-user-select:none;-moz-user-select:none;user-select:none}.sf-drop:focus-visible,.sf-drop:hover{border-color:var(--sf-accent);box-shadow:0 4px 14px color-mix(in srgb,currentColor 10%,transparent);outline:none}.sf-drop p{margin:0}.sf-drop__icon{flex-shrink:0;height:72px;opacity:.6;width:72px}.sf-drop__title{font-size:1.1em;font-weight:600;margin-bottom:4px!important}.sf-drop__count{color:var(--sf-accent);font-weight:600;margin-top:6px!important}.sf-drop--active{background:color-mix(in srgb,var(--sf-accent) 8%,transparent);border-color:var(--sf-accent)}.sf-drop--active .sf-drop__icon{color:var(--sf-accent);opacity:1}.sf-mode{gap:14px}.sf-queue progress{width:120px}.sf-upload>.sf-row{align-items:flex-end}.sf-queue{display:flex;flex-direction:column;gap:4px;list-style:none;margin:0;max-height:200px;overflow:auto;padding:0}.sf-queue li{display:flex;gap:12px;justify-content:space-between}.sf-queue__name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sf-toolbar{display:flex;flex-wrap:wrap;gap:12px;justify-content:space-between}.sf-search{max-width:320px}.sf-table-wrap{border:1px solid var(--sf-border);border-radius:8px;overflow-x:auto}.sf-table{border-collapse:collapse;width:100%}.sf-table td,.sf-table th{border-bottom:1px solid var(--sf-border);padding:8px 10px;text-align:left;vertical-align:middle}.sf-table tbody tr:last-child td{border-bottom:none}.sf-table th{background:var(--sf-subtle);font-weight:600}.sf-col-check{width:36px}.sf-col-actions{text-align:right!important}.sf-dir td{background:var(--sf-subtle)}.sf-dir__toggle{background:none;border:none;color:inherit;cursor:pointer;font:inherit;font-weight:600;padding:0}.sf-busy{opacity:.55}.sf-pill{background:var(--sf-subtle);border:1px solid var(--sf-border);border-radius:999px;color:inherit;cursor:pointer;font:inherit;font-size:12px;padding:2px 10px}.sf-pill--on{background:color-mix(in srgb,#16a34a 18%,transparent);border-color:#16a34a}.sf-overlay{align-items:center;background:rgba(0,0,0,.5);display:flex;inset:0;justify-content:center;padding:16px;position:fixed;z-index:1000}.sf-dialog{--sf-border:color-mix(in srgb,CanvasText 22%,transparent);--sf-subtle:color-mix(in srgb,CanvasText 6%,transparent);background:Canvas;border-radius:10px;color:CanvasText;display:flex;flex-direction:column;gap:12px;max-height:90vh;overflow:auto;padding:18px 20px;text-align:left;width:min(520px,100%)}.sf-dialog__head{align-items:center;display:flex;justify-content:space-between}.sf-dialog__head h3{margin:0}.sf-dialog>.sf-muted{margin:0;word-break:break-all}.sf-dialog__foot{justify-content:flex-end}.sf-fieldset{border:none;display:flex;flex-direction:column;gap:12px;margin:0;padding:0}.sf-fieldset:disabled{opacity:.5}.sf-fieldset p{margin:0}.sf-check{align-items:center;cursor:pointer;display:flex;gap:8px}.sf-check--inline{display:inline-flex}.sf-stack{display:flex;flex-direction:column;gap:12px}.sf-stack p{margin:0}.sf-row--bottom{align-items:flex-end}.sf-field--narrow{flex:0 1 200px;min-width:140px}.sf-admin{border:1px solid var(--sf-border);border-radius:10px;padding:10px 14px}.sf-admin>summary{cursor:pointer;font-weight:600}.sf-admin[open]>summary{margin-bottom:12px}.sf-card{border:1px solid var(--sf-border);border-radius:8px;display:flex;flex-direction:column;gap:10px;padding:12px}.sf-card h4,.sf-card p{margin:0}.sf-code{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;resize:vertical}.sf-roots{display:flex;flex-direction:column;gap:6px;list-style:none;margin:0;padding:0}.sf-roots li{align-items:center;display:flex;gap:12px;justify-content:space-between}.sf-root-select{max-width:420px}.sf-browser{border:1px solid var(--sf-border);border-radius:6px;display:flex;flex-direction:column;max-height:320px;overflow:auto}.sf-browser>*{border-bottom:1px solid var(--sf-border);padding:5px 10px}.sf-browser>:last-child{border-bottom:none}.sf-browser__dir{background:none;border:none;border-bottom:1px solid var(--sf-border);color:inherit;cursor:pointer;font:inherit;text-align:left}.sf-browser__all,.sf-browser__dir:hover{background:var(--sf-subtle)}.sf-browser__all{font-weight:600}.sf-browser__name{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sf-tag{border:1px solid var(--sf-border);border-radius:999px;display:inline-block;font-size:11px;margin-left:8px;padding:0 7px;vertical-align:middle}.sf-tag--warn{border-color:var(--sf-danger);color:var(--sf-danger)}.sf-strike{opacity:.7;text-decoration:line-through}.sf-browser__row{align-items:center;display:flex;gap:8px}.sf-browser__row .sf-browser__dir{border-bottom:none;flex:1;padding:0}.sf-dot{background:var(--sf-danger);border-radius:50%;display:inline-block;height:9px;vertical-align:middle;width:9px}.sf-dot--on{background:#16a34a}.sf-steps{display:flex;flex-direction:column;gap:10px;margin:0;padding-left:20px}.sf-steps li>.sf-row{margin:4px 0}.sf-codeline{background:var(--sf-subtle);border:1px solid var(--sf-border);border-radius:6px;font-size:12px;padding:4px 8px}.sf-codeline,.sf-paircode{font-family:ui-monospace,SFMono-Regular,Menlo,monospace}.sf-paircode{font-size:22px;font-weight:700;letter-spacing:.12em}.sf-small{font-size:12px}.sf-os{display:flex;flex-direction:column;gap:4px;margin-top:6px}";
694
+ var css_248z = ".sf-root{--sf-border:color-mix(in srgb,currentColor 22%,transparent);--sf-subtle:color-mix(in srgb,currentColor 6%,transparent);--sf-accent:#2563eb;--sf-danger:#dc2626;display:flex;flex-direction:column;font-size:14px;gap:16px;margin:0 auto;max-width:1100px;padding:16px;text-align:left}.sf-root *,.sf-root :after,.sf-root :before{box-sizing:border-box}.sf-head{align-items:baseline;display:flex;gap:12px;justify-content:space-between}.sf-head h2{font-size:1.4em;margin:0}.sf-muted{opacity:.7}.sf-error{color:var(--sf-danger)}.sf-row{align-items:center;display:flex;flex-wrap:wrap;gap:8px}.sf-row--end{justify-content:flex-end}.sf-field{display:flex;flex:1;flex-direction:column;gap:4px;min-width:200px}.sf-input{background:var(--sf-subtle);border:1px solid var(--sf-border);border-radius:6px;color:inherit;font:inherit;min-width:0;padding:6px 10px;width:100%}.sf-row .sf-input{flex:1;width:auto}.sf-btn{background:var(--sf-subtle);border:1px solid var(--sf-border);border-radius:6px;color:inherit;cursor:pointer;font:inherit;padding:5px 12px;white-space:nowrap}.sf-btn:hover:not(:disabled){border-color:var(--sf-accent)}.sf-btn:disabled{cursor:default;opacity:.45}.sf-btn--primary{background:var(--sf-accent);border-color:var(--sf-accent);color:#fff}.sf-btn--armed,.sf-btn--danger:hover:not(:disabled){border-color:var(--sf-danger);color:var(--sf-danger)}.sf-btn--armed{background:color-mix(in srgb,var(--sf-danger) 12%,transparent)}.sf-icon-btn{background:none;border:1px solid transparent;border-radius:6px;color:inherit;cursor:pointer;font:inherit;padding:3px 6px}.sf-icon-btn:hover{border-color:var(--sf-border)}.sf-upload{display:flex;flex-direction:column;gap:10px}.sf-drop{align-items:center;border:2px dashed var(--sf-border);border-radius:12px;cursor:pointer;display:flex;gap:20px;padding:28px 24px;transition:border-color .15s,background .15s,box-shadow .15s;-webkit-user-select:none;-moz-user-select:none;user-select:none}.sf-drop:focus-visible,.sf-drop:hover{border-color:var(--sf-accent);box-shadow:0 4px 14px color-mix(in srgb,currentColor 10%,transparent);outline:none}.sf-drop p{margin:0}.sf-drop__icon{flex-shrink:0;height:72px;opacity:.6;width:72px}.sf-drop__title{font-size:1.1em;font-weight:600;margin-bottom:4px!important}.sf-drop__count{color:var(--sf-accent);font-weight:600;margin-top:6px!important}.sf-drop--active{background:color-mix(in srgb,var(--sf-accent) 8%,transparent);border-color:var(--sf-accent)}.sf-drop--active .sf-drop__icon{color:var(--sf-accent);opacity:1}.sf-mode{gap:14px}.sf-queue progress{width:120px}.sf-upload>.sf-row{align-items:flex-end}.sf-queue{display:flex;flex-direction:column;gap:4px;list-style:none;margin:0;max-height:200px;overflow:auto;padding:0}.sf-queue li{display:flex;gap:12px;justify-content:space-between}.sf-queue__name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sf-toolbar{display:flex;flex-wrap:wrap;gap:12px;justify-content:space-between}.sf-search{max-width:320px}.sf-table-wrap{border:1px solid var(--sf-border);border-radius:8px;overflow-x:auto}.sf-table{border-collapse:collapse;width:100%}.sf-table td,.sf-table th{border-bottom:1px solid var(--sf-border);padding:8px 10px;text-align:left;vertical-align:middle}.sf-table tbody tr:last-child td{border-bottom:none}.sf-table th{background:var(--sf-subtle);font-weight:600}.sf-col-check{width:36px}.sf-col-actions{text-align:right!important}.sf-dir td{background:var(--sf-subtle)}.sf-dir__toggle{background:none;border:none;color:inherit;cursor:pointer;font:inherit;font-weight:600;padding:0}.sf-busy{opacity:.55}.sf-pill{background:var(--sf-subtle);border:1px solid var(--sf-border);border-radius:999px;color:inherit;cursor:pointer;font:inherit;font-size:12px;padding:2px 10px}.sf-pill--on{background:color-mix(in srgb,#16a34a 18%,transparent);border-color:#16a34a}.sf-overlay{align-items:center;background:rgba(0,0,0,.5);display:flex;inset:0;justify-content:center;padding:16px;position:fixed;z-index:1000}.sf-dialog{--sf-border:color-mix(in srgb,CanvasText 22%,transparent);--sf-subtle:color-mix(in srgb,CanvasText 6%,transparent);background:Canvas;border-radius:10px;color:CanvasText;display:flex;flex-direction:column;gap:12px;max-height:90vh;overflow:auto;padding:18px 20px;text-align:left;width:min(520px,100%)}.sf-dialog__head{align-items:center;display:flex;justify-content:space-between}.sf-dialog__head h3{margin:0}.sf-dialog>.sf-muted{margin:0;word-break:break-all}.sf-dialog__foot{justify-content:flex-end}.sf-fieldset{border:none;display:flex;flex-direction:column;gap:12px;margin:0;padding:0}.sf-fieldset:disabled{opacity:.5}.sf-fieldset p{margin:0}.sf-check{align-items:center;cursor:pointer;display:flex;gap:8px}.sf-check--inline{display:inline-flex}.sf-stack{display:flex;flex-direction:column;gap:12px}.sf-stack p{margin:0}.sf-row--bottom{align-items:flex-end}.sf-field--narrow{flex:0 1 200px;min-width:140px}.sf-admin{border:1px solid var(--sf-border);border-radius:10px;padding:10px 14px}.sf-admin>summary{cursor:pointer;font-weight:600}.sf-admin[open]>summary{margin-bottom:12px}.sf-card{border:1px solid var(--sf-border);border-radius:8px;display:flex;flex-direction:column;gap:10px;padding:12px}.sf-card h4,.sf-card p{margin:0}.sf-code{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;resize:vertical}.sf-roots{display:flex;flex-direction:column;gap:6px;list-style:none;margin:0;padding:0}.sf-roots li{align-items:center;display:flex;gap:12px;justify-content:space-between}.sf-root-select{max-width:420px}.sf-browser{border:1px solid var(--sf-border);border-radius:6px;display:flex;flex-direction:column;max-height:320px;overflow:auto}.sf-browser>*{border-bottom:1px solid var(--sf-border);padding:5px 10px}.sf-browser>:last-child{border-bottom:none}.sf-browser__dir{background:none;border:none;border-bottom:1px solid var(--sf-border);color:inherit;cursor:pointer;font:inherit;text-align:left}.sf-browser__all,.sf-browser__dir:hover{background:var(--sf-subtle)}.sf-browser__all{font-weight:600}.sf-browser__name{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sf-tag{border:1px solid var(--sf-border);border-radius:999px;display:inline-block;font-size:11px;margin-left:8px;padding:0 7px;vertical-align:middle}.sf-tag--warn{border-color:var(--sf-danger);color:var(--sf-danger)}.sf-strike{opacity:.7;text-decoration:line-through}.sf-browser__row{align-items:center;display:flex;gap:8px}.sf-browser__row .sf-browser__dir{border-bottom:none;flex:1;padding:0}.sf-dot{background:var(--sf-danger);border-radius:50%;display:inline-block;height:9px;vertical-align:middle;width:9px}.sf-dot--on{background:#16a34a}.sf-steps{display:flex;flex-direction:column;gap:10px;margin:0;padding-left:20px}.sf-steps li>.sf-row{margin:4px 0}.sf-codeline{background:var(--sf-subtle);border:1px solid var(--sf-border);border-radius:6px;font-size:12px;padding:4px 8px}.sf-codeline,.sf-paircode{font-family:ui-monospace,SFMono-Regular,Menlo,monospace}.sf-paircode{font-size:22px;font-weight:700;letter-spacing:.12em}.sf-small{font-size:12px}.sf-os{display:flex;flex-direction:column;gap:4px;margin-top:6px}.sf-subhead{font-size:1.1em;margin:8px 0 0}";
546
695
  styleInject(css_248z);
547
696
 
548
697
  function buildTree(files) {
@@ -683,7 +832,7 @@ function SecureFiles({ apiBase }) {
683
832
  rows.push(...ownFiles.map((f) => renderFile(f, depth)));
684
833
  return rows;
685
834
  };
686
- return (jsxs("div", { className: "sf-root", children: [jsxs("header", { className: "sf-head", children: [jsx("h2", { children: "Secure Files" }), user && jsxs("span", { className: "sf-muted", children: ["Signed in as ", user] })] }), jsx(UploadPanel, { api: api, folders: folders.filter((d) => d !== "devices" && !d.startsWith("devices/")), onUploaded: load }), jsx(MyComputers, { api: api, devices: devices, onChange: load }), isAdmin && jsx(ServerFiles, { api: api, folders: folders, onLinked: load }), jsxs("section", { className: "sf-toolbar", children: [jsx("input", { className: "sf-input sf-search", type: "search", placeholder: "Search files\u2026", value: search, onChange: (e) => setSearch(e.target.value) }), jsxs("div", { className: "sf-row", children: [jsxs("span", { className: "sf-muted", children: [selected.size, " selected"] }), folders.length > 0 && (jsxs(Fragment, { children: [jsx("button", { type: "button", className: "sf-btn", onClick: () => setCollapsed(new Set()), children: "Expand all" }), jsx("button", { type: "button", className: "sf-btn", onClick: () => setCollapsed(new Set(folders)), children: "Collapse all" })] })), jsx("button", { type: "button", className: "sf-btn", disabled: !selected.size, onClick: () => download(selectedFiles), children: "Download" }), jsx("button", { type: "button", className: "sf-btn", disabled: !selected.size, onClick: () => setShared(selectedFiles, true), children: "Share" }), jsx("button", { type: "button", className: "sf-btn", disabled: !selected.size, onClick: () => setShared(selectedFiles, false), children: "Unshare" }), jsx(ConfirmButton, { label: "Delete", disabled: !selected.size, onConfirm: () => remove(selectedFiles) })] })] }), notice && (jsx("p", { className: "sf-error", role: "alert", children: notice })), loading ? (jsx("p", { className: "sf-muted", children: "Loading\u2026" })) : error ? (jsx("p", { className: "sf-error", role: "alert", children: error })) : files.length === 0 ? (jsx("p", { className: "sf-muted", children: "No files yet \u2014 upload something above." })) : (jsx("div", { className: "sf-table-wrap", children: jsxs("table", { className: "sf-table", children: [jsx("thead", { children: jsxs("tr", { children: [jsx("th", { className: "sf-col-check", children: jsx("input", { type: "checkbox", "aria-label": "Select all", checked: allVisibleSelected, onChange: (e) => toggle(filtered.map((f) => f.id), e.target.checked) }) }), jsx("th", { children: "Name" }), jsx("th", { children: "Sharing" }), jsx("th", { children: "Downloads" }), jsx("th", { className: "sf-col-actions", children: "Actions" })] }) }), jsx("tbody", { children: renderFolder(tree, 0) })] }) })), editing && (jsx(ShareDialog, { file: files.find((f) => f.id === editing.id) ?? editing, onClose: () => setEditing(null), onSave: saveShare }, editing.id))] }));
835
+ return (jsxs("div", { className: "sf-root", children: [jsxs("header", { className: "sf-head", children: [jsx("h2", { children: "Secure Files" }), user && jsxs("span", { className: "sf-muted", children: ["Signed in as ", user] })] }), jsx(UploadPanel, { api: api, folders: folders.filter((d) => d !== "devices" && !d.startsWith("devices/")), onUploaded: load }), jsx(MyComputers, { api: api, devices: devices, onChange: load }), jsx(People, { api: api, files: files }), isAdmin && jsx(ServerFiles, { api: api, folders: folders, onLinked: load }), jsxs("section", { className: "sf-toolbar", children: [jsx("input", { className: "sf-input sf-search", type: "search", placeholder: "Search files\u2026", value: search, onChange: (e) => setSearch(e.target.value) }), jsxs("div", { className: "sf-row", children: [jsxs("span", { className: "sf-muted", children: [selected.size, " selected"] }), folders.length > 0 && (jsxs(Fragment, { children: [jsx("button", { type: "button", className: "sf-btn", onClick: () => setCollapsed(new Set()), children: "Expand all" }), jsx("button", { type: "button", className: "sf-btn", onClick: () => setCollapsed(new Set(folders)), children: "Collapse all" })] })), jsx("button", { type: "button", className: "sf-btn", disabled: !selected.size, onClick: () => download(selectedFiles), children: "Download" }), jsx("button", { type: "button", className: "sf-btn", disabled: !selected.size, onClick: () => setShared(selectedFiles, true), children: "Share" }), jsx("button", { type: "button", className: "sf-btn", disabled: !selected.size, onClick: () => setShared(selectedFiles, false), children: "Unshare" }), jsx(ConfirmButton, { label: "Delete", disabled: !selected.size, onConfirm: () => remove(selectedFiles) })] })] }), notice && (jsx("p", { className: "sf-error", role: "alert", children: notice })), loading ? (jsx("p", { className: "sf-muted", children: "Loading\u2026" })) : error ? (jsx("p", { className: "sf-error", role: "alert", children: error })) : files.length === 0 ? (jsx("p", { className: "sf-muted", children: "No files yet \u2014 upload something above." })) : (jsx("div", { className: "sf-table-wrap", children: jsxs("table", { className: "sf-table", children: [jsx("thead", { children: jsxs("tr", { children: [jsx("th", { className: "sf-col-check", children: jsx("input", { type: "checkbox", "aria-label": "Select all", checked: allVisibleSelected, onChange: (e) => toggle(filtered.map((f) => f.id), e.target.checked) }) }), jsx("th", { children: "Name" }), jsx("th", { children: "Sharing" }), jsx("th", { children: "Downloads" }), jsx("th", { className: "sf-col-actions", children: "Actions" })] }) }), jsx("tbody", { children: renderFolder(tree, 0) })] }) })), jsx(SharedWithMe, { api: api }), editing && (jsx(ShareDialog, { file: files.find((f) => f.id === editing.id) ?? editing, onClose: () => setEditing(null), onSave: saveShare }, editing.id))] }));
687
836
  }
688
837
 
689
838
  export { ApiError, DEFAULT_API_BASE, SecureFiles, createApi };