Files
IdeA/frontend/src/features/layout/LayoutTabs.tsx
Blomios 307ae71857 feat: add main features
Agents for developpement added + frontend add + backend added. Git viewer created + agent and template creator + layout and project creator
2026-06-06 01:27:01 +02:00

211 lines
7.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* `LayoutTabs` — named-layout tab bar rendered above the terminal grid (L4 / #4).
*
* - Shows one tab per named layout (reuses the shared `Tabs` component).
* - `+` button: offers a dropdown to create a "Terminal" or "Git graph" layout.
* - Double-click a tab label: in-place rename.
* - `×` button: closes the layout (disabled when it is the last one).
* - Switching tabs calls `setActiveLayout` via `useLayouts.setActive`.
* - Tabs for `gitGraph` layouts show a small "⎇" badge.
*
* Pure presentation; all state lives in the `useLayouts` hook.
*/
import { useEffect, useRef, useState } from "react";
import type { LayoutInfo } from "@/domain";
import { cn } from "@/shared";
import { useLayouts } from "./useLayouts";
interface LayoutTabsProps {
projectId: string;
/**
* Called whenever the active layout changes (and on initial load) with the
* full active {@link LayoutInfo} — so the parent knows both its id (to re-key
* the grid) **and its kind** (to switch between the terminal grid and the git
* graph view). This is the single source of truth: the parent must NOT keep a
* separate `useLayouts` instance, or its `kind` would go stale.
*/
onActiveLayoutChange: (info: LayoutInfo) => void;
}
export function LayoutTabs({ projectId, onActiveLayoutChange }: LayoutTabsProps) {
const vm = useLayouts(projectId);
// Propagate the active layout (id + kind) to the parent on every change.
useEffect(() => {
const info = vm.layouts.find((l) => l.id === vm.activeId);
if (info) onActiveLayoutChange(info);
}, [vm.activeId, vm.layouts, onActiveLayoutChange]);
// Track which tab is being renamed (null = none).
const [renamingId, setRenamingId] = useState<string | null>(null);
const [renameValue, setRenameValue] = useState("");
const renameInputRef = useRef<HTMLInputElement | null>(null);
// Show/hide the create-kind dropdown.
const [showCreateMenu, setShowCreateMenu] = useState(false);
async function handleSelect(id: string) {
// The effect above propagates the new active layout (id + kind) to the parent.
await vm.setActive(id);
}
async function handleClose(id: string) {
if (vm.layouts.length <= 1) return;
await vm.deleteLayout(id);
// The hook updates activeId; the effect propagates it to the parent.
}
function startRename(id: string, currentName: string) {
setRenamingId(id);
setRenameValue(currentName);
// Focus the input on next tick once it's rendered.
setTimeout(() => renameInputRef.current?.focus(), 0);
}
async function commitRename() {
if (!renamingId) return;
const trimmed = renameValue.trim();
if (trimmed) await vm.rename(renamingId, trimmed);
setRenamingId(null);
setRenameValue("");
}
async function handleCreate(kind: "terminal" | "gitGraph") {
setShowCreateMenu(false);
const defaultName = kind === "gitGraph" ? "Git Graph" : "Layout";
const name = window.prompt("New layout name:", defaultName);
if (!name?.trim()) return;
const newId = await vm.create(name.trim(), kind);
if (newId) {
// Make it active; the effect propagates the new active layout (with its
// `kind`) to the parent, which then renders the right main view.
await vm.setActive(newId);
}
}
if (vm.layouts.length === 0) return null;
return (
<div
data-testid="layout-tabs"
className="flex shrink-0 items-center gap-1 border-b border-border bg-surface px-2 py-1"
>
{vm.layouts.map((tab) => {
const isActive = tab.id === vm.activeId;
const isRenaming = tab.id === renamingId;
return (
<div
key={tab.id}
className={cn(
"group flex items-center gap-1 rounded-md border px-1 transition-colors",
isActive
? "border-border-strong bg-raised"
: "border-transparent hover:bg-raised",
)}
>
{isRenaming ? (
<input
ref={renameInputRef}
value={renameValue}
onChange={(e) => setRenameValue(e.target.value)}
onBlur={() => void commitRename()}
onKeyDown={(e) => {
if (e.key === "Enter") void commitRename();
if (e.key === "Escape") {
setRenamingId(null);
setRenameValue("");
}
}}
aria-label={`rename layout ${tab.name}`}
className="w-24 rounded border border-border bg-base px-1 py-0.5 text-sm text-content focus:outline-none focus:ring-1 focus:ring-primary"
/>
) : (
<button
type="button"
role="tab"
aria-selected={isActive}
onClick={() => void handleSelect(tab.id)}
onDoubleClick={() => startRename(tab.id, tab.name)}
className={cn(
"flex items-center gap-1 px-2 py-1 text-sm focus-visible:outline-none",
isActive
? "font-semibold text-content"
: "text-muted hover:text-content",
)}
>
{tab.kind === "gitGraph" && (
<span
aria-label="git graph layout"
className="text-xs text-primary"
title="Git Graph"
>
</span>
)}
{tab.name}
</button>
)}
<button
type="button"
aria-label={`delete layout ${tab.name}`}
disabled={vm.layouts.length <= 1}
onClick={() => void handleClose(tab.id)}
className={cn(
"flex h-4 w-4 items-center justify-center rounded text-xs transition-opacity",
vm.layouts.length <= 1
? "cursor-not-allowed opacity-20"
: "opacity-60 hover:opacity-100 group-hover:opacity-100",
)}
>
×
</button>
</div>
);
})}
{/* Create new layout — dropdown to choose kind */}
<div className="relative">
<button
type="button"
aria-label="create layout"
onClick={() => setShowCreateMenu((v) => !v)}
className="flex h-6 w-6 items-center justify-center rounded border border-transparent text-sm text-muted transition-colors hover:border-border hover:text-content"
>
+
</button>
{showCreateMenu && (
<div
role="menu"
className="absolute left-0 top-full z-20 mt-1 flex min-w-max flex-col rounded-md border border-border bg-surface shadow-lg"
>
<button
type="button"
role="menuitem"
aria-label="create terminal layout"
onClick={() => void handleCreate("terminal")}
className="px-3 py-2 text-left text-sm text-content hover:bg-raised"
>
Terminal
</button>
<button
type="button"
role="menuitem"
aria-label="create git graph layout"
onClick={() => void handleCreate("gitGraph")}
className="px-3 py-2 text-left text-sm text-content hover:bg-raised"
>
Git graph
</button>
</div>
)}
</div>
{vm.error && (
<span className="ml-2 text-xs text-danger" role="alert">
{vm.error}
</span>
)}
</div>
);
}