eleven / messenger Public

home+ios+mac: exactly one sign-in door at a time (#1148)

Paul's rule, revising #1112's email-always-beneath: the card shows ONE way
in — last-used, else the platform default — and everything else, email
included, lives behind 'Other ways to sign in'. Mirrored on the native
first-run doors with an inline unfold.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Paul Campbell abe1875a5ea11208709929daee8ad6ad66922890 parent 6da7e5c
3 files changed, +34 −16
  • home/web/static/home.js +10 −14
  • ios/LChat/Views/SignInWithEleven.swift +11 −1
  • ios/LChatMac/MacHomeSignIn.swift +13 −1
diff --git a/home/web/static/home.js b/home/web/static/home.js
index 7314c4f..6d33b00 100644
--- a/home/web/static/home.js
+++ b/home/web/static/home.js
@@ -283,11 +283,12 @@ function primaryDoor() {
return DOORS[last] ? last : platformDefault();
}
-// composeCard arranges the main card: ONE primary door, the email door
-// always beneath it (unless email IS the primary — the email alternative is
-// always available), then the quiet "Other ways to sign in" link. Demoted
-// doors don't render here at all — they live in the all-ways step. Runs at
-// boot and again on sign-out (the marker survives it and may have moved).
+// composeCard arranges the main card: exactly ONE door — last-used when
+// known, else the platform default — then the quiet "Other ways to sign in"
+// link. Never a second door on the card (Paul 2026-07-19, revising #1112's
+// email-always-beneath rule): every demoted door, email included, lives in
+// the all-ways step. Runs at boot and again on sign-out (the marker
+// survives it and may have moved).
function composeCard() {
const primary = primaryDoor();
const apple = $("apple-signin"), email = $("email-signin"), passkey = $("signin");
@@ -295,16 +296,11 @@ function composeCard() {
// Promotions restyle in place: email sheds .secondary when it leads, and
// the passkey door trades its grey link for a proper primary button.
// Wholesale className is safe — these two never carry other classes.
- email.className = primary === "email" ? "" : "secondary";
+ email.className = primary === "email" ? "" : "secondary hidden";
passkey.className = primary === "passkey" ? "" : "passkey-link hidden";
- // DOM order is focus order: the primary leads, then the email alternative,
- // then the link. append MOVES the existing (already-wired) nodes; hidden
- // doors stay parked ahead of them, unfocusable.
- $("main-doors").append(
- { apple, email, passkey }[primary],
- ...(primary === "email" ? [] : [email]),
- $("other-ways"),
- );
+ // DOM order is focus order: the one door, then the link. append MOVES the
+ // existing (already-wired) nodes; hidden doors stay parked, unfocusable.
+ $("main-doors").append({ apple, email, passkey }[primary], $("other-ways"));
}
async function signIn() {
diff --git a/ios/LChat/Views/SignInWithEleven.swift b/ios/LChat/Views/SignInWithEleven.swift
index 0d8a49e..d4714ea 100644
--- a/ios/LChat/Views/SignInWithEleven.swift
+++ b/ios/LChat/Views/SignInWithEleven.swift
@@ -402,6 +402,8 @@ struct SignInFirstRun: View {
// Which door the last tap opened — only so the promoted passkey button
// doesn't spin for an Apple verify that happens to share phase == .working.
@State private var tapped: HomeSignInModel.SignInMethod?
+ // One door at a time: the demoted doors unfold behind this.
+ @State private var showAllDoors = false
@FocusState private var focus: EmailStep?
var body: some View {
@@ -495,10 +497,18 @@ struct SignInFirstRun: View {
}
private var doors: some View {
+ // Exactly ONE door at a time — last-used when known, else the
+ // platform default — with the rest behind "Other ways to sign in"
+ // (Paul 2026-07-19, the web card's rule, mirrored).
VStack(spacing: 12) {
- ForEach(doorOrder, id: \.self) { method in
+ ForEach(showAllDoors ? doorOrder : Array(doorOrder.prefix(1)), id: \.self) { method in
door(method, leading: method == doorOrder.first)
}
+ if !showAllDoors {
+ Button("Other ways to sign in") { showAllDoors = true }
+ .font(.subheadline.weight(.medium))
+ .foregroundStyle(.secondary)
+ }
// The home page's privacy pitch, word for word.
Text("No signup required. We don't store any personal details. We can't spam what we don't have.")
diff --git a/ios/LChatMac/MacHomeSignIn.swift b/ios/LChatMac/MacHomeSignIn.swift
index 3158da8..221319e 100644
--- a/ios/LChatMac/MacHomeSignIn.swift
+++ b/ios/LChatMac/MacHomeSignIn.swift
@@ -531,6 +531,8 @@ struct MacFirstRun: View {
// Which door the last click opened — only so the promoted passkey button
// doesn't spin for an Apple verify that happens to share phase == .working.
@State private var tapped: MacHomeSignIn.SignInMethod?
+ // One door at a time: the demoted doors unfold behind this.
+ @State private var showAllDoors = false
@FocusState private var focus: EmailStep?
var body: some View {
@@ -617,10 +619,20 @@ struct MacFirstRun: View {
}
private var doors: some View {
+ // Exactly ONE door at a time — last-used when known, else the
+ // platform default — with the rest behind "Other ways to sign in"
+ // (Paul 2026-07-19, the web card's rule, mirrored).
VStack(spacing: 10) {
- ForEach(doorOrder, id: \.self) { method in
+ ForEach(showAllDoors ? doorOrder : Array(doorOrder.prefix(1)), id: \.self) { method in
door(method, leading: method == doorOrder.first)
}
+ if !showAllDoors {
+ Button("Other ways to sign in") { showAllDoors = true }
+ .buttonStyle(.plain)
+ .font(.subheadline.weight(.medium))
+ .foregroundStyle(.secondary)
+ .padding(.vertical, 2)
+ }
// The home page's privacy pitch, word for word.
Text("No signup required. We don't store any personal details. We can't spam what we don't have.")