vtc-service 0.7.0

Service for Verifiable Trust Communities
// Tiny inline copy-to-clipboard button.
//
// Renders a 24×24 ghost-styled button with a lucide Clipboard icon
// that flips to a Check + green border for ~1.6s on a successful
// copy. Falls back to a hidden-textarea + `execCommand("copy")` for
// non-secure contexts (http:// during local dev) where
// `navigator.clipboard` is unavailable.
//
// Used inline next to any monospace identifier the operator might
// want to copy — DIDs, JTIs, session IDs, etc. Keep it close to the
// value (not in a separate "actions" column) so the affordance is
// obvious without scanning.

import { useEffect, useRef, useState } from "react";
import { Check, Copy } from "lucide-react";

import { useToast } from "@/lib/toast";

const FEEDBACK_MS = 1600;

interface CopyButtonProps {
  /** The text to copy. When falsy or empty, the button is disabled. */
  value: string | null | undefined;
  /** Accessible label + tooltip. Defaults to "Copy". */
  label?: string;
  /** Optional toast on success. When omitted, only the icon flash fires. */
  successMessage?: string;
}

export function CopyButton({
  value,
  label = "Copy",
  successMessage,
}: CopyButtonProps) {
  const [copied, setCopied] = useState(false);
  const timerRef = useRef<number | null>(null);
  const toast = useToast();
  const empty = !value;

  useEffect(
    () => () => {
      if (timerRef.current !== null) {
        window.clearTimeout(timerRef.current);
      }
    },
    [],
  );

  const onClick = async () => {
    if (!value) return;
    try {
      await copyText(value);
      setCopied(true);
      if (successMessage) toast.push("success", successMessage);
      if (timerRef.current !== null) window.clearTimeout(timerRef.current);
      timerRef.current = window.setTimeout(() => {
        setCopied(false);
        timerRef.current = null;
      }, FEEDBACK_MS);
    } catch (err) {
      toast.pushFromError(err, "Copy failed");
    }
  };

  return (
    <button
      type="button"
      className={`copy-icon-btn${copied ? " copied" : ""}`}
      onClick={onClick}
      disabled={empty}
      aria-label={copied ? "Copied" : label}
      title={empty ? "Nothing to copy" : copied ? "Copied" : label}
    >
      {copied ? (
        <Check size={14} strokeWidth={2} aria-hidden="true" />
      ) : (
        <Copy size={14} strokeWidth={1.75} aria-hidden="true" />
      )}
    </button>
  );
}

async function copyText(text: string) {
  if (navigator.clipboard && window.isSecureContext) {
    await navigator.clipboard.writeText(text);
    return;
  }
  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();
  try {
    document.execCommand("copy");
  } finally {
    document.body.removeChild(ta);
  }
}