Skip to content

Commit b634a94

Browse files
committed
feat(web): migrate welcome empty-state shells
1 parent f95fd09 commit b634a94

5 files changed

Lines changed: 118 additions & 15 deletions

File tree

packages/web/src/components/ui/MIGRATION.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
| Tooltip | 🟢 complete | native `title` hover labels | 0 | 2026-05-09 |
2020
| ProgressBar | 🟢 complete | `--progress-height` patterns | 0 | 2026-05-09 |
2121
| Notice | 🟢 complete | `.settings-page__notice*` | 0 | 2026-05-09 |
22-
| EmptyState | 🟡 partial | feature-specific empty state blocks | centered shared shells plus workspace desktop/mobile no-session or no-workspace empties, agent-panes no-workspace, topbar no-workspace, workspace route resolving/load-failed shells, worktree detail no-diff and empty-tree states, worktree manager list-empty, workspace launch directory-empty, command palette no-results, desktop branch quick-pick loading/idle/filter-empty states, shared git-panel compact worktree/change/history shells, file-tree-panel loading/search-empty/tree fallback shells, the mobile select empty shell, and the mobile supervisor enable shell covered; additional list-style and other feature-owned empty blocks remain | 2026-05-09 |
22+
| EmptyState | 🟡 partial | feature-specific empty state blocks | centered shared shells plus workspace desktop/mobile no-session or no-workspace empties, welcome/not-found page shells, agent-panes no-workspace, topbar no-workspace, workspace route resolving/load-failed shells, worktree detail no-diff and empty-tree states, worktree manager list-empty, workspace launch directory-empty, command palette no-results, desktop branch quick-pick loading/idle/filter-empty states, shared git-panel compact worktree/change/history shells, file-tree-panel loading/search-empty/tree fallback shells, the mobile select empty shell, and the mobile supervisor enable shell covered; additional list-style and other feature-owned empty blocks remain | 2026-05-09 |
2323
| Tabs | 🟢 complete | `.panel-tabs`, `.panel-tab`, `.worktree-tabs`, `.worktree-tab`, feature-local workspace/terminal tab shells | 0 | 2026-05-09 |
2424
| SegmentedControl | 🟢 complete | `.settings-provider-tabs`, `.settings-provider-tab`, `.settings-provider-subnav`, `.settings-provider-subnav-button`, `.shortcuts-category-tabs`, `.shortcuts-category-tab` | 0 | 2026-05-09 |
2525
| Sheet | 🟢 complete | `.mobile-sheet*` | 0 | 2026-05-09 |
@@ -71,6 +71,6 @@
7171

7272
`SegmentedControl` now completes the bounded selector-family inventory: provider chooser tabs, provider sub-navigation, and shortcuts category selectors all use the shared primitive from the public UI barrel while preserving the existing legacy compatibility classes for zero-regression styling.
7373

74-
`EmptyState` now covers a broader bounded shell slice: config editor, terminal panel, git diff/code editor/image preview empties, the desktop workspace no-workspace shell, the agent-panes no-workspace shell, the topbar no-workspace hint, the worktree detail no-diff and empty-tree states, the worktree manager list-empty state, the workspace launch directory-empty state, the command palette no-results state, the desktop branch quick-pick loading and compact empty-result states, the shared git-panel compact worktree/change/history shells, the shared file-tree-panel loading/search-empty/tree fallback shells, the mobile agent empty shell, the mobile select empty shell, the mobile supervisor enable shell, and the workspace route resolving/load-failed shells all use the shared primitive from the public UI barrel while preserving feature-owned chrome such as `workspace-resolving-shell`, `workspace-resolving-card`, `agent-panes-empty`, `topbar-empty-state`, `mobile-select-sheet__empty`, `mobile-supervisor-sheet__empty`, `worktree-empty`, `directory-empty`, `command-palette-empty`, `branch-quick-pick-empty`, `git-panel-empty`, and `file-tree-empty`. Additional list-style and other feature-owned empty-state chromes remain intentionally deferred.
74+
`EmptyState` now covers a broader bounded shell slice: config editor, terminal panel, git diff/code editor/image preview empties, the desktop workspace no-workspace shell, the welcome and not-found page shells, the agent-panes no-workspace shell, the topbar no-workspace hint, the worktree detail no-diff and empty-tree states, the worktree manager list-empty state, the workspace launch directory-empty state, the command palette no-results state, the desktop branch quick-pick loading and compact empty-result states, the shared git-panel compact worktree/change/history shells, the shared file-tree-panel loading/search-empty/tree fallback shells, the mobile agent empty shell, the mobile select empty shell, the mobile supervisor enable shell, and the workspace route resolving/load-failed shells all use the shared primitive from the public UI barrel while preserving feature-owned chrome such as `welcome-container*`, `welcome-card*`, `welcome-kicker`, `welcome-title`, `welcome-body`, `auth-status-panel`, `workspace-resolving-shell`, `workspace-resolving-card`, `agent-panes-empty`, `topbar-empty-state`, `mobile-select-sheet__empty`, `mobile-supervisor-sheet__empty`, `worktree-empty`, `directory-empty`, `command-palette-empty`, `branch-quick-pick-empty`, `git-panel-empty`, and `file-tree-empty`. Additional list-style and other feature-owned empty-state chromes remain intentionally deferred.
7575

7676
`Tabs` now complete the bounded tab-navigation inventory: workspace desktop/mobile/worktree surfaces, the topbar workspace switcher, and the desktop terminal session tabs all use the shared primitive from the public UI barrel while preserving legacy compatibility classes and feature-owned closable-tab shells where secondary close actions must remain siblings of the tab trigger for valid DOM and keyboard semantics.
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
// @vitest-environment jsdom
2+
3+
import { fireEvent, render, screen } from "@testing-library/react";
4+
import { createStore, Provider } from "jotai";
5+
import { MemoryRouter, Route, Routes } from "react-router-dom";
6+
import { beforeEach, describe, expect, it, vi } from "vitest";
7+
import { localeAtom } from "../../atoms/app-ui";
8+
import { NotFoundPage } from "./index";
9+
10+
const viewportMocks = vi.hoisted(() => ({
11+
viewport: "desktop" as "desktop" | "mobile",
12+
}));
13+
14+
vi.mock("../../hooks/use-viewport", () => ({
15+
useViewport: () => viewportMocks.viewport,
16+
}));
17+
18+
describe("NotFoundPage", () => {
19+
beforeEach(() => {
20+
viewportMocks.viewport = "desktop";
21+
});
22+
23+
it("uses the shared empty-state shell while preserving the missing pathname details", () => {
24+
const store = createStore();
25+
store.set(localeAtom, "en");
26+
27+
render(
28+
<Provider store={store}>
29+
<MemoryRouter initialEntries={["/missing/path"]}>
30+
<Routes>
31+
<Route path="*" element={<NotFoundPage />} />
32+
</Routes>
33+
</MemoryRouter>
34+
</Provider>
35+
);
36+
37+
expect(document.querySelector(".welcome-container")).toBeTruthy();
38+
expect(document.querySelector(".welcome-card")).toBeTruthy();
39+
expect(screen.getByText("Requested path")).toBeInTheDocument();
40+
expect(screen.getByText("/missing/path")).toBeInTheDocument();
41+
expect(screen.getByRole("heading", { name: "Page not found" })).toBeInTheDocument();
42+
});
43+
44+
it("navigates home from the primary action", () => {
45+
const store = createStore();
46+
store.set(localeAtom, "en");
47+
48+
render(
49+
<Provider store={store}>
50+
<MemoryRouter initialEntries={["/missing/path"]}>
51+
<Routes>
52+
<Route path="*" element={<NotFoundPage />} />
53+
<Route path="/" element={<div>Home Screen</div>} />
54+
</Routes>
55+
</MemoryRouter>
56+
</Provider>
57+
);
58+
59+
fireEvent.click(screen.getByRole("button", { name: "Go Home" }));
60+
61+
expect(screen.getByText("Home Screen")).toBeInTheDocument();
62+
});
63+
});

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

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
import { useLocation, useNavigate } from "react-router-dom";
2+
import { EmptyState } from "../../components/ui";
23
import { useViewport } from "../../hooks/use-viewport";
34
import { useTranslation } from "../../lib/i18n";
45

6+
const notFoundEmptyStateStyle = {
7+
minHeight: "auto",
8+
padding: 0,
9+
gap: "var(--sp-5)",
10+
};
11+
512
export function NotFoundPage() {
613
const t = useTranslation();
714
const navigate = useNavigate();
@@ -11,16 +18,29 @@ export function NotFoundPage() {
1118
return (
1219
<div className={`welcome-container ${isMobile ? "welcome-container--mobile" : ""}`}>
1320
<div className={`welcome-card ${isMobile ? "welcome-card--mobile" : ""}`}>
14-
<div className="welcome-kicker">{t("not_found.kicker")}</div>
15-
<h1 className="welcome-title">{t("not_found.title")}</h1>
16-
<p className="welcome-body">{t("not_found.description")}</p>
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+
/>
1740
<div className="auth-status-panel">
1841
<div className="auth-status-eyebrow">{t("not_found.path_label")}</div>
1942
<p className="auth-status-detail">{location.pathname}</p>
2043
</div>
21-
<button className="welcome-btn" onClick={() => navigate("/")}>
22-
<span>{t("not_found.go_home")}</span>
23-
</button>
2444
</div>
2545
</div>
2646
);

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

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

8080
expect(document.querySelector(".welcome-container--mobile")).toBeTruthy();
8181
expect(document.querySelector(".welcome-card--mobile")).toBeTruthy();
82+
expect(document.querySelector(".welcome-card.welcome-card--mobile")).toBeTruthy();
8283
});
8384

8485
it("renders translated English copy when locale is set to en", () => {
@@ -97,5 +98,7 @@ describe("WelcomePage", () => {
9798
expect(screen.getByRole("heading", { name: "Welcome to Coder Studio" })).toBeInTheDocument();
9899
expect(screen.getByRole("button", { name: "Open Workspace" })).toBeInTheDocument();
99100
expect(screen.getByRole("button", { name: "Settings" })).toBeInTheDocument();
101+
expect(document.querySelector(".welcome-divider")).toBeTruthy();
102+
expect(document.querySelectorAll(".welcome-feature")).toHaveLength(3);
100103
});
101104
});

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

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { GitBranch, Plus, Settings, Terminal, Zap } from "lucide-react";
99
import type { FC } from "react";
1010
import { useState } from "react";
1111
import { useNavigate } from "react-router-dom";
12+
import { EmptyState } from "../../components/ui";
1213
import { useViewport } from "../../hooks/use-viewport";
1314
import { useTranslation } from "../../lib/i18n";
1415
import { WorkspaceLaunchModal } from "../workspace/views/shared/workspace-launch-modal";
@@ -19,6 +20,12 @@ interface FeatureItem {
1920
description: string;
2021
}
2122

23+
const welcomeEmptyStateStyle = {
24+
minHeight: "auto",
25+
padding: 0,
26+
gap: "var(--sp-5)",
27+
};
28+
2229
/**
2330
* Welcome Page
2431
*
@@ -63,13 +70,23 @@ export const WelcomePage: FC = () => {
6370
<>
6471
<div className={`welcome-container ${isMobile ? "welcome-container--mobile" : ""}`}>
6572
<div className={`welcome-card ${isMobile ? "welcome-card--mobile" : ""}`}>
66-
<div className="welcome-kicker">{t("welcome.kicker")}</div>
67-
<h1 className="welcome-title">{t("welcome.title")}</h1>
68-
<p className="welcome-body">{t("welcome.description")}</p>
69-
<button className="welcome-btn" onClick={handleOpenWorkspace}>
70-
<Plus size={18} />
71-
<span>{t("action.open_workspace")}</span>
72-
</button>
73+
<EmptyState
74+
style={welcomeEmptyStateStyle}
75+
title={
76+
<div>
77+
<div className="welcome-kicker">{t("welcome.kicker")}</div>
78+
<h1 className="welcome-title">{t("welcome.title")}</h1>
79+
</div>
80+
}
81+
description={<p className="welcome-body">{t("welcome.description")}</p>}
82+
action={
83+
<button className="welcome-btn" onClick={handleOpenWorkspace}>
84+
<Plus size={18} />
85+
<span>{t("action.open_workspace")}</span>
86+
</button>
87+
}
88+
/>
89+
7390
<button className="welcome-link" onClick={handleOpenSettings}>
7491
<Settings size={14} />
7592
<span>{t("action.settings")}</span>

0 commit comments

Comments
 (0)