marmar-context / web / context / src / clinician / patient / bits.tsx
bits.tsx
Raw
import type { ReactNode } from "react";
import { useEffect, useState } from "react";
import { cn, Icon, Tip } from "../kit";

/** Copy a value from the page. The tip says Copied for a moment, and a screen reader hears it. */
export function CopyButton({ value, label }: { value: string; label: string }) {
  const [copied, setCopied] = useState(false);
  useEffect(() => {
    if (!copied) return;
    const t = setTimeout(() => setCopied(false), 1600);
    return () => clearTimeout(t);
  }, [copied]);
  const copy = async () => {
    try {
      await navigator.clipboard.writeText(value);
      setCopied(true);
    } catch {
      // No clipboard here: the value stays on screen to select by hand.
    }
  };
  return (
    <span className="inline-flex">
      <Tip label={copied ? "Copied" : "Copy"}>
        <button
          type="button"
          aria-label={label}
          // Prevent the default so the tip stays open to say Copied.
          onPointerDown={(e) => e.preventDefault()}
          onClick={(e) => {
            e.preventDefault();
            void copy();
          }}
          className={cn(
            "inline-flex size-6 cursor-pointer items-center justify-center rounded-md border-0 bg-transparent p-0 transition-colors outline-none hover:bg-quiet hover:text-ink focus-visible:shadow-[0_0_0_2px_var(--color-brand)]",
            copied ? "text-green-icon" : "text-subtle",
          )}
        >
          <Icon name={copied ? "check-line" : "file-copy-line"} size={14} />
        </button>
      </Tip>
      <span role="status" className="sr-only">
        {copied ? "Copied" : ""}
      </span>
    </span>
  );
}

/** A section's quiet line under its title. */
export const SectionNote = ({ children }: { children: ReactNode }) => <p className="m-0 text-[13.5px] leading-5 text-muted">{children}</p>;