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>
3 files changed,
+34
−16
home/web/static/home.js+10 −14ios/LChat/Views/SignInWithEleven.swift+11 −1ios/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.") |