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:
@ -4,20 +4,22 @@
|
||||
* IDE layout (full remaining height), reworked in ticket #16:
|
||||
*
|
||||
* ┌───────────────────────────────────────────────────────┐
|
||||
* │ PROJECT TABS [ alpha × ][ beta × ] │
|
||||
* │ PROJECT TABS [ alpha × ][ beta × ] [ + ] │
|
||||
* ├───────────────────────────────────────────────────────┤
|
||||
* │ MENU BAR File │ View │
|
||||
* │ MENU BAR Panneaux │ Settings │
|
||||
* ├───────────────────────────────────────────────────────┤
|
||||
* │ MAIN — LayoutGrid (fills the FULL width) │
|
||||
* │ — or the projects manager / welcome when no project │
|
||||
* └───────────────────────────────────────────────────────┘
|
||||
*
|
||||
* The former left sidebar is gone: every panel (context, work, tickets, agents,
|
||||
* templates, skills, permissions, memory, git) is now reached from the **View**
|
||||
* menu and shown in a chrome-level {@link FloatingWindow}, freeing the main area
|
||||
* for the terminal grid. Project creation/opening lives under the **File** menu
|
||||
* and, when no project is open, inline in the welcome area (so it is always
|
||||
* reachable without a menu).
|
||||
* templates, skills, permissions, memory, git, projects) is reached from the
|
||||
* single **Panneaux** menu (#26), whose per-panel submenu picks the placement
|
||||
* directly — closed / floating / docked left/right / detached OS window — and
|
||||
* shows it in a chrome-level {@link FloatingWindow} or {@link DockRegion}.
|
||||
* Project creation/switching is the **Projects** panel, opened by the tab-bar
|
||||
* "+" (or `Panneaux → Projects`) and, when no project is open, shown inline in
|
||||
* the welcome area (so it is always reachable).
|
||||
*
|
||||
* Pure presentation: all behaviour comes from {@link useProjects}. Styling via
|
||||
* `@/shared`; no inline styles beyond z-index tokens, no `invoke()`.
|
||||
@ -41,13 +43,14 @@ import {
|
||||
Input,
|
||||
MenuBar,
|
||||
Panel,
|
||||
Tabs,
|
||||
zIndex,
|
||||
type FloatingWindowSize,
|
||||
type MenuBarItem,
|
||||
type MenuBarMenu,
|
||||
} from "@/shared";
|
||||
import { useGateways } from "@/app/di";
|
||||
import { useProjects } from "./useProjects";
|
||||
import { ProjectTabs } from "./ProjectTabs";
|
||||
import { ViewPanelBody, type ViewPanelId } from "./ViewPanelBody";
|
||||
import {
|
||||
PANEL_TITLE,
|
||||
@ -206,14 +209,6 @@ export function ProjectsView() {
|
||||
return next;
|
||||
});
|
||||
}
|
||||
// Menu toggle: open floating when closed, otherwise close (whatever the slot).
|
||||
function toggleFloating(panel: PanelId) {
|
||||
if (placementOf(placements, panel) === "closed") {
|
||||
setPlacement(panel, "floating");
|
||||
} else {
|
||||
closePanel(panel);
|
||||
}
|
||||
}
|
||||
// Dismiss every floating view (docked views stay put). Used when a full-main
|
||||
// surface takes over so a modal window doesn't obscure it.
|
||||
function dismissFloating() {
|
||||
@ -279,88 +274,77 @@ export function ProjectsView() {
|
||||
dismissFloating();
|
||||
}
|
||||
|
||||
// Switch the active project from the always-visible selector: activate its
|
||||
// open tab if present, otherwise open it (adds a tab). Leaving Settings so the
|
||||
// project surface is shown.
|
||||
function selectProject(projectId: string) {
|
||||
setShowSettings(false);
|
||||
if (vm.openTabs.some((t) => t.id === projectId)) {
|
||||
vm.activateTab(projectId);
|
||||
} else {
|
||||
void vm.openProject(projectId);
|
||||
// ── Menus (#26) ─────────────────────────────────────────────────────────
|
||||
// A single « Panneaux » menu: one entry per panel, each opening a submenu with
|
||||
// the placement actions directly (closed / floating / docked left/right /
|
||||
// detached OS window). One place to pick the panel *and* its mode; the current
|
||||
// placement is marked in the submenu (and the parent entry shows ● when the
|
||||
// panel is open somewhere). Order per UX: content panels first, Projects last.
|
||||
const panelOrder: PanelId[] = [
|
||||
"context",
|
||||
"work",
|
||||
"tickets",
|
||||
"agents",
|
||||
"templates",
|
||||
"skills",
|
||||
"permissions",
|
||||
"memory",
|
||||
"git",
|
||||
"projects",
|
||||
];
|
||||
|
||||
// Placement actions for one panel, marking the active slot. `projects` is
|
||||
// main-window-only chrome, so it never offers the detached-window action.
|
||||
function placementSubmenu(panel: PanelId): MenuBarItem[] {
|
||||
const placement = placementOf(placements, panel);
|
||||
const items: MenuBarItem[] = [
|
||||
{
|
||||
id: "closed",
|
||||
label: "Fermé",
|
||||
active: placement === "closed",
|
||||
onSelect: () => closePanel(panel),
|
||||
},
|
||||
{
|
||||
id: "floating",
|
||||
label: "Flottant",
|
||||
active: placement === "floating",
|
||||
onSelect: () => setPlacement(panel, "floating"),
|
||||
},
|
||||
{
|
||||
id: "dock-left",
|
||||
label: "Ancré à gauche",
|
||||
active: isDockedTo(placement, "left"),
|
||||
onSelect: () => setPlacement(panel, { dock: "left" }),
|
||||
},
|
||||
{
|
||||
id: "dock-right",
|
||||
label: "Ancré à droite",
|
||||
active: isDockedTo(placement, "right"),
|
||||
onSelect: () => setPlacement(panel, { dock: "right" }),
|
||||
},
|
||||
];
|
||||
if (panel !== "projects") {
|
||||
items.push({
|
||||
id: "detached",
|
||||
label: "Fenêtre détachée",
|
||||
active: isDetached(placement),
|
||||
disabled: !active,
|
||||
onSelect: () => detachPanel(panel),
|
||||
});
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
// ── Menus ──────────────────────────────────────────────────────────────
|
||||
const viewItems: { id: PanelId; label: string }[] = [
|
||||
{ id: "context", label: "Context" },
|
||||
{ id: "work", label: "Work" },
|
||||
{ id: "tickets", label: "Tickets" },
|
||||
{ id: "agents", label: "Agents" },
|
||||
{ id: "templates", label: "Templates" },
|
||||
{ id: "skills", label: "Skills" },
|
||||
{ id: "permissions", label: "Perms" },
|
||||
{ id: "memory", label: "Memory" },
|
||||
{ id: "git", label: "Git" },
|
||||
];
|
||||
// Always-visible project selector (left of the menus): shows the active
|
||||
// project and lists every known project so the user can switch without opening
|
||||
// the Projects window (#16 UX pass).
|
||||
const projectMenu: MenuBarMenu = {
|
||||
id: "project",
|
||||
label: `Projet : ${active?.name ?? "—"}`,
|
||||
items:
|
||||
vm.projects.length === 0
|
||||
? [
|
||||
{
|
||||
id: "none",
|
||||
label: "No projects yet",
|
||||
disabled: true,
|
||||
onSelect: () => {},
|
||||
},
|
||||
]
|
||||
: vm.projects.map((p) => ({
|
||||
id: p.id,
|
||||
label: p.name,
|
||||
active: p.id === active?.id,
|
||||
onSelect: () => selectProject(p.id),
|
||||
})),
|
||||
};
|
||||
const menus: MenuBarMenu[] = [
|
||||
projectMenu,
|
||||
{
|
||||
id: "file",
|
||||
label: "File",
|
||||
items: [
|
||||
{
|
||||
id: "projects",
|
||||
label: "Projects…",
|
||||
active: placementOf(placements, "projects") !== "closed",
|
||||
onSelect: () => toggleFloating("projects"),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "view",
|
||||
label: "View",
|
||||
items: viewItems.map((it) => ({
|
||||
id: it.id,
|
||||
label: it.label,
|
||||
active: placementOf(placements, it.id) !== "closed",
|
||||
onSelect: () => toggleFloating(it.id),
|
||||
})),
|
||||
},
|
||||
{
|
||||
// #23: pop a view out into its own OS window. Disabled without an active
|
||||
// project (the detached window is project-scoped); active ⇒ detached.
|
||||
id: "window",
|
||||
label: "Window",
|
||||
items: viewItems.map((it) => ({
|
||||
id: it.id,
|
||||
label: `${PANEL_TITLE[it.id]} → nouvelle fenêtre`,
|
||||
active: isDetached(placementOf(placements, it.id)),
|
||||
disabled: !active,
|
||||
onSelect: () => detachPanel(it.id),
|
||||
id: "panels",
|
||||
label: "Panneaux",
|
||||
items: panelOrder.map((panel) => ({
|
||||
id: panel,
|
||||
label: PANEL_TITLE[panel],
|
||||
active: placementOf(placements, panel) !== "closed",
|
||||
onSelect: () => {},
|
||||
submenu: placementSubmenu(panel),
|
||||
})),
|
||||
},
|
||||
{
|
||||
@ -575,20 +559,18 @@ export function ProjectsView() {
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* ── Project tab bar ── */}
|
||||
<div className="flex shrink-0 items-center gap-1 border-b border-border bg-surface px-2 py-1">
|
||||
{vm.openTabs.length === 0 ? (
|
||||
<p className="px-2 text-sm text-muted">No open tabs.</p>
|
||||
) : (
|
||||
<Tabs
|
||||
items={vm.openTabs.map((t) => ({ id: t.id, label: t.name }))}
|
||||
value={vm.activeTabId}
|
||||
onSelect={(id) => vm.activateTab(id)}
|
||||
onClose={(id) => void vm.closeTab(id)}
|
||||
className="flex-1"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{/* ── Project tab bar (#26): tabs = open projects, "+" opens Projects ── */}
|
||||
<ProjectTabs
|
||||
items={vm.openTabs.map((t) => ({ id: t.id, label: t.name }))}
|
||||
activeTabId={vm.activeTabId}
|
||||
onSelect={(id) => vm.activateTab(id)}
|
||||
onClose={(id) => void vm.closeTab(id)}
|
||||
projectsPanelOpen={placementOf(placements, "projects") !== "closed"}
|
||||
onOpenProjectsPanel={() => {
|
||||
setShowSettings(false);
|
||||
setPlacement("projects", "floating");
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* ── Menu bar (replaces the former left sidebar) ── */}
|
||||
<MenuBar menus={menus} />
|
||||
|
||||
Reference in New Issue
Block a user