Skip to content

Show a tooltip when hovering over any DateTime #2003

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
May 1, 2025
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