feat(ui): refonte menus & fenêtres — menu unique Panneaux et onglets projet (#26)

Sous-menus flyout dans MenuBar ; ProjectsView expose un menu unique
Panneaux et supprime les menus View/Window/File ainsi que le sélecteur
de projet ; ProjectTabs gère les onglets et le bouton +. Tests migrés
en conséquence.

QA vert : tsc --noEmit exit 0, vitest 62 fichiers / 616 tests passés,
invariants préservés.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-12 15:08:34 +02:00
parent 805d4b70a2
commit 09e7f212b7
7 changed files with 305 additions and 217 deletions

View File

@ -75,11 +75,19 @@ function tablist() {
}
/**
* Opens a menu-bar dropdown item (#16 chrome). `menu` is a top-level menu
* ("File"/"View"), `item` a panel entry ("Work", "Agents", "Projects…", …).
* The panel then shows in its floating window.
* Opens a panel floating via the single « Panneaux » menu (#26): click the menu,
* click the panel entry (opens its placement submenu), then pick « Flottant ».
* `panelTitle` is the panel's human title ("Work state", "Project context",
* "Agents", "Git", "Projects", …).
*/
function openPanel(menu: string, item: string) {
function openPanel(panelTitle: string) {
fireEvent.click(screen.getByRole("button", { name: "Panneaux" }));
fireEvent.click(screen.getByRole("button", { name: panelTitle }));
fireEvent.click(screen.getByRole("button", { name: "Flottant" }));
}
/** Clicks an item in a flat top-level menu (e.g. Settings). */
function openMenuItem(menu: string, item: string) {
fireEvent.click(screen.getByRole("button", { name: menu }));
fireEvent.click(screen.getByRole("button", { name: item }));
}
@ -97,7 +105,7 @@ describe("ProjectsView (with MockProjectGateway)", () => {
expect(tab.getAttribute("aria-selected")).toBe("true");
expect(within(tab).getByText("alpha")).toBeTruthy();
// And it is listed in the projects manager (File → Projects…) with its root.
openPanel("File", "Projects");
openPanel("Projects");
expect(
(await screen.findAllByText("/home/me/alpha")).length,
).toBeGreaterThan(0);
@ -116,7 +124,7 @@ describe("ProjectsView (with MockProjectGateway)", () => {
// Open the projects manager window so the list persists across activation
// (the inline welcome list is replaced by the terminal grid once active).
openPanel("File", "Projects");
openPanel("Projects");
// Open alpha, then beta — re-querying the list after each activation.
const openInList = (name: string) =>
@ -155,12 +163,12 @@ describe("ProjectsView (with MockProjectGateway)", () => {
fireEvent.click(screen.getByRole("button", { name: "Open" }));
await screen.findByRole("tab", { name: "alpha" });
openPanel("View", "Work");
openPanel("Work state");
expect(await screen.findByText("No agent work state.")).toBeTruthy();
});
it("switches the active project from the always-visible project selector (#16)", async () => {
it("switches the active project via the tab-bar + button → Projects panel (#26)", async () => {
const project = new MockProjectGateway();
await project.createProject("alpha", "/p/a");
await project.createProject("beta", "/p/b");
@ -174,17 +182,15 @@ describe("ProjectsView (with MockProjectGateway)", () => {
fireEvent.click(within(alphaLi).getByRole("button", { name: "Open" }));
await screen.findByRole("tab", { name: "alpha" });
// The selector reflects the active project and lists all known projects.
fireEvent.click(screen.getByRole("button", { name: "Projet : alpha" }));
// Pick beta from the selector — it opens/activates without the Projects window.
fireEvent.click(screen.getByRole("button", { name: "beta" }));
// The "+" button opens the Projects panel (floating); pick beta from its
// known-projects list — no verbose "Projet : …" selector anymore.
fireEvent.click(screen.getByRole("button", { name: "open projects panel" }));
const betaLi = await waitFor(() =>
screen.getAllByRole("listitem").find((n) => within(n).queryByText("beta"))!,
);
fireEvent.click(within(betaLi).getByRole("button", { name: "Open" }));
await waitFor(() =>
expect(screen.getByRole("button", { name: "Projet : beta" })).toBeTruthy(),
);
await waitFor(() =>
expect(screen.getAllByRole("tab")).toHaveLength(2),
);
await waitFor(() => expect(screen.getAllByRole("tab")).toHaveLength(2));
const betaTab = screen
.getAllByRole("tab")
.find((t) => within(t).queryByText("beta"))!;
@ -200,12 +206,12 @@ describe("ProjectsView (with MockProjectGateway)", () => {
expect(screen.queryByLabelText("ai profiles settings")).toBeNull();
// Settings → AI Profiles swaps the main area to the profiles settings.
openPanel("Settings", "AI Profiles");
openMenuItem("Settings", "AI Profiles");
expect(await screen.findByLabelText("ai profiles settings")).toBeTruthy();
expect(screen.queryByLabelText("project name")).toBeNull();
// Settings → Close AI Profiles returns to the project surface.
openPanel("Settings", "Close AI Profiles");
openMenuItem("Settings", "Close AI Profiles");
await waitFor(() =>
expect(screen.getByLabelText("project name")).toBeTruthy(),
);
@ -233,7 +239,7 @@ describe("ProjectsView (with MockProjectGateway)", () => {
// Reopen the projects manager (the inline form is replaced by the grid once
// a project is active), then create a second project at the same root.
openPanel("File", "Projects");
openPanel("Projects");
await createProject("beta", "/dup/root");
const alert = await screen.findByRole("alert");
@ -255,40 +261,40 @@ describe("ProjectsView (with MockProjectGateway)", () => {
);
// File → Projects… opens the projects manager window — form inputs visible.
openPanel("File", "Projects");
openPanel("Projects");
await waitFor(() =>
expect(screen.getByLabelText("project name")).toBeTruthy(),
);
// View → Agents — agent form visible; project form window is replaced.
openPanel("View", "Agents");
openPanel("Agents");
await waitFor(() =>
expect(screen.getByLabelText("agent name")).toBeTruthy(),
);
expect(screen.queryByLabelText("project name")).toBeNull();
// View → Context — shared project context editor visible.
openPanel("View", "Context");
openPanel("Project context");
await waitFor(() =>
expect(screen.getByLabelText("project context")).toBeTruthy(),
);
expect(screen.queryByLabelText("agent name")).toBeNull();
// View → Templates — the "New template" button visible.
openPanel("View", "Templates");
openPanel("Templates");
await waitFor(() =>
expect(screen.getByRole("button", { name: "create template" })).toBeTruthy(),
);
expect(screen.queryByLabelText("agent name")).toBeNull();
// View → Git — commit message input visible.
openPanel("View", "Git");
openPanel("Git");
await waitFor(() =>
expect(screen.getByLabelText("commit message")).toBeTruthy(),
);
// File → Projects… again — form accessible once more.
openPanel("File", "Projects");
openPanel("Projects");
await waitFor(() =>
expect(screen.getByLabelText("project name")).toBeTruthy(),
);
@ -302,7 +308,7 @@ describe("ProjectsView (with MockProjectGateway)", () => {
await screen.findByText("/p/a");
fireEvent.click(screen.getByRole("button", { name: "Open" }));
openPanel("View", "Context");
openPanel("Project context");
const editor = (await screen.findByLabelText(
"project context",