eleven / ocho Public

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>
Paul Campbell 84342b735ac875b5774c5bc728458efb1df5d5cb parent 9434be6
3 files changed, +37 −9
  • changelog/2026-07-07-pending-invite-card.md +4 −0
  • web/static/shell.js +16 −4
  • web/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); }