@thetechfossil/upfiles 0.3.0-beta → 0.3.1-beta

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.mjs CHANGED
@@ -167,19 +167,19 @@ var Uploader = ({
167
167
  const [isUploading, setIsUploading] = useState(false);
168
168
  const [isDragOver, setIsDragOver] = useState(false);
169
169
  const addFiles = useCallback((incoming) => {
170
- const filtered = incoming.filter((f) => {
171
- if (f.size > maxFileSize) {
170
+ const filtered = incoming.filter((f2) => {
171
+ if (f2.size > maxFileSize) {
172
172
  return false;
173
173
  }
174
174
  return true;
175
175
  });
176
176
  const limited = filtered.slice(0, Math.max(0, maxFiles - files.length));
177
- const mapped = limited.map((f) => ({
177
+ const mapped = limited.map((f2) => ({
178
178
  id: Math.random().toString(36).slice(2),
179
- file: f,
180
- name: f.name,
181
- size: f.size,
182
- type: f.type,
179
+ file: f2,
180
+ name: f2.name,
181
+ size: f2.size,
182
+ type: f2.type,
183
183
  progress: 0,
184
184
  status: "pending"
185
185
  }));
@@ -198,7 +198,7 @@ var Uploader = ({
198
198
  }, [addFiles]);
199
199
  const uploadOne = useCallback(async (item) => {
200
200
  const update = (patch) => {
201
- setFiles((prev) => prev.map((f) => f.id === item.id ? { ...f, ...patch } : f));
201
+ setFiles((prev) => prev.map((f2) => f2.id === item.id ? { ...f2, ...patch } : f2));
202
202
  };
203
203
  try {
204
204
  update({ status: "uploading", progress: 1 });
@@ -255,19 +255,19 @@ var Uploader = ({
255
255
  }
256
256
  }, [client, projectId, folderPath, fetchThumbnails]);
257
257
  const uploadAll = useCallback(async () => {
258
- const targets = files.filter((f) => f.status === "pending");
258
+ const targets = files.filter((f2) => f2.status === "pending");
259
259
  if (!targets.length) return;
260
260
  setIsUploading(true);
261
261
  try {
262
262
  const results = [];
263
- for (const f of targets) {
263
+ for (const f2 of targets) {
264
264
  try {
265
- const done = await uploadOne(f);
265
+ const done = await uploadOne(f2);
266
266
  results.push(done);
267
267
  } catch (e) {
268
268
  }
269
269
  }
270
- onComplete?.(results.filter((f) => f.status === "success"));
270
+ onComplete?.(results.filter((f2) => f2.status === "success"));
271
271
  } catch (e) {
272
272
  onError?.(e);
273
273
  } finally {
@@ -321,7 +321,7 @@ var Uploader = ({
321
321
  "button",
322
322
  {
323
323
  className: buttonClassName,
324
- disabled: isUploading || files.every((f) => f.status !== "pending"),
324
+ disabled: isUploading || files.every((f2) => f2.status !== "pending"),
325
325
  onClick: uploadAll,
326
326
  children: isUploading ? "Uploading..." : "Upload"
327
327
  }
@@ -336,21 +336,21 @@ var Uploader = ({
336
336
  }
337
337
  )
338
338
  ] }),
339
- /* @__PURE__ */ jsx("ul", { style: { display: "grid", gap: 8, listStyle: "none", padding: 0 }, children: files.map((f) => /* @__PURE__ */ jsx("li", { style: { border: "1px solid #e5e7eb", borderRadius: 8, padding: 8 }, children: /* @__PURE__ */ jsxs("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center" }, children: [
339
+ /* @__PURE__ */ jsx("ul", { style: { display: "grid", gap: 8, listStyle: "none", padding: 0 }, children: files.map((f2) => /* @__PURE__ */ jsx("li", { style: { border: "1px solid #e5e7eb", borderRadius: 8, padding: 8 }, children: /* @__PURE__ */ jsxs("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center" }, children: [
340
340
  /* @__PURE__ */ jsxs("div", { style: { minWidth: 0 }, children: [
341
- /* @__PURE__ */ jsx("div", { style: { fontWeight: 500, overflow: "hidden", textOverflow: "ellipsis" }, children: f.name }),
341
+ /* @__PURE__ */ jsx("div", { style: { fontWeight: 500, overflow: "hidden", textOverflow: "ellipsis" }, children: f2.name }),
342
342
  /* @__PURE__ */ jsxs("div", { style: { fontSize: 12, color: "#666" }, children: [
343
- (f.size / (1024 * 1024)).toFixed(2),
343
+ (f2.size / (1024 * 1024)).toFixed(2),
344
344
  " MB"
345
345
  ] }),
346
- f.error && /* @__PURE__ */ jsx("div", { style: { fontSize: 12, color: "#b91c1c" }, children: f.error }),
347
- f.publicUrl && f.status === "success" && /* @__PURE__ */ jsx("a", { href: f.publicUrl, target: "_blank", rel: "noreferrer", style: { fontSize: 12, color: "#2563eb" }, children: "View" })
346
+ f2.error && /* @__PURE__ */ jsx("div", { style: { fontSize: 12, color: "#b91c1c" }, children: f2.error }),
347
+ f2.publicUrl && f2.status === "success" && /* @__PURE__ */ jsx("a", { href: f2.publicUrl, target: "_blank", rel: "noreferrer", style: { fontSize: 12, color: "#2563eb" }, children: "View" })
348
348
  ] }),
349
349
  /* @__PURE__ */ jsxs("div", { style: { minWidth: 120, textAlign: "right" }, children: [
350
- /* @__PURE__ */ jsx("div", { style: { height: 6, background: "#e5e7eb", borderRadius: 999, overflow: "hidden" }, children: /* @__PURE__ */ jsx("div", { style: { width: `${f.progress}%`, height: "100%", background: f.status === "error" ? "#ef4444" : "#2563eb" } }) }),
351
- /* @__PURE__ */ jsx("div", { style: { fontSize: 12, color: "#666", marginTop: 4 }, children: f.status })
350
+ /* @__PURE__ */ jsx("div", { style: { height: 6, background: "#e5e7eb", borderRadius: 999, overflow: "hidden" }, children: /* @__PURE__ */ jsx("div", { style: { width: `${f2.progress}%`, height: "100%", background: f2.status === "error" ? "#ef4444" : "#2563eb" } }) }),
351
+ /* @__PURE__ */ jsx("div", { style: { fontSize: 12, color: "#666", marginTop: 4 }, children: f2.status })
352
352
  ] })
353
- ] }) }, f.id)) })
353
+ ] }) }, f2.id)) })
354
354
  ] })
355
355
  ] });
356
356
  };
@@ -395,7 +395,7 @@ var ProjectFilesWidget = ({
395
395
  };
396
396
  }, [client, folderPath]);
397
397
  const visible = files.filter(
398
- (f) => !query ? true : (f.originalName || "").toLowerCase().includes(query.toLowerCase())
398
+ (f2) => !query ? true : (f2.originalName || "").toLowerCase().includes(query.toLowerCase())
399
399
  );
400
400
  return /* @__PURE__ */ jsxs2("div", { className, children: [
401
401
  /* @__PURE__ */ jsxs2("div", { style: { display: "flex", gap: 8, alignItems: "center", marginBottom: 8 }, children: [
@@ -435,31 +435,31 @@ var ProjectFilesWidget = ({
435
435
  loading && /* @__PURE__ */ jsx2("div", { style: { fontSize: 12, color: "#666" }, children: "Loading files\u2026" }),
436
436
  error && /* @__PURE__ */ jsx2("div", { style: { fontSize: 12, color: "#b91c1c" }, children: error }),
437
437
  !loading && !error && /* @__PURE__ */ jsxs2("ul", { className: listClassName, style: { listStyle: "none", padding: 0, margin: 0, display: "grid", gap: 8 }, children: [
438
- visible.map((f) => /* @__PURE__ */ jsxs2(
438
+ visible.map((f2) => /* @__PURE__ */ jsxs2(
439
439
  "li",
440
440
  {
441
441
  className: itemClassName,
442
442
  style: { border: "1px solid #e5e7eb", borderRadius: 8, padding: 10, display: "flex", justifyContent: "space-between", alignItems: "center" },
443
443
  children: [
444
444
  /* @__PURE__ */ jsxs2("div", { style: { minWidth: 0 }, children: [
445
- /* @__PURE__ */ jsx2("div", { style: { fontWeight: 500, overflow: "hidden", textOverflow: "ellipsis" }, children: f.originalName }),
445
+ /* @__PURE__ */ jsx2("div", { style: { fontWeight: 500, overflow: "hidden", textOverflow: "ellipsis" }, children: f2.originalName }),
446
446
  /* @__PURE__ */ jsxs2("div", { style: { fontSize: 12, color: "#666" }, children: [
447
- (f.size / (1024 * 1024)).toFixed(2),
447
+ (f2.size / (1024 * 1024)).toFixed(2),
448
448
  " MB \u2022 ",
449
- f.contentType
449
+ f2.contentType
450
450
  ] })
451
451
  ] }),
452
452
  /* @__PURE__ */ jsxs2("div", { style: { display: "flex", gap: 8, alignItems: "center" }, children: [
453
- /* @__PURE__ */ jsx2("a", { href: f.url, target: "_blank", rel: "noreferrer", style: { fontSize: 12, color: "#2563eb" }, children: "View" }),
453
+ /* @__PURE__ */ jsx2("a", { href: f2.url, target: "_blank", rel: "noreferrer", style: { fontSize: 12, color: "#2563eb" }, children: "View" }),
454
454
  /* @__PURE__ */ jsx2(
455
455
  "button",
456
456
  {
457
457
  onClick: async () => {
458
- const selected = { name: f.originalName, key: f.key, url: f.url, size: f.size, contentType: f.contentType };
458
+ const selected = { name: f2.originalName, key: f2.key, url: f2.url, size: f2.size, contentType: f2.contentType };
459
459
  onSelect(selected);
460
460
  if (typeof onSave === "function" || !!saveUrl) {
461
461
  try {
462
- setSavingKey(f.key);
462
+ setSavingKey(f2.key);
463
463
  if (typeof onSave === "function") {
464
464
  await onSave(selected);
465
465
  onSaved?.();
@@ -483,22 +483,460 @@ var ProjectFilesWidget = ({
483
483
  }
484
484
  }
485
485
  },
486
- disabled: savingKey === f.key,
487
- style: { padding: "6px 10px", background: "#2563eb", color: "#fff", border: "1px solid #1d4ed8", borderRadius: 6, opacity: savingKey === f.key ? 0.7 : 1 },
488
- children: savingKey === f.key ? "Saving\u2026" : "Use"
486
+ disabled: savingKey === f2.key,
487
+ style: { padding: "6px 10px", background: "#2563eb", color: "#fff", border: "1px solid #1d4ed8", borderRadius: 6, opacity: savingKey === f2.key ? 0.7 : 1 },
488
+ children: savingKey === f2.key ? "Saving\u2026" : "Use"
489
489
  }
490
490
  )
491
491
  ] })
492
492
  ]
493
493
  },
494
- f.key
494
+ f2.key
495
495
  )),
496
496
  visible.length === 0 && /* @__PURE__ */ jsx2("li", { style: { fontSize: 12, color: "#666" }, children: "No files found" })
497
497
  ] })
498
498
  ] });
499
499
  };
500
+
501
+ // src/ConnectProjectDialog.tsx
502
+ import * as React3 from "react";
503
+ import * as Dialog from "@radix-ui/react-dialog";
504
+
505
+ // src/projectConnect.ts
506
+ var f = (globalThis.fetch || fetch).bind(globalThis);
507
+ async function httpJson(input, init, useFetch) {
508
+ const doFetch = useFetch ?? f;
509
+ const res = await doFetch(input, init);
510
+ if (!res.ok) {
511
+ let msg = `Request failed (${res.status})`;
512
+ try {
513
+ const j = await res.json();
514
+ msg = j?.error || msg;
515
+ } catch {
516
+ }
517
+ throw new Error(msg);
518
+ }
519
+ return res.json();
520
+ }
521
+ async function listProjects(opts) {
522
+ const url = `${opts.baseUrl.replace(/\/$/, "")}/api/projects`;
523
+ const data = await httpJson(
524
+ url,
525
+ { method: "GET", credentials: "include" },
526
+ opts.fetch
527
+ );
528
+ return data?.projects ?? [];
529
+ }
530
+ async function createProject(opts) {
531
+ const base = opts.baseUrl.replace(/\/$/, "");
532
+ const created = await httpJson(
533
+ `${base}/api/projects`,
534
+ {
535
+ method: "POST",
536
+ credentials: "include",
537
+ headers: { "content-type": "application/json" },
538
+ body: JSON.stringify({ name: opts.name })
539
+ },
540
+ opts.fetch
541
+ );
542
+ const projectId = created?.project?.id;
543
+ if (!projectId) throw new Error("Failed to create project");
544
+ const keyLabel = opts.keyName || `Plugin key ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}`;
545
+ const key = await httpJson(
546
+ `${base}/api/projects/${projectId}/keys`,
547
+ {
548
+ method: "POST",
549
+ credentials: "include",
550
+ headers: { "content-type": "application/json" },
551
+ body: JSON.stringify({ name: keyLabel })
552
+ },
553
+ opts.fetch
554
+ );
555
+ const plaintext = key?.key?.plaintext;
556
+ if (!plaintext) throw new Error("Failed to create API key");
557
+ return { apiKey: plaintext, projectId };
558
+ }
559
+ async function connectProject(opts) {
560
+ const base = opts.baseUrl.replace(/\/$/, "");
561
+ const keyLabel = opts.keyName || `Plugin key ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}`;
562
+ const key = await httpJson(
563
+ `${base}/api/projects/${opts.projectId}/keys`,
564
+ {
565
+ method: "POST",
566
+ credentials: "include",
567
+ headers: { "content-type": "application/json" },
568
+ body: JSON.stringify({ name: keyLabel })
569
+ },
570
+ opts.fetch
571
+ );
572
+ const plaintext = key?.key?.plaintext;
573
+ if (!plaintext) throw new Error("Failed to create API key");
574
+ return { apiKey: plaintext };
575
+ }
576
+ function addApiKeyManually(apiKey) {
577
+ const k = (apiKey || "").trim();
578
+ if (!k) throw new Error("API key is required");
579
+ if (!/^upk_[A-Za-z0-9]+/.test(k)) {
580
+ console.warn("Provided API key does not match expected format upk_*");
581
+ }
582
+ return { apiKey: k };
583
+ }
584
+ function createClientWithKey(baseUrl, apiKey) {
585
+ return new UpfilesClient({ baseUrl, apiKey, apiKeyHeader: "authorization" });
586
+ }
587
+ function connectUsingApiKey(opts) {
588
+ const baseUrl = opts.baseUrl.replace(/\/$/, "");
589
+ const apiKey = (opts.apiKey || "").trim();
590
+ if (!apiKey) throw new Error("apiKey is required");
591
+ const client = new UpfilesClient({ baseUrl, apiKey, apiKeyHeader: opts.apiKeyHeader ?? "authorization" });
592
+ return { apiKey, client };
593
+ }
594
+
595
+ // src/ConnectProjectDialog.tsx
596
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
597
+ function ConnectProjectDialog(props) {
598
+ const { baseUrl, open, onOpenChange, onConnected, fetchImpl, title, description, className } = props;
599
+ const [mode, setMode] = React3.useState("existing");
600
+ const [loading, setLoading] = React3.useState(false);
601
+ const [error, setError] = React3.useState(null);
602
+ const [projects, setProjects] = React3.useState([]);
603
+ const [selectedProjectId, setSelectedProjectId] = React3.useState("");
604
+ const [manualKey, setManualKey] = React3.useState("");
605
+ const [newProjectName, setNewProjectName] = React3.useState("");
606
+ const resetState = React3.useCallback(() => {
607
+ setMode("existing");
608
+ setLoading(false);
609
+ setError(null);
610
+ setProjects([]);
611
+ setSelectedProjectId("");
612
+ setManualKey("");
613
+ setNewProjectName("");
614
+ }, []);
615
+ React3.useEffect(() => {
616
+ const run = async () => {
617
+ if (!open || mode !== "existing") return;
618
+ try {
619
+ setLoading(true);
620
+ setError(null);
621
+ const opts = { baseUrl, fetch: fetchImpl };
622
+ const list = await listProjects(opts);
623
+ setProjects(list);
624
+ if (list.length && !selectedProjectId) setSelectedProjectId(list[0].id);
625
+ } catch (e) {
626
+ setError(e?.message || "Failed to fetch projects");
627
+ } finally {
628
+ setLoading(false);
629
+ }
630
+ };
631
+ run();
632
+ }, [open, mode, baseUrl]);
633
+ const handleConnectExisting = async () => {
634
+ try {
635
+ setLoading(true);
636
+ setError(null);
637
+ if (!selectedProjectId) throw new Error("Please select a project");
638
+ const { apiKey } = await connectProject({ baseUrl, projectId: selectedProjectId, fetch: fetchImpl });
639
+ onConnected(apiKey, { projectId: selectedProjectId, source: "existing" });
640
+ onOpenChange(false);
641
+ resetState();
642
+ } catch (e) {
643
+ setError(e?.message || "Failed to connect project");
644
+ } finally {
645
+ setLoading(false);
646
+ }
647
+ };
648
+ const handleManual = async () => {
649
+ try {
650
+ setLoading(true);
651
+ setError(null);
652
+ const { apiKey } = addApiKeyManually(manualKey);
653
+ onConnected(apiKey, { source: "manual" });
654
+ onOpenChange(false);
655
+ resetState();
656
+ } catch (e) {
657
+ setError(e?.message || "Invalid API key");
658
+ } finally {
659
+ setLoading(false);
660
+ }
661
+ };
662
+ const handleCreate = async () => {
663
+ try {
664
+ setLoading(true);
665
+ setError(null);
666
+ if (!newProjectName.trim()) throw new Error("Project name is required");
667
+ const { apiKey, projectId } = await createProject({ baseUrl, name: newProjectName.trim(), fetch: fetchImpl });
668
+ onConnected(apiKey, { projectId, source: "new" });
669
+ onOpenChange(false);
670
+ resetState();
671
+ } catch (e) {
672
+ setError(e?.message || "Failed to create project");
673
+ } finally {
674
+ setLoading(false);
675
+ }
676
+ };
677
+ const ModeSwitcher = /* @__PURE__ */ jsxs3("div", { className: "grid grid-cols-3 gap-2", children: [
678
+ /* @__PURE__ */ jsx3(
679
+ "button",
680
+ {
681
+ type: "button",
682
+ className: `px-3 py-2 rounded border text-sm ${mode === "existing" ? "bg-primary text-primary-foreground" : "bg-background"} `,
683
+ onClick: () => setMode("existing"),
684
+ children: "Connect existing"
685
+ }
686
+ ),
687
+ /* @__PURE__ */ jsx3(
688
+ "button",
689
+ {
690
+ type: "button",
691
+ className: `px-3 py-2 rounded border text-sm ${mode === "manual" ? "bg-primary text-primary-foreground" : "bg-background"} `,
692
+ onClick: () => setMode("manual"),
693
+ children: "Add API key"
694
+ }
695
+ ),
696
+ /* @__PURE__ */ jsx3(
697
+ "button",
698
+ {
699
+ type: "button",
700
+ className: `px-3 py-2 rounded border text-sm ${mode === "new" ? "bg-primary text-primary-foreground" : "bg-background"} `,
701
+ onClick: () => setMode("new"),
702
+ children: "Create new"
703
+ }
704
+ )
705
+ ] });
706
+ return /* @__PURE__ */ jsx3(Dialog.Root, { open, onOpenChange: (o) => {
707
+ if (!o) resetState();
708
+ onOpenChange(o);
709
+ }, children: /* @__PURE__ */ jsxs3(Dialog.Portal, { children: [
710
+ /* @__PURE__ */ jsx3(Dialog.Overlay, { className: "fixed inset-0 bg-black/40" }),
711
+ /* @__PURE__ */ jsx3(Dialog.Content, { className: `fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-[95vw] max-w-md rounded-lg border bg-white p-4 shadow-lg ${className ?? ""}`, children: /* @__PURE__ */ jsxs3("div", { className: "space-y-3", children: [
712
+ /* @__PURE__ */ jsx3(Dialog.Title, { className: "text-lg font-semibold", children: title ?? "Connect to Upfiles" }),
713
+ /* @__PURE__ */ jsx3(Dialog.Description, { className: "text-sm text-muted-foreground", children: description ?? "Choose how to connect your project to retrieve an API key." }),
714
+ ModeSwitcher,
715
+ error && /* @__PURE__ */ jsx3("div", { className: "rounded border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-700", children: error }),
716
+ mode === "existing" && /* @__PURE__ */ jsxs3("div", { className: "space-y-2", children: [
717
+ /* @__PURE__ */ jsx3("label", { className: "text-sm font-medium", children: "Project" }),
718
+ /* @__PURE__ */ jsxs3(
719
+ "select",
720
+ {
721
+ className: "w-full rounded border px-3 py-2 text-sm",
722
+ value: selectedProjectId,
723
+ onChange: (e) => setSelectedProjectId(e.target.value),
724
+ disabled: loading,
725
+ children: [
726
+ projects.length === 0 && /* @__PURE__ */ jsx3("option", { value: "", children: loading ? "Loading..." : "No projects found" }),
727
+ projects.map((p) => /* @__PURE__ */ jsx3("option", { value: p.id, children: p.name }, p.id))
728
+ ]
729
+ }
730
+ ),
731
+ /* @__PURE__ */ jsxs3("div", { className: "flex justify-end gap-2 pt-2", children: [
732
+ /* @__PURE__ */ jsx3(Dialog.Close, { asChild: true, children: /* @__PURE__ */ jsx3("button", { type: "button", className: "px-3 py-2 text-sm rounded border", children: "Cancel" }) }),
733
+ /* @__PURE__ */ jsx3(
734
+ "button",
735
+ {
736
+ type: "button",
737
+ className: "px-3 py-2 text-sm rounded bg-blue-600 text-white disabled:opacity-60",
738
+ onClick: handleConnectExisting,
739
+ disabled: loading || !selectedProjectId,
740
+ children: loading ? "Connecting\u2026" : "Connect"
741
+ }
742
+ )
743
+ ] })
744
+ ] }),
745
+ mode === "manual" && /* @__PURE__ */ jsxs3("div", { className: "space-y-2", children: [
746
+ /* @__PURE__ */ jsx3("label", { className: "text-sm font-medium", children: "API key" }),
747
+ /* @__PURE__ */ jsx3(
748
+ "input",
749
+ {
750
+ className: "w-full rounded border px-3 py-2 text-sm",
751
+ placeholder: "upk_...",
752
+ value: manualKey,
753
+ onChange: (e) => setManualKey(e.target.value),
754
+ disabled: loading
755
+ }
756
+ ),
757
+ /* @__PURE__ */ jsxs3("div", { className: "flex justify-end gap-2 pt-2", children: [
758
+ /* @__PURE__ */ jsx3(Dialog.Close, { asChild: true, children: /* @__PURE__ */ jsx3("button", { type: "button", className: "px-3 py-2 text-sm rounded border", children: "Cancel" }) }),
759
+ /* @__PURE__ */ jsx3(
760
+ "button",
761
+ {
762
+ type: "button",
763
+ className: "px-3 py-2 text-sm rounded bg-blue-600 text-white disabled:opacity-60",
764
+ onClick: handleManual,
765
+ disabled: loading || !manualKey.trim(),
766
+ children: loading ? "Adding\u2026" : "Add key"
767
+ }
768
+ )
769
+ ] })
770
+ ] }),
771
+ mode === "new" && /* @__PURE__ */ jsxs3("div", { className: "space-y-2", children: [
772
+ /* @__PURE__ */ jsx3("label", { className: "text-sm font-medium", children: "Project name" }),
773
+ /* @__PURE__ */ jsx3(
774
+ "input",
775
+ {
776
+ className: "w-full rounded border px-3 py-2 text-sm",
777
+ placeholder: "My project",
778
+ value: newProjectName,
779
+ onChange: (e) => setNewProjectName(e.target.value),
780
+ disabled: loading
781
+ }
782
+ ),
783
+ /* @__PURE__ */ jsx3("p", { className: "text-xs text-muted-foreground", children: "A project will be created in your Upfiles account and an API key generated." }),
784
+ /* @__PURE__ */ jsxs3("div", { className: "flex justify-end gap-2 pt-2", children: [
785
+ /* @__PURE__ */ jsx3(Dialog.Close, { asChild: true, children: /* @__PURE__ */ jsx3("button", { type: "button", className: "px-3 py-2 text-sm rounded border", children: "Cancel" }) }),
786
+ /* @__PURE__ */ jsx3(
787
+ "button",
788
+ {
789
+ type: "button",
790
+ className: "px-3 py-2 text-sm rounded bg-blue-600 text-white disabled:opacity-60",
791
+ onClick: handleCreate,
792
+ disabled: loading || !newProjectName.trim(),
793
+ children: loading ? "Creating\u2026" : "Create & connect"
794
+ }
795
+ )
796
+ ] })
797
+ ] })
798
+ ] }) })
799
+ ] }) });
800
+ }
801
+
802
+ // src/files.ts
803
+ async function fetchProjectFiles(clientOptions, folderPath) {
804
+ const client = new UpfilesClient(clientOptions);
805
+ return client.getProjectFiles({ folderPath });
806
+ }
807
+ async function fetchFileThumbnails(clientOptions, fileKey) {
808
+ const client = new UpfilesClient(clientOptions);
809
+ return client.getThumbnails(fileKey);
810
+ }
811
+ async function fetchProjectImagesWithThumbs(clientOptions, opts) {
812
+ const files = await fetchProjectFiles(clientOptions, opts?.folderPath);
813
+ const limit = Math.max(1, Math.min(opts?.limitConcurrent ?? 6, 12));
814
+ const queue = [...files];
815
+ const results = [];
816
+ async function worker() {
817
+ while (queue.length) {
818
+ const f2 = queue.shift();
819
+ try {
820
+ const thumbs = await fetchFileThumbnails(clientOptions, f2.key).catch(() => void 0);
821
+ results.push({ ...f2, thumbnails: thumbs });
822
+ } catch {
823
+ results.push({ ...f2 });
824
+ }
825
+ }
826
+ }
827
+ await Promise.all(Array.from({ length: Math.min(limit, files.length || 1) }, () => worker()));
828
+ return results;
829
+ }
830
+
831
+ // src/ImagePickerDialog.tsx
832
+ import * as React4 from "react";
833
+ import * as Dialog2 from "@radix-ui/react-dialog";
834
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
835
+ function ImagePickerDialog(props) {
836
+ const { open, onOpenChange, clientOptions, folderPath, title, description, className, gridClassName, onSelect } = props;
837
+ const [loading, setLoading] = React4.useState(false);
838
+ const [error, setError] = React4.useState(null);
839
+ const [items, setItems] = React4.useState([]);
840
+ const [query, setQuery] = React4.useState("");
841
+ const load = React4.useCallback(async () => {
842
+ try {
843
+ setLoading(true);
844
+ setError(null);
845
+ const list = await fetchProjectImagesWithThumbs(clientOptions, { folderPath, limitConcurrent: 6 });
846
+ setItems(list);
847
+ } catch (e) {
848
+ setError(e?.message || "Failed to load images");
849
+ } finally {
850
+ setLoading(false);
851
+ }
852
+ }, [clientOptions, folderPath]);
853
+ React4.useEffect(() => {
854
+ if (!open) return;
855
+ load();
856
+ }, [open, load]);
857
+ const visible = React4.useMemo(() => {
858
+ const q = query.trim().toLowerCase();
859
+ return !q ? items : items.filter((i) => (i.originalName || "").toLowerCase().includes(q));
860
+ }, [items, query]);
861
+ return /* @__PURE__ */ jsx4(Dialog2.Root, { open, onOpenChange, children: /* @__PURE__ */ jsxs4(Dialog2.Portal, { children: [
862
+ /* @__PURE__ */ jsx4(Dialog2.Overlay, { className: "fixed inset-0 bg-black/40" }),
863
+ /* @__PURE__ */ jsx4(Dialog2.Content, { className: `fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-[95vw] max-w-3xl rounded-lg border bg-white p-4 shadow-lg ${className ?? ""}`, children: /* @__PURE__ */ jsxs4("div", { className: "space-y-3", children: [
864
+ /* @__PURE__ */ jsx4(Dialog2.Title, { className: "text-lg font-semibold", children: title ?? "Select an image" }),
865
+ /* @__PURE__ */ jsx4(Dialog2.Description, { className: "text-sm text-muted-foreground", children: description ?? "Browse your project images. Thumbnails are shown when available." }),
866
+ /* @__PURE__ */ jsxs4("div", { className: "flex gap-2 items-center", children: [
867
+ /* @__PURE__ */ jsx4(
868
+ "input",
869
+ {
870
+ className: "w-full rounded border px-3 py-2 text-sm",
871
+ placeholder: "Search by filename...",
872
+ value: query,
873
+ onChange: (e) => setQuery(e.target.value)
874
+ }
875
+ ),
876
+ /* @__PURE__ */ jsx4("button", { type: "button", className: "px-3 py-2 text-sm rounded border", onClick: load, disabled: loading, children: loading ? "Loading\u2026" : "Refresh" })
877
+ ] }),
878
+ error && /* @__PURE__ */ jsx4("div", { className: "rounded border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-700", children: error }),
879
+ /* @__PURE__ */ jsxs4(
880
+ "div",
881
+ {
882
+ className: gridClassName ?? "grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-3 max-h-[60vh] overflow-auto",
883
+ children: [
884
+ !loading && visible.map((f2) => {
885
+ const thumbUrl = f2.thumbnails?.[0]?.url || f2.url;
886
+ return /* @__PURE__ */ jsxs4(
887
+ "button",
888
+ {
889
+ type: "button",
890
+ onClick: () => {
891
+ onSelect({
892
+ url: f2.url,
893
+ key: f2.key,
894
+ originalName: f2.originalName,
895
+ size: f2.size,
896
+ contentType: f2.contentType,
897
+ thumbnails: f2.thumbnails
898
+ });
899
+ onOpenChange(false);
900
+ },
901
+ className: "group relative aspect-square overflow-hidden rounded border",
902
+ title: f2.originalName,
903
+ children: [
904
+ /* @__PURE__ */ jsx4(
905
+ "img",
906
+ {
907
+ src: thumbUrl,
908
+ alt: f2.originalName,
909
+ className: "h-full w-full object-cover transition-transform group-hover:scale-105"
910
+ }
911
+ ),
912
+ /* @__PURE__ */ jsx4("div", { className: "pointer-events-none absolute inset-x-0 bottom-0 bg-black/50 p-1 text-[10px] text-white", children: (f2.originalName || "").slice(0, 28) })
913
+ ]
914
+ },
915
+ f2.key
916
+ );
917
+ }),
918
+ loading && /* @__PURE__ */ jsx4("div", { className: "col-span-full text-sm text-muted-foreground", children: "Loading\u2026" }),
919
+ !loading && visible.length === 0 && /* @__PURE__ */ jsx4("div", { className: "col-span-full text-sm text-muted-foreground", children: "No images found" })
920
+ ]
921
+ }
922
+ ),
923
+ /* @__PURE__ */ jsx4("div", { className: "flex justify-end", children: /* @__PURE__ */ jsx4(Dialog2.Close, { asChild: true, children: /* @__PURE__ */ jsx4("button", { type: "button", className: "px-3 py-2 text-sm rounded border", children: "Close" }) }) })
924
+ ] }) })
925
+ ] }) });
926
+ }
500
927
  export {
928
+ ConnectProjectDialog,
929
+ ImagePickerDialog,
501
930
  ProjectFilesWidget,
502
931
  UpfilesClient,
503
- Uploader
932
+ Uploader,
933
+ addApiKeyManually,
934
+ connectProject,
935
+ connectUsingApiKey,
936
+ createClientWithKey,
937
+ createProject,
938
+ fetchFileThumbnails,
939
+ fetchProjectFiles,
940
+ fetchProjectImagesWithThumbs,
941
+ listProjects
504
942
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thetechfossil/upfiles",
3
- "version": "0.3.0-beta",
3
+ "version": "0.3.1-beta",
4
4
  "description": "Lightweight client and React components for Upfiles Plugin API (presigned S3)",
5
5
  "license": "MIT",
6
6
  "author": "UpFiles",
@@ -31,9 +31,11 @@
31
31
  "prepublishOnly": "bun run build"
32
32
  },
33
33
  "peerDependencies": {
34
- "react": ">=18"
34
+ "react": ">=18",
35
+ "@radix-ui/react-dialog": ">=1.0.0"
35
36
  },
36
37
  "devDependencies": {
38
+ "@radix-ui/react-dialog": "^1.1.15",
37
39
  "@types/react": "^19",
38
40
  "tsup": "^8.3.0",
39
41
  "typescript": "^5.6.2"