feat: add main features

Agents for developpement added + frontend add + backend added. Git viewer created + agent and template creator + layout and project creator
This commit is contained in:
2026-06-06 01:27:01 +02:00
parent 55b3bee2c8
commit 307ae71857
273 changed files with 48740 additions and 0 deletions

View File

@ -0,0 +1,210 @@
/**
* `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>
);
}