Skip to content

Commit 51bd1fc

Browse files
committed
Redesign auth and welcome entry pages
1 parent de4dfc7 commit 51bd1fc

8 files changed

Lines changed: 239 additions & 207 deletions

File tree

packages/web/src/features/auth/index.test.tsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,25 @@ describe("LoginPage", () => {
4545
expect(screen.getAllByText("连接中").length).toBeGreaterThan(0);
4646
});
4747

48+
it("renders the flat auth shell sections in order while auth status is loading", () => {
49+
globalThis.fetch = vi.fn(() => new Promise(() => {})) as typeof fetch;
50+
51+
const { container } = render(
52+
<Provider>
53+
<LoginPage />
54+
</Provider>
55+
);
56+
57+
const content = container.querySelector(".auth-card-shell__content");
58+
expect(content).toBeTruthy();
59+
expect(container.querySelector(".auth-hero")).toBeTruthy();
60+
expect(container.querySelector(".auth-status-panel")).toBeTruthy();
61+
expect(container.querySelector(".auth-form")).toBeTruthy();
62+
expect(content?.firstElementChild).toHaveClass("auth-hero");
63+
expect(content?.children[1]).toHaveClass("auth-status-panel");
64+
expect(content?.lastElementChild).toHaveClass("auth-form");
65+
});
66+
4867
it("renders the password field with a dedicated hint when auth is available", async () => {
4968
globalThis.fetch = vi.fn().mockResolvedValue({
5069
ok: true,

packages/web/src/features/auth/index.tsx

Lines changed: 8 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,10 @@ import { useAtom, useAtomValue } from "jotai";
22
import { useEffect, useState } from "react";
33
import { authenticatedAtom, localeAtom } from "../../atoms/app-ui";
44
import { authEnabledAtom } from "../../atoms/connection";
5-
import { Button, EmptyState, Input } from "../../components/ui";
5+
import { Button, Input } from "../../components/ui";
66
import { useViewport } from "../../hooks/use-viewport";
77
import { formatDate, useTranslation } from "../../lib/i18n";
88

9-
const authEmptyStateStyle = {
10-
minHeight: "auto",
11-
padding: 0,
12-
gap: "var(--sp-5)",
13-
alignItems: "stretch",
14-
textAlign: "left" as const,
15-
};
16-
179
export function LoginPage({
1810
onAuthenticated,
1911
}: {
@@ -162,20 +154,15 @@ export function LoginPage({
162154
.join(" ")}
163155
>
164156
<div className="auth-card-shell__content">
165-
<EmptyState
166-
style={authEmptyStateStyle}
167-
title={
168-
<div>
169-
<div className="welcome-kicker page-kicker">CODER STUDIO</div>
170-
<h1 className="welcome-title page-title">{t("app.name")}</h1>
171-
</div>
172-
}
173-
description={<p className="welcome-body auth-card-desc meta-text">{description}</p>}
174-
/>
175-
<div className={statusPanelClassName}>
157+
<section className="auth-hero">
158+
<div className="welcome-kicker page-kicker">CODER STUDIO</div>
159+
<h1 className="welcome-title page-title">{t("app.name")}</h1>
160+
<p className="welcome-body auth-card-desc meta-text">{description}</p>
161+
</section>
162+
<section className={statusPanelClassName}>
176163
<div className="auth-status-eyebrow">{t("auth.status_title")}</div>
177164
<p className="auth-status-detail">{error ?? statusDetail}</p>
178-
</div>
165+
</section>
179166
<form className="auth-form" onSubmit={handleSubmit}>
180167
<Input
181168
className="auth-input"

packages/web/src/features/auth/session-gate.test.tsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,25 @@ describe("SessionGatePage", () => {
6060
expect(screen.getByRole("button", { name: "重新进入" })).toBeInTheDocument();
6161
});
6262

63+
it("uses the same segmented auth shell without rendering a password field", () => {
64+
const store = createStore();
65+
store.set(authEnabledAtom, false);
66+
store.set(activationStatusAtom, "gated");
67+
68+
const { container } = render(
69+
<Provider store={store}>
70+
<MemoryRouter>
71+
<SessionGatePage />
72+
</MemoryRouter>
73+
</Provider>
74+
);
75+
76+
expect(container.querySelector(".auth-hero")).toBeTruthy();
77+
expect(container.querySelector(".auth-status-panel")).toBeTruthy();
78+
expect(container.querySelector(".auth-actions")).toBeTruthy();
79+
expect(screen.queryByLabelText("密码")).not.toBeInTheDocument();
80+
});
81+
6382
it("returns to the app bootstrap entry when re-enter is clicked", async () => {
6483
const store = createStore();
6584
store.set(authEnabledAtom, false);

packages/web/src/features/auth/session-gate.tsx

Lines changed: 24 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,7 @@
1-
import { Button, EmptyState } from "../../components/ui";
1+
import { Button } from "../../components/ui";
22
import { useViewport } from "../../hooks/use-viewport";
33
import { useTranslation } from "../../lib/i18n";
44

5-
const gateEmptyStateStyle = {
6-
minHeight: "auto",
7-
padding: 0,
8-
gap: "var(--sp-5)",
9-
alignItems: "stretch",
10-
textAlign: "left" as const,
11-
};
12-
135
export function SessionGatePage() {
146
const t = useTranslation();
157
const isMobile = useViewport() === "mobile";
@@ -33,31 +25,30 @@ export function SessionGatePage() {
3325
.filter(Boolean)
3426
.join(" ")}
3527
>
36-
<EmptyState
37-
style={gateEmptyStateStyle}
38-
title={
39-
<div>
40-
<div className="welcome-kicker">{t("auth.session_gate_title")}</div>
41-
<h1 className="welcome-title">{t("app.name")}</h1>
42-
</div>
43-
}
44-
description={
45-
<p className="welcome-body auth-card-desc">{t("auth.session_gate_description")}</p>
46-
}
47-
/>
48-
<div className="auth-status-panel auth-status-panel-error">
49-
<div className="auth-status-eyebrow">{t("auth.status_title")}</div>
50-
<p className="auth-status-detail">{t("auth.session_gate_detail")}</p>
28+
<div className="auth-card-shell__content">
29+
<section className="auth-hero">
30+
<div className="welcome-kicker page-kicker">{t("auth.session_gate_title")}</div>
31+
<h1 className="welcome-title page-title">{t("app.name")}</h1>
32+
<p className="welcome-body auth-card-desc meta-text">
33+
{t("auth.session_gate_description")}
34+
</p>
35+
</section>
36+
<section className="auth-status-panel auth-status-panel-error">
37+
<div className="auth-status-eyebrow">{t("auth.status_title")}</div>
38+
<p className="auth-status-detail">{t("auth.session_gate_detail")}</p>
39+
</section>
40+
<div className="auth-actions">
41+
<Button
42+
className="auth-submit"
43+
variant="primary"
44+
size="lg"
45+
type="button"
46+
onClick={() => window.location.replace("/")}
47+
>
48+
{t("auth.session_gate_reenter")}
49+
</Button>
50+
</div>
5151
</div>
52-
<Button
53-
className="auth-submit"
54-
variant="primary"
55-
size="lg"
56-
type="button"
57-
onClick={() => window.location.replace("/")}
58-
>
59-
{t("auth.session_gate_reenter")}
60-
</Button>
6152
</div>
6253
</div>
6354
);

packages/web/src/features/welcome/index.test.tsx

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,10 +98,10 @@ describe("WelcomePage", () => {
9898
expect(screen.getByRole("heading", { name: "Welcome to Coder Studio" })).toBeInTheDocument();
9999
expect(document.querySelector(".welcome-card__hero")).toBeTruthy();
100100
expect(document.querySelector(".welcome-card__actions")).toBeTruthy();
101-
expect(document.querySelector(".welcome-card__panel")).toBeTruthy();
101+
expect(document.querySelector(".welcome-card__features")).toBeTruthy();
102+
expect(document.querySelector(".welcome-actions-group")).toBeTruthy();
102103
const openWorkspaceButton = screen.getByRole("button", { name: "Open Workspace" });
103104
const settingsButton = screen.getByRole("button", { name: "Settings" });
104-
expect(document.querySelector(".welcome-divider")).toBeTruthy();
105105
const featureCards = Array.from(document.querySelectorAll(".welcome-feature"));
106106

107107
expect(featureCards).toHaveLength(3);
@@ -123,4 +123,23 @@ describe("WelcomePage", () => {
123123
)
124124
).toBe(true);
125125
});
126+
127+
it("renders the flat welcome shell with hero, actions, and features sections", () => {
128+
const store = createStore();
129+
store.set(localeAtom, "en");
130+
131+
const { container } = render(
132+
<Provider store={store}>
133+
<MemoryRouter>
134+
<WelcomePage />
135+
</MemoryRouter>
136+
</Provider>
137+
);
138+
139+
expect(container.querySelector(".welcome-card__hero")).toBeTruthy();
140+
expect(container.querySelector(".welcome-card__actions")).toBeTruthy();
141+
expect(container.querySelector(".welcome-card__features")).toBeTruthy();
142+
expect(container.querySelector(".welcome-actions-group")).toBeTruthy();
143+
expect(container.querySelector(".welcome-card__panel")).toBeFalsy();
144+
});
126145
});

packages/web/src/features/welcome/index.tsx

Lines changed: 15 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import type { FC } from "react";
99
import { useState } from "react";
1010
import { useNavigate } from "react-router-dom";
11-
import { EmptyState, ThemedIcon } from "../../components/ui";
11+
import { ThemedIcon } from "../../components/ui";
1212
import { useViewport } from "../../hooks/use-viewport";
1313
import { useTranslation } from "../../lib/i18n";
1414
import type { IconSemantic } from "../../theme";
@@ -20,12 +20,6 @@ interface FeatureItem {
2020
description: string;
2121
}
2222

23-
const welcomeEmptyStateStyle = {
24-
minHeight: "auto",
25-
padding: 0,
26-
gap: "var(--sp-5)",
27-
};
28-
2923
/**
3024
* Welcome Page
3125
*
@@ -71,33 +65,26 @@ export const WelcomePage: FC = () => {
7165
<div className={`welcome-container ${isMobile ? "welcome-container--mobile" : ""}`}>
7266
<div className={`welcome-card ${isMobile ? "welcome-card--mobile" : ""}`}>
7367
<div className="welcome-card__hero">
74-
<EmptyState
75-
style={welcomeEmptyStateStyle}
76-
title={
77-
<div>
78-
<div className="welcome-kicker page-kicker">{t("welcome.kicker")}</div>
79-
<h1 className="welcome-title page-title">{t("welcome.title")}</h1>
80-
</div>
81-
}
82-
description={<p className="welcome-body meta-text">{t("welcome.description")}</p>}
83-
/>
68+
<div className="welcome-kicker page-kicker">{t("welcome.kicker")}</div>
69+
<h1 className="welcome-title page-title">{t("welcome.title")}</h1>
70+
<p className="welcome-body meta-text">{t("welcome.description")}</p>
8471
</div>
8572

8673
<div className="welcome-card__actions">
87-
<button className="welcome-btn" onClick={handleOpenWorkspace}>
88-
<ThemedIcon semantic="nav.newWorkspace" size={18} />
89-
<span>{t("action.open_workspace")}</span>
90-
</button>
74+
<div className="welcome-actions-group">
75+
<button className="welcome-btn" onClick={handleOpenWorkspace}>
76+
<ThemedIcon semantic="nav.newWorkspace" size={18} />
77+
<span>{t("action.open_workspace")}</span>
78+
</button>
9179

92-
<button className="welcome-link" onClick={handleOpenSettings}>
93-
<ThemedIcon semantic="nav.settings" size={14} />
94-
<span>{t("action.settings")}</span>
95-
</button>
80+
<button className="welcome-link" onClick={handleOpenSettings}>
81+
<ThemedIcon semantic="nav.settings" size={14} />
82+
<span>{t("action.settings")}</span>
83+
</button>
84+
</div>
9685
</div>
9786

98-
<div className="welcome-card__panel">
99-
<div className="welcome-divider" />
100-
87+
<div className="welcome-card__features">
10188
<div className="welcome-features">
10289
{features.map((feature) => (
10390
<div className="welcome-feature" key={feature.iconSemantic}>

0 commit comments

Comments
 (0)