@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/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { createContext, createElement, useContext, useRef, useState, useLayoutEffect, useEffect, useCallback, useMemo } from 'react';
2
2
  import { useFileExplorer } from '@vryzel/file-next-headless';
3
- import { Music, FileCode, FileVideo, FileAudio, FileImage, Archive, FileText, ChevronRight, File, Folder, Link2, MoreHorizontal, ChevronUp, ChevronDown, List, LayoutGrid, Search, Upload, FolderPlus, Trash, Copy, RotateCcw, X, Eye, Download, Pencil, ListChecks, Inbox, AlertCircleIcon, Minus, Plus } from 'lucide-react';
3
+ import { Music, FileCode, FileVideo, FileAudio, FileImage, Archive, FileText, ChevronRight, File, Folder, Link2, MoreHorizontal, ChevronUp, ChevronDown, List, LayoutGrid, Search, Upload, FolderPlus, Trash, Copy, RotateCcw, X, Eye, Download, Pencil, ListChecks, ListX, Inbox, AlertCircleIcon, Minus, Plus } from 'lucide-react';
4
4
  import { clsx } from 'clsx';
5
5
  import { twMerge } from 'tailwind-merge';
6
6
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
@@ -1446,33 +1446,51 @@ function SelectionActions({
1446
1446
  }
1447
1447
  )
1448
1448
  ] }),
1449
- /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-1.5", children: trashOpen ? /* @__PURE__ */ jsxs(Fragment, { children: [
1450
- /* @__PURE__ */ jsxs("button", { type: "button", className: btn2, onClick: onRestore, children: [
1451
- /* @__PURE__ */ jsx(RotateCcw, { "aria-hidden": "true", className: "size-3.5" }),
1452
- labels.restore
1449
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap gap-1.5", children: [
1450
+ trashOpen ? /* @__PURE__ */ jsxs(Fragment, { children: [
1451
+ /* @__PURE__ */ jsxs("button", { type: "button", className: btn2, onClick: onRestore, children: [
1452
+ /* @__PURE__ */ jsx(RotateCcw, { "aria-hidden": "true", className: "size-3.5" }),
1453
+ labels.restore
1454
+ ] }),
1455
+ onPurge ? /* @__PURE__ */ jsxs("button", { type: "button", className: cn(btn2, "text-destructive"), onClick: onPurge, children: [
1456
+ /* @__PURE__ */ jsx(X, { "aria-hidden": "true", className: "size-3.5" }),
1457
+ labels.deleteForever
1458
+ ] }) : null
1459
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
1460
+ onDownload && downloadable.length > 0 ? /* @__PURE__ */ jsxs("button", { type: "button", className: btn2, onClick: () => downloadable.forEach(onDownload), children: [
1461
+ /* @__PURE__ */ jsx(Download, { "aria-hidden": "true", className: "size-3.5" }),
1462
+ labels.download
1463
+ ] }) : null,
1464
+ onCopy ? /* @__PURE__ */ jsxs("button", { type: "button", className: btn2, onClick: onCopy, children: [
1465
+ /* @__PURE__ */ jsx(Copy, { "aria-hidden": "true", className: "size-3.5" }),
1466
+ labels.copy
1467
+ ] }) : null,
1468
+ onShare && downloadable.length > 0 ? /* @__PURE__ */ jsxs("button", { type: "button", className: btn2, onClick: onShare, children: [
1469
+ /* @__PURE__ */ jsx(Link2, { "aria-hidden": "true", className: "size-3.5" }),
1470
+ labels.share
1471
+ ] }) : null,
1472
+ /* @__PURE__ */ jsxs("button", { type: "button", disabled: hasProtected, className: cn(btn2, "text-destructive"), onClick: onDelete, children: [
1473
+ /* @__PURE__ */ jsx(X, { "aria-hidden": "true", className: "size-3.5" }),
1474
+ labels.moveToTrash
1475
+ ] })
1453
1476
  ] }),
1454
- onPurge ? /* @__PURE__ */ jsxs("button", { type: "button", className: cn(btn2, "text-destructive"), onClick: onPurge, children: [
1455
- /* @__PURE__ */ jsx(X, { "aria-hidden": "true", className: "size-3.5" }),
1456
- labels.deleteForever
1457
- ] }) : null
1458
- ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
1459
- onDownload && downloadable.length > 0 ? /* @__PURE__ */ jsxs("button", { type: "button", className: btn2, onClick: () => downloadable.forEach(onDownload), children: [
1460
- /* @__PURE__ */ jsx(Download, { "aria-hidden": "true", className: "size-3.5" }),
1461
- labels.download
1462
- ] }) : null,
1463
- onCopy ? /* @__PURE__ */ jsxs("button", { type: "button", className: btn2, onClick: onCopy, children: [
1464
- /* @__PURE__ */ jsx(Copy, { "aria-hidden": "true", className: "size-3.5" }),
1465
- labels.copy
1466
- ] }) : null,
1467
- onShare && downloadable.length > 0 ? /* @__PURE__ */ jsxs("button", { type: "button", className: btn2, onClick: onShare, children: [
1468
- /* @__PURE__ */ jsx(Link2, { "aria-hidden": "true", className: "size-3.5" }),
1469
- labels.share
1470
- ] }) : null,
1471
- /* @__PURE__ */ jsxs("button", { type: "button", disabled: hasProtected, className: cn(btn2, "text-destructive"), onClick: onDelete, children: [
1472
- /* @__PURE__ */ jsx(X, { "aria-hidden": "true", className: "size-3.5" }),
1473
- labels.moveToTrash
1474
- ] })
1475
- ] }) })
1477
+ /* @__PURE__ */ jsxs(
1478
+ "button",
1479
+ {
1480
+ type: "button",
1481
+ className: btn2,
1482
+ onClick: (event) => {
1483
+ event.stopPropagation();
1484
+ swallowClickThrough();
1485
+ onClear();
1486
+ },
1487
+ children: [
1488
+ /* @__PURE__ */ jsx(ListX, { "aria-hidden": "true", className: "size-3.5" }),
1489
+ labels.clearSelection
1490
+ ]
1491
+ }
1492
+ )
1493
+ ] })
1476
1494
  ] });
1477
1495
  }
1478
1496
  function ExplorerClipboardToast({
@@ -2753,6 +2771,15 @@ function clampPreviewZoom(scale) {
2753
2771
  function canPreviewFile(file) {
2754
2772
  return previewKind(file.mimeType, file.name) !== "none";
2755
2773
  }
2774
+ function pdfFrameMode(src, pageOrigin) {
2775
+ try {
2776
+ const url = new URL(src, pageOrigin);
2777
+ if (url.protocol !== "http:" && url.protocol !== "https:") return "blob";
2778
+ return url.origin === new URL(pageOrigin).origin ? "blob" : "direct";
2779
+ } catch {
2780
+ return "blob";
2781
+ }
2782
+ }
2756
2783
  function previewKind(mimeType, name) {
2757
2784
  const mime = mimeType.toLowerCase();
2758
2785
  const ext = name.split(".").pop()?.toLowerCase() ?? "";
@@ -2768,6 +2795,74 @@ function previewKind(mimeType, name) {
2768
2795
  if (mime.startsWith("text/") || ["txt", "csv", "md"].includes(ext)) return "text";
2769
2796
  return "none";
2770
2797
  }
2798
+ function PreviewSpinner({ label }) {
2799
+ return /* @__PURE__ */ jsxs("div", { role: "status", "aria-label": label, className: "absolute inset-0 z-10 flex items-center justify-center", children: [
2800
+ /* @__PURE__ */ jsx("span", { className: "sr-only", children: label }),
2801
+ /* @__PURE__ */ jsx(
2802
+ "span",
2803
+ {
2804
+ "aria-hidden": "true",
2805
+ className: "size-8 rounded-full border-2 border-white/25 border-t-white motion-safe:animate-spin"
2806
+ }
2807
+ )
2808
+ ] });
2809
+ }
2810
+ function PdfFrame({
2811
+ src,
2812
+ title,
2813
+ onClick
2814
+ }) {
2815
+ const labels = useExplorerLabels();
2816
+ const [frameSrc, setFrameSrc] = useState(null);
2817
+ const [failed, setFailed] = useState(false);
2818
+ const [painted, setPainted] = useState(false);
2819
+ useEffect(() => {
2820
+ setPainted(false);
2821
+ if (pdfFrameMode(src, window.location.origin) === "direct") {
2822
+ setFrameSrc(src);
2823
+ setFailed(false);
2824
+ return;
2825
+ }
2826
+ const controller = new AbortController();
2827
+ let cancelled = false;
2828
+ let objectUrl = null;
2829
+ setFrameSrc(null);
2830
+ setFailed(false);
2831
+ void fetch(src, { credentials: "same-origin", signal: controller.signal }).then(async (response) => {
2832
+ if (!response.ok) throw new Error(String(response.status));
2833
+ const blob = new Blob([await response.arrayBuffer()], { type: "application/pdf" });
2834
+ if (cancelled) return;
2835
+ objectUrl = URL.createObjectURL(blob);
2836
+ setFrameSrc(objectUrl);
2837
+ }).catch((error) => {
2838
+ if (cancelled || error instanceof DOMException && error.name === "AbortError") return;
2839
+ setFailed(true);
2840
+ });
2841
+ return () => {
2842
+ cancelled = true;
2843
+ controller.abort();
2844
+ if (objectUrl) URL.revokeObjectURL(objectUrl);
2845
+ };
2846
+ }, [src]);
2847
+ if (failed) {
2848
+ return /* @__PURE__ */ jsx("p", { className: "text-sm text-white/70", children: labels.previewUnavailable });
2849
+ }
2850
+ return /* @__PURE__ */ jsxs("div", { className: "relative h-full w-full max-w-5xl", children: [
2851
+ !painted ? /* @__PURE__ */ jsx(PreviewSpinner, { label: labels.loading }) : null,
2852
+ frameSrc ? /* @__PURE__ */ jsx(
2853
+ "iframe",
2854
+ {
2855
+ src: frameSrc,
2856
+ title,
2857
+ referrerPolicy: "no-referrer",
2858
+ className: painted ? "h-full w-full bg-white" : "absolute h-px w-px opacity-0",
2859
+ onClick,
2860
+ onLoad: () => setPainted(true),
2861
+ onError: () => setPainted(true)
2862
+ }
2863
+ ) : null
2864
+ ] });
2865
+ }
2771
2866
  function FilePreviewDialog({
2772
2867
  file,
2773
2868
  src,
@@ -2777,11 +2872,13 @@ function FilePreviewDialog({
2777
2872
  const labels = useExplorerLabels();
2778
2873
  const [scale, setScale] = useState(1);
2779
2874
  const [pan, setPan] = useState({ x: 0, y: 0 });
2875
+ const [ready, setReady] = useState(false);
2780
2876
  const drag = useRef(null);
2781
2877
  const closeRef = useRef(null);
2782
2878
  useEffect(() => {
2783
2879
  setScale(1);
2784
2880
  setPan({ x: 0, y: 0 });
2881
+ setReady(false);
2785
2882
  closeRef.current?.focus();
2786
2883
  }, [file?.id, src]);
2787
2884
  useEffect(() => {
@@ -2831,61 +2928,83 @@ function FilePreviewDialog({
2831
2928
  }
2832
2929
  )
2833
2930
  ] }),
2834
- /* @__PURE__ */ jsx("div", { className: "flex h-full items-center justify-center px-4 pt-12 pb-16", children: kind === "image" ? /* @__PURE__ */ jsx(
2835
- "img",
2836
- {
2837
- src,
2838
- alt: file.name,
2839
- draggable: false,
2840
- className: "max-h-full max-w-full cursor-zoom-in select-none object-contain",
2841
- style: { transform: `translate(${pan.x}px, ${pan.y}px) scale(${scale})` },
2842
- onClick: stop,
2843
- onDoubleClick: (event) => {
2844
- stop(event);
2845
- zoom(scale === 1 ? 2 : 1);
2846
- },
2847
- onWheel: (event) => {
2848
- event.preventDefault();
2849
- stop(event);
2850
- zoom(scale + (event.deltaY < 0 ? 0.25 : -0.25));
2851
- },
2852
- onPointerDown: (event) => {
2853
- if (scale === 1) return;
2854
- stop(event);
2855
- event.currentTarget.setPointerCapture(event.pointerId);
2856
- drag.current = { x: event.clientX, y: event.clientY, px: pan.x, py: pan.y };
2857
- },
2858
- onPointerMove: (event) => {
2859
- if (!drag.current) return;
2860
- setPan({
2861
- x: drag.current.px + event.clientX - drag.current.x,
2862
- y: drag.current.py + event.clientY - drag.current.y
2863
- });
2864
- },
2865
- onPointerUp: () => {
2866
- drag.current = null;
2931
+ /* @__PURE__ */ jsxs("div", { className: "relative flex h-full w-full items-center justify-center px-4 pt-12 pb-16", children: [
2932
+ kind !== "pdf" && !ready ? /* @__PURE__ */ jsx(PreviewSpinner, { label: labels.loading }) : null,
2933
+ kind === "image" ? /* @__PURE__ */ jsx(
2934
+ "img",
2935
+ {
2936
+ src,
2937
+ alt: file.name,
2938
+ draggable: false,
2939
+ className: `max-h-full max-w-full cursor-zoom-in select-none object-contain ${ready ? "" : "absolute h-px w-px opacity-0"}`,
2940
+ onLoad: () => setReady(true),
2941
+ onError: () => setReady(true),
2942
+ style: { transform: `translate(${pan.x}px, ${pan.y}px) scale(${scale})` },
2943
+ onClick: stop,
2944
+ onDoubleClick: (event) => {
2945
+ stop(event);
2946
+ zoom(scale === 1 ? 2 : 1);
2947
+ },
2948
+ onWheel: (event) => {
2949
+ event.preventDefault();
2950
+ stop(event);
2951
+ zoom(scale + (event.deltaY < 0 ? 0.25 : -0.25));
2952
+ },
2953
+ onPointerDown: (event) => {
2954
+ if (scale === 1) return;
2955
+ stop(event);
2956
+ event.currentTarget.setPointerCapture(event.pointerId);
2957
+ drag.current = { x: event.clientX, y: event.clientY, px: pan.x, py: pan.y };
2958
+ },
2959
+ onPointerMove: (event) => {
2960
+ if (!drag.current) return;
2961
+ setPan({
2962
+ x: drag.current.px + event.clientX - drag.current.x,
2963
+ y: drag.current.py + event.clientY - drag.current.y
2964
+ });
2965
+ },
2966
+ onPointerUp: () => {
2967
+ drag.current = null;
2968
+ }
2867
2969
  }
2868
- }
2869
- ) : kind === "pdf" || kind === "text" ? /* @__PURE__ */ jsx(
2870
- "iframe",
2871
- {
2872
- src,
2873
- title: file.name,
2874
- className: "h-full w-full max-w-5xl bg-white",
2875
- onClick: stop
2876
- }
2877
- ) : kind === "video" ? /* @__PURE__ */ jsx(
2878
- "video",
2879
- {
2880
- controls: true,
2881
- playsInline: true,
2882
- preload: "metadata",
2883
- src,
2884
- className: "max-h-full max-w-full",
2885
- onClick: stop,
2886
- children: labels.previewUnavailable
2887
- }
2888
- ) : kind === "audio" ? /* @__PURE__ */ jsx("audio", { controls: true, preload: "metadata", src, className: "w-full max-w-md", onClick: stop }) : /* @__PURE__ */ jsx("p", { className: "text-sm text-white/70", children: labels.previewUnavailable }) }),
2970
+ ) : kind === "pdf" ? /* @__PURE__ */ jsx(PdfFrame, { src, title: file.name, onClick: stop }) : kind === "text" ? /* @__PURE__ */ jsx(
2971
+ "iframe",
2972
+ {
2973
+ src,
2974
+ title: file.name,
2975
+ sandbox: "",
2976
+ referrerPolicy: "no-referrer",
2977
+ className: ready ? "h-full w-full max-w-5xl bg-white" : "absolute h-px w-px max-w-5xl opacity-0",
2978
+ onClick: stop,
2979
+ onLoad: () => setReady(true),
2980
+ onError: () => setReady(true)
2981
+ }
2982
+ ) : kind === "video" ? /* @__PURE__ */ jsx(
2983
+ "video",
2984
+ {
2985
+ controls: true,
2986
+ playsInline: true,
2987
+ preload: "metadata",
2988
+ src,
2989
+ className: `max-h-full max-w-full ${ready ? "" : "absolute h-px w-px opacity-0"}`,
2990
+ onClick: stop,
2991
+ onLoadedData: () => setReady(true),
2992
+ onError: () => setReady(true),
2993
+ children: labels.previewUnavailable
2994
+ }
2995
+ ) : kind === "audio" ? /* @__PURE__ */ jsx(
2996
+ "audio",
2997
+ {
2998
+ controls: true,
2999
+ preload: "metadata",
3000
+ src,
3001
+ className: `w-full max-w-md ${ready ? "" : "absolute h-px w-px opacity-0"}`,
3002
+ onClick: stop,
3003
+ onLoadedMetadata: () => setReady(true),
3004
+ onError: () => setReady(true)
3005
+ }
3006
+ ) : /* @__PURE__ */ jsx("p", { className: "text-sm text-white/70", children: labels.previewUnavailable })
3007
+ ] }),
2889
3008
  kind === "image" ? /* @__PURE__ */ jsxs(
2890
3009
  "div",
2891
3010
  {