feat(web): panneau Fichiers — navigation projet + téléchargements fichier/dossier ZIP — #211

This commit is contained in:
2026-08-15 10:38:53 +02:00
parent bb1fdecf7c
commit 79fc3d184d
22 changed files with 1648 additions and 78 deletions

View File

@ -31,16 +31,7 @@ import { useGateways } from "@/app/di";
import { useProjectWorkState } from "@/features/workstate/useProjectWorkState";
import { LayoutGrid, LayoutTabs } from "@/features/layout";
import { ProjectContextPanel } from "@/features/projects/ProjectContextPanel";
import {
PANEL_TITLE,
floatingPanels,
isDockedTo,
panelsDockedTo,
placementOf,
type PanelId,
type ViewPlacement,
type ViewPlacements,
} from "@/features/projects/viewPlacement";
import { isDockedTo, type ViewPlacement } from "@/features/projects/viewPlacement";
import { AgentsPanel } from "@/features/agents";
import { TemplatesPanel } from "@/features/templates";
import { SkillsPanel } from "@/features/skills";
@ -69,6 +60,7 @@ import { WebTicketsView } from "./tickets/WebTicketsView";
import { WebSprintsView } from "./tickets/WebSprintsView";
import { WebProjectsPanel } from "./WebProjectsPanel";
import { WebMenuSheet } from "./WebMenuSheet";
import { ProjectFilesPanel } from "./ProjectFilesPanel";
import {
WEB_PANEL_META,
type WebOpenablePanelId,
@ -205,19 +197,50 @@ function useMobileWorkspace(): boolean {
return mobile;
}
const WEB_PANEL_SIZE: Record<PanelId, FloatingWindowSize> = {
type WebDesktopPanelId = WebProjectPanelId;
type WebDesktopPlacements = Partial<Record<WebDesktopPanelId, ViewPlacement>>;
const WEB_PANEL_SIZE: Record<WebDesktopPanelId, FloatingWindowSize> = {
projects: "md",
context: "lg",
work: "lg",
tickets: "lg",
sprints: "lg",
agents: "lg",
templates: "lg",
skills: "lg",
permissions: "md",
memory: "lg",
git: "lg",
files: "lg",
};
function webPanelTitle(panel: WebDesktopPanelId): string {
return WEB_PANEL_META[panel].label;
}
function webPlacementOf(
placements: WebDesktopPlacements,
panel: WebDesktopPanelId,
): ViewPlacement {
return placements[panel] ?? "closed";
}
function webPanelsDockedTo(
placements: WebDesktopPlacements,
side: "left" | "right",
): WebDesktopPanelId[] {
return (Object.keys(WEB_PANEL_META) as WebDesktopPanelId[]).filter((panel) =>
isDockedTo(webPlacementOf(placements, panel), side),
);
}
function webFloatingPanels(placements: WebDesktopPlacements): WebDesktopPanelId[] {
return (Object.keys(WEB_PANEL_META) as WebDesktopPanelId[]).filter(
(panel) => webPlacementOf(placements, panel) === "floating",
);
}
function WebDesktopWorkspace({
projects,
openId,
@ -243,7 +266,7 @@ function WebDesktopWorkspace({
onRefreshProjects: () => void;
onViewSprintTickets: (sprintId: string) => void;
}) {
const [placements, setPlacements] = useState<ViewPlacements>({
const [placements, setPlacements] = useState<WebDesktopPlacements>({
work: { dock: "right" },
});
const [leftDockWidth, setLeftDockWidth] = useState(340);
@ -259,11 +282,11 @@ function WebDesktopWorkspace({
setViewerConversation(null);
}, [openId]);
function setPlacement(panel: PanelId, placement: Exclude<ViewPlacement, "detached">) {
function setPlacement(panel: WebDesktopPanelId, placement: Exclude<ViewPlacement, "detached">) {
setPlacements((prev) => {
const next: ViewPlacements = { ...prev };
const next: WebDesktopPlacements = { ...prev };
if (placement === "floating") {
for (const other of Object.keys(next) as PanelId[]) {
for (const other of Object.keys(next) as WebDesktopPanelId[]) {
if (other !== panel && next[other] === "floating") delete next[other];
}
}
@ -272,7 +295,7 @@ function WebDesktopWorkspace({
});
}
function closePanel(panel: PanelId) {
function closePanel(panel: WebDesktopPanelId) {
setPlacements((prev) => {
if (!(panel in prev)) return prev;
const next = { ...prev };
@ -285,7 +308,7 @@ function WebDesktopWorkspace({
setPlacements((prev) => {
let changed = false;
const next = { ...prev };
for (const panel of Object.keys(next) as PanelId[]) {
for (const panel of Object.keys(next) as WebDesktopPanelId[]) {
if (next[panel] === "floating") {
delete next[panel];
changed = true;
@ -301,22 +324,23 @@ function WebDesktopWorkspace({
dismissFloating();
}
const panelOrder: PanelId[] = [
const panelOrder: WebDesktopPanelId[] = [
"context",
"work",
"tickets",
"sprints",
"agents",
"templates",
"skills",
"permissions",
"memory",
"git",
"files",
"projects",
];
const panelToWebId = (panel: PanelId): WebProjectPanelId => panel;
function placementSubmenu(panel: PanelId): MenuBarItem[] {
const placement = placementOf(placements, panel);
function placementSubmenu(panel: WebDesktopPanelId): MenuBarItem[] {
const placement = webPlacementOf(placements, panel);
return [
{
id: "closed",
@ -351,8 +375,8 @@ function WebDesktopWorkspace({
label: "Panneaux",
items: panelOrder.map((panel) => ({
id: panel,
label: PANEL_TITLE[panel],
active: placementOf(placements, panel) !== "closed",
label: webPanelTitle(panel),
active: webPlacementOf(placements, panel) !== "closed",
onSelect: () => {},
submenu: placementSubmenu(panel),
})),
@ -369,14 +393,14 @@ function WebDesktopWorkspace({
/>
);
function renderPanel(panel: PanelId) {
function renderPanel(panel: WebDesktopPanelId) {
if (panel === "projects") return projectsManager;
if (!openId) {
return <p className="text-sm text-muted">Ouvrir un projet pour utiliser ce panneau.</p>;
}
return (
<WebProjectPanelBody
panel={panelToWebId(panel) as WebOpenablePanelId}
panel={panel as WebOpenablePanelId}
projectId={openId}
root={openRoot}
focusSprintId={focusSprintId}
@ -385,9 +409,9 @@ function WebDesktopWorkspace({
);
}
function DockControls({ panel }: { panel: PanelId }) {
const placement = placementOf(placements, panel);
const title = PANEL_TITLE[panel];
function DockControls({ panel }: { panel: WebDesktopPanelId }) {
const placement = webPlacementOf(placements, panel);
const title = webPanelTitle(panel);
const activeClass = "bg-raised text-content ring-1 ring-border-strong";
return (
<div className="flex shrink-0 items-center gap-1">
@ -433,16 +457,16 @@ function WebDesktopWorkspace({
);
}
function renderDockedView(panel: PanelId) {
function renderDockedView(panel: WebDesktopPanelId) {
return (
<section
key={panel}
aria-label={`${PANEL_TITLE[panel]} panel`}
aria-label={`${webPanelTitle(panel)} panel`}
className="flex min-h-0 flex-1 flex-col border-b border-border last:border-b-0"
>
<header className="flex shrink-0 items-center justify-between gap-2 border-b border-border px-3 py-2">
<span className="truncate text-sm font-medium text-content">
{PANEL_TITLE[panel]}
{webPanelTitle(panel)}
</span>
<DockControls panel={panel} />
</header>
@ -451,12 +475,12 @@ function WebDesktopWorkspace({
);
}
const leftPanels = panelsDockedTo(placements, "left");
const rightPanels = panelsDockedTo(placements, "right");
const floatingList = floatingPanels(placements);
const leftPanels = webPanelsDockedTo(placements, "left");
const rightPanels = webPanelsDockedTo(placements, "right");
const floatingList = webFloatingPanels(placements);
const activeLayoutKind = activeLayout?.kind ?? "terminal";
const showInlineProjects =
!openProject && placementOf(placements, "projects") === "closed";
!openProject && webPlacementOf(placements, "projects") === "closed";
return (
<div
@ -564,7 +588,7 @@ function WebDesktopWorkspace({
<FloatingWindow
key={panel}
open
title={PANEL_TITLE[panel]}
title={webPanelTitle(panel)}
size={WEB_PANEL_SIZE[panel]}
onClose={() => closePanel(panel)}
>
@ -611,6 +635,7 @@ function WebMobileTaskFlowShell({
{ id: "work", label: "Travail" },
{ id: "tickets", label: "Tickets" },
{ id: "sprints", label: "Sprints" },
{ id: "files", label: "Fichiers" },
{ id: "agents", label: "Agent" },
];
@ -665,7 +690,7 @@ function WebMobileTaskFlowShell({
<nav
data-testid="web-mobile-bottom-nav"
aria-label="Navigation mobile"
className="grid shrink-0 grid-cols-5 border-t border-border bg-surface pb-[env(safe-area-inset-bottom)]"
className="grid shrink-0 grid-cols-6 border-t border-border bg-surface pb-[env(safe-area-inset-bottom)]"
>
{navItems.map((item) => {
const disabled = item.id !== "projects" && !openId;
@ -858,6 +883,8 @@ export function WebProjectPanelBody({
);
case "git":
return <GitPanel projectId={projectId} />;
case "files":
return <ProjectFilesPanel projectId={projectId} mobile={mobile} />;
}
}