fix(brand/ux): real brand C on header, yellow read-tick, sign-in spinner, notif DP

- #5: use the ACTUAL brand C (extracted transparent white C + gold dot from the master
  app-icon) on the blue headers instead of a hand-drawn mark; login card keeps the app icon.
- #3: read-receipt double tick on my own (blue) bubble is now brand yellow (was blue-on-blue,
  invisible).
- #2: Sign-in button shows a spinner + 'Signing in…' on submit.
- #1: web notification uses the sender/group DP URL directly as the icon (drawing an external
  DP to canvas tainted it → silently fell back to initials).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-02 17:39:38 +05:30
parent 7674c456f2
commit 9bc109528e
6 changed files with 23 additions and 24 deletions
+1 -1
View File
@@ -76,7 +76,7 @@
<script>if(new URLSearchParams(location.search).get('embed')==='1')document.documentElement.classList.add('embed');</script> <script>if(new URLSearchParams(location.search).get('embed')==='1')document.documentElement.classList.add('embed');</script>
<a href="/home" id="homeLink"><span data-ic="arrowLeft" data-sz="16"></span> Home</a> <a href="/home" id="homeLink"><span data-ic="arrowLeft" data-sz="16"></span> Home</a>
<div class="topbar" id="topbar"> <div class="topbar" id="topbar">
<div class="brandrow"><img src="/mark-light.svg" alt="" style="height:46px;width:auto;max-width:190px;border-radius:8px;object-fit:contain;image-rendering:-webkit-optimize-contrast" onerror="this.replaceWith(Object.assign(document.createElement('div'),{className:'logo',textContent:'B'}))"><div class="brand">Biz <span>Connect</span></div></div> <div class="brandrow"><img src="/mark-light.png" alt="" style="height:46px;width:auto;max-width:190px;border-radius:8px;object-fit:contain;image-rendering:-webkit-optimize-contrast" onerror="this.replaceWith(Object.assign(document.createElement('div'),{className:'logo',textContent:'B'}))"><div class="brand">Biz <span>Connect</span></div></div>
<div class="agentchip" id="agentChip"></div> <div class="agentchip" id="agentChip"></div>
</div> </div>
<div class="topbar2" id="bar"><span id="barStatus"></span><button id="endBtn">End session</button></div> <div class="topbar2" id="bar"><span id="barStatus"></span><button id="endBtn">End session</button></div>
+1 -1
View File
@@ -92,7 +92,7 @@
</head> </head>
<body> <body>
<header> <header>
<div class="brandrow"><img src="/mark-light.svg" alt="" style="height:46px;width:auto;max-width:190px;border-radius:8px;object-fit:contain;image-rendering:-webkit-optimize-contrast" onerror="this.replaceWith(Object.assign(document.createElement('div'),{className:'logo',textContent:'B'}))"><div class="brand">Biz <span class="y">Connect</span> <span class="tag">· Dashboard</span></div></div> <div class="brandrow"><img src="/mark-light.png" alt="" style="height:46px;width:auto;max-width:190px;border-radius:8px;object-fit:contain;image-rendering:-webkit-optimize-contrast" onerror="this.replaceWith(Object.assign(document.createElement('div'),{className:'logo',textContent:'B'}))"><div class="brand">Biz <span class="y">Connect</span> <span class="tag">· Dashboard</span></div></div>
<div class="row" id="hdrRight"></div> <div class="row" id="hdrRight"></div>
</header> </header>
<main id="app"></main> <main id="app"></main>
+20 -16
View File
@@ -301,8 +301,9 @@
.sys-msg{align-self:center;font-size:.76rem;color:var(--muted);background:rgba(0,0,0,.04);padding:.25rem .7rem;border-radius:99px;margin:.2rem 0;max-width:90%;text-align:center;} .sys-msg{align-self:center;font-size:.76rem;color:var(--muted);background:rgba(0,0,0,.04);padding:.25rem .7rem;border-radius:99px;margin:.2rem 0;max-width:90%;text-align:center;}
.bubble .t{display:flex;align-items:center;justify-content:flex-end;gap:.25rem;font-size:.64rem;opacity:.65;margin-top:.15rem;} .bubble .t{display:flex;align-items:center;justify-content:flex-end;gap:.25rem;font-size:.64rem;opacity:.65;margin-top:.15rem;}
.bubble .rcpt{display:inline-flex;} .bubble .rcpt{display:inline-flex;}
.bubble.mine .rcpt{opacity:.8;} .bubble.mine .rcpt{opacity:.85;}
.bubble.mine .rcpt.seen{color:#8fd3ff;opacity:1;} /* Read receipt on my own (blue) bubble: brand yellow so the double tick actually stands out. */
.bubble.mine .rcpt.seen{color:var(--brand);opacity:1;}
.att-img{cursor:zoom-in;} .att-img{cursor:zoom-in;}
.fmt-bar{display:flex;align-items:center;gap:.05rem;padding:.3rem .4rem .1rem;flex-wrap:wrap;border-bottom:1px dashed var(--line);} .fmt-bar{display:flex;align-items:center;gap:.05rem;padding:.3rem .4rem .1rem;flex-wrap:wrap;border-bottom:1px dashed var(--line);}
.fmt-bar button{border:none;background:transparent;color:var(--muted);cursor:pointer;width:30px;height:30px;border-radius:7px;display:grid;place-items:center;} .fmt-bar button{border:none;background:transparent;color:var(--muted);cursor:pointer;width:30px;height:30px;border-radius:7px;display:grid;place-items:center;}
@@ -662,6 +663,9 @@
.authcard .sub{color:var(--muted);font-size:.9rem;text-align:center;margin-bottom:1.2rem;} .authcard .sub{color:var(--muted);font-size:.9rem;text-align:center;margin-bottom:1.2rem;}
/* Blue card + gold CTA so the sign-in screen carries BOTH brand colours (never mono-blue). */ /* Blue card + gold CTA so the sign-in screen carries BOTH brand colours (never mono-blue). */
.authcard .gobtn{background:var(--brand);color:var(--blue-d);} .authcard .gobtn{background:var(--brand);color:var(--blue-d);}
.gobtn:disabled{opacity:.9;cursor:default;}
.btn-spin{display:inline-block;width:15px;height:15px;border:2px solid rgba(22,41,79,.3);border-top-color:var(--blue-d);border-radius:50%;animation:btnspin .6s linear infinite;vertical-align:-3px;margin-right:7px;}
@keyframes btnspin{to{transform:rotate(360deg);}}
.authcard .gobtn:hover{filter:brightness(.95);} .authcard .gobtn:hover{filter:brightness(.95);}
.authtabs{display:flex;gap:.5rem;margin-bottom:1.1rem;} .authtabs{display:flex;gap:.5rem;margin-bottom:1.1rem;}
.authtabs button{flex:1;background:#eef1f6;color:var(--muted);font-weight:600;border:none;border-radius:9px;padding:.5rem;cursor:pointer;font-size:.9rem;} .authtabs button{flex:1;background:#eef1f6;color:var(--muted);font-weight:600;border:none;border-radius:9px;padding:.5rem;cursor:pointer;font-size:.9rem;}
@@ -760,7 +764,7 @@
<body> <body>
<script src="/icons.js?v=4"></script> <script src="/icons.js?v=4"></script>
<script src="https://cdn.jsdelivr.net/npm/@twemoji/api@15.1.0/dist/twemoji.min.js" crossorigin="anonymous"></script> <script src="https://cdn.jsdelivr.net/npm/@twemoji/api@15.1.0/dist/twemoji.min.js" crossorigin="anonymous"></script>
<script>window.__BUILD='2026-07-02-batch35';console.log('%cBiz Connect','color:#1F3B73;font-weight:bold','build '+window.__BUILD); <script>window.__BUILD='2026-07-02-batch36';console.log('%cBiz Connect','color:#1F3B73;font-weight:bold','build '+window.__BUILD);
// Render modern (Twemoji) emojis in place of the OS's flat ones. No-op if the CDN didn't load // Render modern (Twemoji) emojis in place of the OS's flat ones. No-op if the CDN didn't load
// (emojis stay as plain Unicode). (#5) // (emojis stay as plain Unicode). (#5)
function twemojify(el){ try{ if(el && window.twemoji) window.twemoji.parse(el, { folder:'svg', ext:'.svg' }); }catch(_){} }</script> function twemojify(el){ try{ if(el && window.twemoji) window.twemoji.parse(el, { folder:'svg', ext:'.svg' }); }catch(_){} }</script>
@@ -769,7 +773,7 @@ function twemojify(el){ try{ if(el && window.twemoji) window.twemoji.parse(el, {
<header> <header>
<div class="brandrow"> <div class="brandrow">
<button class="navtoggle" id="navToggle" title="Toggle menu" aria-label="Toggle menu"><svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg></button> <button class="navtoggle" id="navToggle" title="Toggle menu" aria-label="Toggle menu"><svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg></button>
<img src="/mark-light.svg" alt="" style="height:46px;width:auto;max-width:190px;border-radius:8px;object-fit:contain;image-rendering:-webkit-optimize-contrast" onerror="this.replaceWith(Object.assign(document.createElement('div'),{className:'logo',textContent:'B'}))"> <img src="/mark-light.png" alt="" style="height:46px;width:auto;max-width:190px;border-radius:8px;object-fit:contain;image-rendering:-webkit-optimize-contrast" onerror="this.replaceWith(Object.assign(document.createElement('div'),{className:'logo',textContent:'B'}))">
<div class="brand">Biz <span class="y">Connect</span></div> <div class="brand">Biz <span class="y">Connect</span></div>
</div> </div>
<div id="hdrRight"></div> <div id="hdrRight"></div>
@@ -2015,16 +2019,14 @@ function notify(title, body, kind, id){
}).catch(()=>{}); }).catch(()=>{});
return; return;
} }
// Web / PWA: standard Notification. Icon = the sender's/group's DP (falling back to their // Web / PWA: standard Notification. Use the sender's/group's DP URL DIRECTLY as the icon —
// initials, then the brand app icon) — never the old wordmark. badge = brand mark. // the browser fetches notification icons cross-origin fine, whereas drawing an external DP to a
// canvas taints it and silently fell back to initials (the bug). Brand icon only if no DP.
if(!('Notification' in window) || Notification.permission!=='granted') return; if(!('Notification' in window) || Notification.permission!=='granted') return;
notifAvatarDataUrl(kind, id, String(title||'').replace(/^[^\p{L}\p{N}]+/u,'')).then(avatar=>{ const _row=rowFor(kind,id)||{};
const n=new Notification(title, { body, icon: avatar || '/icon-192.png', badge:'/icon-192.png' }); const n=new Notification(title, { body, icon: _row.avatar || '/icon-192.png', badge:'/icon-192.png' });
n.onclick=()=>{ n.close(); openFromNotif(kind, id); }; n.onclick=()=>{ n.close(); openFromNotif(kind, id); };
setTimeout(()=>{ try{ n.close(); }catch(_){} }, 8000); setTimeout(()=>{ try{ n.close(); }catch(_){} }, 8000);
}).catch(()=>{
try{ const n=new Notification(title, { body, icon:'/icon-192.png' }); n.onclick=()=>{ n.close(); openFromNotif(kind, id); }; }catch(_){}
});
}catch(_){} }catch(_){}
} }
let _audioCtx=null; let _audioCtx=null;
@@ -2845,7 +2847,7 @@ async function renderLogin(){
const aw=document.getElementById('authwrap'); aw.style.display='flex'; const aw=document.getElementById('authwrap'); aw.style.display='flex';
let regOpen=false; try{ regOpen=(await (await fetch('/api/setup-state')).json()).registrationOpen; }catch(_){} let regOpen=false; try{ regOpen=(await (await fetch('/api/setup-state')).json()).registrationOpen; }catch(_){}
aw.innerHTML=`<div class="authcard"> aw.innerHTML=`<div class="authcard">
<div class="auth-brand"><img src="/mark-light.svg" alt="Biz Connect"><div class="wm">Biz <b>Connect</b></div></div> <div class="auth-brand"><img src="/icon-192.png" alt="Biz Connect"><div class="wm">Biz <b>Connect</b></div></div>
<h1>Welcome back</h1> <h1>Welcome back</h1>
<div class="sub">Sign in to access chats, screen share and connect.</div> <div class="sub">Sign in to access chats, screen share and connect.</div>
${regOpen?`<div class="authtabs"> ${regOpen?`<div class="authtabs">
@@ -2880,10 +2882,12 @@ async function renderLogin(){
} }
async function doLogin(){ async function doLogin(){
clearErr('li_err'); clearErr('li_err');
const btn=document.getElementById('li_btn'); const orig=btn?btn.innerHTML:'';
if(btn){ btn.disabled=true; btn.innerHTML='<span class="btn-spin"></span>Signing in…'; } // show progress on Enter/click
try{ try{
await postJSON('/api/login',{email:document.getElementById('li_email').value,password:document.getElementById('li_pw').value,remember:document.getElementById('li_remember').checked}); await postJSON('/api/login',{email:document.getElementById('li_email').value,password:document.getElementById('li_pw').value,remember:document.getElementById('li_remember').checked});
location.reload(); location.reload(); // the branded splash takes over from here
}catch(e){ showErr('li_err', /invalid credentials/i.test(e.message)?'Incorrect email or password. Please try again.':e.message); } }catch(e){ if(btn){ btn.disabled=false; btn.innerHTML=orig; } showErr('li_err', /invalid credentials/i.test(e.message)?'Incorrect email or password. Please try again.':e.message); }
} }
async function doRegister(){ async function doRegister(){
clearErr('rg_err'); clearErr('rg_err');
+1 -1
View File
@@ -55,7 +55,7 @@
<body> <body>
<header> <header>
<div class="brandrow"> <div class="brandrow">
<img src="/mark-light.svg" alt="" style="height:40px;width:auto;max-width:170px;border-radius:8px;object-fit:contain" onerror="this.style.display='none'"> <img src="/mark-light.png" alt="" style="height:40px;width:auto;max-width:170px;border-radius:8px;object-fit:contain" onerror="this.style.display='none'">
<div class="brand">Biz <span>Connect</span></div> <div class="brand">Biz <span>Connect</span></div>
</div> </div>
<div class="hdr-right"> <div class="hdr-right">
Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

-5
View File
@@ -1,5 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="Biz Connect">
<!-- White C + gold dot on a transparent background — for placing directly on the blue header -->
<path d="M40 10 A22 22 0 1 0 40 54" fill="none" stroke="#ffffff" stroke-width="9" stroke-linecap="round"/>
<circle cx="40" cy="32" r="6.5" fill="#FFC708"/>
</svg>

Before

Width:  |  Height:  |  Size: 366 B