Clicking anywhere on the URI row copies to clipboard (not just the icon). Shows "Copied!" text for 2 seconds replacing the copy icon.
278 lines
7.2 KiB
TypeScript
278 lines
7.2 KiB
TypeScript
// Copyright (C) 2026 Whiterun LLC,
|
|
// This software is licensed under the GNU Lesser General Public License (LGPL), version 3.0 or later.
|
|
// A copy of the license can be found in the LICENSE file or at https://www.gnu.org/licenses/lgpl-3.0.html
|
|
|
|
import React, { useCallback, useState, useRef } from "react";
|
|
import { createPortal } from "react-dom";
|
|
import { AlphanumericQRCode } from "./AlphanumericQRCode.js";
|
|
import { WIZARDCONNECT_LOGO } from "./logo.js";
|
|
import type {
|
|
WizardConnectQRDialogProps,
|
|
WizardConnectQRTheme,
|
|
} from "../types.js";
|
|
|
|
const defaults: Required<
|
|
Pick<
|
|
WizardConnectQRTheme,
|
|
| "backdropColor"
|
|
| "dialogBackground"
|
|
| "headerBackground"
|
|
| "titleColor"
|
|
| "subtitleColor"
|
|
| "qrForeground"
|
|
| "qrBackground"
|
|
| "uriRowBackground"
|
|
| "uriTextColor"
|
|
| "borderColor"
|
|
| "closeButtonColor"
|
|
| "copyButtonColor"
|
|
| "qrSize"
|
|
>
|
|
> = {
|
|
backdropColor: "rgba(0,0,0,0.5)",
|
|
dialogBackground: "#1a1f2e",
|
|
headerBackground: "#1a1f2e",
|
|
titleColor: "#ffffff",
|
|
subtitleColor: "#9ca3af",
|
|
qrForeground: "#1e2a4a",
|
|
qrBackground: "#ffffff",
|
|
uriRowBackground: "rgba(31,41,55,0.6)",
|
|
uriTextColor: "#9ca3af",
|
|
borderColor: "#374151",
|
|
closeButtonColor: "#9ca3af",
|
|
copyButtonColor: "#9ca3af",
|
|
qrSize: 280,
|
|
};
|
|
|
|
function CloseIcon({ color }: { color: string }) {
|
|
return (
|
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
|
|
<path
|
|
d="M5 5L15 15M15 5L5 15"
|
|
stroke={color}
|
|
strokeWidth="2"
|
|
strokeLinecap="round"
|
|
/>
|
|
</svg>
|
|
);
|
|
}
|
|
|
|
function CopyIcon({ color }: { color: string }) {
|
|
return (
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none">
|
|
<rect
|
|
x="9"
|
|
y="9"
|
|
width="13"
|
|
height="13"
|
|
rx="2"
|
|
stroke={color}
|
|
strokeWidth="2"
|
|
/>
|
|
<path
|
|
d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"
|
|
stroke={color}
|
|
strokeWidth="2"
|
|
/>
|
|
</svg>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* A portal-based modal dialog that displays a WizardConnect QR code
|
|
* for wallet pairing. Framework-independent (inline styles, no Tailwind).
|
|
*/
|
|
export function WizardConnectQRDialog({
|
|
show,
|
|
onClose,
|
|
uri,
|
|
qrUri,
|
|
onCopy,
|
|
theme,
|
|
className,
|
|
subtitle = "Scan with your wallet to connect",
|
|
title = "WizardConnect",
|
|
}: WizardConnectQRDialogProps) {
|
|
const t = { ...defaults, ...theme };
|
|
|
|
const [copied, setCopied] = useState(false);
|
|
const copiedTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
|
|
const handleCopy = useCallback(() => {
|
|
if (onCopy) {
|
|
onCopy(uri);
|
|
} else if (typeof navigator !== "undefined" && navigator.clipboard) {
|
|
navigator.clipboard.writeText(uri).catch(() => {});
|
|
}
|
|
setCopied(true);
|
|
if (copiedTimerRef.current) clearTimeout(copiedTimerRef.current);
|
|
copiedTimerRef.current = setTimeout(() => setCopied(false), 2000);
|
|
}, [uri, onCopy]);
|
|
|
|
const handleBackdropClick = useCallback(
|
|
(e: React.MouseEvent) => {
|
|
if (e.target === e.currentTarget) onClose();
|
|
},
|
|
[onClose],
|
|
);
|
|
|
|
if (!show) return null;
|
|
|
|
return createPortal(
|
|
<div
|
|
className={className}
|
|
onClick={handleBackdropClick}
|
|
style={{
|
|
position: "fixed",
|
|
inset: 0,
|
|
zIndex: 40,
|
|
display: "grid",
|
|
placeItems: "center",
|
|
padding: "16px",
|
|
backgroundColor: t.backdropColor,
|
|
backdropFilter: "blur(4px)",
|
|
}}
|
|
>
|
|
<div
|
|
style={{
|
|
width: "100%",
|
|
maxWidth: "384px",
|
|
borderRadius: "16px",
|
|
overflow: "hidden",
|
|
boxShadow: "0 25px 50px -12px rgba(0,0,0,0.5)",
|
|
border: `1px solid ${t.borderColor}`,
|
|
}}
|
|
>
|
|
{/* Header */}
|
|
<div
|
|
style={{
|
|
display: "flex",
|
|
alignItems: "center",
|
|
justifyContent: "space-between",
|
|
padding: "16px 20px",
|
|
backgroundColor: t.headerBackground,
|
|
}}
|
|
>
|
|
<div style={{ display: "flex", alignItems: "center", gap: "10px" }}>
|
|
<img
|
|
src={WIZARDCONNECT_LOGO}
|
|
alt=""
|
|
style={{ width: "28px", height: "28px" }}
|
|
/>
|
|
<span
|
|
style={{
|
|
color: t.titleColor,
|
|
fontWeight: 600,
|
|
fontSize: "18px",
|
|
}}
|
|
>
|
|
{title}
|
|
</span>
|
|
</div>
|
|
<button
|
|
onClick={onClose}
|
|
style={{
|
|
background: "none",
|
|
border: "none",
|
|
cursor: "pointer",
|
|
padding: "4px",
|
|
display: "flex",
|
|
alignItems: "center",
|
|
}}
|
|
aria-label="Close"
|
|
>
|
|
<CloseIcon color={t.closeButtonColor} />
|
|
</button>
|
|
</div>
|
|
|
|
{/* Body */}
|
|
<div
|
|
style={{
|
|
backgroundColor: t.dialogBackground,
|
|
padding: "0 20px 20px",
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
alignItems: "center",
|
|
gap: "16px",
|
|
}}
|
|
>
|
|
<p style={{ color: t.subtitleColor, fontSize: "14px", margin: 0 }}>
|
|
{subtitle}
|
|
</p>
|
|
|
|
{/* QR container */}
|
|
<div
|
|
style={{
|
|
backgroundColor: t.qrBackground,
|
|
padding: "12px",
|
|
borderRadius: "12px",
|
|
}}
|
|
>
|
|
<AlphanumericQRCode
|
|
value={qrUri}
|
|
size={t.qrSize}
|
|
foreground={t.qrForeground}
|
|
background={t.qrBackground}
|
|
/>
|
|
</div>
|
|
|
|
{/* Copy URI row — entire row is clickable */}
|
|
<button
|
|
onClick={handleCopy}
|
|
style={{
|
|
width: "100%",
|
|
display: "flex",
|
|
alignItems: "center",
|
|
gap: "8px",
|
|
padding: "10px 12px",
|
|
backgroundColor: t.uriRowBackground,
|
|
borderRadius: "8px",
|
|
border: `1px solid ${t.borderColor}`,
|
|
cursor: "pointer",
|
|
textAlign: "left",
|
|
font: "inherit",
|
|
transition: "border-color 0.15s",
|
|
}}
|
|
aria-label="Copy URI"
|
|
>
|
|
<p
|
|
style={{
|
|
color: t.uriTextColor,
|
|
fontSize: "12px",
|
|
margin: 0,
|
|
flex: 1,
|
|
wordBreak: "break-all",
|
|
overflow: "hidden",
|
|
display: "-webkit-box",
|
|
WebkitLineClamp: 2,
|
|
WebkitBoxOrient: "vertical",
|
|
}}
|
|
>
|
|
{uri}
|
|
</p>
|
|
{copied ? (
|
|
<span
|
|
style={{
|
|
color: "#0307fe",
|
|
fontSize: "12px",
|
|
fontWeight: 600,
|
|
flexShrink: 0,
|
|
whiteSpace: "nowrap",
|
|
}}
|
|
>
|
|
Copied!
|
|
</span>
|
|
) : (
|
|
<span
|
|
style={{ flexShrink: 0, display: "flex", alignItems: "center" }}
|
|
>
|
|
<CopyIcon color={t.copyButtonColor} />
|
|
</span>
|
|
)}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>,
|
|
document.body,
|
|
);
|
|
}
|