/** * `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(null); const [renameValue, setRenameValue] = useState(""); const renameInputRef = useRef(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 (
{vm.layouts.map((tab) => { const isActive = tab.id === vm.activeId; const isRenaming = tab.id === renamingId; return (
{isRenaming ? ( 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" /> ) : ( )}
); })} {/* Create new layout — dropdown to choose kind */}
{showCreateMenu && (
)}
{vm.error && ( {vm.error} )}
); }