Mejorar la vistaprevia PDF
This commit is contained in:
parent
97cafb3818
commit
f8bd2f3078
@ -276,12 +276,36 @@ export const useProformaDetailPageController = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleOpenPreviewPdf = () => {
|
const handleOpenPreviewPdf = async () => {
|
||||||
if (!previewPdfUrl) {
|
if (previewPdfUrl) {
|
||||||
|
window.open(previewPdfUrl, "_blank", "noopener,noreferrer");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
window.open(previewPdfUrl, "_blank", "noopener,noreferrer");
|
const result = await previewPdfQuery.refetch();
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
showErrorToast(
|
||||||
|
t("proformas.detail.document.error_title"),
|
||||||
|
result.error instanceof Error ? result.error.message : t("proformas.detail.document.error")
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isUsablePdfBlob(result.data)) {
|
||||||
|
showErrorToast(
|
||||||
|
t("proformas.detail.document.empty_title"),
|
||||||
|
t("proformas.detail.document.empty_description")
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const objectUrl = URL.createObjectURL(result.data);
|
||||||
|
window.open(objectUrl, "_blank", "noopener,noreferrer");
|
||||||
|
|
||||||
|
window.setTimeout(() => {
|
||||||
|
URL.revokeObjectURL(objectUrl);
|
||||||
|
}, 60_000);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePrintPreview = () => {
|
const handlePrintPreview = () => {
|
||||||
@ -379,6 +403,7 @@ export const useProformaDetailPageController = () => {
|
|||||||
changeStatus: t("pages.proformas.list.actions.change_status"),
|
changeStatus: t("pages.proformas.list.actions.change_status"),
|
||||||
issue: t("pages.proformas.list.actions.issue"),
|
issue: t("pages.proformas.list.actions.issue"),
|
||||||
duplicate: t("pages.proformas.list.actions.duplicate"),
|
duplicate: t("pages.proformas.list.actions.duplicate"),
|
||||||
|
openPdf: t("proformas.detail.document.actions.open"),
|
||||||
downloadPdf: t("pages.proformas.list.actions.download_pdf"),
|
downloadPdf: t("pages.proformas.list.actions.download_pdf"),
|
||||||
archive: t("pages.proformas.list.actions.archive"),
|
archive: t("pages.proformas.list.actions.archive"),
|
||||||
unarchive: t("pages.proformas.list.actions.unarchive"),
|
unarchive: t("pages.proformas.list.actions.unarchive"),
|
||||||
@ -398,6 +423,7 @@ export const useProformaDetailPageController = () => {
|
|||||||
loading: {
|
loading: {
|
||||||
issue: issueDialogCtrl.isSubmitting,
|
issue: issueDialogCtrl.isSubmitting,
|
||||||
duplicate: duplicateMutation.isPending,
|
duplicate: duplicateMutation.isPending,
|
||||||
|
"open-pdf": previewPdfQuery.isFetching && !previewPdfBlob,
|
||||||
"download-pdf": previewPdfQuery.isFetching && !previewPdfBlob,
|
"download-pdf": previewPdfQuery.isFetching && !previewPdfBlob,
|
||||||
archive: archiveMutation.isPending,
|
archive: archiveMutation.isPending,
|
||||||
unarchive: unarchiveMutation.isPending,
|
unarchive: unarchiveMutation.isPending,
|
||||||
@ -438,6 +464,14 @@ export const useProformaDetailPageController = () => {
|
|||||||
case "duplicate":
|
case "duplicate":
|
||||||
handleDuplicate();
|
handleDuplicate();
|
||||||
return;
|
return;
|
||||||
|
case "open-pdf":
|
||||||
|
handleOpenPreviewPdf().catch((error: unknown) => {
|
||||||
|
showErrorToast(
|
||||||
|
t("proformas.detail.document.error_title"),
|
||||||
|
error instanceof Error ? error.message : t("proformas.detail.document.error")
|
||||||
|
);
|
||||||
|
});
|
||||||
|
return;
|
||||||
case "download-pdf":
|
case "download-pdf":
|
||||||
handleDownload();
|
handleDownload();
|
||||||
return;
|
return;
|
||||||
|
|||||||
@ -135,6 +135,7 @@ export type ProformaDetailActionKey =
|
|||||||
| "change-status"
|
| "change-status"
|
||||||
| "issue"
|
| "issue"
|
||||||
| "duplicate"
|
| "duplicate"
|
||||||
|
| "open-pdf"
|
||||||
| "download-pdf"
|
| "download-pdf"
|
||||||
| "archive"
|
| "archive"
|
||||||
| "unarchive";
|
| "unarchive";
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import {
|
|||||||
CopyIcon,
|
CopyIcon,
|
||||||
DownloadIcon,
|
DownloadIcon,
|
||||||
FileOutputIcon,
|
FileOutputIcon,
|
||||||
|
ExternalLinkIcon,
|
||||||
MoreHorizontalIcon,
|
MoreHorizontalIcon,
|
||||||
PencilIcon,
|
PencilIcon,
|
||||||
RefreshCwIcon,
|
RefreshCwIcon,
|
||||||
@ -30,9 +31,7 @@ export const ProformaDetailHeaderActions = ({
|
|||||||
onExecuteAction,
|
onExecuteAction,
|
||||||
}: ProformaDetailHeaderActionsProps) => {
|
}: ProformaDetailHeaderActionsProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const visibleItems = actions.items.filter(
|
const visibleItems = actions.items.filter((item) => item.visibility === "visible");
|
||||||
(item) => item.visibility === "visible" && item.key !== "download-pdf"
|
|
||||||
);
|
|
||||||
const primaryAction = visibleItems.find((item) => item.placement === "primary") ?? null;
|
const primaryAction = visibleItems.find((item) => item.placement === "primary") ?? null;
|
||||||
const secondaryActions = visibleItems.filter((item) => item.placement === "secondary");
|
const secondaryActions = visibleItems.filter((item) => item.placement === "secondary");
|
||||||
const menuActions = visibleItems.filter((item) => item.placement === "menu");
|
const menuActions = visibleItems.filter((item) => item.placement === "menu");
|
||||||
@ -45,6 +44,8 @@ export const ProformaDetailHeaderActions = ({
|
|||||||
return FileOutputIcon;
|
return FileOutputIcon;
|
||||||
case "duplicate":
|
case "duplicate":
|
||||||
return CopyIcon;
|
return CopyIcon;
|
||||||
|
case "open-pdf":
|
||||||
|
return ExternalLinkIcon;
|
||||||
case "download-pdf":
|
case "download-pdf":
|
||||||
return DownloadIcon;
|
return DownloadIcon;
|
||||||
case "archive":
|
case "archive":
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { PDFViewer } from "@repo/rdx-ui/components";
|
import { PDFViewer } from "@repo/rdx-ui/components";
|
||||||
import { Alert, AlertDescription, AlertTitle, Button } from "@repo/shadcn-ui/components";
|
import { Alert, AlertDescription, AlertTitle, Button } from "@repo/shadcn-ui/components";
|
||||||
import { cn } from "@repo/shadcn-ui/lib/utils";
|
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||||
import { useId } from "react";
|
import { useEffect, useId, useState } from "react";
|
||||||
|
|
||||||
import { useTranslation } from "../../../../i18n";
|
import { useTranslation } from "../../../../i18n";
|
||||||
import type { ProformaDetailDocumentState, ProformaDetailDocumentViewModel } from "../../entities";
|
import type { ProformaDetailDocumentState, ProformaDetailDocumentViewModel } from "../../entities";
|
||||||
@ -25,22 +25,51 @@ export const ProformaDocumentPane = ({
|
|||||||
}: ProformaDocumentPaneProps) => {
|
}: ProformaDocumentPaneProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const headingId = useId();
|
const headingId = useId();
|
||||||
|
const [isDesktopViewport, setIsDesktopViewport] = useState(() =>
|
||||||
|
typeof window === "undefined" ? false : window.matchMedia("(min-width: 1024px)").matches
|
||||||
|
);
|
||||||
|
const supportsNativeInlinePdf =
|
||||||
|
typeof navigator !== "undefined" && navigator.pdfViewerEnabled === true;
|
||||||
|
const shouldUseNativeInlinePdf = supportsNativeInlinePdf && isDesktopViewport;
|
||||||
const isRefreshing = state.isFetching && Boolean(document.src);
|
const isRefreshing = state.isFetching && Boolean(document.src);
|
||||||
const shouldRenderBlockingError = state.isError && !document.src;
|
const shouldRenderBlockingError = state.isError && !document.src;
|
||||||
const shouldRenderStaleError = state.isError && Boolean(document.src);
|
const shouldRenderStaleError = state.isError && Boolean(document.src);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof window === "undefined") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mediaQuery = window.matchMedia("(min-width: 1024px)");
|
||||||
|
const handleChange = (event: MediaQueryListEvent) => {
|
||||||
|
setIsDesktopViewport(event.matches);
|
||||||
|
};
|
||||||
|
|
||||||
|
setIsDesktopViewport(mediaQuery.matches);
|
||||||
|
mediaQuery.addEventListener("change", handleChange);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
mediaQuery.removeEventListener("change", handleChange);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
aria-labelledby={headingId}
|
aria-labelledby={headingId}
|
||||||
className="min-w-0 overflow-visible border-b bg-muted/30 lg:min-h-0 lg:overflow-hidden lg:border-b-0"
|
className="min-w-0 overflow-visible border-b bg-muted/30 lg:min-h-0 lg:overflow-hidden lg:border-b-0"
|
||||||
>
|
>
|
||||||
<div className="h-full min-h-0 p-4 sm:p-6">
|
<div
|
||||||
|
className={cn(
|
||||||
|
"h-full min-h-0",
|
||||||
|
shouldUseNativeInlinePdf ? "flex flex-col overflow-hidden p-0" : "p-2 sm:p-4 lg:p-6"
|
||||||
|
)}
|
||||||
|
>
|
||||||
<h2 className="sr-only" id={headingId}>
|
<h2 className="sr-only" id={headingId}>
|
||||||
{document.title}
|
{document.title}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
{shouldRenderStaleError ? (
|
{shouldRenderStaleError ? (
|
||||||
<Alert className="mb-4">
|
<Alert className={cn(shouldUseNativeInlinePdf ? "m-4 mb-0 shrink-0" : "mb-4")}>
|
||||||
<AlertTitle>{t("proformas.detail.document.error_title")}</AlertTitle>
|
<AlertTitle>{t("proformas.detail.document.error_title")}</AlertTitle>
|
||||||
<AlertDescription className="space-y-3">
|
<AlertDescription className="space-y-3">
|
||||||
<p>{state.errorMessage ?? t("proformas.detail.document.error")}</p>
|
<p>{state.errorMessage ?? t("proformas.detail.document.error")}</p>
|
||||||
@ -55,7 +84,8 @@ export const ProformaDocumentPane = ({
|
|||||||
|
|
||||||
<PDFViewer
|
<PDFViewer
|
||||||
className={cn(
|
className={cn(
|
||||||
"min-h-[32rem] overflow-hidden border-border/80 bg-background shadow-none lg:h-full lg:min-h-full"
|
"min-h-[56svh] overflow-hidden border-border/80 bg-background shadow-none sm:min-h-[32rem] lg:h-full lg:min-h-full",
|
||||||
|
shouldUseNativeInlinePdf && "flex-1 min-h-0"
|
||||||
)}
|
)}
|
||||||
defaultZoom="fit"
|
defaultZoom="fit"
|
||||||
description={document.description}
|
description={document.description}
|
||||||
@ -81,8 +111,12 @@ export const ProformaDocumentPane = ({
|
|||||||
onOpen={onOpen}
|
onOpen={onOpen}
|
||||||
onPrint={onPrint}
|
onPrint={onPrint}
|
||||||
onRetry={onRetry}
|
onRetry={onRetry}
|
||||||
|
showDownloadAction={false}
|
||||||
|
showOpenAction={false}
|
||||||
|
showPrinterAction={false}
|
||||||
src={document.src}
|
src={document.src}
|
||||||
title={document.title}
|
title={document.title}
|
||||||
|
variant={shouldUseNativeInlinePdf ? "native-inline" : "panel"}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@ -11,6 +11,7 @@ export interface ProformaDetailActionLabels {
|
|||||||
changeStatus: string;
|
changeStatus: string;
|
||||||
issue: string;
|
issue: string;
|
||||||
duplicate: string;
|
duplicate: string;
|
||||||
|
openPdf: string;
|
||||||
downloadPdf: string;
|
downloadPdf: string;
|
||||||
archive: string;
|
archive: string;
|
||||||
unarchive: string;
|
unarchive: string;
|
||||||
@ -120,10 +121,19 @@ export const buildProformaDetailActions = ({
|
|||||||
loading.duplicate ?? false,
|
loading.duplicate ?? false,
|
||||||
null
|
null
|
||||||
),
|
),
|
||||||
|
buildAction(
|
||||||
|
"open-pdf",
|
||||||
|
labels.openPdf,
|
||||||
|
"secondary",
|
||||||
|
"visible",
|
||||||
|
!(loading["open-pdf"] ?? false),
|
||||||
|
loading["open-pdf"] ?? false,
|
||||||
|
loading["open-pdf"] ? labels.disabledReasons.operationInProgress : null
|
||||||
|
),
|
||||||
buildAction(
|
buildAction(
|
||||||
"download-pdf",
|
"download-pdf",
|
||||||
labels.downloadPdf,
|
labels.downloadPdf,
|
||||||
isApproved ? "secondary" : "secondary",
|
"secondary",
|
||||||
"visible",
|
"visible",
|
||||||
!(loading["download-pdf"] ?? false),
|
!(loading["download-pdf"] ?? false),
|
||||||
loading["download-pdf"] ?? false,
|
loading["download-pdf"] ?? false,
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
Spinner,
|
Spinner,
|
||||||
} from "@repo/shadcn-ui/components";
|
} from "@repo/shadcn-ui/components";
|
||||||
|
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||||
import {
|
import {
|
||||||
DownloadIcon,
|
DownloadIcon,
|
||||||
ExternalLinkIcon,
|
ExternalLinkIcon,
|
||||||
@ -47,6 +48,10 @@ type PDFViewerProps = {
|
|||||||
title?: string;
|
title?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
variant?: "panel" | "native-inline";
|
||||||
|
showOpenAction?: boolean;
|
||||||
|
showDownloadAction?: boolean;
|
||||||
|
showPrinterAction?: boolean;
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
isRefreshing?: boolean;
|
isRefreshing?: boolean;
|
||||||
error?: string | null;
|
error?: string | null;
|
||||||
@ -71,6 +76,10 @@ export function PDFViewer({
|
|||||||
title = "Vista previa",
|
title = "Vista previa",
|
||||||
description = "Documento PDF embebido en la pantalla.",
|
description = "Documento PDF embebido en la pantalla.",
|
||||||
className,
|
className,
|
||||||
|
variant = "panel",
|
||||||
|
showOpenAction = true,
|
||||||
|
showDownloadAction = true,
|
||||||
|
showPrinterAction = true,
|
||||||
loading = false,
|
loading = false,
|
||||||
isRefreshing = false,
|
isRefreshing = false,
|
||||||
error = null,
|
error = null,
|
||||||
@ -87,6 +96,8 @@ export function PDFViewer({
|
|||||||
const [zoom, setZoom] = useState<string>(() =>
|
const [zoom, setZoom] = useState<string>(() =>
|
||||||
defaultZoom === FIT_ZOOM_VALUE ? FIT_ZOOM_VALUE : String(clampZoom(defaultZoom, zoomOptions))
|
defaultZoom === FIT_ZOOM_VALUE ? FIT_ZOOM_VALUE : String(clampZoom(defaultZoom, zoomOptions))
|
||||||
);
|
);
|
||||||
|
const minZoom = Math.min(...zoomOptions);
|
||||||
|
const maxZoom = Math.max(...zoomOptions);
|
||||||
const resolvedLabels: PDFViewerLabels = {
|
const resolvedLabels: PDFViewerLabels = {
|
||||||
zoomOut: "Alejar",
|
zoomOut: "Alejar",
|
||||||
zoomIn: "Acercar",
|
zoomIn: "Acercar",
|
||||||
@ -109,9 +120,13 @@ export function PDFViewer({
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (variant === "native-inline") {
|
||||||
|
return src;
|
||||||
|
}
|
||||||
|
|
||||||
const separator = src.includes("#") ? "&" : "#";
|
const separator = src.includes("#") ? "&" : "#";
|
||||||
return `${src}${separator}toolbar=0&navpanes=0&scrollbar=1&view=FitH`;
|
return `${src}${separator}toolbar=0&navpanes=0&scrollbar=1&page=1&zoom=page-width`;
|
||||||
}, [src]);
|
}, [src, variant]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (zoom !== FIT_ZOOM_VALUE) {
|
if (zoom !== FIT_ZOOM_VALUE) {
|
||||||
@ -127,13 +142,9 @@ export function PDFViewer({
|
|||||||
const styles = window.getComputedStyle(element);
|
const styles = window.getComputedStyle(element);
|
||||||
const paddingX =
|
const paddingX =
|
||||||
Number.parseFloat(styles.paddingLeft) + Number.parseFloat(styles.paddingRight);
|
Number.parseFloat(styles.paddingLeft) + Number.parseFloat(styles.paddingRight);
|
||||||
const paddingY =
|
|
||||||
Number.parseFloat(styles.paddingTop) + Number.parseFloat(styles.paddingBottom);
|
|
||||||
const availableWidth = Math.max(element.clientWidth - paddingX, 320);
|
const availableWidth = Math.max(element.clientWidth - paddingX, 320);
|
||||||
const availableHeight = Math.max(element.clientHeight - paddingY, 320);
|
|
||||||
const widthScale = availableWidth / PDF_PAGE_WIDTH;
|
const widthScale = availableWidth / PDF_PAGE_WIDTH;
|
||||||
const heightScale = availableHeight / PDF_PAGE_HEIGHT;
|
const nextScale = Math.min(widthScale, maxZoom / 100);
|
||||||
const nextScale = Math.min(widthScale, heightScale, 1);
|
|
||||||
setFitZoom(Math.max(Math.round(nextScale * 100), 25));
|
setFitZoom(Math.max(Math.round(nextScale * 100), 25));
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -147,12 +158,11 @@ export function PDFViewer({
|
|||||||
observer.disconnect();
|
observer.disconnect();
|
||||||
window.removeEventListener("resize", updateFitZoom);
|
window.removeEventListener("resize", updateFitZoom);
|
||||||
};
|
};
|
||||||
}, [zoom]);
|
}, [maxZoom, zoom]);
|
||||||
|
|
||||||
const effectiveZoom = zoom === FIT_ZOOM_VALUE ? fitZoom : Number(zoom);
|
const effectiveZoom = zoom === FIT_ZOOM_VALUE ? fitZoom : Number(zoom);
|
||||||
const zoomScale = effectiveZoom / 100;
|
const zoomScale = effectiveZoom / 100;
|
||||||
const minZoom = Math.min(...zoomOptions);
|
const scaledPageHeight = PDF_PAGE_HEIGHT * zoomScale;
|
||||||
const maxZoom = Math.max(...zoomOptions);
|
|
||||||
|
|
||||||
const handleOpen = () => {
|
const handleOpen = () => {
|
||||||
if (onOpen) {
|
if (onOpen) {
|
||||||
@ -192,6 +202,97 @@ export function PDFViewer({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const viewerContent = (
|
||||||
|
<FormSectionGrid
|
||||||
|
className={cn("gap-y-6 md:grid-cols-1", variant === "native-inline" && "h-full")}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex-1 overflow-auto bg-muted p-1.5 sm:p-3 md:p-5",
|
||||||
|
variant === "native-inline" && "flex h-full min-h-0 flex-col bg-transparent p-0 md:p-0"
|
||||||
|
)}
|
||||||
|
ref={viewportRef}
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<div className="flex h-full min-h-[480px] items-center justify-center rounded-xl border border-dashed bg-background/80">
|
||||||
|
<div className="flex items-center gap-3 text-sm text-muted-foreground">
|
||||||
|
<Spinner className="size-4" />
|
||||||
|
{resolvedLabels.loading}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{!loading && error ? (
|
||||||
|
<div className="flex h-full min-h-[480px] items-center justify-center rounded-xl border border-dashed bg-background/80 px-6 text-center">
|
||||||
|
<Alert className="max-w-md text-left">
|
||||||
|
<AlertTitle>{resolvedLabels.errorTitle}</AlertTitle>
|
||||||
|
<AlertDescription className="space-y-4">
|
||||||
|
<p>{error}</p>
|
||||||
|
{onRetry ? (
|
||||||
|
<Button onClick={onRetry} type="button" variant="outline">
|
||||||
|
{resolvedLabels.retry}
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{!(loading || error) && resolvedSrc ? (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"mx-auto flex min-h-full justify-center",
|
||||||
|
variant === "native-inline" && "flex-1 min-h-0 w-full"
|
||||||
|
)}
|
||||||
|
style={variant === "native-inline" ? undefined : { minHeight: `${scaledPageHeight}px` }}
|
||||||
|
>
|
||||||
|
{variant === "native-inline" ? (
|
||||||
|
<iframe
|
||||||
|
className="h-full min-h-0 w-full bg-background"
|
||||||
|
src={resolvedSrc}
|
||||||
|
style={{ border: "0" }}
|
||||||
|
title={title}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
className="origin-top overflow-hidden rounded-lg border border-black/8 bg-white shadow-[0_10px_30px_rgba(15,23,42,0.10),0_2px_8px_rgba(15,23,42,0.06)] transition-transform"
|
||||||
|
style={{
|
||||||
|
transform: `scale(${zoomScale})`,
|
||||||
|
transformOrigin: "top center",
|
||||||
|
width: `${PDF_PAGE_WIDTH}px`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<iframe
|
||||||
|
className="block w-full bg-white"
|
||||||
|
src={resolvedSrc}
|
||||||
|
style={{ border: "0", height: `${PDF_PAGE_HEIGHT}px` }}
|
||||||
|
title={title}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{loading || error || resolvedSrc ? null : (
|
||||||
|
<div className="flex h-full min-h-[480px] items-center justify-center rounded-xl border border-dashed bg-background/80 px-6 text-center">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p className="font-medium text-foreground">{resolvedLabels.emptyTitle}</p>
|
||||||
|
<p className="text-sm text-muted-foreground">{resolvedLabels.emptyDescription}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</FormSectionGrid>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (variant === "native-inline") {
|
||||||
|
return (
|
||||||
|
<div className={cn("flex h-full min-h-[32rem] min-h-0 flex-col", className)}>
|
||||||
|
{viewerContent}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormSectionCard
|
<FormSectionCard
|
||||||
actions={
|
actions={
|
||||||
@ -248,31 +349,37 @@ export function PDFViewer({
|
|||||||
<ZoomInIcon className="size-4" />
|
<ZoomInIcon className="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button disabled={loading || !src} onClick={handleOpen} type="button" variant="outline">
|
{showOpenAction ? (
|
||||||
<ExternalLinkIcon className="mr-2 size-4" />
|
<Button disabled={loading || !src} onClick={handleOpen} type="button" variant="outline">
|
||||||
{resolvedLabels.open}
|
<ExternalLinkIcon className="mr-2 size-4" />
|
||||||
</Button>
|
{resolvedLabels.open}
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<Button
|
{showDownloadAction ? (
|
||||||
disabled={loading || !src}
|
<Button
|
||||||
onClick={handleDownload}
|
disabled={loading || !src}
|
||||||
type="button"
|
onClick={handleDownload}
|
||||||
variant="outline"
|
type="button"
|
||||||
>
|
variant="outline"
|
||||||
<DownloadIcon className="mr-2 size-4" />
|
>
|
||||||
{resolvedLabels.download}
|
<DownloadIcon className="mr-2 size-4" />
|
||||||
</Button>
|
{resolvedLabels.download}
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<Button
|
{showPrinterAction ? (
|
||||||
aria-label={resolvedLabels.print}
|
<Button
|
||||||
disabled={loading || !src}
|
aria-label={resolvedLabels.print}
|
||||||
onClick={handlePrint}
|
disabled={loading || !src}
|
||||||
size="icon-sm"
|
onClick={handlePrint}
|
||||||
type="button"
|
size="icon-sm"
|
||||||
variant="outline"
|
type="button"
|
||||||
>
|
variant="outline"
|
||||||
<PrinterIcon className="size-4" />
|
>
|
||||||
</Button>
|
<PrinterIcon className="size-4" />
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
|
||||||
{isRefreshing && src ? (
|
{isRefreshing && src ? (
|
||||||
<div className="inline-flex items-center gap-2 text-sm text-muted-foreground">
|
<div className="inline-flex items-center gap-2 text-sm text-muted-foreground">
|
||||||
@ -282,71 +389,14 @@ export function PDFViewer({
|
|||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
className="shadow-sm"
|
className={cn("shadow-sm", className)}
|
||||||
contentClassName="space-y-5"
|
contentClassName="space-y-3 px-2 sm:space-y-4 sm:px-4 md:space-y-5 md:px-6"
|
||||||
description={description}
|
description={description}
|
||||||
|
headerClassName="px-2 sm:px-4 md:px-6"
|
||||||
icon={<EyeIcon className="size-5" />}
|
icon={<EyeIcon className="size-5" />}
|
||||||
title={title}
|
title={title}
|
||||||
>
|
>
|
||||||
<FormSectionGrid className="gap-y-6 md:grid-cols-1">
|
{viewerContent}
|
||||||
<div className="flex-1 overflow-auto bg-muted p-4 md:p-5" ref={viewportRef}>
|
|
||||||
{loading ? (
|
|
||||||
<div className="flex h-full min-h-[480px] items-center justify-center rounded-xl border border-dashed bg-background/80">
|
|
||||||
<div className="flex items-center gap-3 text-sm text-muted-foreground">
|
|
||||||
<Spinner className="size-4" />
|
|
||||||
{resolvedLabels.loading}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{!loading && error ? (
|
|
||||||
<div className="flex h-full min-h-[480px] items-center justify-center rounded-xl border border-dashed bg-background/80 px-6 text-center">
|
|
||||||
<Alert className="max-w-md text-left">
|
|
||||||
<AlertTitle>{resolvedLabels.errorTitle}</AlertTitle>
|
|
||||||
<AlertDescription className="space-y-4">
|
|
||||||
<p>{error}</p>
|
|
||||||
{onRetry ? (
|
|
||||||
<Button onClick={onRetry} type="button" variant="outline">
|
|
||||||
{resolvedLabels.retry}
|
|
||||||
</Button>
|
|
||||||
) : null}
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{!(loading || error) && resolvedSrc ? (
|
|
||||||
<div className="mx-auto flex min-h-full justify-center">
|
|
||||||
<div
|
|
||||||
className="origin-top rounded-xl border bg-red-500 shadow-sm transition-transform"
|
|
||||||
style={{
|
|
||||||
transform: `scale(${zoomScale})`,
|
|
||||||
transformOrigin: "top center",
|
|
||||||
width: `${PDF_PAGE_WIDTH}px`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<iframe
|
|
||||||
className="w-full rounded-xl bg-amber-500"
|
|
||||||
src={resolvedSrc}
|
|
||||||
style={{ height: `${PDF_PAGE_HEIGHT}px` }}
|
|
||||||
title={title}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{loading || error || resolvedSrc ? null : (
|
|
||||||
<div className="flex h-full min-h-[480px] items-center justify-center rounded-xl border border-dashed bg-background/80 px-6 text-center">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<p className="font-medium text-foreground">{resolvedLabels.emptyTitle}</p>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
{resolvedLabels.emptyDescription}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</FormSectionGrid>
|
|
||||||
</FormSectionCard>
|
</FormSectionCard>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user