first commit
Agent Release / build (push) Has been cancelled
Server Deploy / deploy (push) Has been cancelled

This commit is contained in:
domrichardson
2026-06-15 13:58:45 +01:00
commit c9868b2108
55 changed files with 11076 additions and 0 deletions
+10
View File
@@ -0,0 +1,10 @@
"use client";
import { QueryClientProvider } from "@tanstack/react-query";
import { queryClient } from "@/lib/query-client";
export function Providers({ children }: { children: React.ReactNode }) {
return (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
}
+78
View File
@@ -0,0 +1,78 @@
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { clsx } from "clsx";
interface NavItem {
href: string;
label: string;
icon: React.ReactNode;
}
function ServerIcon() {
return (
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M5.25 14.25h13.5m-13.5 0a3 3 0 01-3-3m3 3a3 3 0 100 6h13.5a3 3 0 100-6m-16.5-3a3 3 0 013-3h13.5a3 3 0 013 3m-19.5 0a4.5 4.5 0 01.9-2.7L5.737 5.1a3.375 3.375 0 012.7-1.35h7.126c1.062 0 2.062.5 2.7 1.35l2.587 3.45a4.5 4.5 0 01.9 2.7m0 0a3 3 0 01-3 3m0 3h.008v.008h-.008v-.008zm0-6h.008v.008h-.008v-.008zm-3 6h.008v.008h-.008v-.008zm0-6h.008v.008h-.008v-.008z" />
</svg>
);
}
function KeyIcon() {
return (
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 5.25a3 3 0 013 3m3 0a6 6 0 01-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1121.75 8.25z" />
</svg>
);
}
const navItems: NavItem[] = [
{ href: "/servers", label: "Servers", icon: <ServerIcon /> },
{ href: "/keys", label: "SSH Keys", icon: <KeyIcon /> },
];
export function Sidebar() {
const pathname = usePathname();
return (
<aside className="flex h-screen w-60 flex-col border-r border-border bg-surface">
<div className="flex h-16 items-center gap-3 border-b border-border px-5">
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-accent">
<svg className="h-4 w-4 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 5.25a3 3 0 013 3m3 0a6 6 0 01-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1121.75 8.25z" />
</svg>
</div>
<span className="text-base font-semibold text-text-primary">KeyManager</span>
</div>
<nav className="flex-1 overflow-y-auto px-3 py-4">
<ul className="space-y-1">
{navItems.map((item) => {
const isActive =
pathname === item.href || pathname.startsWith(item.href + "/");
return (
<li key={item.href}>
<Link
href={item.href}
className={clsx(
"flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-colors",
isActive
? "bg-accent/15 text-accent"
: "text-text-secondary hover:bg-surface-2 hover:text-text-primary"
)}
>
{item.icon}
{item.label}
</Link>
</li>
);
})}
</ul>
</nav>
<div className="border-t border-border px-4 py-3">
<p className="text-xs text-text-secondary">KeyManager v1.0</p>
</div>
</aside>
);
}
+40
View File
@@ -0,0 +1,40 @@
import { clsx } from "clsx";
type Variant = "success" | "warning" | "danger" | "neutral" | "accent";
interface BadgeProps {
variant?: Variant;
children: React.ReactNode;
className?: string;
}
const variantClasses: Record<Variant, string> = {
success: "bg-success/15 text-success border-success/30",
warning: "bg-warning/15 text-warning border-warning/30",
danger: "bg-danger/15 text-danger border-danger/30",
neutral: "bg-surface-2 text-text-secondary border-border",
accent: "bg-accent/15 text-accent border-accent/30",
};
export function Badge({ variant = "neutral", children, className }: BadgeProps) {
return (
<span
className={clsx(
"inline-flex items-center gap-1 rounded-full border px-2.5 py-0.5 text-xs font-medium",
variantClasses[variant],
className
)}
>
{(variant === "success" || variant === "warning" || variant === "danger") && (
<span
className={clsx("h-1.5 w-1.5 rounded-full", {
"bg-success": variant === "success",
"bg-warning": variant === "warning",
"bg-danger": variant === "danger",
})}
/>
)}
{children}
</span>
);
}
+75
View File
@@ -0,0 +1,75 @@
import { ButtonHTMLAttributes, forwardRef } from "react";
import { clsx } from "clsx";
type Variant = "primary" | "secondary" | "danger" | "ghost";
type Size = "sm" | "md" | "lg";
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: Variant;
size?: Size;
loading?: boolean;
}
const variantClasses: Record<Variant, string> = {
primary:
"bg-accent hover:bg-accent-hover text-white border-transparent",
secondary:
"bg-surface-2 hover:bg-[#2d3048] text-text-primary border-border",
danger:
"bg-danger hover:bg-danger-hover text-white border-transparent",
ghost:
"bg-transparent hover:bg-surface-2 text-text-secondary hover:text-text-primary border-transparent",
};
const sizeClasses: Record<Size, string> = {
sm: "px-3 py-1.5 text-sm",
md: "px-4 py-2 text-sm",
lg: "px-5 py-2.5 text-base",
};
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
(
{ variant = "primary", size = "md", loading, className, children, disabled, ...props },
ref
) => {
return (
<button
ref={ref}
disabled={disabled || loading}
className={clsx(
"inline-flex items-center gap-2 rounded-lg border font-medium transition-colors duration-150 focus:outline-none focus:ring-2 focus:ring-accent focus:ring-offset-2 focus:ring-offset-background disabled:opacity-50 disabled:cursor-not-allowed",
variantClasses[variant],
sizeClasses[size],
className
)}
{...props}
>
{loading && (
<svg
className="animate-spin h-4 w-4"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
/>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
/>
</svg>
)}
{children}
</button>
);
}
);
Button.displayName = "Button";
+37
View File
@@ -0,0 +1,37 @@
import { clsx } from "clsx";
import { HTMLAttributes } from "react";
interface CardProps extends HTMLAttributes<HTMLDivElement> {
padding?: boolean;
}
export function Card({ className, padding = true, children, ...props }: CardProps) {
return (
<div
className={clsx(
"rounded-xl border border-border bg-surface",
padding && "p-6",
className
)}
{...props}
>
{children}
</div>
);
}
export function CardHeader({ className, children, ...props }: HTMLAttributes<HTMLDivElement>) {
return (
<div className={clsx("mb-4 flex items-center justify-between", className)} {...props}>
{children}
</div>
);
}
export function CardTitle({ className, children, ...props }: HTMLAttributes<HTMLHeadingElement>) {
return (
<h2 className={clsx("text-lg font-semibold text-text-primary", className)} {...props}>
{children}
</h2>
);
}
+67
View File
@@ -0,0 +1,67 @@
import { clsx } from "clsx";
import { HTMLAttributes, TdHTMLAttributes, ThHTMLAttributes } from "react";
export function Table({ className, children, ...props }: HTMLAttributes<HTMLTableElement>) {
return (
<div className="overflow-x-auto">
<table
className={clsx("w-full border-collapse text-sm", className)}
{...props}
>
{children}
</table>
</div>
);
}
export function Thead({ className, children, ...props }: HTMLAttributes<HTMLTableSectionElement>) {
return (
<thead className={clsx("border-b border-border", className)} {...props}>
{children}
</thead>
);
}
export function Tbody({ className, children, ...props }: HTMLAttributes<HTMLTableSectionElement>) {
return (
<tbody className={clsx("divide-y divide-border", className)} {...props}>
{children}
</tbody>
);
}
export function Tr({ className, children, ...props }: HTMLAttributes<HTMLTableRowElement>) {
return (
<tr
className={clsx("transition-colors hover:bg-surface-2/50", className)}
{...props}
>
{children}
</tr>
);
}
export function Th({ className, children, ...props }: ThHTMLAttributes<HTMLTableCellElement>) {
return (
<th
className={clsx(
"px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-text-secondary",
className
)}
{...props}
>
{children}
</th>
);
}
export function Td({ className, children, ...props }: TdHTMLAttributes<HTMLTableCellElement>) {
return (
<td
className={clsx("px-4 py-3 text-text-primary", className)}
{...props}
>
{children}
</td>
);
}
+4
View File
@@ -0,0 +1,4 @@
export { Button } from "./Button";
export { Badge } from "./Badge";
export { Card, CardHeader, CardTitle } from "./Card";
export { Table, Thead, Tbody, Tr, Th, Td } from "./Table";