@sparkstudio/storage-ui 1.0.34 → 1.0.35

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.cjs CHANGED
@@ -521,8 +521,15 @@ var import_react4 = require("react");
521
521
  // src/components/FileIconCard.tsx
522
522
  var import_react3 = require("react");
523
523
  var import_jsx_runtime2 = require("react/jsx-runtime");
524
- function sanitizeIconHtml(input) {
525
- return input.replace(/<script[\s\S]*?>[\s\S]*?<\/script>/gi, "").replace(/\son\w+="[^"]*"/gi, "").replace(/\son\w+='[^']*'/gi, "").replace(/javascript:/gi, "");
524
+ function useIsMobile(breakpoint = 640) {
525
+ const getValue = () => typeof window !== "undefined" ? window.innerWidth <= breakpoint : false;
526
+ const [isMobile, setIsMobile] = (0, import_react3.useState)(getValue);
527
+ (0, import_react3.useEffect)(() => {
528
+ const onResize = () => setIsMobile(getValue());
529
+ window.addEventListener("resize", onResize);
530
+ return () => window.removeEventListener("resize", onResize);
531
+ }, [breakpoint]);
532
+ return isMobile;
526
533
  }
527
534
  var FileIconCard = (props) => {
528
535
  const {
@@ -532,17 +539,12 @@ var FileIconCard = (props) => {
532
539
  onSelect,
533
540
  onDeleteFile,
534
541
  onClickFile,
535
- icon,
536
- iconHtml,
537
542
  title,
538
543
  className
539
544
  } = props;
545
+ const isMobile = useIsMobile();
540
546
  const [deleting, setDeleting] = (0, import_react3.useState)(false);
541
547
  const [error, setError] = (0, import_react3.useState)(null);
542
- const safeIconHtml = (0, import_react3.useMemo)(() => {
543
- if (!iconHtml) return null;
544
- return sanitizeIconHtml(iconHtml);
545
- }, [iconHtml]);
546
548
  const canDelete = !!onDeleteFile && !deleteDisabled && !deleting;
547
549
  const handleClick = () => {
548
550
  onSelect?.(file);
@@ -584,69 +586,115 @@ var FileIconCard = (props) => {
584
586
  title: title ?? file.Name ?? "File",
585
587
  style: {
586
588
  display: "flex",
587
- alignItems: "center",
588
- gap: 12,
589
- padding: 12,
589
+ flexDirection: "column",
590
+ gap: 10,
591
+ padding: isMobile ? 14 : 12,
590
592
  border: `1px solid ${borderColor}`,
591
593
  boxShadow,
592
- borderRadius: 12,
594
+ borderRadius: 16,
593
595
  userSelect: "none",
594
596
  cursor: "pointer",
595
- background: "white"
597
+ background: "white",
598
+ minWidth: 0,
599
+ width: "100%",
600
+ boxSizing: "border-box"
596
601
  },
597
602
  children: [
598
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
603
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
599
604
  "div",
600
605
  {
601
606
  style: {
602
- width: 44,
603
- height: 44,
604
- borderRadius: 10,
605
- display: "grid",
606
- placeItems: "center",
607
- background: "rgba(0,0,0,0.04)",
608
- flex: "0 0 auto"
607
+ display: "flex",
608
+ alignItems: "center",
609
+ justifyContent: "space-between",
610
+ gap: 12,
611
+ minWidth: 0,
612
+ width: "100%"
609
613
  },
610
- children: icon ? icon : safeIconHtml ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { "aria-hidden": true, dangerouslySetInnerHTML: { __html: safeIconHtml } }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(DefaultFileIcon, {})
614
+ children: [
615
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
616
+ "div",
617
+ {
618
+ style: {
619
+ minWidth: 0,
620
+ flex: "1 1 auto",
621
+ overflow: "hidden"
622
+ },
623
+ children: [
624
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
625
+ "div",
626
+ {
627
+ style: {
628
+ fontWeight: 700,
629
+ fontSize: isMobile ? 15 : 14,
630
+ lineHeight: 1.2,
631
+ overflow: "hidden",
632
+ textOverflow: "ellipsis",
633
+ whiteSpace: "nowrap"
634
+ },
635
+ children: title ?? file.Name ?? "Untitled"
636
+ }
637
+ ),
638
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
639
+ "div",
640
+ {
641
+ style: {
642
+ fontSize: 12,
643
+ opacity: 0.7,
644
+ marginTop: 4,
645
+ overflow: "hidden",
646
+ textOverflow: "ellipsis",
647
+ whiteSpace: "nowrap"
648
+ },
649
+ children: typeof file.FileSize === "number" ? formatBytes(file.FileSize) : ""
650
+ }
651
+ )
652
+ ]
653
+ }
654
+ ),
655
+ !deleteDisabled ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
656
+ "button",
657
+ {
658
+ type: "button",
659
+ onClick: handleDelete,
660
+ disabled: !canDelete,
661
+ "aria-label": deleting ? "Deleting file" : "Delete file",
662
+ title: deleting ? "Deleting..." : "Delete",
663
+ style: {
664
+ flex: "0 0 auto",
665
+ width: isMobile ? 40 : "auto",
666
+ height: isMobile ? 40 : "auto",
667
+ minWidth: isMobile ? 40 : 110,
668
+ border: "1px solid rgba(0,0,0,0.18)",
669
+ background: canDelete ? "white" : "rgba(0,0,0,0.04)",
670
+ borderRadius: 12,
671
+ padding: isMobile ? 0 : "8px 10px",
672
+ cursor: canDelete ? "pointer" : "not-allowed",
673
+ display: "flex",
674
+ alignItems: "center",
675
+ justifyContent: "center",
676
+ gap: 8,
677
+ boxSizing: "border-box"
678
+ },
679
+ children: [
680
+ deleting ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(SmallSpinner, {}) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(TrashIcon, {}),
681
+ !isMobile ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: { fontSize: 14, fontWeight: 600 }, children: deleting ? "Deleting\u2026" : "Delete" }) : null
682
+ ]
683
+ }
684
+ ) : null
685
+ ]
611
686
  }
612
687
  ),
613
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { minWidth: 0, flex: "1 1 auto" }, children: [
614
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
615
- "div",
616
- {
617
- style: {
618
- fontWeight: 600,
619
- overflow: "hidden",
620
- textOverflow: "ellipsis",
621
- whiteSpace: "nowrap"
622
- },
623
- children: title ?? file.Name ?? "Untitled"
624
- }
625
- ),
626
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { fontSize: 12, opacity: 0.7 }, children: typeof file.FileSize === "number" ? formatBytes(file.FileSize) : "" }),
627
- error ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { fontSize: 12, color: "crimson", marginTop: 4 }, children: error }) : null
628
- ] }),
629
- !deleteDisabled ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
630
- "button",
688
+ error ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
689
+ "div",
631
690
  {
632
- type: "button",
633
- onClick: handleDelete,
634
- disabled: !canDelete,
635
- "aria-label": "Delete file",
636
691
  style: {
637
- border: "1px solid rgba(0,0,0,0.18)",
638
- background: canDelete ? "white" : "rgba(0,0,0,0.04)",
639
- borderRadius: 10,
640
- padding: "8px 10px",
641
- cursor: canDelete ? "pointer" : "not-allowed",
642
- display: "flex",
643
- alignItems: "center",
644
- gap: 8
692
+ fontSize: 12,
693
+ color: "crimson",
694
+ marginTop: -2,
695
+ wordBreak: "break-word"
645
696
  },
646
- children: [
647
- deleting ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(SmallSpinner, {}) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(TrashIcon, {}),
648
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: { fontSize: 13 }, children: deleting ? "Deleting\u2026" : "Delete" })
649
- ]
697
+ children: error
650
698
  }
651
699
  ) : null
652
700
  ]
@@ -665,33 +713,33 @@ function formatBytes(bytes) {
665
713
  return `${v.toFixed(i === 0 ? 0 : 1)} ${units[i]}`;
666
714
  }
667
715
  function SmallSpinner() {
668
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
669
- "span",
670
- {
671
- "aria-hidden": true,
672
- style: {
673
- width: 14,
674
- height: 14,
675
- borderRadius: "50%",
676
- border: "2px solid rgba(0,0,0,0.2)",
677
- borderTopColor: "rgba(0,0,0,0.7)",
678
- display: "inline-block",
679
- animation: "fileIconSpin 0.8s linear infinite"
680
- }
681
- }
682
- );
683
- }
684
- function DefaultFileIcon() {
685
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", "aria-hidden": true, children: [
716
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
717
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("style", { children: `
718
+ @keyframes fileIconSpin {
719
+ from {
720
+ transform: rotate(0deg);
721
+ }
722
+ to {
723
+ transform: rotate(360deg);
724
+ }
725
+ }
726
+ ` }),
686
727
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
687
- "path",
728
+ "span",
688
729
  {
689
- d: "M7 3h7l3 3v15a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2Z",
690
- stroke: "currentColor",
691
- strokeWidth: "2"
730
+ "aria-hidden": true,
731
+ style: {
732
+ width: 14,
733
+ height: 14,
734
+ borderRadius: "50%",
735
+ border: "2px solid rgba(0,0,0,0.2)",
736
+ borderTopColor: "rgba(0,0,0,0.7)",
737
+ display: "inline-block",
738
+ boxSizing: "border-box",
739
+ animation: "fileIconSpin 0.8s linear infinite"
740
+ }
692
741
  }
693
- ),
694
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M14 3v4a1 1 0 0 0 1 1h4", stroke: "currentColor", strokeWidth: "2" })
742
+ )
695
743
  ] });
696
744
  }
697
745
  function TrashIcon() {
@@ -727,25 +775,29 @@ function TrashIcon() {
727
775
 
728
776
  // src/components/FileIconGrid.tsx
729
777
  var import_jsx_runtime3 = require("react/jsx-runtime");
778
+ function useIsMobile2(breakpoint = 640) {
779
+ const getValue = () => typeof window !== "undefined" ? window.innerWidth <= breakpoint : false;
780
+ const [isMobile, setIsMobile] = (0, import_react4.useState)(getValue);
781
+ (0, import_react4.useEffect)(() => {
782
+ const onResize = () => setIsMobile(getValue());
783
+ window.addEventListener("resize", onResize);
784
+ return () => window.removeEventListener("resize", onResize);
785
+ }, [breakpoint]);
786
+ return isMobile;
787
+ }
730
788
  function FileIconGrid(props) {
731
- const {
732
- files,
733
- deleteDisabled,
734
- onDeleted,
735
- selectedId,
736
- onSelect,
737
- icon,
738
- iconHtml,
739
- className
740
- } = props;
789
+ const { files, deleteDisabled, onDeleted, selectedId, onSelect, className } = props;
790
+ const isMobile = useIsMobile2();
741
791
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
742
792
  "div",
743
793
  {
744
794
  className,
745
795
  style: {
746
796
  display: "grid",
747
- gridTemplateColumns: "repeat(auto-fill, minmax(280px, 1fr))",
748
- gap: 12
797
+ gridTemplateColumns: isMobile ? "minmax(0, 1fr)" : "repeat(auto-fill, minmax(280px, 1fr))",
798
+ gap: 12,
799
+ width: "100%",
800
+ minWidth: 0
749
801
  },
750
802
  children: files.map((f) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
751
803
  FileIconCard,
@@ -754,9 +806,7 @@ function FileIconGrid(props) {
754
806
  deleteDisabled,
755
807
  onDeleteFile: onDeleted,
756
808
  onSelect,
757
- selected: selectedId === f.Id,
758
- icon,
759
- iconHtml
809
+ selected: selectedId === f.Id
760
810
  },
761
811
  f.Id
762
812
  ))
@@ -766,6 +816,16 @@ function FileIconGrid(props) {
766
816
 
767
817
  // src/components/ContainerIdGridPanel.tsx
768
818
  var import_jsx_runtime4 = require("react/jsx-runtime");
819
+ function useIsMobile3(breakpoint = 640) {
820
+ const getValue = () => typeof window !== "undefined" ? window.innerWidth <= breakpoint : false;
821
+ const [isMobile, setIsMobile] = (0, import_react5.useState)(getValue);
822
+ (0, import_react5.useEffect)(() => {
823
+ const onResize = () => setIsMobile(getValue());
824
+ window.addEventListener("resize", onResize);
825
+ return () => window.removeEventListener("resize", onResize);
826
+ }, [breakpoint]);
827
+ return isMobile;
828
+ }
769
829
  function ContainerIdGridPanel(props) {
770
830
  const {
771
831
  containerApiBaseUrl,
@@ -776,12 +836,11 @@ function ContainerIdGridPanel(props) {
776
836
  multiple = true,
777
837
  selectedId,
778
838
  onSelect,
779
- icon,
780
- iconHtml,
781
839
  deleteDisabled,
782
840
  className,
783
841
  onDeleted
784
842
  } = props;
843
+ const isMobile = useIsMobile3();
785
844
  const sdkDb = (0, import_react5.useMemo)(
786
845
  () => new SparkStudioStorageSDK(containerApiBaseUrl),
787
846
  [containerApiBaseUrl]
@@ -822,12 +881,11 @@ function ContainerIdGridPanel(props) {
822
881
  if (cancelled) return;
823
882
  const map = /* @__PURE__ */ new Map();
824
883
  for (const r of results) if (r?.Id) map.set(r.Id, r);
825
- setFiles(
826
- ids.map((id) => map.get(id)).filter(Boolean)
827
- );
884
+ setFiles(ids.map((id) => map.get(id)).filter(Boolean));
828
885
  } catch (e) {
829
- if (!cancelled)
886
+ if (!cancelled) {
830
887
  setErrMsg(e instanceof Error ? e.message : "Failed to load files");
888
+ }
831
889
  } finally {
832
890
  if (!cancelled) setLoading(false);
833
891
  }
@@ -914,106 +972,149 @@ function ContainerIdGridPanel(props) {
914
972
  if (!list || list.length === 0) return;
915
973
  startUploadsIfNeeded(list);
916
974
  };
917
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className, style: { display: "grid", gap: 12 }, children: [
918
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { children: [
919
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(UploadProgressList, { uploads }),
920
- errMsg ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { fontSize: 12, color: "crimson", marginTop: 6 }, children: errMsg }) : null
921
- ] }),
922
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
923
- "div",
924
- {
925
- onDragEnter: (e) => {
926
- e.preventDefault();
927
- e.stopPropagation();
928
- setDragOver(true);
929
- },
930
- onDragOver: (e) => {
931
- e.preventDefault();
932
- e.stopPropagation();
933
- setDragOver(true);
934
- },
935
- onDragLeave: (e) => {
936
- e.preventDefault();
937
- e.stopPropagation();
938
- setDragOver(false);
939
- },
940
- onDrop,
941
- style: {
942
- position: "relative",
943
- borderRadius: 14,
944
- border: `2px dashed ${dragOver ? "rgba(13,110,253,0.9)" : "rgba(0,0,0,0.15)"}`,
945
- background: dragOver ? "rgba(13,110,253,0.06)" : "transparent",
946
- padding: 12
947
- },
948
- children: [
949
- dragOver ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
950
- "div",
951
- {
952
- style: {
953
- position: "absolute",
954
- inset: 0,
955
- borderRadius: 14,
956
- display: "grid",
957
- placeItems: "center",
958
- pointerEvents: "none",
959
- background: "rgba(13,110,253,0.10)",
960
- fontWeight: 800
961
- },
962
- children: "Drop files to upload"
963
- }
964
- ) : null,
965
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
975
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
976
+ "div",
977
+ {
978
+ className,
979
+ style: {
980
+ display: "grid",
981
+ gap: 12,
982
+ width: "100%",
983
+ minWidth: 0
984
+ },
985
+ children: [
986
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: { minWidth: 0 }, children: [
987
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(UploadProgressList, { uploads }),
988
+ errMsg ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
966
989
  "div",
967
990
  {
968
991
  style: {
969
- display: "flex",
970
- justifyContent: "space-between",
971
- marginBottom: 10
992
+ fontSize: 12,
993
+ color: "crimson",
994
+ marginTop: 6,
995
+ wordBreak: "break-word"
972
996
  },
973
- children: [
974
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { fontWeight: 700 }, children: "Files" }),
975
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
976
- "button",
977
- {
978
- type: "button",
979
- onClick: openPicker,
980
- style: {
981
- border: "1px solid rgba(0,0,0,0.18)",
982
- background: "white",
983
- borderRadius: 10,
984
- padding: "8px 10px",
985
- cursor: "pointer",
986
- fontWeight: 600
987
- },
988
- children: "Browse\u2026"
989
- }
990
- )
991
- ]
997
+ children: errMsg
992
998
  }
993
- ),
994
- loading ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
995
- "div",
996
- {
997
- className: "d-flex justify-content-center align-items-center",
998
- style: { minHeight: 120 },
999
- children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "spinner-border text-secondary", role: "status" })
1000
- }
1001
- ) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1002
- FileIconGrid,
1003
- {
1004
- files,
1005
- deleteDisabled,
1006
- onDeleted: handleDelete,
1007
- selectedId,
1008
- onSelect,
1009
- icon,
1010
- iconHtml
1011
- }
1012
- )
1013
- ]
1014
- }
1015
- )
1016
- ] });
999
+ ) : null
1000
+ ] }),
1001
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
1002
+ "div",
1003
+ {
1004
+ onDragEnter: (e) => {
1005
+ e.preventDefault();
1006
+ e.stopPropagation();
1007
+ setDragOver(true);
1008
+ },
1009
+ onDragOver: (e) => {
1010
+ e.preventDefault();
1011
+ e.stopPropagation();
1012
+ setDragOver(true);
1013
+ },
1014
+ onDragLeave: (e) => {
1015
+ e.preventDefault();
1016
+ e.stopPropagation();
1017
+ setDragOver(false);
1018
+ },
1019
+ onDrop,
1020
+ style: {
1021
+ position: "relative",
1022
+ borderRadius: 14,
1023
+ border: `2px dashed ${dragOver ? "rgba(13,110,253,0.9)" : "rgba(0,0,0,0.15)"}`,
1024
+ background: dragOver ? "rgba(13,110,253,0.06)" : "transparent",
1025
+ padding: isMobile ? 10 : 12,
1026
+ width: "100%",
1027
+ minWidth: 0,
1028
+ overflow: "hidden",
1029
+ boxSizing: "border-box"
1030
+ },
1031
+ children: [
1032
+ dragOver ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1033
+ "div",
1034
+ {
1035
+ style: {
1036
+ position: "absolute",
1037
+ inset: 0,
1038
+ borderRadius: 14,
1039
+ display: "grid",
1040
+ placeItems: "center",
1041
+ pointerEvents: "none",
1042
+ background: "rgba(13,110,253,0.10)",
1043
+ fontWeight: 800,
1044
+ textAlign: "center",
1045
+ padding: 12
1046
+ },
1047
+ children: "Drop files to upload"
1048
+ }
1049
+ ) : null,
1050
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
1051
+ "div",
1052
+ {
1053
+ style: {
1054
+ display: "flex",
1055
+ flexDirection: isMobile ? "column" : "row",
1056
+ alignItems: isMobile ? "stretch" : "center",
1057
+ justifyContent: "space-between",
1058
+ gap: 10,
1059
+ marginBottom: 10,
1060
+ minWidth: 0
1061
+ },
1062
+ children: [
1063
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1064
+ "div",
1065
+ {
1066
+ style: {
1067
+ fontWeight: 700,
1068
+ minWidth: 0
1069
+ },
1070
+ children: "Files"
1071
+ }
1072
+ ),
1073
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1074
+ "button",
1075
+ {
1076
+ type: "button",
1077
+ onClick: openPicker,
1078
+ style: {
1079
+ border: "1px solid rgba(0,0,0,0.18)",
1080
+ background: "white",
1081
+ borderRadius: 10,
1082
+ padding: isMobile ? "10px 12px" : "8px 10px",
1083
+ cursor: "pointer",
1084
+ fontWeight: 600,
1085
+ width: isMobile ? "100%" : "auto",
1086
+ maxWidth: "100%",
1087
+ boxSizing: "border-box"
1088
+ },
1089
+ children: "Browse\u2026"
1090
+ }
1091
+ )
1092
+ ]
1093
+ }
1094
+ ),
1095
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { width: "100%", minWidth: 0, overflow: "hidden" }, children: loading ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1096
+ "div",
1097
+ {
1098
+ className: "d-flex justify-content-center align-items-center",
1099
+ style: { minHeight: 120 },
1100
+ children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "spinner-border text-secondary", role: "status" })
1101
+ }
1102
+ ) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1103
+ FileIconGrid,
1104
+ {
1105
+ files,
1106
+ deleteDisabled,
1107
+ onDeleted: handleDelete,
1108
+ selectedId,
1109
+ onSelect
1110
+ }
1111
+ ) })
1112
+ ]
1113
+ }
1114
+ )
1115
+ ]
1116
+ }
1117
+ );
1017
1118
  }
1018
1119
 
1019
1120
  // src/components/ContainerUploadPanel.tsx
@@ -1592,8 +1693,6 @@ function FileGridUploadPanel(props) {
1592
1693
  multiple = true,
1593
1694
  selectedId,
1594
1695
  onSelect,
1595
- icon,
1596
- iconHtml,
1597
1696
  deleteDisabled
1598
1697
  } = props;
1599
1698
  const { containers, setContainers, reload, loading } = UseContainers({
@@ -1732,9 +1831,7 @@ function FileGridUploadPanel(props) {
1732
1831
  deleteDisabled,
1733
1832
  onDeleted: handleDeleteFile,
1734
1833
  selectedId,
1735
- onSelect,
1736
- icon,
1737
- iconHtml
1834
+ onSelect
1738
1835
  }
1739
1836
  )
1740
1837
  ]
@@ -1940,6 +2037,7 @@ function SingleImageUploadEditor(props) {
1940
2037
  const pendingContainerRef = (0, import_react13.useRef)(null);
1941
2038
  const [isDeleting, setIsDeleting] = (0, import_react13.useState)(false);
1942
2039
  const [isLoadingImage, setIsLoadingImage] = (0, import_react13.useState)(false);
2040
+ const [isPickingFile, setIsPickingFile] = (0, import_react13.useState)(false);
1943
2041
  const [currentImage, setCurrentImage] = (0, import_react13.useState)(null);
1944
2042
  const sdkDb = (0, import_react13.useMemo)(
1945
2043
  () => new SparkStudioStorageSDK(containerApiBaseUrl),
@@ -1949,7 +2047,6 @@ function SingleImageUploadEditor(props) {
1949
2047
  () => new SparkStudioStorageSDK(storageApiBaseUrl),
1950
2048
  [storageApiBaseUrl]
1951
2049
  );
1952
- const busy = disabled || isDeleting || isLoadingImage;
1953
2050
  function logError(context, err) {
1954
2051
  console.error(`[SingleImageUploadEditor] ${context}`, err);
1955
2052
  }
@@ -1958,6 +2055,12 @@ function SingleImageUploadEditor(props) {
1958
2055
  setError(null);
1959
2056
  pendingContainerRef.current = null;
1960
2057
  }
2058
+ function getErrorMessage(err, fallback) {
2059
+ return err instanceof Error ? err.message : fallback;
2060
+ }
2061
+ function isImageFile(file) {
2062
+ return file.type.startsWith("image/");
2063
+ }
1961
2064
  (0, import_react13.useEffect)(() => {
1962
2065
  let cancelled = false;
1963
2066
  async function loadImage() {
@@ -1990,12 +2093,6 @@ function SingleImageUploadEditor(props) {
1990
2093
  cancelled = true;
1991
2094
  };
1992
2095
  }, [value, sdkDb]);
1993
- function getErrorMessage(err, fallback) {
1994
- return err instanceof Error ? err.message : fallback;
1995
- }
1996
- function isImageFile(file) {
1997
- return file.type.startsWith("image/");
1998
- }
1999
2096
  async function createContainerForFile(file) {
2000
2097
  const contentType = file.type || "application/octet-stream";
2001
2098
  return sdkDb.container.CreateFileContainer(
@@ -2036,6 +2133,7 @@ function SingleImageUploadEditor(props) {
2036
2133
  onChange?.(uploaded.Id);
2037
2134
  }
2038
2135
  pendingContainerRef.current = null;
2136
+ setIsPickingFile(false);
2039
2137
  },
2040
2138
  onUploadError: async (_file, err) => {
2041
2139
  const pending = pendingContainerRef.current;
@@ -2043,10 +2141,14 @@ function SingleImageUploadEditor(props) {
2043
2141
  try {
2044
2142
  await sdkDb.container.DeleteContainer(pending.Id);
2045
2143
  } catch (cleanupErr) {
2046
- logError("Failed to cleanup pending container after upload error", cleanupErr);
2144
+ logError(
2145
+ "Failed to cleanup pending container after upload error",
2146
+ cleanupErr
2147
+ );
2047
2148
  }
2048
2149
  }
2049
2150
  pendingContainerRef.current = null;
2151
+ setIsPickingFile(false);
2050
2152
  logError("Upload failed", err);
2051
2153
  setCurrentImage(null);
2052
2154
  setError(getErrorMessage(err, "Upload failed"));
@@ -2054,7 +2156,10 @@ function SingleImageUploadEditor(props) {
2054
2156
  });
2055
2157
  const visibleUploads = uploads.filter((u) => u.status !== "success");
2056
2158
  const activeUpload = visibleUploads[0];
2159
+ const isUploading = !!activeUpload;
2057
2160
  const showBottomProgress = !!activeUpload;
2161
+ const busy = disabled || isDeleting || isLoadingImage || isPickingFile || isUploading;
2162
+ const busyText = isUploading ? "Uploading..." : isDeleting ? "Removing..." : isLoadingImage ? "Loading..." : isPickingFile ? "Preparing..." : "";
2058
2163
  async function replaceWithFile(file) {
2059
2164
  if (busy) {
2060
2165
  return;
@@ -2065,12 +2170,15 @@ function SingleImageUploadEditor(props) {
2065
2170
  }
2066
2171
  try {
2067
2172
  setError(null);
2173
+ setIsPickingFile(true);
2068
2174
  if (value) {
2069
2175
  setIsDeleting(true);
2070
2176
  try {
2071
2177
  await deleteContainerFile(value);
2072
2178
  } catch (err) {
2073
2179
  logError(`Failed to delete existing container '${String(value)}'`, err);
2180
+ } finally {
2181
+ setIsDeleting(false);
2074
2182
  }
2075
2183
  setCurrentImage(null);
2076
2184
  onChange?.(null);
@@ -2082,7 +2190,7 @@ function SingleImageUploadEditor(props) {
2082
2190
  logError("Failed to replace image", err);
2083
2191
  resetState();
2084
2192
  setError(getErrorMessage(err, "Failed to replace image."));
2085
- } finally {
2193
+ setIsPickingFile(false);
2086
2194
  setIsDeleting(false);
2087
2195
  }
2088
2196
  }
@@ -2124,6 +2232,29 @@ function SingleImageUploadEditor(props) {
2124
2232
  setIsDeleting(false);
2125
2233
  }
2126
2234
  }
2235
+ function SpinnerLabel(props2) {
2236
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
2237
+ "span",
2238
+ {
2239
+ style: {
2240
+ display: "inline-flex",
2241
+ alignItems: "center",
2242
+ gap: 8
2243
+ },
2244
+ children: [
2245
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
2246
+ "span",
2247
+ {
2248
+ className: "spinner-border spinner-border-sm",
2249
+ role: "status",
2250
+ "aria-hidden": "true"
2251
+ }
2252
+ ),
2253
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { children: props2.text })
2254
+ ]
2255
+ }
2256
+ );
2257
+ }
2127
2258
  return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { style: { display: "grid", gap: 8 }, children: [
2128
2259
  error ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { style: { fontSize: 12, color: "crimson" }, children: error }) : null,
2129
2260
  /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
@@ -2209,7 +2340,9 @@ function SingleImageUploadEditor(props) {
2209
2340
  width: "100%",
2210
2341
  height,
2211
2342
  objectFit: "contain",
2212
- background: "rgba(0,0,0,0.02)"
2343
+ background: "rgba(0,0,0,0.02)",
2344
+ opacity: busy ? 0.45 : 1,
2345
+ transition: "opacity 120ms ease"
2213
2346
  }
2214
2347
  }
2215
2348
  )
@@ -2240,9 +2373,10 @@ function SingleImageUploadEditor(props) {
2240
2373
  padding: "8px 10px",
2241
2374
  cursor: busy ? "not-allowed" : "pointer",
2242
2375
  fontWeight: 600,
2243
- boxShadow: "0 2px 8px rgba(0,0,0,0.08)"
2376
+ boxShadow: "0 2px 8px rgba(0,0,0,0.08)",
2377
+ minWidth: 110
2244
2378
  },
2245
- children: "Browse\u2026"
2379
+ children: isUploading || isPickingFile ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(SpinnerLabel, { text: "Uploading..." }) : "Browse\u2026"
2246
2380
  }
2247
2381
  ),
2248
2382
  /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
@@ -2259,9 +2393,10 @@ function SingleImageUploadEditor(props) {
2259
2393
  padding: "8px 10px",
2260
2394
  cursor: busy ? "not-allowed" : "pointer",
2261
2395
  fontWeight: 600,
2262
- boxShadow: "0 2px 8px rgba(0,0,0,0.08)"
2396
+ boxShadow: "0 2px 8px rgba(0,0,0,0.08)",
2397
+ minWidth: 110
2263
2398
  },
2264
- children: "Remove"
2399
+ children: isDeleting ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(SpinnerLabel, { text: "Removing..." }) : "Remove"
2265
2400
  }
2266
2401
  )
2267
2402
  ]
@@ -2301,17 +2436,59 @@ function SingleImageUploadEditor(props) {
2301
2436
  borderRadius: 10,
2302
2437
  padding: "10px 16px",
2303
2438
  cursor: busy ? "not-allowed" : "pointer",
2304
- fontWeight: 600
2439
+ fontWeight: 600,
2440
+ minWidth: 140
2305
2441
  },
2306
- children: "Browse\u2026"
2442
+ children: isUploading || isPickingFile ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(SpinnerLabel, { text: "Uploading..." }) : isLoadingImage ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(SpinnerLabel, { text: "Loading..." }) : "Browse\u2026"
2307
2443
  }
2308
2444
  ),
2309
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { style: { fontSize: 13, opacity: 0.7 }, children: "Drag and drop an image here" })
2445
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { style: { fontSize: 13, opacity: 0.7 }, children: busyText || "Drag and drop an image here" })
2310
2446
  ]
2311
2447
  }
2312
2448
  )
2313
2449
  }
2314
2450
  ),
2451
+ busy ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
2452
+ "div",
2453
+ {
2454
+ style: {
2455
+ position: "absolute",
2456
+ inset: 0,
2457
+ display: "grid",
2458
+ placeItems: "center",
2459
+ background: "rgba(255,255,255,0.55)",
2460
+ backdropFilter: "blur(1px)",
2461
+ zIndex: 5
2462
+ },
2463
+ children: /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
2464
+ "div",
2465
+ {
2466
+ style: {
2467
+ display: "inline-flex",
2468
+ alignItems: "center",
2469
+ gap: 10,
2470
+ background: "white",
2471
+ border: "1px solid rgba(0,0,0,0.08)",
2472
+ borderRadius: 12,
2473
+ padding: "10px 14px",
2474
+ boxShadow: "0 8px 24px rgba(0,0,0,0.12)",
2475
+ fontWeight: 600
2476
+ },
2477
+ children: [
2478
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
2479
+ "span",
2480
+ {
2481
+ className: "spinner-border spinner-border-sm",
2482
+ role: "status",
2483
+ "aria-hidden": "true"
2484
+ }
2485
+ ),
2486
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { children: busyText || "Working..." })
2487
+ ]
2488
+ }
2489
+ )
2490
+ }
2491
+ ) : null,
2315
2492
  showBottomProgress ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
2316
2493
  "div",
2317
2494
  {
@@ -2322,7 +2499,7 @@ function SingleImageUploadEditor(props) {
2322
2499
  bottom: 0,
2323
2500
  height: 4,
2324
2501
  background: "rgba(0,0,0,0.08)",
2325
- zIndex: 4
2502
+ zIndex: 6
2326
2503
  },
2327
2504
  title: activeUpload.file.name,
2328
2505
  children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(