Initial commit: StackPilot Phase 1 (Core)
Self-hosted Docker Compose manager. - Backend: FastAPI + docker-py + SQLite (JWT auth, file-first stacks, lifecycle, live status, WebSocket logs, docker-run converter, audit log) - Frontend: React + Vite + Tailwind (login/setup, dashboard, stacks, stack detail, Monaco editor, dark/light theme) - Deployment: docker-compose.yml, Dockerfiles, nginx reverse proxy Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
commit
f732cb080b
59 files changed
+3677
No files matched your search
@@ -0,0 +1,138 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import type { ButtonHTMLAttributes, InputHTMLAttributes, ReactNode } from "react";
|
||||
import type { StackStatus } from "@/types";
|
||||
|
||||
export function Card({
|
||||
className,
|
||||
children,
|
||||
}: {
|
||||
className?: string;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"rounded-xl border border-slate-200 bg-card p-4 shadow-sm",
|
||||
"dark:border-slate-700 dark:bg-card-dark",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type Variant = "primary" | "ghost" | "danger" | "outline";
|
||||
|
||||
const variantClasses: Record<Variant, string> = {
|
||||
primary:
|
||||
"bg-accent text-white hover:bg-sky-600 dark:bg-accent-dark dark:text-slate-900 dark:hover:bg-sky-300",
|
||||
ghost:
|
||||
"bg-transparent text-slate-600 hover:bg-slate-100 dark:text-slate-300 dark:hover:bg-slate-700",
|
||||
danger: "bg-red-600 text-white hover:bg-red-700",
|
||||
outline:
|
||||
"border border-slate-300 bg-transparent text-slate-700 hover:bg-slate-100 dark:border-slate-600 dark:text-slate-200 dark:hover:bg-slate-700",
|
||||
};
|
||||
|
||||
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
variant?: Variant;
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
export function Button({
|
||||
variant = "primary",
|
||||
loading,
|
||||
className,
|
||||
children,
|
||||
disabled,
|
||||
...props
|
||||
}: ButtonProps) {
|
||||
return (
|
||||
<button
|
||||
className={cn(
|
||||
"inline-flex items-center justify-center gap-2 rounded-lg px-3 py-2 text-sm font-medium transition-colors",
|
||||
"disabled:cursor-not-allowed disabled:opacity-50",
|
||||
variantClasses[variant],
|
||||
className
|
||||
)}
|
||||
disabled={disabled || loading}
|
||||
{...props}
|
||||
>
|
||||
{loading && <Loader2 className="h-4 w-4 animate-spin" />}
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export function Input({
|
||||
className,
|
||||
...props
|
||||
}: InputHTMLAttributes<HTMLInputElement>) {
|
||||
return (
|
||||
<input
|
||||
className={cn(
|
||||
"w-full rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm text-slate-900 outline-none",
|
||||
"focus:border-accent focus:ring-1 focus:ring-accent",
|
||||
"dark:border-slate-600 dark:bg-slate-800 dark:text-slate-100 dark:focus:border-accent-dark",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const statusColor: Record<StackStatus, string> = {
|
||||
running: "bg-green-500",
|
||||
partial: "bg-yellow-500",
|
||||
stopped: "bg-slate-400",
|
||||
error: "bg-red-500",
|
||||
updating: "bg-sky-500 animate-pulse",
|
||||
unknown: "bg-slate-300",
|
||||
};
|
||||
|
||||
export function StatusDot({ status }: { status: StackStatus }) {
|
||||
return (
|
||||
<span
|
||||
className={cn("inline-block h-2.5 w-2.5 rounded-full", statusColor[status])}
|
||||
title={status}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function Badge({
|
||||
status,
|
||||
children,
|
||||
}: {
|
||||
status?: StackStatus;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
const tone = status
|
||||
? {
|
||||
running: "bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-300",
|
||||
partial: "bg-yellow-100 text-yellow-700 dark:bg-yellow-900/40 dark:text-yellow-300",
|
||||
stopped: "bg-slate-100 text-slate-600 dark:bg-slate-700 dark:text-slate-300",
|
||||
error: "bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-300",
|
||||
updating: "bg-sky-100 text-sky-700 dark:bg-sky-900/40 dark:text-sky-300",
|
||||
unknown: "bg-slate-100 text-slate-500 dark:bg-slate-700 dark:text-slate-400",
|
||||
}[status]
|
||||
: "bg-slate-100 text-slate-600 dark:bg-slate-700 dark:text-slate-300";
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
"inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs font-medium",
|
||||
tone
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export function Spinner() {
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center p-8">
|
||||
<Loader2 className="h-6 w-6 animate-spin text-accent dark:text-accent-dark" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user