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