feat: redesign authenticated UI with home overview and settings hub

- Add HomePage with open sessions grid, projects overview, and session composer
- Add SettingsPage with tabs for General, SSH Keys, Tool Types, Tool Configs
- Update navigation to Home, Projects, Settings
- Redirect legacy routes (/sessions, /ssh-keys, /tool-types, /tool-configs)
- Apply Inter font and warm editorial styling
- Update tests for new dashboard and projects pages

Quality gates: typecheck pass, lint pass, 15/15 tests pass

Refs: openspec/changes/ui-redesign-home-settings
This commit is contained in:
2026-05-22 20:34:39 +02:00
parent 575e5837ea
commit 6f35eb77ae
18 changed files with 3036 additions and 202 deletions
+41 -8
View File
@@ -1,4 +1,5 @@
import { cleanup, fireEvent, render, screen, waitFor, within } from "@testing-library/react";
import { MemoryRouter } from "react-router-dom";
import { afterEach, describe, expect, it, vi } from "vitest";
import { ProjectsPage } from "./projects";
@@ -29,13 +30,21 @@ afterEach(() => {
describe("ProjectsPage", () => {
it("renders loading state initially", () => {
vi.spyOn(projectsApi, "listProjects").mockImplementation(() => new Promise(() => {}));
render(<ProjectsPage />);
render(
<MemoryRouter>
<ProjectsPage />
</MemoryRouter>
);
expect(screen.getByText(/loading projects/i)).toBeInTheDocument();
});
it("renders project list after loading", async () => {
vi.spyOn(projectsApi, "listProjects").mockResolvedValue(mockProjects);
render(<ProjectsPage />);
render(
<MemoryRouter>
<ProjectsPage />
</MemoryRouter>
);
await waitFor(() => {
expect(screen.getByText("Alpha Project")).toBeInTheDocument();
@@ -46,7 +55,11 @@ describe("ProjectsPage", () => {
it("renders empty state when no projects", async () => {
vi.spyOn(projectsApi, "listProjects").mockResolvedValue([]);
render(<ProjectsPage />);
render(
<MemoryRouter>
<ProjectsPage />
</MemoryRouter>
);
await waitFor(() => {
expect(screen.getByText(/no projects yet/i)).toBeInTheDocument();
@@ -55,7 +68,11 @@ describe("ProjectsPage", () => {
it("renders error state with retry button", async () => {
vi.spyOn(projectsApi, "listProjects").mockRejectedValue(new Error("fail"));
render(<ProjectsPage />);
render(
<MemoryRouter>
<ProjectsPage />
</MemoryRouter>
);
await waitFor(() => {
expect(screen.getByText(/failed to load projects/i)).toBeInTheDocument();
@@ -67,7 +84,11 @@ describe("ProjectsPage", () => {
const listMock = vi.spyOn(projectsApi, "listProjects").mockResolvedValue([]);
const createMock = vi.spyOn(projectsApi, "createProject").mockResolvedValue(mockProjects[0]);
render(<ProjectsPage />);
render(
<MemoryRouter>
<ProjectsPage />
</MemoryRouter>
);
await waitFor(() => {
expect(screen.getByText(/no projects yet/i)).toBeInTheDocument();
@@ -96,7 +117,11 @@ describe("ProjectsPage", () => {
it("shows validation error when name is empty", async () => {
vi.spyOn(projectsApi, "listProjects").mockResolvedValue([]);
render(<ProjectsPage />);
render(
<MemoryRouter>
<ProjectsPage />
</MemoryRouter>
);
await waitFor(() => {
expect(screen.getByText(/no projects yet/i)).toBeInTheDocument();
@@ -112,7 +137,11 @@ describe("ProjectsPage", () => {
const listMock = vi.spyOn(projectsApi, "listProjects").mockResolvedValue(mockProjects);
const updateMock = vi.spyOn(projectsApi, "updateProject").mockResolvedValue(mockProjects[0]);
render(<ProjectsPage />);
render(
<MemoryRouter>
<ProjectsPage />
</MemoryRouter>
);
await waitFor(() => {
expect(screen.getByText("Alpha Project")).toBeInTheDocument();
@@ -141,7 +170,11 @@ describe("ProjectsPage", () => {
const listMock = vi.spyOn(projectsApi, "listProjects").mockResolvedValue(mockProjects);
const deleteMock = vi.spyOn(projectsApi, "deleteProject").mockResolvedValue(undefined);
render(<ProjectsPage />);
render(
<MemoryRouter>
<ProjectsPage />
</MemoryRouter>
);
await waitFor(() => {
expect(screen.getByText("Alpha Project")).toBeInTheDocument();