@vryzel/file-next-ui 0.6.0 → 0.6.2

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
@@ -2773,6 +2773,15 @@ function clampPreviewZoom(scale) {
2773
2773
  function canPreviewFile(file) {
2774
2774
  return previewKind(file.mimeType, file.name) !== "none";
2775
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
+ }
2776
2785
  function previewKind(mimeType, name) {
2777
2786
  const mime = mimeType.toLowerCase();
2778
2787
  const ext = name.split(".").pop()?.toLowerCase() ?? "";
@@ -2788,6 +2797,74 @@ function previewKind(mimeType, name) {
2788
2797
  if (mime.startsWith("text/") || ["txt", "csv", "md"].includes(ext)) return "text";
2789
2798
  return "none";
2790
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: "h-full w-full bg-white",
2861
+ onClick,
2862
+ onLoad: () => setPainted(true),
2863
+ onError: () => setPainted(true)
2864
+ }
2865
+ ) : null
2866
+ ] });
2867
+ }
2791
2868
  function FilePreviewDialog({
2792
2869
  file,
2793
2870
  src,
@@ -2797,11 +2874,13 @@ function FilePreviewDialog({
2797
2874
  const labels = useExplorerLabels();
2798
2875
  const [scale, setScale] = react.useState(1);
2799
2876
  const [pan, setPan] = react.useState({ x: 0, y: 0 });
2877
+ const [ready, setReady] = react.useState(false);
2800
2878
  const drag = react.useRef(null);
2801
2879
  const closeRef = react.useRef(null);
2802
2880
  react.useEffect(() => {
2803
2881
  setScale(1);
2804
2882
  setPan({ x: 0, y: 0 });
2883
+ setReady(false);
2805
2884
  closeRef.current?.focus();
2806
2885
  }, [file?.id, src]);
2807
2886
  react.useEffect(() => {
@@ -2851,63 +2930,83 @@ function FilePreviewDialog({
2851
2930
  }
2852
2931
  )
2853
2932
  ] }),
2854
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex h-full items-center justify-center px-4 pt-12 pb-16", children: kind === "image" ? /* @__PURE__ */ jsxRuntime.jsx(
2855
- "img",
2856
- {
2857
- src,
2858
- alt: file.name,
2859
- draggable: false,
2860
- className: "max-h-full max-w-full cursor-zoom-in select-none object-contain",
2861
- style: { transform: `translate(${pan.x}px, ${pan.y}px) scale(${scale})` },
2862
- onClick: stop,
2863
- onDoubleClick: (event) => {
2864
- stop(event);
2865
- zoom(scale === 1 ? 2 : 1);
2866
- },
2867
- onWheel: (event) => {
2868
- event.preventDefault();
2869
- stop(event);
2870
- zoom(scale + (event.deltaY < 0 ? 0.25 : -0.25));
2871
- },
2872
- onPointerDown: (event) => {
2873
- if (scale === 1) return;
2874
- stop(event);
2875
- event.currentTarget.setPointerCapture(event.pointerId);
2876
- drag.current = { x: event.clientX, y: event.clientY, px: pan.x, py: pan.y };
2877
- },
2878
- onPointerMove: (event) => {
2879
- if (!drag.current) return;
2880
- setPan({
2881
- x: drag.current.px + event.clientX - drag.current.x,
2882
- y: drag.current.py + event.clientY - drag.current.y
2883
- });
2884
- },
2885
- onPointerUp: () => {
2886
- 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
+ }
2887
2971
  }
2888
- }
2889
- ) : kind === "pdf" || kind === "text" ? /* @__PURE__ */ jsxRuntime.jsx(
2890
- "iframe",
2891
- {
2892
- src,
2893
- title: file.name,
2894
- sandbox: "",
2895
- referrerPolicy: "no-referrer",
2896
- className: "h-full w-full max-w-5xl bg-white",
2897
- onClick: stop
2898
- }
2899
- ) : kind === "video" ? /* @__PURE__ */ jsxRuntime.jsx(
2900
- "video",
2901
- {
2902
- controls: true,
2903
- playsInline: true,
2904
- preload: "metadata",
2905
- src,
2906
- className: "max-h-full max-w-full",
2907
- onClick: stop,
2908
- children: labels.previewUnavailable
2909
- }
2910
- ) : 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
+ ] }),
2911
3010
  kind === "image" ? /* @__PURE__ */ jsxRuntime.jsxs(
2912
3011
  "div",
2913
3012
  {