Account · Sign in: log in, sign up, providers, a link by email, a second factor

LoginForm takes an email and a password, keeps one signed in on a box,
and signs in on Enter once the address reads and a password is typed;
a failed try says why above Sign in. SignupForm ticks a new password's
rules as they hold and waits for all of them and the terms.
OAuthButtons offer each provider, one spinning while the rest rest.
MagicLinkForm and ForgotPassword send a link to an address and, once
sent, say where it went and offer it again. TwoFactorInput takes a
six-digit code at once and, after one fails, clears the boxes through
forms::PinInput's new attempt, keeping its focus. The Account chapter
splits into sign-in, profile and access, and billing and team.
This commit is contained in:
ZacharyZhang-NY committed 2026-09-27 13:12:49 -04:00
1 parent 69befddeb3
commit c9e3400a71
15 files changed
+1455 -10

No files matched your search

+46
View File
@@ -0,0 +1,46 @@
use gpui::{AnyElement, App, Window, div, prelude::*};
mod sign_in;
use super::Page;
use crate::script::Step;
pub const PAGE: Page = Page {
number: 36,
slug: "account",
title: "Account",
summary: "Signing in and up, a link by email, a second factor, and the account once inside.",
render,
script: SCRIPT,
};
const SCRIPT: &[Step] = &[
Step::DownAt("account-login", 150.0, 280.0),
Step::UpAt("account-login", 150.0, 280.0),
Step::Type("ada@example.com"),
Step::Key("tab"),
Step::Type("hunter22"),
Step::Key("enter"),
Step::Wait(200),
Step::Shot("sign-in-failed"),
Step::DownAt("account-magic", 150.0, 150.0),
Step::UpAt("account-magic", 150.0, 150.0),
Step::Type("ada@example.com"),
Step::Key("enter"),
Step::Wait(200),
Step::Shot("link-sent"),
Step::DownAt("account-factor", 120.0, 110.0),
Step::UpAt("account-factor", 120.0, 110.0),
Step::Type("123456"),
Step::Wait(200),
Step::Shot("code-failed"),
];
fn render(window: &mut Window, cx: &mut App) -> AnyElement {
div()
.child(sign_in::sign_in(window, cx))
.child(sign_in::sign_up(window, cx))
.child(sign_in::by_email(window, cx))
.child(sign_in::second_factor(window, cx))
.into_any_element()
}
+177
View File
@@ -0,0 +1,177 @@
use ely_gpui_component::{
account::{ForgotPassword, LoginForm, MagicLinkForm, OAuthButtons, SignupForm, TwoFactorInput},
feedback::InlineMessage,
layout::{Card, CardHeader},
primitives::{Divider, Severity},
};
use gpui::{App, IntoElement, ParentElement, SharedString, Styled, Window, div, px};
use crate::{
probe::probe,
ui::{change, keep, section},
};
/// The sign-in demo: the provider on its way, and why the last try failed.
#[derive(Default)]
struct SignIn {
provider: Option<SharedString>,
error: Option<SharedString>,
}
pub fn sign_in(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let state = keep("account-sign-in", SignIn::default, window, cx);
let now = state.read(cx);
let (provider, error) = (now.provider.clone(), now.error.clone());
let [picked, tried] = [(); 2].map(|_| state.clone());
let mut providers = OAuthButtons::new(
"account-oauth",
[
("github", "GitHub"),
("google", "Google"),
("apple", "Apple"),
],
)
.on_pick(move |key, _, cx| change(&picked, cx, |state| state.provider = Some(key.clone())));
if let Some(provider) = provider {
providers = providers.busy(provider);
}
let mut form = LoginForm::new("account-login")
.on_submit(move |_, _, cx| {
change(&tried, cx, |state| {
state.error = Some("No account uses that email and password.".into())
})
})
.on_forgot(|_, _| log::info!("gallery: forgot password"))
.on_sign_up(|_, _| log::info!("gallery: sign up"));
if let Some(error) = error {
form = form.error(error);
}
section(
"LoginForm · OAuthButtons",
"Sign in with a provider or with an email and a password. Sign in rests until the address reads and a password is typed; Enter in a field signs in, and a failed try says why above the button.",
cx,
)
.child(probe(
"account-login",
div().w(px(380.)).child(
Card::new()
.header(CardHeader::new("Sign in to Ely").description("Welcome back."))
.child(div().flex().flex_col().gap_5().child(providers).child(Divider::horizontal().label("or")).child(form)),
),
))
}
/// The sign-up demo: why the last try failed.
#[derive(Default)]
struct SignUp {
error: Option<SharedString>,
}
pub fn sign_up(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let state = keep("account-sign-up", SignUp::default, window, cx);
let error = state.read(cx).error.clone();
let tried = state.clone();
let mut form = SignupForm::new("account-signup")
.terms("I agree to the terms of service")
.on_submit(move |signup, _, cx| {
let taken = format!("An account already uses {}.", signup.email);
change(&tried, cx, |state| state.error = Some(taken.into()))
})
.on_sign_in(|_, _| log::info!("gallery: sign in"));
if let Some(error) = error {
form = form.error(error);
}
section(
"SignupForm",
"A name, an email and a new password, whose rules tick as they hold. Create account waits for every rule and the terms.",
cx,
)
.child(div().w(px(380.)).child(Card::new().header(CardHeader::new("Create an account")).child(form)))
}
/// The email demos: whether each link went.
#[derive(Default)]
struct ByEmail {
magic: bool,
reset: bool,
}
pub fn by_email(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let state = keep("account-by-email", ByEmail::default, window, cx);
let now = state.read(cx);
let (magic, reset) = (now.magic, now.reset);
let [sent, other, asked, back] = [(); 4].map(|_| state.clone());
section(
"MagicLinkForm · ForgotPassword",
"A link by email: one that signs in, one that sets a new password. Once sent, each says where it went and offers it again.",
cx,
)
.child(
div()
.flex()
.flex_wrap()
.gap_6()
.child(probe(
"account-magic",
div().w(px(360.)).child(
Card::new().header(CardHeader::new("Sign in by email")).child(
MagicLinkForm::new("account-magic")
.sent(magic)
.on_send(move |_, _, cx| change(&sent, cx, |state| state.magic = true))
.on_other(move |_, cx| change(&other, cx, |state| state.magic = false)),
),
),
))
.child(
div().w(px(360.)).child(
Card::new().header(CardHeader::new("Reset your password")).child(
ForgotPassword::new("account-forgot")
.sent(reset)
.on_send(move |_, _, cx| change(&asked, cx, |state| state.reset = true))
.on_back(move |_, cx| change(&back, cx, |state| state.reset = false)),
),
),
),
)
}
/// The code the demo takes.
const CODE: &str = "246810";
/// The second-factor demo: codes that failed, and whether one passed.
#[derive(Default)]
struct Factor {
failed: usize,
passed: bool,
}
pub fn second_factor(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let state = keep("account-factor", Factor::default, window, cx);
let now = state.read(cx);
let (failed, passed) = (now.failed, now.passed);
let checked = state.clone();
section(
"TwoFactorInput",
format!("Six boxes for an authenticator's code; a full code goes at once. A code that fails clears the boxes and says so. The demo takes {CODE}."),
cx,
)
.child(probe(
"account-factor",
div().w(px(380.)).child(
Card::new()
.header(CardHeader::new("Two-step sign-in"))
.child(
TwoFactorInput::new("account-factor")
.failed(failed)
.on_code(move |code, _, cx| {
change(&checked, cx, |state| match code.as_ref() == CODE {
true => state.passed = true,
false => state.failed += 1,
})
})
.on_recovery(|_, _| log::info!("gallery: recovery code")),
)
.children(passed.then(|| InlineMessage::new(Severity::Success, "Signed in."))),
),
))
}
+2
View File
@@ -1,3 +1,4 @@
mod account;
mod agent;
mod buttons;
mod calendar;
@@ -85,6 +86,7 @@ pub const ALL: &[Page] = &[
devtools::PAGE,
dashboard::PAGE,
settings::PAGE,
account::PAGE,
theme::PAGE,
];