"use client"; import { useEffect, useRef, useState, type ReactNode } from "react"; import { createPortal } from "react-dom"; import { DownloadIcon } from "lucide-react"; import { toast } from "sonner"; import { LocaleBoundary, useTranslation } from "@/components/i18n/locale-provider"; import type { Locale } from "@/lib/i18n/locales"; import { Button } from "@/components/ui/button"; import { Spinner } from "@/components/ui/spinner"; import { exportText, withoutNotesFileName } from "@/lib/guides/export-text"; import { cn } from "@/lib/utils"; const EXPORT_WIDTH = 1200; function downloadFileName(value: string): string { return value.replace(/[\\/:*?"<>|]/gu, "-"); } export function GuideImageExport({ children, fileName, width = EXPORT_WIDTH, height, format = "png", disabled = false, captureClassName, buttonLabel, allowWithoutNote = false, }: { children: ReactNode; fileName: string; width?: number; height?: number; format?: "png" | "webp"; disabled?: boolean; captureClassName?: string; buttonLabel?: string; allowWithoutNote?: boolean; }) { const { e, locale } = useTranslation(); const captureRef = useRef(null); const [captureOptions, setCaptureOptions] = useState<{ mode: "with-notes" | "without-notes"; locale: Locale; fileName: string } | null>(null); const captureMode = captureOptions?.mode; const capturing = captureOptions !== null; useEffect(() => { if (!captureOptions) return; const exportLocale = captureOptions.locale; const exportFileName = captureOptions.fileName; let cancelled = false; async function capture() { try { const node = captureRef.current; if (!node) throw new Error("Capture target is unavailable"); if (captureMode === "without-notes") { node.querySelectorAll("[data-export-note]").forEach((note) => { note.style.display = "none"; }); // A note can be the only remaining content in the artifact right column. const right = node.querySelector(".artifact-export-right"); if (right) { const sections = right.querySelectorAll( "[data-artifact-export-section]", ); const hasVisibleSection = Array.from(sections).some( (section) => getComputedStyle(section).display !== "none", ); if (!hasVisibleSection) right.style.display = "none"; } } const { domToBlob, waitUntilLoad } = await import("modern-screenshot"); await Promise.all([document.fonts.ready, waitUntilLoad(node)]); const blob = await domToBlob(node, { backgroundColor: getComputedStyle(node).backgroundColor, type: `image/${format}`, scale: 1, width, height, }); if (cancelled) return; const url = URL.createObjectURL(blob); const link = document.createElement("a"); link.download = downloadFileName( captureMode === "without-notes" ? withoutNotesFileName(exportFileName) : exportFileName, ); link.href = url; document.body.append(link); link.click(); link.remove(); window.setTimeout(() => URL.revokeObjectURL(url), 0); } catch { if (!cancelled) toast.error(exportText("exportError", {}, exportLocale)); } finally { if (!cancelled) setCaptureOptions(null); } } void capture(); return () => { cancelled = true; }; }, [captureOptions, captureMode, format, height, width]); return ( <>
{(["with-notes", ...(allowWithoutNote ? ["without-notes"] as const : [])] as const).map((mode) => ( ))}
{capturing ? createPortal( , document.body, ) : null} ); }