anydrop/web/src/components/DevicePairingPanel.tsx
ordinarthur c18d995c3f feat(web): Paper & Envelope design system
Replace generic slate/indigo dark theme with a custom editorial
direction: warm paper neutrals, oxblood signal, Fraunces serif
display, Inter body, JetBrains Mono for codes. SVG paper-texture
noise overlay and thin rules across the app.

Refactored: Home, Settings, JoinRoom, Pair, Share, plus every
modal and panel (DropZone, DevicePairingPanel, PublicRoomPanel,
ProfileSetup, TextShareModal, ReceiveDialog, TransferProgress,
PeerList, PeerAvatar).

Also drops three pre-existing Uint8Array/BlobPart strictness
errors so the production build is green again.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-20 10:49:15 +02:00

188 lines
6.3 KiB
TypeScript

import { useState, useEffect } from "react";
import { useProfileStore } from "../stores/useProfileStore";
import { useStore } from "../stores/useStore";
interface DevicePairingPanelProps {
onRequestCode: (groupId: string) => void;
onResolveCode: (code: string) => void;
}
export default function DevicePairingPanel({
onRequestCode,
onResolveCode,
}: DevicePairingPanelProps) {
const { groupId, setGroupId } = useProfileStore();
const pairCode = useStore((s) => s.pairCode);
const error = useStore((s) => s.error);
const [showModal, setShowModal] = useState(false);
const [mode, setMode] = useState<"show" | "enter">("show");
const [inputCode, setInputCode] = useState("");
const [pairError, setPairError] = useState<string | null>(null);
useEffect(() => {
if (showModal && mode === "show") {
let gid = groupId;
if (!gid) {
gid = crypto.randomUUID();
setGroupId(gid);
}
useStore.getState().setPairCode(null);
onRequestCode(gid);
}
}, [showModal, mode]);
useEffect(() => {
if (error && error.includes("appairage")) {
setPairError(error);
useStore.getState().setError(null);
}
}, [error]);
const handleClose = () => {
setShowModal(false);
useStore.getState().setPairCode(null);
setPairError(null);
setInputCode("");
};
const handleSubmitCode = () => {
if (inputCode.length >= 6) {
setPairError(null);
onResolveCode(inputCode);
}
};
const currentGroupId = useProfileStore((s) => s.groupId);
useEffect(() => {
if (mode === "enter" && showModal && currentGroupId && currentGroupId !== groupId) {
handleClose();
}
}, [currentGroupId]);
return (
<>
<button
onClick={() => { setMode("show"); setShowModal(true); }}
className="paper-panel px-4 py-4 flex flex-col items-start gap-1
hover:border-ink transition-colors duration-fast ease-crisp
text-left"
>
<span className="font-mono text-[10px] uppercase tracking-[0.22em] text-ink-muted">
Pair device
</span>
<span className="text-sm text-ink">Link your own </span>
</button>
{showModal && (
<div
className="fixed inset-0 z-50 bg-ink/40 flex items-center justify-center p-4"
onClick={handleClose}
>
<div
className="paper-panel shadow-lift rounded-sm p-6 max-w-sm w-full"
onClick={(e) => e.stopPropagation()}
>
<div className="text-xs uppercase tracking-[0.2em] text-ink-muted">
Pair device
</div>
<h3 className="font-display text-2xl text-ink mt-1 mb-5">
Link your devices
</h3>
{/* Tab switcher */}
<div className="flex border border-paper-edge rounded-sm overflow-hidden mb-5">
<button
onClick={() => setMode("show")}
className={`flex-1 py-2 text-xs uppercase tracking-[0.15em] transition-colors
${mode === "show"
? "bg-ink text-paper"
: "bg-paper text-ink-muted hover:text-ink"}`}
>
Show code
</button>
<button
onClick={() => { setMode("enter"); setPairError(null); }}
className={`flex-1 py-2 text-xs uppercase tracking-[0.15em] transition-colors
${mode === "enter"
? "bg-ink text-paper"
: "bg-paper text-ink-muted hover:text-ink"}`}
>
Enter code
</button>
</div>
{mode === "show" ? (
<>
<p className="text-sm text-ink-muted mb-5">
Enter this code on your other device.
</p>
{pairCode ? (
<p className="font-mono text-4xl text-signal tracking-[0.3em] text-center">
{pairCode}
</p>
) : (
<div className="flex justify-center py-3">
<div className="w-5 h-5 border border-signal border-t-transparent rounded-full animate-spin" />
</div>
)}
<p className="text-xs text-ink-muted mt-5 leading-relaxed text-center">
Code expires in 5 minutes. Pairing is permanent.
</p>
</>
) : (
<>
<p className="text-sm text-ink-muted mb-4">
Enter the code shown on the other device.
</p>
<input
type="text"
value={inputCode}
onChange={(e) => {
setInputCode(e.target.value.toUpperCase().replace(/[^A-Z0-9]/g, ""));
setPairError(null);
}}
placeholder="ABC123"
maxLength={6}
autoFocus
className="w-full px-4 py-3 bg-paper border border-paper-edge
rounded-sm text-ink text-2xl text-center font-mono tracking-[0.3em]
placeholder:text-ink-faint focus:outline-none focus:border-ink
transition-colors duration-fast ease-crisp"
onKeyDown={(e) => {
if (e.key === "Enter") handleSubmitCode();
}}
/>
{pairError && (
<p className="text-sm text-fail mt-3">{pairError}</p>
)}
<button
onClick={handleSubmitCode}
disabled={inputCode.length < 6}
className="mt-5 w-full py-2.5 bg-ink text-paper rounded-sm text-sm
font-medium hover:bg-signal transition-colors duration-fast
ease-crisp disabled:opacity-30 disabled:cursor-not-allowed"
>
Pair
</button>
</>
)}
<button
onClick={handleClose}
className="mt-6 w-full py-2.5 border border-paper-edge hover:border-ink
text-sm text-ink rounded-sm transition-colors duration-fast ease-crisp"
>
Close
</button>
</div>
</div>
)}
</>
);
}