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:
2026-07-12 19:32:37 +02:00
parent c864d4a692
commit 73859a05e1
2 changed files with 56 additions and 40 deletions

View File

@ -40,9 +40,11 @@ import {
Button,
DockRegion,
FloatingWindow,
IconButton,
Input,
MenuBar,
Panel,
cn,
zIndex,
type FloatingWindowSize,
type MenuBarItem,
@ -469,58 +471,72 @@ export function ProjectsView() {
const rightPanels = panelsDockedTo(placements, "right");
const floatingList = floatingPanels(placements);
// Placement toggle buttons for a view's header — the "flottant ↔ ancré
// (gauche/droite)" control from the contract. The button for the current slot
// is disabled so the active placement reads at a glance.
// Placement controls for a view's header — labelled icon-buttons aligned 1:1
// with the « Panneaux » menu wording (#42). Order: ⇤ gauche · ⇥ droite · □
// 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 }) {
const placement = placementOf(placements, 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 (
<div className="flex shrink-0 items-center gap-1">
<Button
<IconButton
size="sm"
variant="ghost"
aria-label={`dock ${title} left`}
disabled={isDockedTo(placement, "left")}
title="Ancré à gauche"
aria-label={`Ancré à gauche — ${title}`}
aria-pressed={isDockedTo(placement, "left")}
className={cn(isDockedTo(placement, "left") && activeClass)}
onClick={() => setPlacement(panel, { dock: "left" })}
>
</Button>
<Button
</IconButton>
<IconButton
size="sm"
variant="ghost"
aria-label={`dock ${title} right`}
disabled={isDockedTo(placement, "right")}
title="Ancré à droite"
aria-label={`Ancré à droite — ${title}`}
aria-pressed={isDockedTo(placement, "right")}
className={cn(isDockedTo(placement, "right") && activeClass)}
onClick={() => setPlacement(panel, { dock: "right" })}
>
</Button>
<Button
</IconButton>
<IconButton
size="sm"
variant="ghost"
aria-label={`float ${title}`}
disabled={placement === "floating"}
title="Flottant"
aria-label={`Flottant — ${title}`}
aria-pressed={placement === "floating"}
className={cn(placement === "floating" && activeClass)}
onClick={() => setPlacement(panel, "floating")}
>
</Button>
<Button
</IconButton>
{panel !== "projects" && (
<IconButton
size="sm"
title="Fenêtre détachée"
aria-label={`Fenêtre détachée — ${title}`}
aria-pressed={isDetached(placement)}
disabled={!active}
className={cn(isDetached(placement) && activeClass)}
onClick={() => detachPanel(panel)}
>
</IconButton>
)}
<IconButton
size="sm"
variant="ghost"
aria-label={`open ${title} in new window`}
disabled={!active || isDetached(placement)}
onClick={() => detachPanel(panel)}
>
</Button>
<Button
size="sm"
variant="ghost"
aria-label={`close ${title}`}
title="Fermé"
aria-label={`Fermé — ${title}`}
onClick={() => closePanel(panel)}
>
</Button>
×
</IconButton>
</div>
);
}