Files
IdeA/frontend/src/features/web/WebMenuSheet.tsx
Blomios 30f6415d51 feat(frontend): ajoute les menus Panneaux/Paramètres à la version web (#90)
WebWorkspace gagne un menu « Panneaux » qui route la surface principale du
projet ouvert vers les panneaux transport-neutres déjà réutilisables
(contexte, agents, templates, skills, permissions, mémoire, git) en plus des
surfaces web existantes (travail live, tickets, sprints), ainsi qu'un variant
web du panneau Projets (création par chemin serveur saisi manuellement, sans
browse natif). WebApp gagne un menu « Paramètres » (Profils IA, Appareils,
Déploiement désactivé "Desktop uniquement") qui remplace l'ancien bouton
unique "Appareils".

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-21 18:33:22 +02:00

102 lines
3.6 KiB
TypeScript

/**
* Full-screen navigation sheet for the web client (ticket #90): a mobile-first
* menu — never a desktop dropdown — reused by both the "Panneaux" menu
* ({@link WebWorkspace}) and the "Paramètres" menu ({@link WebApp}). Same shell
* as {@link WebTicketPickerSheet}: fixed header with a close action, an optional
* shared note, and a scrollable list of entries. The active entry is marked with
* "●"; a disabled entry stays visible (never removed) so its reason is
* discoverable, via `hint` (title tooltip) and/or `secondaryLabel` (always-visible
* text — the only way to communicate "why" on a touch device with no hover).
*/
import { useEffect, useRef } from "react";
import { Button, cn, zIndex } from "@/shared";
export interface WebMenuEntry {
id: string;
label: string;
active: boolean;
disabled?: boolean;
/** Tooltip shown on hover/focus (desktop browsers); not relied on for touch. */
hint?: string;
/** Always-visible secondary text, e.g. "Desktop uniquement". */
secondaryLabel?: string;
onSelect: () => void;
}
export interface WebMenuSheetProps {
title: string;
entries: WebMenuEntry[];
/** Shared note shown once above the list (e.g. why some entries are disabled). */
note?: string;
onClose: () => void;
}
export function WebMenuSheet({ title, entries, note, onClose }: WebMenuSheetProps) {
const closeRef = useRef<HTMLButtonElement>(null);
useEffect(() => {
closeRef.current?.focus();
}, []);
useEffect(() => {
function onKeyDown(e: KeyboardEvent) {
if (e.key === "Escape") onClose();
}
window.addEventListener("keydown", onKeyDown);
return () => window.removeEventListener("keydown", onKeyDown);
}, [onClose]);
return (
<div
className="fixed inset-0 flex flex-col bg-canvas"
role="dialog"
aria-modal="true"
aria-label={title}
style={{ zIndex: zIndex.floatingWindowNested }}
>
<header className="flex shrink-0 items-center justify-between gap-3 border-b border-border px-4 py-3 pt-[max(0.75rem,env(safe-area-inset-top))]">
<span className="min-w-0 truncate text-sm font-medium text-content">{title}</span>
<Button ref={closeRef} size="sm" variant="ghost" onClick={onClose}>
Fermer
</Button>
</header>
{note && (
<p className="shrink-0 border-b border-border px-4 py-2 text-xs text-muted">{note}</p>
)}
<ul className="min-h-0 flex-1 overflow-auto px-2 py-2 pb-[max(0.5rem,env(safe-area-inset-bottom))]">
{entries.map((entry) => (
<li key={entry.id}>
<button
type="button"
disabled={entry.disabled}
title={entry.hint}
aria-current={entry.active ? "true" : undefined}
onClick={() => {
if (entry.disabled) return;
entry.onSelect();
}}
className={cn(
"flex min-h-[44px] w-full items-center gap-2 rounded-md px-3 py-2 text-left text-sm transition-colors",
"hover:bg-raised focus:bg-raised focus:outline-none disabled:opacity-50 disabled:hover:bg-transparent",
entry.active && "bg-raised font-medium text-content",
)}
>
<span aria-hidden="true" className="w-3 shrink-0 text-primary">
{entry.active ? "●" : ""}
</span>
<span className="min-w-0 flex-1 truncate">{entry.label}</span>
{entry.secondaryLabel && (
<span className="shrink-0 text-xs text-muted">{entry.secondaryLabel}</span>
)}
</button>
</li>
))}
</ul>
</div>
);
}