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>
188 lines
6.3 KiB
TypeScript
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>
|
|
)}
|
|
</>
|
|
);
|
|
}
|