Fix #1 (input-focus zoom), #6 (last-image arrow), #7 (join sound)

#1: focusing a text box zoomed + stretched the whole page on iOS. Several
inputs were < 16px and the existing 16px rule lived in a width-based mobile
@media that misses iPad/landscape (and maximum-scale is unreliable on iOS).
Added a @media (pointer: coarse) rule forcing 16px on every focusable field
for ALL touch devices; desktop is untouched.

#6: the image lightbox used visibility:hidden for the end arrows (invisible but
still occupying space / reading as a ghost button). Switched to display toggling
so there's truly no right arrow at the last image (and no left at the first).

#7: play a soft two-note chime + a brief "<name> joined the call" toast when a
new participant joins the meeting.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-08-11 22:44:00 +05:30
parent ecc8be3dba
commit 276c5e0929
+21 -1
View File
@@ -367,6 +367,14 @@
.composer-box:focus-within{border-color:var(--blue);} .composer-box:focus-within{border-color:var(--blue);}
.composer-row{display:flex;align-items:flex-end;gap:.15rem;padding:.15rem .3rem;} .composer-row{display:flex;align-items:flex-end;gap:.15rem;padding:.15rem .3rem;}
.composer-row input,.composer-row textarea{flex:1;min-width:0;box-sizing:border-box;border:none;background:transparent;padding:.5rem .4rem;margin:0;font-size:16px;color:var(--ink);font-family:inherit;resize:none;line-height:1.4;max-height:140px;overflow-y:hidden;display:block;scrollbar-width:thin;} /* 16px UNCONDITIONALLY (not just in the mobile @media) — anything under 16px makes iOS Safari/PWA zoom the page on focus, and the mobile media query doesn't match on iPad / landscape. */ .composer-row input,.composer-row textarea{flex:1;min-width:0;box-sizing:border-box;border:none;background:transparent;padding:.5rem .4rem;margin:0;font-size:16px;color:var(--ink);font-family:inherit;resize:none;line-height:1.4;max-height:140px;overflow-y:hidden;display:block;scrollbar-width:thin;} /* 16px UNCONDITIONALLY (not just in the mobile @media) — anything under 16px makes iOS Safari/PWA zoom the page on focus, and the mobile media query doesn't match on iPad / landscape. */
/* #1: iOS zooms + stretches the whole page when a focused field is < 16px. maximum-scale=1 is unreliable
(ignored by iOS Safari/PWA) and the width-based mobile @media misses iPad / landscape. Target ALL touch
devices by POINTER TYPE instead — the exact set that has the focus-zoom problem — and force 16px on every
focusable field so selecting a text box never zooms. Desktop (fine pointer) is untouched. */
@media (pointer: coarse){
input:not([type=checkbox]):not([type=radio]):not([type=range]):not([type=color]):not([type=file]),
textarea, select{ font-size:16px !important; }
}
.composer-row input:focus,.composer-row textarea:focus{outline:none;} .composer-row input:focus,.composer-row textarea:focus{outline:none;}
.ic-btn{border:none;background:transparent;color:var(--muted);cursor:pointer;width:36px;height:36px;border-radius:10px;display:grid;place-items:center;flex:0 0 auto;} .ic-btn{border:none;background:transparent;color:var(--muted);cursor:pointer;width:36px;height:36px;border-radius:10px;display:grid;place-items:center;flex:0 0 auto;}
.ic-btn:hover{color:var(--blue);background:var(--blue-soft);} .ic-btn:hover{color:var(--blue);background:var(--blue-soft);}
@@ -3106,7 +3114,7 @@ function openLightbox(src){
},{passive:false}); },{passive:false});
img.addEventListener('touchend',()=>{ pinchD=0; }); img.addEventListener('touchend',()=>{ pinchD=0; });
const syncArrows=()=>{ if(!multi) return; const pB=ov.querySelector('.lb-prev'), nB=ov.querySelector('.lb-next'); if(pB) pB.style.visibility=idx<=0?'hidden':'visible'; if(nB) nB.style.visibility=idx>=gallery.length-1?'hidden':'visible'; }; const syncArrows=()=>{ if(!multi) return; const pB=ov.querySelector('.lb-prev'), nB=ov.querySelector('.lb-next'); if(pB) pB.style.display=idx<=0?'none':''; if(nB) nB.style.display=idx>=gallery.length-1?'none':''; }; // #6: fully hide (not just invisible) the arrow with nothing beyond it — no left arrow at the first image, no right arrow at the last
const show=(i)=>{ if(!multi) return; idx=Math.max(0, Math.min(gallery.length-1, i)); const u=gallery[idx]; img.src=u; if(dl) dl.setAttribute('href',u); resetZoom(); syncArrows(); }; // clamp (no wrap → no blank slide past the last) + hide the end arrow const show=(i)=>{ if(!multi) return; idx=Math.max(0, Math.min(gallery.length-1, i)); const u=gallery[idx]; img.src=u; if(dl) dl.setAttribute('href',u); resetZoom(); syncArrows(); }; // clamp (no wrap → no blank slide past the last) + hide the end arrow
const close=()=>{ ov.remove(); document.removeEventListener('keydown', onKey); }; const close=()=>{ ov.remove(); document.removeEventListener('keydown', onKey); };
// #8: animated pager — the current image slides fully out in the swipe direction, then the next/prev // #8: animated pager — the current image slides fully out in the swipe direction, then the next/prev
@@ -4182,6 +4190,17 @@ function playPing(){
tone(1174.7, 0.11, 0.40, 0.34); // D6 — rises, rings out -> "ti-doo" tone(1174.7, 0.11, 0.40, 0.34); // D6 — rises, rings out -> "ti-doo"
}catch(_){} }catch(_){}
} }
// #7: a soft, friendly two-note chime when a NEW participant joins the meeting (distinct from the message ping).
function playJoinChime(){
try{
_audioCtx=_audioCtx||new (window.AudioContext||window.webkitAudioContext)();
if(_audioCtx.state==='suspended') _audioCtx.resume();
const t=_audioCtx.currentTime;
const tone=(f,start,dur,peak)=>{ const o=_audioCtx.createOscillator(), g=_audioCtx.createGain(); o.type='sine'; o.frequency.value=f; g.gain.setValueAtTime(0.0001,t+start); g.gain.exponentialRampToValueAtTime(peak,t+start+0.02); g.gain.exponentialRampToValueAtTime(0.0001,t+start+dur); o.connect(g); g.connect(_audioCtx.destination); o.start(t+start); o.stop(t+start+dur+0.03); };
tone(587.33, 0.00, 0.42, 0.15); // D5
tone(880.0, 0.12, 0.55, 0.14); // A5 — gentle rising "doo-dee"
}catch(_){}
}
// Continuous incoming-call ring. A soft, soothing bell chime — a gentle ascending arpeggio // Continuous incoming-call ring. A soft, soothing bell chime — a gentle ascending arpeggio
// (C major) of pure sine tones with an octave shimmer and a long smooth decay, so it rings // (C major) of pure sine tones with an octave shimmer and a long smooth decay, so it rings
// rather than buzzes. Ref-counted: several pending invites share one ring; it stops on the last close. // rather than buzzes. Ref-counted: several pending invites share one ring; it stops on the last close.
@@ -5609,6 +5628,7 @@ async function onMeetMsg(e){
if(m.type==='meeting-ended'){ toast(m.reason==='unanswered'?'No answer':'Call ended'); leaveMeeting(true); return; } // 1:1 hangup / host ended / unanswered if(m.type==='meeting-ended'){ toast(m.reason==='unanswered'?'No answer':'Call ended'); leaveMeeting(true); return; } // 1:1 hangup / host ended / unanswered
if(m.type==='meeting-peer-joined'){ if(m.type==='meeting-peer-joined'){
stopRingback(); removeWaitingTile(); _dmCallWaiting=null; // #17/#7: they answered → stop ring + drop the ringing tile stopRingback(); removeWaitingTile(); _dmCallWaiting=null; // #17/#7: they answered → stop ring + drop the ringing tile
try{ playJoinChime(); toast((m.name||'Someone')+' joined the call'); }catch(_){} // #7: sound + a brief who-joined note
meetNames.set(m.peerId,m.name); if(m.avatar) meetAvatars.set(m.peerId,m.avatar); if(m.uid){ meetPeerUids.set(m.peerId,m.uid); meetInviteJoined(m.uid); dropDupPeers(m.uid, m.peerId); } meetNames.set(m.peerId,m.name); if(m.avatar) meetAvatars.set(m.peerId,m.avatar); if(m.uid){ meetPeerUids.set(m.peerId,m.uid); meetInviteJoined(m.uid); dropDupPeers(m.uid, m.peerId); }
const pc=meetMakePeer(m.peerId,m.name); // I'm an existing peer → I OFFER to the newcomer (carries my screen) const pc=meetMakePeer(m.peerId,m.name); // I'm an existing peer → I OFFER to the newcomer (carries my screen)
if(pc){ try{ const offer=await pc.createOffer(); await pc.setLocalDescription(offer); meetSend({type:'meeting-signal',to:m.peerId,data:{sdp:pc.localDescription}}); }catch(_){} } // (SFU: LiveKit handles media, no offer) if(pc){ try{ const offer=await pc.createOffer(); await pc.setLocalDescription(offer); meetSend({type:'meeting-signal',to:m.peerId,data:{sdp:pc.localDescription}}); }catch(_){} } // (SFU: LiveKit handles media, no offer)