@vryzel/file-next-ui 0.5.3 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -73,7 +73,7 @@ What you get with `FileExplorer`:
73
73
  - List / grid, sort, persisted view (`persistViewKey`)
74
74
  - Search, trash, upload (multi-file + OS drop)
75
75
  - New folder / rename in place (Enter or blur saves, Escape cancels)
76
- - Multi-select, shift-click, ⌘A, copy/paste, drag onto folders
76
+ - Multi-select, shift-click, ⌘A, deselect all from the selection toast, copy/paste, drag onto folders
77
77
  - Pagination (`limit` 50 + Load more)
78
78
  - Quota footer, selection toast, upload queue
79
79
 
package/dist/index.cjs CHANGED
@@ -1448,33 +1448,51 @@ function SelectionActions({
1448
1448
  }
1449
1449
  )
1450
1450
  ] }),
1451
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-wrap gap-1.5", children: trashOpen ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1452
- /* @__PURE__ */ jsxRuntime.jsxs("button", { type: "button", className: btn2, onClick: onRestore, children: [
1453
- /* @__PURE__ */ jsxRuntime.jsx(lucideReact.RotateCcw, { "aria-hidden": "true", className: "size-3.5" }),
1454
- labels.restore
1451
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap gap-1.5", children: [
1452
+ trashOpen ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1453
+ /* @__PURE__ */ jsxRuntime.jsxs("button", { type: "button", className: btn2, onClick: onRestore, children: [
1454
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.RotateCcw, { "aria-hidden": "true", className: "size-3.5" }),
1455
+ labels.restore
1456
+ ] }),
1457
+ onPurge ? /* @__PURE__ */ jsxRuntime.jsxs("button", { type: "button", className: cn(btn2, "text-destructive"), onClick: onPurge, children: [
1458
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { "aria-hidden": "true", className: "size-3.5" }),
1459
+ labels.deleteForever
1460
+ ] }) : null
1461
+ ] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1462
+ onDownload && downloadable.length > 0 ? /* @__PURE__ */ jsxRuntime.jsxs("button", { type: "button", className: btn2, onClick: () => downloadable.forEach(onDownload), children: [
1463
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Download, { "aria-hidden": "true", className: "size-3.5" }),
1464
+ labels.download
1465
+ ] }) : null,
1466
+ onCopy ? /* @__PURE__ */ jsxRuntime.jsxs("button", { type: "button", className: btn2, onClick: onCopy, children: [
1467
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Copy, { "aria-hidden": "true", className: "size-3.5" }),
1468
+ labels.copy
1469
+ ] }) : null,
1470
+ onShare && downloadable.length > 0 ? /* @__PURE__ */ jsxRuntime.jsxs("button", { type: "button", className: btn2, onClick: onShare, children: [
1471
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Link2, { "aria-hidden": "true", className: "size-3.5" }),
1472
+ labels.share
1473
+ ] }) : null,
1474
+ /* @__PURE__ */ jsxRuntime.jsxs("button", { type: "button", disabled: hasProtected, className: cn(btn2, "text-destructive"), onClick: onDelete, children: [
1475
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { "aria-hidden": "true", className: "size-3.5" }),
1476
+ labels.moveToTrash
1477
+ ] })
1455
1478
  ] }),
1456
- onPurge ? /* @__PURE__ */ jsxRuntime.jsxs("button", { type: "button", className: cn(btn2, "text-destructive"), onClick: onPurge, children: [
1457
- /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { "aria-hidden": "true", className: "size-3.5" }),
1458
- labels.deleteForever
1459
- ] }) : null
1460
- ] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1461
- onDownload && downloadable.length > 0 ? /* @__PURE__ */ jsxRuntime.jsxs("button", { type: "button", className: btn2, onClick: () => downloadable.forEach(onDownload), children: [
1462
- /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Download, { "aria-hidden": "true", className: "size-3.5" }),
1463
- labels.download
1464
- ] }) : null,
1465
- onCopy ? /* @__PURE__ */ jsxRuntime.jsxs("button", { type: "button", className: btn2, onClick: onCopy, children: [
1466
- /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Copy, { "aria-hidden": "true", className: "size-3.5" }),
1467
- labels.copy
1468
- ] }) : null,
1469
- onShare && downloadable.length > 0 ? /* @__PURE__ */ jsxRuntime.jsxs("button", { type: "button", className: btn2, onClick: onShare, children: [
1470
- /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Link2, { "aria-hidden": "true", className: "size-3.5" }),
1471
- labels.share
1472
- ] }) : null,
1473
- /* @__PURE__ */ jsxRuntime.jsxs("button", { type: "button", disabled: hasProtected, className: cn(btn2, "text-destructive"), onClick: onDelete, children: [
1474
- /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { "aria-hidden": "true", className: "size-3.5" }),
1475
- labels.moveToTrash
1476
- ] })
1477
- ] }) })
1479
+ /* @__PURE__ */ jsxRuntime.jsxs(
1480
+ "button",
1481
+ {
1482
+ type: "button",
1483
+ className: btn2,
1484
+ onClick: (event) => {
1485
+ event.stopPropagation();
1486
+ swallowClickThrough();
1487
+ onClear();
1488
+ },
1489
+ children: [
1490
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ListX, { "aria-hidden": "true", className: "size-3.5" }),
1491
+ labels.clearSelection
1492
+ ]
1493
+ }
1494
+ )
1495
+ ] })
1478
1496
  ] });
1479
1497
  }
1480
1498
  function ExplorerClipboardToast({
@@ -2755,6 +2773,15 @@ function clampPreviewZoom(scale) {
2755
2773
  function canPreviewFile(file) {
2756
2774
  return previewKind(file.mimeType, file.name) !== "none";
2757
2775
  }
2776
+ function pdfFrameMode(src, pageOrigin) {
2777
+ try {
2778
+ const url = new URL(src, pageOrigin);
2779
+ if (url.protocol !== "http:" && url.protocol !== "https:") return "blob";
2780
+ return url.origin === new URL(pageOrigin).origin ? "blob" : "direct";
2781
+ } catch {
2782
+ return "blob";
2783
+ }
2784
+ }
2758
2785
  function previewKind(mimeType, name) {
2759
2786
  const mime = mimeType.toLowerCase();
2760
2787
  const ext = name.split(".").pop()?.toLowerCase() ?? "";
@@ -2770,6 +2797,74 @@ function previewKind(mimeType, name) {
2770
2797
  if (mime.startsWith("text/") || ["txt", "csv", "md"].includes(ext)) return "text";
2771
2798
  return "none";
2772
2799
  }
2800
+ function PreviewSpinner({ label }) {
2801
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { role: "status", "aria-label": label, className: "absolute inset-0 z-10 flex items-center justify-center", children: [
2802
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "sr-only", children: label }),
2803
+ /* @__PURE__ */ jsxRuntime.jsx(
2804
+ "span",
2805
+ {
2806
+ "aria-hidden": "true",
2807
+ className: "size-8 rounded-full border-2 border-white/25 border-t-white motion-safe:animate-spin"
2808
+ }
2809
+ )
2810
+ ] });
2811
+ }
2812
+ function PdfFrame({
2813
+ src,
2814
+ title,
2815
+ onClick
2816
+ }) {
2817
+ const labels = useExplorerLabels();
2818
+ const [frameSrc, setFrameSrc] = react.useState(null);
2819
+ const [failed, setFailed] = react.useState(false);
2820
+ const [painted, setPainted] = react.useState(false);
2821
+ react.useEffect(() => {
2822
+ setPainted(false);
2823
+ if (pdfFrameMode(src, window.location.origin) === "direct") {
2824
+ setFrameSrc(src);
2825
+ setFailed(false);
2826
+ return;
2827
+ }
2828
+ const controller = new AbortController();
2829
+ let cancelled = false;
2830
+ let objectUrl = null;
2831
+ setFrameSrc(null);
2832
+ setFailed(false);
2833
+ void fetch(src, { credentials: "same-origin", signal: controller.signal }).then(async (response) => {
2834
+ if (!response.ok) throw new Error(String(response.status));
2835
+ const blob = new Blob([await response.arrayBuffer()], { type: "application/pdf" });
2836
+ if (cancelled) return;
2837
+ objectUrl = URL.createObjectURL(blob);
2838
+ setFrameSrc(objectUrl);
2839
+ }).catch((error) => {
2840
+ if (cancelled || error instanceof DOMException && error.name === "AbortError") return;
2841
+ setFailed(true);
2842
+ });
2843
+ return () => {
2844
+ cancelled = true;
2845
+ controller.abort();
2846
+ if (objectUrl) URL.revokeObjectURL(objectUrl);
2847
+ };
2848
+ }, [src]);
2849
+ if (failed) {
2850
+ return /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-white/70", children: labels.previewUnavailable });
2851
+ }
2852
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative h-full w-full max-w-5xl", children: [
2853
+ !painted ? /* @__PURE__ */ jsxRuntime.jsx(PreviewSpinner, { label: labels.loading }) : null,
2854
+ frameSrc ? /* @__PURE__ */ jsxRuntime.jsx(
2855
+ "iframe",
2856
+ {
2857
+ src: frameSrc,
2858
+ title,
2859
+ referrerPolicy: "no-referrer",
2860
+ className: painted ? "h-full w-full bg-white" : "absolute h-px w-px opacity-0",
2861
+ onClick,
2862
+ onLoad: () => setPainted(true),
2863
+ onError: () => setPainted(true)
2864
+ }
2865
+ ) : null
2866
+ ] });
2867
+ }
2773
2868
  function FilePreviewDialog({
2774
2869
  file,
2775
2870
  src,
@@ -2779,11 +2874,13 @@ function FilePreviewDialog({
2779
2874
  const labels = useExplorerLabels();
2780
2875
  const [scale, setScale] = react.useState(1);
2781
2876
  const [pan, setPan] = react.useState({ x: 0, y: 0 });
2877
+ const [ready, setReady] = react.useState(false);
2782
2878
  const drag = react.useRef(null);
2783
2879
  const closeRef = react.useRef(null);
2784
2880
  react.useEffect(() => {
2785
2881
  setScale(1);
2786
2882
  setPan({ x: 0, y: 0 });
2883
+ setReady(false);
2787
2884
  closeRef.current?.focus();
2788
2885
  }, [file?.id, src]);
2789
2886
  react.useEffect(() => {
@@ -2833,61 +2930,83 @@ function FilePreviewDialog({
2833
2930
  }
2834
2931
  )
2835
2932
  ] }),
2836
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex h-full items-center justify-center px-4 pt-12 pb-16", children: kind === "image" ? /* @__PURE__ */ jsxRuntime.jsx(
2837
- "img",
2838
- {
2839
- src,
2840
- alt: file.name,
2841
- draggable: false,
2842
- className: "max-h-full max-w-full cursor-zoom-in select-none object-contain",
2843
- style: { transform: `translate(${pan.x}px, ${pan.y}px) scale(${scale})` },
2844
- onClick: stop,
2845
- onDoubleClick: (event) => {
2846
- stop(event);
2847
- zoom(scale === 1 ? 2 : 1);
2848
- },
2849
- onWheel: (event) => {
2850
- event.preventDefault();
2851
- stop(event);
2852
- zoom(scale + (event.deltaY < 0 ? 0.25 : -0.25));
2853
- },
2854
- onPointerDown: (event) => {
2855
- if (scale === 1) return;
2856
- stop(event);
2857
- event.currentTarget.setPointerCapture(event.pointerId);
2858
- drag.current = { x: event.clientX, y: event.clientY, px: pan.x, py: pan.y };
2859
- },
2860
- onPointerMove: (event) => {
2861
- if (!drag.current) return;
2862
- setPan({
2863
- x: drag.current.px + event.clientX - drag.current.x,
2864
- y: drag.current.py + event.clientY - drag.current.y
2865
- });
2866
- },
2867
- onPointerUp: () => {
2868
- drag.current = null;
2933
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative flex h-full w-full items-center justify-center px-4 pt-12 pb-16", children: [
2934
+ kind !== "pdf" && !ready ? /* @__PURE__ */ jsxRuntime.jsx(PreviewSpinner, { label: labels.loading }) : null,
2935
+ kind === "image" ? /* @__PURE__ */ jsxRuntime.jsx(
2936
+ "img",
2937
+ {
2938
+ src,
2939
+ alt: file.name,
2940
+ draggable: false,
2941
+ className: `max-h-full max-w-full cursor-zoom-in select-none object-contain ${ready ? "" : "absolute h-px w-px opacity-0"}`,
2942
+ onLoad: () => setReady(true),
2943
+ onError: () => setReady(true),
2944
+ style: { transform: `translate(${pan.x}px, ${pan.y}px) scale(${scale})` },
2945
+ onClick: stop,
2946
+ onDoubleClick: (event) => {
2947
+ stop(event);
2948
+ zoom(scale === 1 ? 2 : 1);
2949
+ },
2950
+ onWheel: (event) => {
2951
+ event.preventDefault();
2952
+ stop(event);
2953
+ zoom(scale + (event.deltaY < 0 ? 0.25 : -0.25));
2954
+ },
2955
+ onPointerDown: (event) => {
2956
+ if (scale === 1) return;
2957
+ stop(event);
2958
+ event.currentTarget.setPointerCapture(event.pointerId);
2959
+ drag.current = { x: event.clientX, y: event.clientY, px: pan.x, py: pan.y };
2960
+ },
2961
+ onPointerMove: (event) => {
2962
+ if (!drag.current) return;
2963
+ setPan({
2964
+ x: drag.current.px + event.clientX - drag.current.x,
2965
+ y: drag.current.py + event.clientY - drag.current.y
2966
+ });
2967
+ },
2968
+ onPointerUp: () => {
2969
+ drag.current = null;
2970
+ }
2869
2971
  }
2870
- }
2871
- ) : kind === "pdf" || kind === "text" ? /* @__PURE__ */ jsxRuntime.jsx(
2872
- "iframe",
2873
- {
2874
- src,
2875
- title: file.name,
2876
- className: "h-full w-full max-w-5xl bg-white",
2877
- onClick: stop
2878
- }
2879
- ) : kind === "video" ? /* @__PURE__ */ jsxRuntime.jsx(
2880
- "video",
2881
- {
2882
- controls: true,
2883
- playsInline: true,
2884
- preload: "metadata",
2885
- src,
2886
- className: "max-h-full max-w-full",
2887
- onClick: stop,
2888
- children: labels.previewUnavailable
2889
- }
2890
- ) : kind === "audio" ? /* @__PURE__ */ jsxRuntime.jsx("audio", { controls: true, preload: "metadata", src, className: "w-full max-w-md", onClick: stop }) : /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-white/70", children: labels.previewUnavailable }) }),
2972
+ ) : kind === "pdf" ? /* @__PURE__ */ jsxRuntime.jsx(PdfFrame, { src, title: file.name, onClick: stop }) : kind === "text" ? /* @__PURE__ */ jsxRuntime.jsx(
2973
+ "iframe",
2974
+ {
2975
+ src,
2976
+ title: file.name,
2977
+ sandbox: "",
2978
+ referrerPolicy: "no-referrer",
2979
+ className: ready ? "h-full w-full max-w-5xl bg-white" : "absolute h-px w-px max-w-5xl opacity-0",
2980
+ onClick: stop,
2981
+ onLoad: () => setReady(true),
2982
+ onError: () => setReady(true)
2983
+ }
2984
+ ) : kind === "video" ? /* @__PURE__ */ jsxRuntime.jsx(
2985
+ "video",
2986
+ {
2987
+ controls: true,
2988
+ playsInline: true,
2989
+ preload: "metadata",
2990
+ src,
2991
+ className: `max-h-full max-w-full ${ready ? "" : "absolute h-px w-px opacity-0"}`,
2992
+ onClick: stop,
2993
+ onLoadedData: () => setReady(true),
2994
+ onError: () => setReady(true),
2995
+ children: labels.previewUnavailable
2996
+ }
2997
+ ) : kind === "audio" ? /* @__PURE__ */ jsxRuntime.jsx(
2998
+ "audio",
2999
+ {
3000
+ controls: true,
3001
+ preload: "metadata",
3002
+ src,
3003
+ className: `w-full max-w-md ${ready ? "" : "absolute h-px w-px opacity-0"}`,
3004
+ onClick: stop,
3005
+ onLoadedMetadata: () => setReady(true),
3006
+ onError: () => setReady(true)
3007
+ }
3008
+ ) : /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-white/70", children: labels.previewUnavailable })
3009
+ ] }),
2891
3010
  kind === "image" ? /* @__PURE__ */ jsxRuntime.jsxs(
2892
3011
  "div",
2893
3012
  {