oncodsl / web /app /CopyButton.tsx
govindbalki's picture
Upload folder using huggingface_hub
0fff343 verified
Raw
History Blame Contribute Delete
3.84 kB
"use client";
import { useEffect, useRef, useState } from "react";
interface Props {
text: string;
/** Visible label, e.g. "Copy program". Pass empty string to render
* icon-only (the visually-hidden `aria-label` still applies). */
label?: string;
ariaLabel?: string;
/** "default" = full bordered button with label; "icon" = compact
* icon-only (for inside tiles). */
variant?: "default" | "icon";
/** Optional className override. */
className?: string;
/** Click handler for the host to know a copy happened (e.g. analytics
* or downstream UI). */
onCopied?: () => void;
}
/**
* Reusable copy-to-clipboard button.
*
* Uses ``navigator.clipboard.writeText`` when available; degrades to a
* hidden ``<textarea>`` + ``document.execCommand("copy")`` fallback when
* the page isn't served over a secure context.
*
* Shows a brief "Copied" affordance after a successful copy.
*/
export default function CopyButton({
text,
label = "Copy",
ariaLabel,
variant = "default",
className,
onCopied,
}: Props) {
const [copied, setCopied] = useState(false);
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
useEffect(() => {
return () => {
if (timerRef.current) clearTimeout(timerRef.current);
};
}, []);
async function copy(e: React.MouseEvent) {
e.stopPropagation();
e.preventDefault();
let ok = false;
try {
if (navigator.clipboard?.writeText) {
await navigator.clipboard.writeText(text);
ok = true;
} else {
ok = legacyCopy(text);
}
} catch {
ok = legacyCopy(text);
}
if (ok) {
setCopied(true);
if (timerRef.current) clearTimeout(timerRef.current);
timerRef.current = setTimeout(() => setCopied(false), 1400);
onCopied?.();
}
}
if (variant === "icon") {
return (
<button
type="button"
onClick={copy}
aria-label={ariaLabel ?? "Copy program text"}
title={copied ? "Copied" : "Copy program text"}
className={
className ??
"inline-flex h-5 w-5 items-center justify-center rounded text-muted hover:text-accent focus:outline-none focus-visible:ring-2 focus-visible:ring-accent"
}
>
{copied ? <CheckGlyph /> : <CopyGlyph />}
</button>
);
}
return (
<button
type="button"
onClick={copy}
aria-label={ariaLabel ?? label}
className={
className ??
"inline-flex items-center gap-1.5 rounded-md border border-border bg-card px-2.5 py-1 text-[11px] font-medium text-ink hover:border-accent hover:text-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent"
}
>
{copied ? <CheckGlyph /> : <CopyGlyph />}
<span>{copied ? "Copied" : label}</span>
</button>
);
}
function CopyGlyph() {
return (
<svg
aria-hidden
width="12"
height="12"
viewBox="0 0 16 16"
fill="none"
stroke="currentColor"
strokeWidth="1.4"
>
<rect x="5" y="3" width="9" height="11" rx="1.5" />
<path d="M3 11.5V3.5A1.5 1.5 0 0 1 4.5 2H11" />
</svg>
);
}
function CheckGlyph() {
return (
<svg
aria-hidden
width="12"
height="12"
viewBox="0 0 16 16"
fill="none"
stroke="currentColor"
strokeWidth="1.6"
>
<path d="M3 8.5L6.5 12L13 5" />
</svg>
);
}
function legacyCopy(text: string): boolean {
try {
const ta = document.createElement("textarea");
ta.value = text;
ta.setAttribute("readonly", "");
ta.style.position = "fixed";
ta.style.opacity = "0";
document.body.appendChild(ta);
ta.select();
const ok = document.execCommand("copy");
document.body.removeChild(ta);
return ok;
} catch {
return false;
}
}