feat(ui): clarification des contrôles d'en-tête DockControls (#42)
Refinement UX suite #26 : clarifie les contrôles DockControls dans l'en-tête des panneaux. Frontend-pur. QA vert : tsc --noEmit exit 0, vitest 62 fichiers / 616 tests passés. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -68,7 +68,7 @@ describe("ProjectsView view docking (#22)", () => {
|
|||||||
expect(dialog).toBeTruthy();
|
expect(dialog).toBeTruthy();
|
||||||
|
|
||||||
// Dock it to the left via the header control.
|
// Dock it to the left via the header control.
|
||||||
fireEvent.click(within(dialog).getByRole("button", { name: "dock Git left" }));
|
fireEvent.click(within(dialog).getByRole("button", { name: "Ancré à gauche — Git" }));
|
||||||
|
|
||||||
// The modal window is gone; the Git view now lives in the left dock column.
|
// The modal window is gone; the Git view now lives in the left dock column.
|
||||||
await waitFor(() =>
|
await waitFor(() =>
|
||||||
@ -85,13 +85,13 @@ describe("ProjectsView view docking (#22)", () => {
|
|||||||
|
|
||||||
openPanelFloating("Git");
|
openPanelFloating("Git");
|
||||||
const dialog = await screen.findByRole("dialog", { name: "Git" });
|
const dialog = await screen.findByRole("dialog", { name: "Git" });
|
||||||
fireEvent.click(within(dialog).getByRole("button", { name: "dock Git left" }));
|
fireEvent.click(within(dialog).getByRole("button", { name: "Ancré à gauche — Git" }));
|
||||||
|
|
||||||
const leftDock = await screen.findByRole("complementary", { name: "left dock" });
|
const leftDock = await screen.findByRole("complementary", { name: "left dock" });
|
||||||
const leftHeader = within(leftDock).getByLabelText("Git panel");
|
const leftHeader = within(leftDock).getByLabelText("Git panel");
|
||||||
// Move it to the right dock from the docked header control.
|
// Move it to the right dock from the docked header control.
|
||||||
fireEvent.click(
|
fireEvent.click(
|
||||||
within(leftHeader).getByRole("button", { name: "dock Git right" }),
|
within(leftHeader).getByRole("button", { name: "Ancré à droite — Git" }),
|
||||||
);
|
);
|
||||||
|
|
||||||
await waitFor(() =>
|
await waitFor(() =>
|
||||||
@ -109,7 +109,7 @@ describe("ProjectsView view docking (#22)", () => {
|
|||||||
fireEvent.click(
|
fireEvent.click(
|
||||||
within(await screen.findByRole("dialog", { name: "Git" })).getByRole(
|
within(await screen.findByRole("dialog", { name: "Git" })).getByRole(
|
||||||
"button",
|
"button",
|
||||||
{ name: "dock Git left" },
|
{ name: "Ancré à gauche — Git" },
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
// Dock Agents right.
|
// Dock Agents right.
|
||||||
@ -117,7 +117,7 @@ describe("ProjectsView view docking (#22)", () => {
|
|||||||
fireEvent.click(
|
fireEvent.click(
|
||||||
within(await screen.findByRole("dialog", { name: "Agents" })).getByRole(
|
within(await screen.findByRole("dialog", { name: "Agents" })).getByRole(
|
||||||
"button",
|
"button",
|
||||||
{ name: "dock Agents right" },
|
{ name: "Ancré à droite — Agents" },
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -40,9 +40,11 @@ import {
|
|||||||
Button,
|
Button,
|
||||||
DockRegion,
|
DockRegion,
|
||||||
FloatingWindow,
|
FloatingWindow,
|
||||||
|
IconButton,
|
||||||
Input,
|
Input,
|
||||||
MenuBar,
|
MenuBar,
|
||||||
Panel,
|
Panel,
|
||||||
|
cn,
|
||||||
zIndex,
|
zIndex,
|
||||||
type FloatingWindowSize,
|
type FloatingWindowSize,
|
||||||
type MenuBarItem,
|
type MenuBarItem,
|
||||||
@ -469,58 +471,72 @@ export function ProjectsView() {
|
|||||||
const rightPanels = panelsDockedTo(placements, "right");
|
const rightPanels = panelsDockedTo(placements, "right");
|
||||||
const floatingList = floatingPanels(placements);
|
const floatingList = floatingPanels(placements);
|
||||||
|
|
||||||
// Placement toggle buttons for a view's header — the "flottant ↔ ancré
|
// Placement controls for a view's header — labelled icon-buttons aligned 1:1
|
||||||
// (gauche/droite)" control from the contract. The button for the current slot
|
// with the « Panneaux » menu wording (#42). Order: ⇤ gauche · ⇥ droite · □
|
||||||
// is disabled so the active placement reads at a glance.
|
// flottant · ↗ fenêtre détachée · × fermé. The *current* placement reads as
|
||||||
|
// ACTIVE (aria-pressed + ring), never disabled — a disabled control loses its
|
||||||
|
// tooltip/keyboard focus and conflates "impossible" with "already selected".
|
||||||
|
// Only genuinely-impossible actions are disabled (detach with no active
|
||||||
|
// project). All labelling flows through `title` + `aria-label`; no visible
|
||||||
|
// text. The detach control is hidden for the main-window-only `projects` view.
|
||||||
function DockControls({ panel }: { panel: PanelId }) {
|
function DockControls({ panel }: { panel: PanelId }) {
|
||||||
const placement = placementOf(placements, panel);
|
const placement = placementOf(placements, panel);
|
||||||
const title = PANEL_TITLE[panel];
|
const title = PANEL_TITLE[panel];
|
||||||
|
// Active (current-placement) styling — visually distinct, still clickable.
|
||||||
|
const activeClass = "bg-raised text-content ring-1 ring-border-strong";
|
||||||
return (
|
return (
|
||||||
<div className="flex shrink-0 items-center gap-1">
|
<div className="flex shrink-0 items-center gap-1">
|
||||||
<Button
|
<IconButton
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="ghost"
|
title="Ancré à gauche"
|
||||||
aria-label={`dock ${title} left`}
|
aria-label={`Ancré à gauche — ${title}`}
|
||||||
disabled={isDockedTo(placement, "left")}
|
aria-pressed={isDockedTo(placement, "left")}
|
||||||
|
className={cn(isDockedTo(placement, "left") && activeClass)}
|
||||||
onClick={() => setPlacement(panel, { dock: "left" })}
|
onClick={() => setPlacement(panel, { dock: "left" })}
|
||||||
>
|
>
|
||||||
◧
|
⇤
|
||||||
</Button>
|
</IconButton>
|
||||||
<Button
|
<IconButton
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="ghost"
|
title="Ancré à droite"
|
||||||
aria-label={`dock ${title} right`}
|
aria-label={`Ancré à droite — ${title}`}
|
||||||
disabled={isDockedTo(placement, "right")}
|
aria-pressed={isDockedTo(placement, "right")}
|
||||||
|
className={cn(isDockedTo(placement, "right") && activeClass)}
|
||||||
onClick={() => setPlacement(panel, { dock: "right" })}
|
onClick={() => setPlacement(panel, { dock: "right" })}
|
||||||
>
|
>
|
||||||
◨
|
⇥
|
||||||
</Button>
|
</IconButton>
|
||||||
<Button
|
<IconButton
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="ghost"
|
title="Flottant"
|
||||||
aria-label={`float ${title}`}
|
aria-label={`Flottant — ${title}`}
|
||||||
disabled={placement === "floating"}
|
aria-pressed={placement === "floating"}
|
||||||
|
className={cn(placement === "floating" && activeClass)}
|
||||||
onClick={() => setPlacement(panel, "floating")}
|
onClick={() => setPlacement(panel, "floating")}
|
||||||
>
|
>
|
||||||
⧉
|
□
|
||||||
</Button>
|
</IconButton>
|
||||||
<Button
|
{panel !== "projects" && (
|
||||||
|
<IconButton
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="ghost"
|
title="Fenêtre détachée"
|
||||||
aria-label={`open ${title} in new window`}
|
aria-label={`Fenêtre détachée — ${title}`}
|
||||||
disabled={!active || isDetached(placement)}
|
aria-pressed={isDetached(placement)}
|
||||||
|
disabled={!active}
|
||||||
|
className={cn(isDetached(placement) && activeClass)}
|
||||||
onClick={() => detachPanel(panel)}
|
onClick={() => detachPanel(panel)}
|
||||||
>
|
>
|
||||||
⤢
|
↗
|
||||||
</Button>
|
</IconButton>
|
||||||
<Button
|
)}
|
||||||
|
<IconButton
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="ghost"
|
title="Fermé"
|
||||||
aria-label={`close ${title}`}
|
aria-label={`Fermé — ${title}`}
|
||||||
onClick={() => closePanel(panel)}
|
onClick={() => closePanel(panel)}
|
||||||
>
|
>
|
||||||
✕
|
×
|
||||||
</Button>
|
</IconButton>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user