Skip to content
104 changes: 27 additions & 77 deletions apps/webapp/app/components/primitives/ClipboardField.tsx
Original file line number Diff line number Diff line change
@@ -1,70 +1,62 @@
import { CheckIcon } from "@heroicons/react/20/solid";
import { useCallback, useEffect, useRef, useState } from "react";
import { useEffect, useRef, useState } from "react";
import { cn } from "~/utils/cn";
import { Button } from "./Buttons";
import { ClipboardCheckIcon, ClipboardIcon } from "lucide-react";
import { CopyButton } from "./CopyButton";

const variants = {
"primary/small": {
container:
"flex items-center text-text-dimmed font-mono rounded border bg-charcoal-750 text-xs transition hover:bg-charcoal-700 focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0 focus:border-transparent focus:outline-none focus:ring-0 focus:ring-transparent",
input:
"bg-transparent border-0 text-xs px-2 w-auto rounded-l h-6 leading-6 focus:ring-transparent",
buttonVariant: "primary/small" as const,
buttonVariant: "primary" as const,
size: "small" as const,
button: "rounded-l-none",
iconSize: "h-3 w-3",
iconPadding: "pl-1",
},
"secondary/small": {
container:
"flex items-center text-text-dimmed font-mono rounded border bg-charcoal-750 text-xs transition hover:bg-charcoal-700 focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0 focus:border-transparent focus:outline-none focus:ring-0 focus:ring-transparent",
input:
"bg-transparent border-0 text-xs px-2 w-auto rounded-l h-6 leading-6 focus:ring-transparent",
buttonVariant: "tertiary/small" as const,
buttonVariant: "tertiary" as const,
size: "small" as const,
button: "rounded-l-none border-l border-charcoal-750",
iconSize: "h-3 w-3",
iconPadding: "pl-1",
},
"tertiary/small": {
container:
"group/clipboard flex items-center text-text-dimmed font-mono rounded bg-transparent border border-transparent text-xs transition duration-150 hover:border-charcoal-700 focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0 focus:border-transparent focus:outline-none focus:ring-0 focus:ring-transparent",
input:
"bg-transparent border-0 text-xs px-2 w-auto rounded-l h-6 leading-6 focus:ring-transparent",
buttonVariant: "minimal/small" as const,
buttonVariant: "minimal" as const,
size: "small" as const,
button:
"rounded-l-none border-l border-transparent transition group-hover/clipboard:border-charcoal-700",
iconSize: "h-3 w-3",
iconPadding: "pl-1",
},
"primary/medium": {
container:
"flex items-center text-text-dimmed font-mono rounded border bg-charcoal-750 text-sm transition hover:bg-charcoal-700 focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0 focus:border-transparent focus:outline-none focus:ring-0 focus:ring-transparent",
input:
"bg-transparent border-0 text-sm px-3 w-auto rounded-l h-8 leading-6 focus:ring-transparent",
buttonVariant: "primary/medium" as const,
buttonVariant: "primary" as const,
size: "medium" as const,
button: "rounded-l-none",
iconSize: "h-4 w-4",
iconPadding: "pl-2",
},
"secondary/medium": {
container:
"flex items-center text-text-dimmed font-mono rounded bg-charcoal-750 text-sm transition hover:bg-charcoal-700 focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0 focus:border-transparent focus:outline-none focus:ring-0 focus:ring-transparent",
input:
"bg-transparent border-0 text-sm px-3 w-auto rounded-l h-8 leading-6 focus:ring-transparent",
buttonVariant: "tertiary/medium" as const,
buttonVariant: "tertiary" as const,
size: "medium" as const,
button: "rounded-l-none border-l border-charcoal-750",
iconSize: "h-4 w-4",
iconPadding: "pl-2",
},
"tertiary/medium": {
container:
"group flex items-center text-text-dimmed font-mono rounded bg-transparent border border-transparent text-sm transition hover:border-charcoal-700 focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0 focus:border-transparent focus:outline-none focus:ring-0 focus:ring-transparent",
input:
"bg-transparent border-0 text-sm px-3 w-auto rounded-l h-8 leading-6 focus:ring-transparent",
buttonVariant: "minimal/medium" as const,
buttonVariant: "minimal" as const,
size: "medium" as const,
button: "rounded-l-none border-l border-transparent transition group-hover:border-charcoal-700",
iconSize: "h-4 w-4",
iconPadding: "pl-2",
},
};

Expand All @@ -88,36 +80,19 @@ export function ClipboardField({
fullWidth = true,
}: ClipboardFieldProps) {
const [isSecure, setIsSecure] = useState(secure !== undefined && secure);
const [copied, setCopied] = useState(false);

const copy = useCallback(
(event: React.MouseEvent<HTMLButtonElement>) => {
event.preventDefault();
event.stopPropagation();
navigator.clipboard.writeText(value);
setCopied(true);
setTimeout(() => {
setCopied(false);
}, 1500);
},
[value]
);
const inputIcon = useRef<HTMLInputElement>(null);
const { container, input, buttonVariant, button, size } = variants[variant];

useEffect(() => {
setIsSecure(secure !== undefined && secure);
}, [secure]);

const { container, input, buttonVariant, button } = variants[variant];
const iconClassName = variants[variant].iconSize;
const iconPosition = variants[variant].iconPadding;
const inputIcon = useRef<HTMLInputElement>(null);

return (
<span className={cn(container, fullWidth ? "w-full" : "max-w-fit", className)}>
{icon && (
<span
onClick={() => inputIcon.current && inputIcon.current.focus()}
className={cn(iconPosition, "flex items-center")}
className="flex items-center pl-1"
>
{icon}
</span>
Expand All @@ -132,51 +107,26 @@ export function ClipboardField({
fullWidth ? "w-full" : "max-w-fit",
input
)}
// size={value.length}
// maxLength={3}
onFocus={(e) => {
if (secure) {
setIsSecure((i) => false);
setIsSecure(false);
}
e.currentTarget.select();
}}
onBlur={() => {
if (secure) {
setIsSecure((i) => true);
setIsSecure(true);
}
}}
/>
{iconButton ? (
<Button
variant={buttonVariant}
onClick={copy}
className={cn("shrink grow-0 px-1.5", button)}
>
{copied ? (
<ClipboardCheckIcon
className={cn(
"h-4 w-4",
buttonVariant === "primary/small" || buttonVariant === "primary/medium"
? "text-background-dimmed"
: "text-green-500"
)}
/>
) : (
<ClipboardIcon
className={cn(
"h-4 w-4",
buttonVariant === "primary/small" || buttonVariant === "primary/medium"
? "text-background-dimmed"
: "text-text-dimmed"
)}
/>
)}
</Button>
) : (
<Button variant={buttonVariant} onClick={copy} className={cn("shrink-0 grow-0", button)}>
{copied ? <CheckIcon className="mx-[0.4rem] h-4 w-4 text-green-500" /> : "Copy"}
</Button>
)}
<CopyButton
value={value}
variant={iconButton ? "icon" : "button"}
buttonVariant={buttonVariant}
size={size}
buttonClassName={button}
showTooltip={false}
/>
</span>
);
}
100 changes: 100 additions & 0 deletions apps/webapp/app/components/primitives/CopyButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { ClipboardCheckIcon, ClipboardIcon } from "lucide-react";
import { useCopy } from "~/hooks/useCopy";
import { cn } from "~/utils/cn";
import { Button } from "./Buttons";
import { SimpleTooltip } from "./Tooltip";

const sizes = {
"extra-small": {
icon: "size-3",
button: "h-5 px-1",
},
small: {
icon: "size-3.5",
button: "h-6 px-1",
},
medium: {
icon: "size-4",
button: "h-8 px-1.5",
},
};

type CopyButtonProps = {
value: string;
variant?: "icon" | "button";
size?: keyof typeof sizes;
className?: string;
buttonClassName?: string;
showTooltip?: boolean;
buttonVariant?: "primary" | "secondary" | "tertiary" | "minimal";
};

export function CopyButton({
value,
variant = "button",
size = "medium",
className,
buttonClassName,
showTooltip = true,
buttonVariant = "tertiary",
}: CopyButtonProps) {
const { copy, copied } = useCopy(value);

const { icon: iconSize, button: buttonSize } = sizes[size];

const button =
variant === "icon" ? (
<span
onClick={copy}
className={cn(
buttonSize,
"flex items-center justify-center rounded border border-charcoal-650 bg-charcoal-750",
copied
? "text-green-500"
: "text-text-dimmed hover:border-charcoal-600 hover:bg-charcoal-700 hover:text-text-bright",
buttonClassName
)}
>
{copied ? (
<ClipboardCheckIcon className={iconSize} />
) : (
<ClipboardIcon className={iconSize} />
)}
</span>
) : (
<Button
variant={`${buttonVariant}/${size === "extra-small" ? "small" : size}`}
onClick={copy}
className={cn("shrink-0", buttonClassName)}
>
{copied ? (
<ClipboardCheckIcon
className={cn(
iconSize,
buttonVariant === "primary" ? "text-background-dimmed" : "text-green-500"
)}
/>
) : (
<ClipboardIcon
className={cn(
iconSize,
buttonVariant === "primary" ? "text-background-dimmed" : "text-text-dimmed"
)}
/>
)}
</Button>
);

if (!showTooltip) return <span className={className}>{button}</span>;

return (
<span className={className}>
<SimpleTooltip
button={button}
content={copied ? "Copied!" : "Copy"}
className="font-sans"
disableHoverableContent
/>
</span>
);
}
20 changes: 4 additions & 16 deletions apps/webapp/app/components/primitives/CopyableText.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,12 @@
import { useCallback, useState } from "react";
import { SimpleTooltip } from "~/components/primitives/Tooltip";
import { ClipboardCheckIcon, ClipboardIcon } from "lucide-react";
import { useState } from "react";
import { SimpleTooltip } from "~/components/primitives/Tooltip";
import { useCopy } from "~/hooks/useCopy";
import { cn } from "~/utils/cn";

export function CopyableText({ value, className }: { value: string; className?: string }) {
const [isHovered, setIsHovered] = useState(false);
const [copied, setCopied] = useState(false);

const copy = useCallback(
(e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
navigator.clipboard.writeText(value);
setCopied(true);
setTimeout(() => {
setCopied(false);
}, 1500);
},
[value]
);
const { copy, copied } = useCopy(value);

return (
<span
Expand Down
Loading
Loading