@@ -23,10 +23,12 @@ export function PairingQrCode() {
2323 const [ loading , setLoading ] = useState ( false ) ;
2424 const [ svgHtml , setSvgHtml ] = useState < string | null > ( null ) ;
2525 const [ expiresIn , setExpiresIn ] = useState < number | null > ( null ) ;
26+ const [ copied , setCopied ] = useState ( false ) ;
2627
2728 const fetchPairingLink = useCallback ( async ( ) => {
2829 setLoading ( true ) ;
2930 setError ( null ) ;
31+ setCopied ( false ) ;
3032 try {
3133 const origin = resolveServerHttpOrigin ( ) ;
3234 const response = await fetch ( `${ origin } /api/pairing?ttl=300` ) ;
@@ -80,6 +82,17 @@ export function PairingQrCode() {
8082 return ( ) => clearInterval ( interval ) ;
8183 } , [ pairing ?. expiresAt , fetchPairingLink ] ) ;
8284
85+ const handleCopyLink = async ( ) => {
86+ if ( ! pairing ?. pairingUrl ) return ;
87+ try {
88+ await navigator . clipboard . writeText ( pairing . pairingUrl ) ;
89+ setCopied ( true ) ;
90+ setTimeout ( ( ) => setCopied ( false ) , 2000 ) ;
91+ } catch {
92+ // Fallback: select the text in the details element
93+ }
94+ } ;
95+
8396 const formatTime = ( seconds : number ) => {
8497 const m = Math . floor ( seconds / 60 ) ;
8598 const s = seconds % 60 ;
@@ -116,24 +129,23 @@ export function PairingQrCode() {
116129 { expiresIn > 0 ? < > Expires in { formatTime ( expiresIn ) } </ > : < > Refreshing...</ > }
117130 </ p >
118131 ) }
119- < Button
120- variant = "ghost"
121- size = "sm"
122- onClick = { ( ) => void fetchPairingLink ( ) }
123- disabled = { loading }
124- >
125- { loading ? "Generating..." : "Generate new code" }
126- </ Button >
127- { pairing ?. pairingUrl && (
128- < details className = "w-full max-w-xs" >
129- < summary className = "cursor-pointer text-xs text-muted-foreground hover:text-foreground" >
130- Show pairing link
131- </ summary >
132- < code className = "mt-1 block break-all rounded bg-muted px-2 py-1 text-[10px]" >
133- { pairing . pairingUrl }
134- </ code >
135- </ details >
136- ) }
132+ < div className = "flex flex-wrap items-center justify-center gap-2" >
133+ < Button variant = "outline" size = "sm" onClick = { ( ) => void handleCopyLink ( ) } >
134+ { copied ? "Copied!" : "Copy pairing link" }
135+ </ Button >
136+ < Button
137+ variant = "ghost"
138+ size = "sm"
139+ onClick = { ( ) => void fetchPairingLink ( ) }
140+ disabled = { loading }
141+ >
142+ { loading ? "Generating..." : "Refresh" }
143+ </ Button >
144+ </ div >
145+ < p className = "max-w-xs text-center text-[11px] leading-relaxed text-muted-foreground/70" >
146+ Scan the QR code with your phone camera, or copy the link and paste it in the mobile
147+ app.
148+ </ p >
137149 </ >
138150 ) : loading ? (
139151 < div className = "flex h-[220px] w-[220px] items-center justify-center rounded-xl border border-border bg-muted" >
0 commit comments