eleven / messenger Public

web: the signed-out landing is a single-CTA screen — branding + Sign in (#374)

The landing now reads as one decision: the server's photo + name, and a single
prominent Sign in button (the plain link undersold the primary action). The
invite-paste form folds behind a quiet "Have an invite link?" text link — kept
reachable because the iOS Home-Screen app can only consume an invite link by
pasting it here (the A2HS onboarding sends people to do exactly that). With no
home server configured (or in the native wrapper, where the ceremony can't
run), the form shows up front as before.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Paul Campbell a91a7ecf5065f554194a9930a1e657aa09104274 parent 4ad5e0a
3 files changed, +48 −20
  • web/pages/index.html +20 −11
  • web/static/shell.js +14 −1
  • web/static/style.css +14 −8
diff --git a/web/pages/index.html b/web/pages/index.html
index ce8d0e5..ccc5696 100644
--- a/web/pages/index.html
+++ b/web/pages/index.html
@@ -179,24 +179,33 @@
</template>
<template id="noinvite-tpl">
+ <!-- The signed-out landing: a single-CTA screen — the server's branding and
+ one button, Sign in. New members arrive on an invite LINK (which opens
+ /join/… directly), so the paste form is folded behind "Have an invite
+ link?" — still reachable, because the iOS Home-Screen app has no other
+ way to consume a link (the A2HS onboarding says to paste it here). -->
<div class="center">
- <div class="card">
+ <div class="card landing">
<!-- Instance branding (plaintext by design — see CLAUDE.md): the photo is
revealed by showNoInvite once /api/group confirms one is set. -->
<div class="join-brand">
<img id="join-brand-photo" class="join-brand-photo hidden" alt="" />
<div class="join-brand-name">__GROUP_NAME__</div>
</div>
- <h1>Join the chat</h1>
- <p id="join-intro">Paste your secret invite link to join.</p>
- <form id="paste-join" autocomplete="off">
- <input id="link" placeholder="Paste your invite link" autocomplete="off" />
- <input id="name" placeholder="Screen name (e.g. Sophie)" maxlength="32" autocomplete="off" class="hidden" style="margin-top: 10px" />
- <div class="row"><button type="submit">Join the chat</button></div>
- </form>
- <p id="join-err" class="banner warn hidden" style="text-align:left;padding:0;margin-top:10px"></p>
- <div class="row" id="restore-row" style="display:none;margin-top:14px">
- <button type="button" id="restore-btn" class="signin-link">Sign in</button>
+ <div class="row" id="restore-row" style="display:none">
+ <button type="button" id="restore-btn" style="width:100%">Sign in</button>
+ </div>
+ <div id="join-fold">
+ <p id="join-intro" class="hidden">Paste your secret invite link to join.</p>
+ <form id="paste-join" autocomplete="off" class="hidden">
+ <input id="link" placeholder="Paste your invite link" autocomplete="off" />
+ <input id="name" placeholder="Screen name (e.g. Sophie)" maxlength="32" autocomplete="off" class="hidden" style="margin-top: 10px" />
+ <div class="row"><button type="submit">Join the chat</button></div>
+ </form>
+ <p id="join-err" class="banner warn hidden" style="text-align:left;padding:0;margin-top:10px"></p>
+ <div class="row" id="invite-reveal-row">
+ <button type="button" id="invite-reveal" class="signin-link">Have an invite link?</button>
+ </div>
</div>
</div>
</div>
diff --git a/web/static/shell.js b/web/static/shell.js
index 59288e0..93ab96f 100644
--- a/web/static/shell.js
+++ b/web/static/shell.js
@@ -6231,11 +6231,24 @@ function showNoInvite() {
}
};
- // Returning member on a fresh browser: offer passkey restore alongside paste.
+ // The one CTA: Sign in (passkey restore via home). The paste form stays
+ // folded behind "Have an invite link?" — still essential for the iOS
+ // Home-Screen app, which can only consume an invite link by pasting it here.
+ const showJoinForm = () => {
+ document.getElementById("join-intro").classList.remove("hidden");
+ document.getElementById("paste-join").classList.remove("hidden");
+ document.getElementById("invite-reveal-row").classList.add("hidden");
+ linkInput.focus();
+ };
+ document.getElementById("invite-reveal").onclick = showJoinForm;
const restoreRow = document.getElementById("restore-row");
if (restoreRow && canRestore()) {
restoreRow.style.display = "";
document.getElementById("restore-btn").onclick = () => { launchRestore(); };
+ } else {
+ // No home server configured (or the native wrapper, where the ceremony
+ // can't run): the join form is the only door — show it up front.
+ showJoinForm();
}
}
diff --git a/web/static/style.css b/web/static/style.css
index ff3d02c..43240dd 100644
--- a/web/static/style.css
+++ b/web/static/style.css
@@ -318,18 +318,24 @@ input:focus, textarea:focus { outline: 2px solid var(--accent); border-color: tr
}
.card .row { display: flex; gap: 10px; margin-top: 14px; }
-/* Landing branding: the server photo + name above "Join the chat" — plaintext
- by design (CLAUDE.md branding exception), so a signed-out visitor sees whose
- door they're at. The photo stays hidden until /api/group confirms one. */
-.join-brand { display: flex; flex-direction: column; align-items: center; gap: 8px; margin-bottom: 16px; }
-.join-brand-photo { width: 64px; height: 64px; border-radius: 16px; object-fit: cover; }
-.join-brand-name { font-weight: 600; font-size: 15px; }
-/* "Sign in" as a plain link, not a pill — returning members know the word. */
+/* Landing branding: the server photo + name — plaintext by design (CLAUDE.md
+ branding exception), so a signed-out visitor sees whose door they're at.
+ The photo stays hidden until /api/group confirms one. */
+.join-brand { display: flex; flex-direction: column; align-items: center; gap: 10px; margin: 6px 0 22px; }
+.join-brand-photo { width: 72px; height: 72px; border-radius: 18px; object-fit: cover; }
+.join-brand-name { font-weight: 650; font-size: 18px; font-family: var(--font-display); }
+/* The single-CTA landing: branding, ONE Sign in button, and the invite-paste
+ form folded behind a quiet text link ("Have an invite link?"). */
+.card.landing { text-align: center; }
+.card.landing .row { justify-content: center; margin-top: 16px; }
+.card.landing #paste-join .row { margin-top: 14px; }
+/* The folded-form reveal — quiet, link-like. */
.card .signin-link {
background: none; border: none; padding: 0; font: inherit; font-size: 14px;
- color: var(--link); text-decoration: underline; text-underline-offset: 2px;
+ color: var(--muted); text-decoration: underline; text-underline-offset: 2px;
cursor: pointer;
}
+.card .signin-link:hover { color: var(--link); }