import { cva } from "class-variance-authority"; import React, { useCallback } from "react"; import { Button } from "@sparkle/components/"; import { useCopyToClipboard } from "@sparkle/hooks"; import { ArrowDownOnSquareIcon, ClipboardCheckIcon, ClipboardIcon, } from "@sparkle/icons/app"; import { cn } from "@sparkle/lib/utils"; export type SupportedContentType = "application/json" | "text/csv"; const contentTypeExtensions: Record = { "application/json": ".json", "text/csv": ".csv", }; const wrapperVariants = cva("s-group s-relative s-w-full !s-overflow-visible", { variants: { buttonDisplay: { inside: "s-mt-0", outside: "s-mt-11", }, }, defaultVariants: { buttonDisplay: "outside", }, }); const stickyContainerVariants = cva("s-sticky s-z-[1] s-h-0", { variants: { buttonDisplay: { inside: "s-top-0", outside: "s-top-11", }, }, defaultVariants: { buttonDisplay: "outside", }, }); const actionsVariants = cva( "s-absolute s-right-2 s-flex s-items-center s-gap-1 s-py-2", { variants: { buttonDisplay: { inside: "", outside: "s-bottom-0 s-h-11", }, displayActions: { hover: "s-opacity-0 s-transition-opacity s-duration-200 group-hover:s-opacity-100", always: "", }, }, defaultVariants: { buttonDisplay: "outside", displayActions: "always", }, } ); export interface ContentToDownload { content: string; filename: string; type: SupportedContentType; } export type GetContentToDownloadFunction = () => Promise; type ClipboardContent = { "text/plain"?: string; "text/html"?: string; }; interface ContentBlockWrapperProps { children: React.ReactNode; className?: string; innerClassName?: string; content?: ClipboardContent | string; getContentToDownload?: GetContentToDownloadFunction; actions?: React.ReactNode[] | React.ReactNode; displayActions?: "hover" | "always"; buttonDisplay?: "inside" | "outside" | null; // null to hide buttons } export function ContentBlockWrapper({ children, className, innerClassName, content, actions, displayActions = "always", getContentToDownload, buttonDisplay = "outside", }: ContentBlockWrapperProps) { const [isCopied, copyToClipboard] = useCopyToClipboard(); const handleCopyToClipboard = useCallback( (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); if (!content) { return; } const rawContent: ClipboardContent = typeof content === "string" ? { "text/plain": content } : content; // Replace invisible non-breaking spaces with regular spaces. if (rawContent["text/plain"]) { rawContent["text/plain"] = rawContent["text/plain"] .replaceAll("\xa0", " ") .trim(); } const data = new ClipboardItem( Object.entries(rawContent).reduce( (acc, [type, data]) => { acc[type] = new Blob([data], { type }); return acc; }, {} as Record ) ); void copyToClipboard(data); }, [content, copyToClipboard] ); const handleDownload = useCallback( async (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); if (!getContentToDownload) { return; } const { content, filename, type } = await getContentToDownload(); const blob = new Blob([content], { type }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `${filename}.${contentTypeExtensions[type]}`; a.click(); }, [getContentToDownload] ); return (
{buttonDisplay !== null && (
{actions && actions} {getContentToDownload && (
)}
{children}
); }