Skip to content

Commit 4dcbcba

Browse files
committed
feat: polish desktop web styling
1 parent 0d3d704 commit 4dcbcba

28 files changed

Lines changed: 1680 additions & 207 deletions

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,9 @@ describe("LoginPage", () => {
3838
expect(document.querySelector(".welcome-card")).toBeTruthy();
3939
expect(document.querySelector(".auth-form")).toBeTruthy();
4040
expect(document.querySelector(".auth-status-panel")).toBeTruthy();
41-
expect(container.querySelector(".auth-card-shell > .auth-status-panel")).toBeTruthy();
42-
expect(container.querySelector(".auth-card-shell > .auth-form")).toBeTruthy();
41+
expect(container.querySelector(".auth-card-shell__content")).toBeTruthy();
42+
expect(container.querySelector(".auth-card-shell__content > .auth-status-panel")).toBeTruthy();
43+
expect(container.querySelector(".auth-card-shell__content > .auth-form")).toBeTruthy();
4344
expect(screen.getByRole("button")).toBeDisabled();
4445
expect(screen.getAllByText("连接中").length).toBeGreaterThan(0);
4546
});

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

Lines changed: 35 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -161,40 +161,42 @@ export function LoginPage({
161161
.filter(Boolean)
162162
.join(" ")}
163163
>
164-
<EmptyState
165-
style={authEmptyStateStyle}
166-
title={
167-
<div>
168-
<div className="welcome-kicker">CODER STUDIO</div>
169-
<h1 className="welcome-title">{t("app.name")}</h1>
170-
</div>
171-
}
172-
description={<p className="welcome-body auth-card-desc">{description}</p>}
173-
/>
174-
<div className={statusPanelClassName}>
175-
<div className="auth-status-eyebrow">{t("auth.status_title")}</div>
176-
<p className="auth-status-detail">{error ?? statusDetail}</p>
177-
</div>
178-
<form className="auth-form" onSubmit={handleSubmit}>
179-
<Input
180-
className="auth-input"
181-
type="password"
182-
size="lg"
183-
value={password}
184-
onChange={(e) => setPassword(e.target.value)}
185-
aria-label={t("settings.auth.password")}
186-
placeholder={t("settings.auth.password")}
164+
<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>}
187174
/>
188-
<Button
189-
className="auth-submit"
190-
variant="primary"
191-
size="lg"
192-
type="submit"
193-
disabled={checkingStatus || submitting || !password.trim()}
194-
>
195-
{submitLabel}
196-
</Button>
197-
</form>
175+
<div className={statusPanelClassName}>
176+
<div className="auth-status-eyebrow">{t("auth.status_title")}</div>
177+
<p className="auth-status-detail">{error ?? statusDetail}</p>
178+
</div>
179+
<form className="auth-form" onSubmit={handleSubmit}>
180+
<Input
181+
className="auth-input"
182+
type="password"
183+
size="lg"
184+
value={password}
185+
onChange={(e) => setPassword(e.target.value)}
186+
aria-label={t("settings.auth.password")}
187+
placeholder={t("settings.auth.password")}
188+
/>
189+
<Button
190+
className="auth-submit"
191+
variant="primary"
192+
size="lg"
193+
type="submit"
194+
disabled={checkingStatus || submitting || !password.trim()}
195+
>
196+
{submitLabel}
197+
</Button>
198+
</form>
199+
</div>
198200
</div>
199201
</div>
200202
);

packages/web/src/features/command-palette/components/command-palette.test.tsx

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,43 @@ describe("CommandPalette", () => {
233233
expect(store.get(terminalPanelVisibleAtom)).toBe(true);
234234
});
235235

236+
it("keeps the desktop palette overlay, chrome, and selected item styling visible", () => {
237+
const store = createStore();
238+
store.set(localeAtom, "en");
239+
store.set(commandPaletteOpenAtom, true);
240+
store.set(workspacesAtom, {
241+
"ws-1": createWorkspace("ws-1", "/tmp/one"),
242+
});
243+
store.set(workspaceOrderAtom, ["ws-1"]);
244+
store.set(workspacesLoadStateAtom, "ready");
245+
246+
render(
247+
<Provider store={store}>
248+
<CommandPalette />
249+
</Provider>
250+
);
251+
252+
const palette = document.querySelector(".command-palette");
253+
const overlay = document.querySelector(".command-palette-overlay");
254+
const searchInput = screen.getByRole("textbox");
255+
256+
expect(overlay).toBeTruthy();
257+
expect(palette).toBeTruthy();
258+
expect(document.querySelector(".command-palette-header")).toBeTruthy();
259+
expect(document.querySelector(".command-palette-search")).toBeTruthy();
260+
expect(document.querySelector(".command-palette-hint")).toBeTruthy();
261+
expect(document.querySelector(".command-palette-list")).toBeTruthy();
262+
expect(palette).toContainElement(searchInput);
263+
264+
fireEvent.change(searchInput, {
265+
target: { value: "settings" },
266+
});
267+
268+
fireEvent.keyDown(palette!, { key: "ArrowDown" });
269+
270+
expect(document.querySelector(".command-palette-item-selected")).toBeTruthy();
271+
});
272+
236273
it("closes the mobile palette before opening the workspace launcher", () => {
237274
viewportMocks.viewport = "mobile";
238275

packages/web/src/features/command-palette/components/command-palette.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,7 @@ export function CommandPalette() {
257257
return (
258258
<div className="command-palette-overlay" onClick={() => setIsOpen(false)}>
259259
<div
260-
className="command-palette"
260+
className="command-palette command-palette--desktop"
261261
onClick={(e) => e.stopPropagation()}
262262
onKeyDown={handleKeyDown}
263263
>

packages/web/src/features/not-found/index.test.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ describe("NotFoundPage", () => {
3636

3737
expect(document.querySelector(".welcome-container")).toBeTruthy();
3838
expect(document.querySelector(".welcome-card")).toBeTruthy();
39+
expect(document.querySelector(".welcome-card__panel")).toBeTruthy();
40+
expect(document.querySelector(".welcome-card__panel .auth-status-panel")).toBeTruthy();
3941
expect(screen.getByText("Requested path")).toBeInTheDocument();
4042
expect(screen.getByText("/missing/path")).toBeInTheDocument();
4143
expect(screen.getByRole("heading", { name: "Page not found" })).toBeInTheDocument();

packages/web/src/features/not-found/index.tsx

Lines changed: 26 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -18,28 +18,32 @@ export function NotFoundPage() {
1818
return (
1919
<div className={`welcome-container ${isMobile ? "welcome-container--mobile" : ""}`}>
2020
<div className={`welcome-card ${isMobile ? "welcome-card--mobile" : ""}`}>
21-
<EmptyState
22-
style={notFoundEmptyStateStyle}
23-
title={
24-
<div>
25-
<div className="welcome-kicker">{t("not_found.kicker")}</div>
26-
<h1 className="welcome-title">{t("not_found.title")}</h1>
27-
</div>
28-
}
29-
description={
30-
<div className="welcome-content">
31-
<p className="welcome-body">{t("not_found.description")}</p>
32-
</div>
33-
}
34-
action={
35-
<button className="welcome-btn" onClick={() => navigate("/")}>
36-
<span>{t("not_found.go_home")}</span>
37-
</button>
38-
}
39-
/>
40-
<div className="auth-status-panel">
41-
<div className="auth-status-eyebrow">{t("not_found.path_label")}</div>
42-
<p className="auth-status-detail">{location.pathname}</p>
21+
<div className="welcome-card__hero">
22+
<EmptyState
23+
style={notFoundEmptyStateStyle}
24+
title={
25+
<div>
26+
<div className="welcome-kicker page-kicker">{t("not_found.kicker")}</div>
27+
<h1 className="welcome-title page-title">{t("not_found.title")}</h1>
28+
</div>
29+
}
30+
description={
31+
<div className="welcome-content">
32+
<p className="welcome-body meta-text">{t("not_found.description")}</p>
33+
</div>
34+
}
35+
/>
36+
</div>
37+
<div className="welcome-card__actions">
38+
<button className="welcome-btn" onClick={() => navigate("/")}>
39+
<span>{t("not_found.go_home")}</span>
40+
</button>
41+
</div>
42+
<div className="welcome-card__panel">
43+
<div className="auth-status-panel">
44+
<div className="auth-status-eyebrow">{t("not_found.path_label")}</div>
45+
<p className="auth-status-detail">{location.pathname}</p>
46+
</div>
4347
</div>
4448
</div>
4549
</div>

packages/web/src/features/settings/components/settings-page.test.tsx

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,18 @@ describe("SettingsPage", () => {
207207
renderSettingsPage(store);
208208

209209
expect(screen.getByText("v0.3.0")).toBeInTheDocument();
210+
expect(document.querySelector(".settings-footer__meta")).toBeTruthy();
211+
});
212+
213+
it("wraps desktop settings content in the shared content surface", async () => {
214+
const store = createConnectedStore(vi.fn().mockResolvedValue({}));
215+
216+
renderSettingsPage(store);
217+
218+
await waitFor(() => {
219+
expect(document.querySelector(".settings-content-surface")).toBeTruthy();
220+
});
221+
expect(document.querySelector(".settings-content-surface .settings-section")).toBeTruthy();
210222
});
211223

212224
it("renders desktop and mobile settings entry icons through themed semantics", async () => {
@@ -892,6 +904,28 @@ describe("SettingsPage", () => {
892904
expect(routerMocks.navigate).toHaveBeenCalledWith("/");
893905
});
894906

907+
it("renders a dedicated desktop header with the active section pill", async () => {
908+
const sendCommand = vi.fn().mockResolvedValue({});
909+
const store = createConnectedStore(sendCommand);
910+
911+
renderSettingsPage(store);
912+
913+
const desktopHeader = document.querySelector(".settings-header__desktop") as HTMLElement | null;
914+
const mobileHeader = document.querySelector(
915+
".settings-header .mobile-page-header"
916+
) as HTMLElement | null;
917+
const sectionPill = document.querySelector(
918+
".settings-header__section-pill"
919+
) as HTMLElement | null;
920+
921+
expect(desktopHeader).not.toBeNull();
922+
expect(mobileHeader).toBeNull();
923+
expect(screen.getByRole("heading", { name: "设置" })).toBeInTheDocument();
924+
expect(within(desktopHeader as HTMLElement).getByText("Coder Studio")).toBeInTheDocument();
925+
expect(sectionPill).not.toBeNull();
926+
expect(within(sectionPill as HTMLElement).getByText("通用")).toBeInTheDocument();
927+
});
928+
895929
it("renders a mobile category list and returns from detail content to the settings root", async () => {
896930
viewportMocks.viewport = "mobile";
897931
const sendCommand = vi.fn().mockImplementation(async (op: string, args: unknown) => {

packages/web/src/features/settings/components/settings-page.tsx

Lines changed: 59 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import {
2222
resolveSupervisorRetryOnTimeout,
2323
} from "@coder-studio/core";
2424
import { useAtom, useAtomValue, useSetAtom } from "jotai";
25-
import { Check, ChevronRight } from "lucide-react";
25+
import { Check, ChevronLeft, ChevronRight } from "lucide-react";
2626
import { useEffect, useId, useRef, useState } from "react";
2727
import { useNavigate } from "react-router-dom";
2828
import { localeAtom, themeAtom } from "../../../atoms/app-ui";
@@ -464,12 +464,40 @@ export function SettingsPage() {
464464
return (
465465
<div className={`settings-page ${isMobile ? "settings-page--mobile" : ""}`}>
466466
<header className="settings-header">
467-
<MobilePageHeader
468-
title={headerTitle}
469-
titleAs="div"
470-
onBack={handleBack}
471-
backLabel={t("action.back")}
472-
/>
467+
{isMobile ? (
468+
<MobilePageHeader
469+
title={headerTitle}
470+
titleAs="div"
471+
onBack={handleBack}
472+
backLabel={t("action.back")}
473+
/>
474+
) : (
475+
<div className="settings-header__desktop">
476+
<button
477+
type="button"
478+
className="page-header__back settings-header__back"
479+
onClick={handleBack}
480+
>
481+
<ChevronLeft size={16} />
482+
<span>{t("action.back")}</span>
483+
</button>
484+
<div className="settings-header__copy">
485+
<div className="page-kicker settings-header__kicker">{t("app.name")}</div>
486+
<h1 className="page-title settings-header__title">{t("settings.title")}</h1>
487+
<p className="settings-header__summary meta-text">
488+
<span>{t(activeSectionMeta.labelKey)}</span>
489+
<span className="settings-header__summary-separator" aria-hidden="true">
490+
491+
</span>
492+
<span>{t("settings.autosave_hint")}</span>
493+
</p>
494+
</div>
495+
<div className="settings-header__section-pill">
496+
<ThemedIcon semantic={activeSectionMeta.iconSemantic} size={14} />
497+
<span>{t(activeSectionMeta.labelKey)}</span>
498+
</div>
499+
</div>
500+
)}
473501
</header>
474502

475503
{shouldShowMobileRoot ? (
@@ -497,31 +525,35 @@ export function SettingsPage() {
497525
<main
498526
className={`settings-content ${isMobile ? "settings-content--mobile" : ""} ${isMobileDetailView ? "settings-content--mobile-detail" : ""} ${contentLayoutMode === "fill-height" ? "settings-content--fill-height" : ""}`}
499527
>
500-
{settingsLoadError && (
501-
<Notice
502-
role="alert"
503-
tone="error"
504-
title={t("settings.load_failed")}
505-
message={settingsLoadError}
506-
action={
507-
<button
508-
type="button"
509-
className="settings-link"
510-
onClick={() => setSettingsRefreshKey((value) => value + 1)}
511-
>
512-
{t("action.refresh")}
513-
</button>
514-
}
515-
/>
516-
)}
517-
{renderContent()}
528+
<div className="settings-content-surface">
529+
{settingsLoadError && (
530+
<Notice
531+
role="alert"
532+
tone="error"
533+
title={t("settings.load_failed")}
534+
message={settingsLoadError}
535+
action={
536+
<button
537+
type="button"
538+
className="settings-link"
539+
onClick={() => setSettingsRefreshKey((value) => value + 1)}
540+
>
541+
{t("action.refresh")}
542+
</button>
543+
}
544+
/>
545+
)}
546+
{renderContent()}
547+
</div>
518548
</main>
519549
</div>
520550
)}
521551

522552
<footer className={`settings-footer ${isMobile ? "settings-footer--mobile" : ""}`}>
523-
<span className="settings-autosave">{t("settings.autosave_hint")}</span>
524-
<span className="settings-version">v{serverInfo?.version ?? "0.0.0"}</span>
553+
<div className="settings-footer__meta">
554+
<span className="settings-autosave">{t("settings.autosave_hint")}</span>
555+
<span className="settings-version">v{serverInfo?.version ?? "0.0.0"}</span>
556+
</div>
525557
</footer>
526558
</div>
527559
);

packages/web/src/features/terminal-panel/__tests__/terminal-panel.test.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,8 @@ describe("TerminalPanel", () => {
108108
);
109109

110110
expect(screen.getByText("No terminals")).toBeInTheDocument();
111+
expect(document.querySelector(".bottom-terminal-empty")).toBeTruthy();
112+
expect(document.querySelector(".bottom-terminal-empty-icon")).toBeTruthy();
111113
expect(subscribe).toHaveBeenCalledWith([Topics.terminalsAll("ws-test")], expect.any(Function));
112114

113115
await act(async () => {

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,9 @@ describe("WelcomePage", () => {
9696

9797
expect(screen.getByText("DEPLOY ONCE, CODE EVERYWHERE")).toBeInTheDocument();
9898
expect(screen.getByRole("heading", { name: "Welcome to Coder Studio" })).toBeInTheDocument();
99+
expect(document.querySelector(".welcome-card__hero")).toBeTruthy();
100+
expect(document.querySelector(".welcome-card__actions")).toBeTruthy();
101+
expect(document.querySelector(".welcome-card__panel")).toBeTruthy();
99102
const openWorkspaceButton = screen.getByRole("button", { name: "Open Workspace" });
100103
const settingsButton = screen.getByRole("button", { name: "Settings" });
101104
expect(document.querySelector(".welcome-divider")).toBeTruthy();

0 commit comments

Comments
 (0)