web: refine the pending-invite card to the product's design language (#282)
* web: refine the pending-invite card — invitee tile, clearer copy, primary Copy link Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * web: pending card wide enough for the invite link on one line Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> ---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
3 files changed,
+37
−9
changelog/2026-07-07-pending-invite-card.md+4 −0web/static/shell.js+16 −4web/static/style.css+17 −5
diff --git a/changelog/2026-07-07-pending-invite-card.md b/changelog/2026-07-07-pending-invite-card.md| new file mode 100644 |
| index 0000000..824ab93 |
| --- /dev/null |
| +++ b/changelog/2026-07-07-pending-invite-card.md |
| @@ -0,0 +1,4 @@ |
| +🤖 While you wait for someone you've invited to join, their chat now shows a |
| +friendlier card — their name and picture up top, a clearer note that the link |
| +is their key into the chat (so share it privately), and a proper Copy link |
| +button. |
diff --git a/web/static/shell.js b/web/static/shell.js| index b9d6863..90cba58 100644 |
| --- a/web/static/shell.js |
| +++ b/web/static/shell.js |
| @@ -5293,11 +5293,24 @@ function updatePendingBanner(t) { |
| const card = document.createElement("div"); |
| card.className = "pending-card"; |
| + // The invitee's identity leads — the same tile the sidebar and header use — |
| + // so the card reads as "them", not as a system notice. |
| + const av = avatarEl(invitee.label); |
| + av.classList.add("avatar-lg", "pending-avatar"); |
| + card.appendChild(av); |
| + |
| + const title = document.createElement("div"); |
| + title.className = "pending-title"; |
| + title.textContent = invitee.label ? `${invitee.label} hasn't joined yet` : "They haven't joined yet"; |
| + card.appendChild(title); |
| + |
| + const link = inviteLinks[t.id]; |
| const msg = document.createElement("p"); |
| - msg.textContent = `${invitee.label || "They"} hasn't joined yet. Share this link so they can — anyone with it can read this chat.`; |
| + msg.textContent = link |
| + ? "Send them this link to let them in. It's the key to this chat — anyone who has it can read it, so share it privately." |
| + : "This device doesn't hold their invite link. Create a fresh one to send them — it's the key to this chat, so share it privately."; |
| card.appendChild(msg); |
| - const link = inviteLinks[t.id]; |
| if (link) { |
| const box = document.createElement("div"); |
| box.className = "key-box mono"; |
| @@ -5309,14 +5322,13 @@ function updatePendingBanner(t) { |
| const row = document.createElement("div"); |
| row.className = "row"; |
| if (link) { |
| + // Copying the link is THE action here — it gets the primary pill. |
| const copyBtn = document.createElement("button"); |
| - copyBtn.className = "secondary"; |
| copyBtn.textContent = "Copy link"; |
| copyBtn.onclick = () => copyToButton(link, copyBtn); |
| row.appendChild(copyBtn); |
| } else { |
| const get = document.createElement("button"); |
| - get.className = "secondary"; |
| get.textContent = "Get invite link"; |
| get.onclick = async () => { |
| get.disabled = true; |
diff --git a/web/static/style.css b/web/static/style.css| index 28dc2b7..3efe90e 100644 |
| --- a/web/static/style.css |
| +++ b/web/static/style.css |
| @@ -1446,15 +1446,27 @@ input:focus, textarea:focus { outline: 2px solid var(--accent); border-color: tr |
| } |
| .pending-card { |
| width: 100%; |
| - max-width: 480px; |
| + max-width: 580px; /* wide enough that a typical invite link fits on one line */ |
| text-align: center; |
| - padding: 22px 20px; |
| + padding: 28px 24px; |
| border: 1px solid var(--border); |
| - border-radius: 16px; |
| + border-radius: 24px; /* the onboarding-card radius — same family as join */ |
| background: var(--bubble-them); |
| } |
| -.pending-card p { margin: 0 0 14px; font-size: 15px; color: var(--muted); } |
| -.pending-card .key-box { margin: 0 0 14px; text-align: left; } |
| +.pending-card .pending-avatar { margin: 0 auto 4px; } |
| +.pending-card .pending-title { |
| + font-family: var(--font-display); |
| + font-weight: 700; |
| + font-size: 20px; |
| + margin: 8px 0 6px; |
| +} |
| +.pending-card p { margin: 0 0 16px; font-size: 15px; color: var(--muted); } |
| +/* The link well hugs the link (centered), so it wraps only when the pane is |
| + genuinely too narrow for one line. */ |
| +.pending-card .key-box { |
| + width: fit-content; max-width: 100%; |
| + margin: 0 auto 16px; text-align: left; background: var(--panel); |
| +} |
| .pending-card .row { justify-content: center; gap: 10px; flex-wrap: wrap; } |
| .linkish.danger { color: var(--danger); } |