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,54 @@
/** Button — the primary action control of the design system (LD). */
import { forwardRef } from "react";
import { cn } from "../lib/cn";
import { Spinner } from "./Spinner";
export type ButtonVariant = "primary" | "secondary" | "ghost" | "danger";
export type ButtonSize = "sm" | "md";
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
/** Visual emphasis. Defaults to `secondary`. */
variant?: ButtonVariant;
/** Control height. Defaults to `md`. */
size?: ButtonSize;
/** When true, shows a spinner and disables the button. */
loading?: boolean;
}
const BASE =
"inline-flex items-center justify-center gap-1.5 rounded-md font-medium " +
"transition-colors select-none disabled:opacity-50 disabled:cursor-not-allowed " +
"focus-visible:outline-none";
const SIZES: Record<ButtonSize, string> = {
sm: "h-7 px-2.5 text-xs",
md: "h-9 px-3.5 text-sm",
};
const VARIANTS: Record<ButtonVariant, string> = {
primary: "bg-primary text-on-primary hover:bg-primary-hover",
secondary: "bg-raised text-content border border-border hover:border-border-strong",
ghost: "text-muted hover:text-content hover:bg-raised",
danger: "bg-danger text-white hover:brightness-110",
};
/** A themeable button with variants, sizes and a loading state. */
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(
{ variant = "secondary", size = "md", loading = false, disabled, className, children, ...rest },
ref,
) {
return (
<button
ref={ref}
type={rest.type ?? "button"}
disabled={disabled || loading}
className={cn(BASE, SIZES[size], VARIANTS[variant], className)}
{...rest}
>
{loading && <Spinner size={size === "sm" ? 12 : 14} />}
{children}
</button>
);
});

View File

@ -0,0 +1,53 @@
/** Field — a labelled wrapper for form controls with hint/error text (LD). */
import { useId } from "react";
import { cn } from "../lib/cn";
export interface FieldProps {
/** Visible label text. */
label: string;
/** Optional helper text shown under the control. */
hint?: string;
/** Optional error message; when set, it replaces the hint and is announced. */
error?: string;
/** Visually hide the label (kept for screen readers). */
hideLabel?: boolean;
className?: string;
/**
* Render-prop receiving the generated `id` so the control can wire
* `id`/`aria-describedby` to the label and message.
*/
children: (props: { id: string; describedBy?: string }) => React.ReactNode;
}
/** Associates a label (and optional hint/error) with a single control. */
export function Field({ label, hint, error, hideLabel, className, children }: FieldProps) {
const id = useId();
const msgId = `${id}-msg`;
const message = error ?? hint;
return (
<div className={cn("flex flex-col gap-1", className)}>
<label
htmlFor={id}
className={cn(
"text-xs font-medium text-muted",
hideLabel && "sr-only",
)}
>
{label}
</label>
{children({ id, describedBy: message ? msgId : undefined })}
{message && (
<p
id={msgId}
role={error ? "alert" : undefined}
className={cn("text-xs", error ? "text-danger" : "text-faint")}
>
{message}
</p>
)}
</div>
);
}

View File

@ -0,0 +1,42 @@
/** IconButton — a square, icon-only button (close ×, toolbar actions) (LD). */
import { forwardRef } from "react";
import { cn } from "../lib/cn";
export type IconButtonSize = "sm" | "md";
export interface IconButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
/** Required accessible name (icon-only buttons have no text). */
"aria-label": string;
/** Control size. Defaults to `md`. */
size?: IconButtonSize;
}
const SIZES: Record<IconButtonSize, string> = {
sm: "h-6 w-6 text-xs",
md: "h-8 w-8 text-sm",
};
/** A compact, icon-only button styled as a ghost control. */
export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(function IconButton(
{ size = "md", className, children, ...rest },
ref,
) {
return (
<button
ref={ref}
type={rest.type ?? "button"}
className={cn(
"inline-flex items-center justify-center rounded-md text-muted transition-colors",
"hover:bg-raised hover:text-content focus-visible:outline-none",
"disabled:opacity-50 disabled:cursor-not-allowed",
SIZES[size],
className,
)}
{...rest}
>
{children}
</button>
);
});

View File

@ -0,0 +1,31 @@
/** Input — a single-line text field (LD). */
import { forwardRef } from "react";
import { cn } from "../lib/cn";
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
/** When true, paints the danger border (paired with a Field error). */
invalid?: boolean;
}
/** A themed text input; width is controlled by the parent (defaults to full). */
export const Input = forwardRef<HTMLInputElement, InputProps>(function Input(
{ invalid = false, className, ...rest },
ref,
) {
return (
<input
ref={ref}
aria-invalid={invalid || undefined}
className={cn(
"h-9 w-full rounded-md bg-raised px-3 text-sm text-content",
"border placeholder:text-faint outline-none transition-colors",
invalid ? "border-danger" : "border-border focus:border-primary",
"disabled:opacity-50 disabled:cursor-not-allowed",
className,
)}
{...rest}
/>
);
});

View File

@ -0,0 +1,35 @@
/** Panel — a bordered surface/card, optionally with a header (LD). */
import { cn } from "../lib/cn";
export interface PanelProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "title"> {
/** Optional header title. */
title?: React.ReactNode;
/** Optional content rendered on the right of the header (actions). */
actions?: React.ReactNode;
/** Remove the default body padding (e.g. to host a flush list/terminal). */
flush?: boolean;
}
/** A raised surface used to group related content. */
export function Panel({ title, actions, flush, className, children, ...rest }: PanelProps) {
const hasHeader = title != null || actions != null;
return (
<section
className={cn("rounded-lg border border-border bg-surface", className)}
{...rest}
>
{hasHeader && (
<header className="flex items-center justify-between gap-2 border-b border-border px-4 py-2.5">
{typeof title === "string" ? (
<h3 className="text-sm font-semibold text-content">{title}</h3>
) : (
title
)}
{actions}
</header>
)}
<div className={cn(!flush && "p-4")}>{children}</div>
</section>
);
}

View File

@ -0,0 +1,27 @@
/** Spinner — an indeterminate loading indicator (LD). */
import { cn } from "../lib/cn";
export interface SpinnerProps {
/** Diameter in pixels. Defaults to 16. */
size?: number;
/** Extra classes (e.g. a colour override). */
className?: string;
/** Accessible label; defaults to "Loading". */
label?: string;
}
/** A CSS-only spinning ring that inherits the current text colour. */
export function Spinner({ size = 16, className, label = "Loading" }: SpinnerProps) {
return (
<span
role="status"
aria-label={label}
className={cn(
"inline-block animate-spin rounded-full border-2 border-current border-t-transparent",
className,
)}
style={{ width: size, height: size }}
/>
);
}

View File

@ -0,0 +1,76 @@
/** Tabs — a horizontal, optionally closable tab bar (LD).
*
* Presentation-only and controlled: the parent owns the active id and the open
* set. Used for the project tab bar (one tab per open project, ARCHITECTURE §10)
* but generic enough for any tabbed surface.
*/
import { cn } from "../lib/cn";
import { IconButton } from "./IconButton";
export interface TabItem {
/** Stable id, returned by `onSelect`/`onClose`. */
id: string;
/** Visible label. */
label: string;
}
export interface TabsProps {
/** Tabs to render, left to right. */
items: TabItem[];
/** Currently-active tab id. */
value: string | null;
/** Called with the id of the tab the user activates. */
onSelect: (id: string) => void;
/** When provided, each tab shows a close (×) control. */
onClose?: (id: string) => void;
className?: string;
}
/** A themed tab strip with selection and optional per-tab close. */
export function Tabs({ items, value, onSelect, onClose, className }: TabsProps) {
return (
<div
role="tablist"
className={cn("flex flex-wrap items-stretch gap-1", className)}
>
{items.map((tab) => {
const active = tab.id === value;
return (
<div
key={tab.id}
className={cn(
"group flex items-center gap-1 rounded-md border px-1 transition-colors",
active
? "border-border-strong bg-raised"
: "border-transparent hover:bg-raised",
)}
>
<button
type="button"
role="tab"
aria-selected={active}
onClick={() => onSelect(tab.id)}
className={cn(
"px-2 py-1 text-sm focus-visible:outline-none",
active ? "font-semibold text-content" : "text-muted hover:text-content",
)}
>
{tab.label}
</button>
{onClose && (
<IconButton
size="sm"
aria-label={`close ${tab.label}`}
onClick={() => onClose(tab.id)}
className="opacity-60 group-hover:opacity-100"
>
×
</IconButton>
)}
</div>
);
})}
</div>
);
}

View File

@ -0,0 +1,27 @@
/** Toolbar — a thin horizontal action bar (LD). */
import { cn } from "../lib/cn";
export interface ToolbarProps extends React.HTMLAttributes<HTMLDivElement> {
/** Push trailing children to the right with an auto margin. */
justify?: "start" | "between" | "end";
}
const JUSTIFY = {
start: "justify-start",
between: "justify-between",
end: "justify-end",
} as const;
/** A flex row for grouping buttons/controls with consistent spacing. */
export function Toolbar({ justify = "start", className, children, ...rest }: ToolbarProps) {
return (
<div
role="toolbar"
className={cn("flex items-center gap-2", JUSTIFY[justify], className)}
{...rest}
>
{children}
</div>
);
}

View File

@ -0,0 +1,117 @@
/**
* LD — design-system component tests. Pure UI, rendered with React Testing
* Library; no backend, no gateways. We assert behaviour and accessibility
* (roles, names, aria-*) rather than exact Tailwind classes, so the visual
* design can evolve without breaking the suite.
*/
import { describe, it, expect, vi } from "vitest";
import { render, screen, fireEvent } from "@testing-library/react";
import { Button } from "./Button";
import { Input } from "./Input";
import { Field } from "./Field";
import { Tabs } from "./Tabs";
describe("Button", () => {
it("renders its label and fires onClick", () => {
const onClick = vi.fn();
render(<Button onClick={onClick}>Save</Button>);
fireEvent.click(screen.getByRole("button", { name: "Save" }));
expect(onClick).toHaveBeenCalledOnce();
});
it("is disabled and shows a spinner while loading", () => {
const onClick = vi.fn();
render(
<Button loading onClick={onClick}>
Save
</Button>,
);
const btn = screen.getByRole("button", { name: /Save/ }) as HTMLButtonElement;
expect(btn.disabled).toBe(true);
expect(screen.getByRole("status")).toBeTruthy(); // spinner
fireEvent.click(btn);
expect(onClick).not.toHaveBeenCalled();
});
it("defaults to type=button to avoid accidental form submits", () => {
render(<Button>Go</Button>);
expect(screen.getByRole("button", { name: "Go" }).getAttribute("type")).toBe(
"button",
);
});
});
describe("Input", () => {
it("flags aria-invalid when invalid", () => {
render(<Input aria-label="email" invalid />);
expect(screen.getByLabelText("email").getAttribute("aria-invalid")).toBe("true");
});
it("is not aria-invalid by default", () => {
render(<Input aria-label="email" />);
expect(screen.getByLabelText("email").getAttribute("aria-invalid")).toBeNull();
});
});
describe("Field", () => {
it("associates the label with the control via id", () => {
render(
<Field label="Project name">
{({ id, describedBy }) => (
<input id={id} aria-describedby={describedBy} />
)}
</Field>,
);
// getByLabelText resolves the label→control association.
expect(screen.getByLabelText("Project name")).toBeTruthy();
});
it("announces an error and wires aria-describedby", () => {
render(
<Field label="Root" error="must be absolute">
{({ id, describedBy }) => (
<input id={id} aria-describedby={describedBy} />
)}
</Field>,
);
const alert = screen.getByRole("alert");
expect(alert.textContent).toBe("must be absolute");
const input = screen.getByLabelText("Root");
expect(input.getAttribute("aria-describedby")).toBe(alert.id);
});
});
describe("Tabs", () => {
const items = [
{ id: "a", label: "alpha" },
{ id: "b", label: "beta" },
];
it("marks the active tab with aria-selected and reports selection", () => {
const onSelect = vi.fn();
render(<Tabs items={items} value="a" onSelect={onSelect} />);
const tabs = screen.getAllByRole("tab");
expect(tabs).toHaveLength(2);
const [alpha, beta] = tabs;
expect(alpha.getAttribute("aria-selected")).toBe("true");
expect(beta.getAttribute("aria-selected")).toBe("false");
fireEvent.click(beta);
expect(onSelect).toHaveBeenCalledWith("b");
});
it("renders a close control per tab only when onClose is given", () => {
const onClose = vi.fn();
const { rerender } = render(
<Tabs items={items} value="a" onSelect={() => {}} />,
);
expect(screen.queryByRole("button", { name: "close alpha" })).toBeNull();
rerender(<Tabs items={items} value="a" onSelect={() => {}} onClose={onClose} />);
fireEvent.click(screen.getByRole("button", { name: "close beta" }));
expect(onClose).toHaveBeenCalledWith("b");
});
});