diff --git a/README.md b/README.md
index 730ae9f..3a87572 100644
--- a/README.md
+++ b/README.md
@@ -107,12 +107,21 @@ packages/
Set these variables in `apps/api/.env`:
```bash
+STACKFORGE_PROVIDER=openrouter
OPENROUTER_API_KEY=your_key_here
OPENROUTER_ENDPOINT=https://openrouter.ai/api/v1/chat/completions
OPENROUTER_APP_NAME=stackforge-api
OPENROUTER_APP_URL=http://localhost:3001
```
+`STACKFORGE_PROVIDER` supports:
+- `openrouter`: forces real LLM execution (requires `OPENROUTER_API_KEY`)
+- `mock`: forces deterministic offline agent responses
+- omitted/`auto`: uses `openrouter` when API key is present, otherwise `mock`
+
+Runtime/provider health is exposed at:
+- `GET /api/runtime`
+
---
## ๐ Token Tuning Guide
diff --git a/apps/api/.env.example b/apps/api/.env.example
index 36fdf67..16b112f 100644
--- a/apps/api/.env.example
+++ b/apps/api/.env.example
@@ -1,5 +1,6 @@
PORT=3001
NODE_ENV=development
+STACKFORGE_PROVIDER=auto
OPENROUTER_API_KEY=
OPENROUTER_ENDPOINT=https://openrouter.ai/api/v1/chat/completions
OPENROUTER_APP_NAME=stackforge-api
diff --git a/apps/api/src/controllers/generate.controller.ts b/apps/api/src/controllers/generate.controller.ts
index b217f1e..7c450a9 100644
--- a/apps/api/src/controllers/generate.controller.ts
+++ b/apps/api/src/controllers/generate.controller.ts
@@ -1,6 +1,6 @@
import type { Request, Response, NextFunction } from "express";
import { GenerateRequestSchema } from "@stackforge/shared";
-import { generateProject } from "../services/generate.service.js";
+import { generateProject, getRuntimeStatus } from "../services/generate.service.js";
export function generateController(req: Request, res: Response, next: NextFunction): void {
const parsed = GenerateRequestSchema.safeParse(req.body);
@@ -12,6 +12,15 @@ export function generateController(req: Request, res: Response, next: NextFuncti
const { prompt, projectName } = parsed.data;
const resolvedName = projectName ?? prompt.slice(0, 40).replace(/\s+/g, "-").toLowerCase();
+ const runtime = getRuntimeStatus();
+
+ if (!runtime.ready) {
+ res.status(503).json({
+ error: runtime.reason ?? "LLM provider is not configured",
+ provider: runtime.provider,
+ });
+ return;
+ }
try {
const job = generateProject(prompt, resolvedName);
diff --git a/apps/api/src/controllers/jobs.controller.ts b/apps/api/src/controllers/jobs.controller.ts
index dd326ea..85bd867 100644
--- a/apps/api/src/controllers/jobs.controller.ts
+++ b/apps/api/src/controllers/jobs.controller.ts
@@ -1,8 +1,14 @@
import type { Request, Response, NextFunction } from "express";
import { JobIdParamSchema, JOB_STATUS } from "@stackforge/shared";
import { getJob, listJobs, summarizeJobTokenUsage } from "../store/job.store.js";
+import { getRuntimeStatus } from "../services/generate.service.js";
import { subscribe, unsubscribe } from "../services/sse.service.js";
+export function runtimeController(_req: Request, res: Response): void {
+ const runtime = getRuntimeStatus();
+ res.status(runtime.ready ? 200 : 503).json(runtime);
+}
+
export function listJobsController(_req: Request, res: Response): void {
const jobs = listJobs().map((job) => ({
id: job.id,
diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts
index a8445a6..5bc1997 100644
--- a/apps/api/src/index.ts
+++ b/apps/api/src/index.ts
@@ -8,6 +8,18 @@ const PORT = process.env["PORT"] ?? "3001";
app.use(express.json());
+// CORS for Vite dev server
+app.use((_req, res, next) => {
+ res.setHeader("Access-Control-Allow-Origin", "*");
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS");
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
+ if (_req.method === "OPTIONS") {
+ res.sendStatus(204);
+ return;
+ }
+ next();
+});
+
app.get("/healthz", (_req, res) => {
res.json({ status: "ok", service: "stackforge-api", ts: new Date().toISOString() });
});
diff --git a/apps/api/src/routes/index.ts b/apps/api/src/routes/index.ts
index 04661ac..88f5185 100644
--- a/apps/api/src/routes/index.ts
+++ b/apps/api/src/routes/index.ts
@@ -1,10 +1,16 @@
import { Router, type IRouter } from "express";
import { generateController } from "../controllers/generate.controller.js";
-import { listJobsController, getJobController, streamController } from "../controllers/jobs.controller.js";
+import {
+ runtimeController,
+ listJobsController,
+ getJobController,
+ streamController,
+} from "../controllers/jobs.controller.js";
const router: IRouter = Router();
router.post("/generate", generateController);
+router.get("/runtime", runtimeController);
router.get("/jobs", listJobsController);
router.get("/jobs/:jobId", getJobController);
router.get("/stream/:jobId", streamController);
diff --git a/apps/api/src/services/generate.service.ts b/apps/api/src/services/generate.service.ts
index e95c08a..3c12bf2 100644
--- a/apps/api/src/services/generate.service.ts
+++ b/apps/api/src/services/generate.service.ts
@@ -1,7 +1,13 @@
import type { SSEEvent, AgentName } from "@stackforge/shared";
import { JOB_STATUS } from "@stackforge/shared";
-import { OpenRouterProvider, AgentCache, runOrchestrator } from "@stackforge/agents";
+import {
+ OpenRouterProvider,
+ MockProvider,
+ AgentCache,
+ runOrchestrator,
+ type LLMProvider,
+} from "@stackforge/agents";
import {
createJob,
getJob,
@@ -11,6 +17,29 @@ import {
} from "../store/job.store.js";
import { broadcast, closeJobClients } from "./sse.service.js";
+type ProviderMode = "openrouter" | "mock";
+
+export type RuntimeStatus = {
+ provider: ProviderMode;
+ ready: boolean;
+ reason?: string;
+};
+
+function resolveProviderMode(): ProviderMode {
+ const configured = (process.env["STACKFORGE_PROVIDER"] ?? "auto").trim().toLowerCase();
+
+ if (configured === "openrouter") {
+ return "openrouter";
+ }
+
+ if (configured === "mock") {
+ return "mock";
+ }
+
+ const hasOpenRouterKey = (process.env["OPENROUTER_API_KEY"] ?? "").trim().length > 0;
+ return hasOpenRouterKey ? "openrouter" : "mock";
+}
+
function readEnv(name: string): string {
const value = process.env[name];
if (value === undefined || value.trim().length === 0) {
@@ -19,7 +48,7 @@ function readEnv(name: string): string {
return value;
}
-function buildProvider(): OpenRouterProvider {
+function buildOpenRouterProvider(): OpenRouterProvider {
const endpoint = process.env["OPENROUTER_ENDPOINT"];
const options = {
apiKey: readEnv("OPENROUTER_API_KEY"),
@@ -31,16 +60,38 @@ function buildProvider(): OpenRouterProvider {
return new OpenRouterProvider(options);
}
-let provider: OpenRouterProvider | undefined;
+function buildProvider(mode: ProviderMode): LLMProvider {
+ if (mode === "mock") {
+ return new MockProvider();
+ }
+
+ return buildOpenRouterProvider();
+}
-function getProvider(): OpenRouterProvider {
+let provider: LLMProvider | undefined;
+let providerMode: ProviderMode | undefined;
+
+function getProvider(): LLMProvider {
if (provider === undefined) {
- provider = buildProvider();
+ providerMode = resolveProviderMode();
+ provider = buildProvider(providerMode);
}
return provider;
}
+export function getRuntimeStatus(): RuntimeStatus {
+ const mode = providerMode ?? resolveProviderMode();
+
+ try {
+ void getProvider();
+ return { provider: mode, ready: true };
+ } catch (error) {
+ const reason = error instanceof Error ? error.message : String(error);
+ return { provider: mode, ready: false, reason };
+ }
+}
+
const cache = new AgentCache();
function buildEmitter(jobId: string): (event: SSEEvent) => void {
diff --git a/apps/api/test/integration.test.ts b/apps/api/test/integration.test.ts
index d96ff26..032bd8d 100644
--- a/apps/api/test/integration.test.ts
+++ b/apps/api/test/integration.test.ts
@@ -28,6 +28,15 @@ describe("StackForge API Integration", () => {
expect(json.error).toBe("Validation failed");
});
+ it("should return runtime provider status", async () => {
+ const res = await fetch(`${baseUrl}/api/runtime`);
+ expect([200, 503]).toContain(res.status);
+
+ const data = await res.json();
+ expect(["openrouter", "mock"]).toContain(data.provider);
+ expect(typeof data.ready).toBe("boolean");
+ });
+
it("should create a job and return 202 accepted", async () => {
const res = await fetch(`${baseUrl}/api/generate`, {
method: "POST",
diff --git a/apps/web/index.html b/apps/web/index.html
new file mode 100644
index 0000000..21d6096
--- /dev/null
+++ b/apps/web/index.html
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+ StackForge โ AI Project Scaffolding
+
+
+
+
+
+
+
+
+
diff --git a/apps/web/package.json b/apps/web/package.json
index 663acc5..eb81a9d 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -4,11 +4,24 @@
"private": true,
"type": "module",
"scripts": {
- "dev": "echo 'web: not implemented yet'",
- "build": "echo 'web: not implemented yet'",
- "lint": "echo 'web: not implemented yet'"
+ "dev": "vite",
+ "build": "tsc -b && vite build",
+ "typecheck": "tsc --noEmit",
+ "preview": "vite preview"
},
"dependencies": {
- "@stackforge/shared": "workspace:*"
+ "@stackforge/ui": "workspace:*",
+ "react": "^19.0.0",
+ "react-dom": "^19.0.0",
+ "react-router-dom": "^7.1.0"
+ },
+ "devDependencies": {
+ "@tailwindcss/vite": "^4.1.0",
+ "@types/react": "^19.0.0",
+ "@types/react-dom": "^19.0.0",
+ "@vitejs/plugin-react": "^4.3.0",
+ "tailwindcss": "^4.1.0",
+ "typescript": "^5.7.3",
+ "vite": "^6.2.0"
}
}
diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx
new file mode 100644
index 0000000..6f8b056
--- /dev/null
+++ b/apps/web/src/App.tsx
@@ -0,0 +1,21 @@
+import React from "react";
+import { BrowserRouter, Routes, Route } from "react-router-dom";
+import { ToastProvider } from "@stackforge/ui";
+import { Layout } from "./components/Layout";
+import { Home } from "./pages/Home";
+import { JobPage } from "./pages/JobPage";
+
+export function App() {
+ return (
+
+
+
+
+ } />
+ } />
+
+
+
+
+ );
+}
diff --git a/apps/web/src/components/AgentCard.tsx b/apps/web/src/components/AgentCard.tsx
new file mode 100644
index 0000000..2eabb90
--- /dev/null
+++ b/apps/web/src/components/AgentCard.tsx
@@ -0,0 +1,88 @@
+import React from "react";
+import { Badge, Card, Spinner } from "@stackforge/ui";
+import type { AgentState } from "../hooks/useJobStream";
+
+const AGENT_META: Record = {
+ planner: { icon: "๐", label: "Planner", description: "Defines project structure and tech stack" },
+ schema: { icon: "๐๏ธ", label: "Schema", description: "Designs database entities and relationships" },
+ api: { icon: "โก", label: "API", description: "Plans API routes and endpoints" },
+ frontend: { icon: "๐จ", label: "Frontend", description: "Creates frontend pages and components" },
+ devops: { icon: "๐", label: "DevOps", description: "Sets up CI/CD, Docker, and deployment" },
+ reviewer: { icon: "๐", label: "Reviewer", description: "Reviews the entire blueprint for quality" },
+};
+
+export function AgentCard({ agent }: { agent: AgentState }) {
+ const meta = AGENT_META[agent.name] ?? { icon: "๐ค", label: agent.name, description: "" };
+
+ const borderColor =
+ agent.status === "running"
+ ? "rgba(56, 189, 248, 0.3)"
+ : agent.status === "completed"
+ ? "rgba(52, 211, 153, 0.2)"
+ : agent.status === "failed"
+ ? "rgba(244, 63, 94, 0.2)"
+ : "#23232f";
+
+ return (
+
+
+ {/* Icon */}
+
+ {agent.status === "running" ? : meta.icon}
+
+
+ {/* Info */}
+
+
+ {meta.label}
+
+
+
{meta.description}
+
+
+ {/* Duration / details */}
+
+ {agent.durationMs != null && (
+
+ {(agent.durationMs / 1000).toFixed(1)}s
+
+ )}
+ {agent.totalTokens != null && (
+
+ {agent.totalTokens.toLocaleString()} tokens
+
+ )}
+ {agent.error && (
+
{agent.error}
+ )}
+
+
+
+ );
+}
diff --git a/apps/web/src/components/AgentTimeline.tsx b/apps/web/src/components/AgentTimeline.tsx
new file mode 100644
index 0000000..5d31522
--- /dev/null
+++ b/apps/web/src/components/AgentTimeline.tsx
@@ -0,0 +1,35 @@
+import React from "react";
+import { AgentCard } from "./AgentCard";
+import type { AgentState } from "../hooks/useJobStream";
+
+export function AgentTimeline({ agents }: { agents: AgentState[] }) {
+ return (
+
+ {/* Vertical connector line */}
+
+
+
+ {agents.map((agent, i) => (
+
+ ))}
+
+
+ );
+}
diff --git a/apps/web/src/components/BlueprintView.tsx b/apps/web/src/components/BlueprintView.tsx
new file mode 100644
index 0000000..5c9a426
--- /dev/null
+++ b/apps/web/src/components/BlueprintView.tsx
@@ -0,0 +1,252 @@
+import React from "react";
+import { Card, Button } from "@stackforge/ui";
+import { CollapsibleSection } from "./CollapsibleSection";
+import { FileTree } from "./FileTree";
+import type { Blueprint } from "../lib/api";
+
+export function BlueprintView({ blueprint }: { blueprint: Blueprint }) {
+ function handleDownload() {
+ const blob = new Blob([JSON.stringify(blueprint, null, 2)], { type: "application/json" });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement("a");
+ a.href = url;
+ a.download = `${blueprint.projectName}-blueprint.json`;
+ a.click();
+ URL.revokeObjectURL(url);
+ }
+
+ return (
+
+ {/* Stack summary card */}
+
+
+
+
+ {blueprint.projectName}
+
+
+ {Object.entries(blueprint.stack)
+ .filter(([k]) => k !== "monorepo")
+ .map(([key, value]) => (
+
+ {key}: {String(value)}
+
+ ))}
+
+
+
+
+
+
+ {/* Entities */}
+
+
+ {blueprint.entities.map((entity) => (
+
+
+ {entity.name}
+ ({entity.tableName})
+
+
+ {entity.fields.map((f) => (
+
+ {f.name}: {f.type}
+ {f.nullable ? "?" : ""}
+ {f.foreignKey ? ` โ ${f.foreignKey}` : ""}
+
+ ))}
+
+
+ ))}
+
+
+
+ {/* API Routes */}
+
+
+
+
+
+ {["Method", "Path", "Description", "Auth"].map((h) => (
+ |
+ {h}
+ |
+ ))}
+
+
+
+ {blueprint.routePlan.map((r, i) => (
+
+ |
+
+ {r.method}
+
+ |
+ {r.path} |
+ {r.description} |
+ {r.auth ? "๐" : "โ"} |
+
+ ))}
+
+
+
+
+
+ {/* Frontend Pages */}
+
+
+ {blueprint.frontendPages.map((page) => (
+
+
+ {page.name}
+ {page.route}
+ {page.auth && ๐ Auth}
+
+
{page.description}
+
+ {page.components.map((c) => (
+
+ {c}
+
+ ))}
+
+
+ ))}
+
+
+
+ {/* DevOps */}
+
+
+
+
+
+
+
+
+
+ {/* Reviewer Notes */}
+
+
+ {blueprint.reviewerNotes.map((note, i) => (
+
+
+ [{note.agent}]
+ {" "}
+ {note.note}
+
+ ))}
+
+
+
+ {/* File Tree */}
+
+
+
+
+ );
+}
+
+function Row({ label, value }: { label: string; value: string }) {
+ return (
+
+ {label}
+ {value}
+
+ );
+}
+
+function methodColor(method: string): string {
+ switch (method) {
+ case "GET": return "rgba(52, 211, 153, 0.3)";
+ case "POST": return "rgba(56, 189, 248, 0.3)";
+ case "PUT": return "rgba(251, 191, 36, 0.3)";
+ case "PATCH": return "rgba(168, 85, 247, 0.3)";
+ case "DELETE": return "rgba(244, 63, 94, 0.3)";
+ default: return "rgba(92, 92, 111, 0.3)";
+ }
+}
diff --git a/apps/web/src/components/CollapsibleSection.tsx b/apps/web/src/components/CollapsibleSection.tsx
new file mode 100644
index 0000000..33c52a4
--- /dev/null
+++ b/apps/web/src/components/CollapsibleSection.tsx
@@ -0,0 +1,88 @@
+import React, { useState } from "react";
+
+interface CollapsibleSectionProps {
+ title: string;
+ icon?: string;
+ defaultOpen?: boolean;
+ count?: number;
+ children: React.ReactNode;
+}
+
+export function CollapsibleSection({
+ title,
+ icon,
+ defaultOpen = false,
+ count,
+ children,
+}: CollapsibleSectionProps) {
+ const [open, setOpen] = useState(defaultOpen);
+
+ return (
+
+
+
+ {open && (
+
+ {children}
+
+ )}
+
+ );
+}
diff --git a/apps/web/src/components/FileTree.tsx b/apps/web/src/components/FileTree.tsx
new file mode 100644
index 0000000..5154216
--- /dev/null
+++ b/apps/web/src/components/FileTree.tsx
@@ -0,0 +1,54 @@
+import React from "react";
+
+interface FolderNode {
+ path: string;
+ type: "file" | "dir";
+ description?: string;
+}
+
+export function FileTree({ nodes }: { nodes: FolderNode[] }) {
+ if (nodes.length === 0) {
+ return No folder structure available.
;
+ }
+
+ return (
+
+ {nodes.map((node) => {
+ const depth = node.path.split("/").length - 1;
+ const name = node.path.split("/").pop() ?? node.path;
+ const isDir = node.type === "dir";
+
+ return (
+
+
+ {isDir ? "๐" : "๐"}
+
+
+ {name}
+
+ {node.description && (
+
+ โ {node.description}
+
+ )}
+
+ );
+ })}
+
+ );
+}
diff --git a/apps/web/src/components/Layout.tsx b/apps/web/src/components/Layout.tsx
new file mode 100644
index 0000000..a8f5d0a
--- /dev/null
+++ b/apps/web/src/components/Layout.tsx
@@ -0,0 +1,125 @@
+import React from "react";
+import { Link, useLocation } from "react-router-dom";
+
+export function Layout({ children }: { children: React.ReactNode }) {
+ const location = useLocation();
+
+ return (
+
+ {/* โโโ Navbar โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */}
+
+
+ {/* โโโ Main โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */}
+
{children}
+
+ {/* โโโ Footer โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */}
+
+
+ );
+}
+
+function NavLink({
+ to,
+ active,
+ children,
+}: {
+ to: string;
+ active: boolean;
+ children: React.ReactNode;
+}) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/apps/web/src/hooks/useJobStream.ts b/apps/web/src/hooks/useJobStream.ts
new file mode 100644
index 0000000..1f4a3d1
--- /dev/null
+++ b/apps/web/src/hooks/useJobStream.ts
@@ -0,0 +1,161 @@
+import { useState, useEffect, useRef, useCallback } from "react";
+import { runDemoSimulation } from "../lib/mock-data";
+
+export type AgentStatus = "waiting" | "running" | "completed" | "failed";
+
+export interface AgentState {
+ name: string;
+ status: AgentStatus;
+ startedAt?: string;
+ completedAt?: string;
+ durationMs?: number;
+ error?: string;
+ model?: string;
+ totalTokens?: number;
+}
+
+export interface JobStreamState {
+ agents: AgentState[];
+ jobStatus: "queued" | "running" | "completed" | "failed";
+ jobError?: string;
+ connected: boolean;
+}
+
+const AGENT_ORDER = ["planner", "schema", "api", "frontend", "devops", "reviewer"] as const;
+
+function createInitialAgents(): AgentState[] {
+ return AGENT_ORDER.map((name) => ({ name, status: "waiting" as AgentStatus }));
+}
+
+export function useJobStream(jobId: string | undefined, isDemo = false): JobStreamState {
+ const [agents, setAgents] = useState(createInitialAgents);
+ const [jobStatus, setJobStatus] = useState("queued");
+ const [jobError, setJobError] = useState();
+ const [connected, setConnected] = useState(false);
+ const sourceRef = useRef(null);
+
+ const handleEvent = useCallback((data: Record) => {
+ const type = data["type"] as string;
+
+ switch (type) {
+ case "job_created":
+ setJobStatus("running");
+ break;
+
+ case "agent_started":
+ setAgents((prev) =>
+ prev.map((a) =>
+ a.name === data["agent"]
+ ? { ...a, status: "running" as AgentStatus, startedAt: data["timestamp"] as string }
+ : a,
+ ),
+ );
+ break;
+
+ case "agent_completed": {
+ const payload = data["payload"] as Record;
+ setAgents((prev) =>
+ prev.map((a) =>
+ a.name === data["agent"]
+ ? {
+ ...a,
+ status: "completed" as AgentStatus,
+ completedAt: data["timestamp"] as string,
+ durationMs: payload["durationMs"] as number,
+ model: payload["model"] as string | undefined,
+ totalTokens: payload["totalTokens"] as number | undefined,
+ }
+ : a,
+ ),
+ );
+ break;
+ }
+
+ case "agent_failed": {
+ const payload = data["payload"] as Record;
+ setAgents((prev) =>
+ prev.map((a) =>
+ a.name === data["agent"]
+ ? { ...a, status: "failed" as AgentStatus, error: payload["error"] as string }
+ : a,
+ ),
+ );
+ break;
+ }
+
+ case "job_completed":
+ setJobStatus("completed");
+ break;
+
+ case "job_failed": {
+ const payload = data["payload"] as Record;
+ setJobStatus("failed");
+ setJobError(payload["error"] as string);
+ break;
+ }
+ }
+ }, []);
+
+ // Demo mode โ simulate locally
+ useEffect(() => {
+ if (!jobId || !isDemo) return;
+
+ setAgents(createInitialAgents());
+ setJobStatus("queued");
+ setJobError(undefined);
+ setConnected(true);
+
+ const cancel = runDemoSimulation(
+ jobId,
+ (event) => handleEvent(event as unknown as Record),
+ () => setConnected(false),
+ );
+
+ return cancel;
+ }, [jobId, isDemo, handleEvent]);
+
+ // Real SSE mode
+ useEffect(() => {
+ if (!jobId || isDemo) return;
+
+ setAgents(createInitialAgents());
+ setJobStatus("queued");
+ setJobError(undefined);
+
+ const source = new EventSource(`/api/stream/${jobId}`);
+ sourceRef.current = source;
+
+ source.onopen = () => setConnected(true);
+
+ source.addEventListener("job_created", (e) => {
+ handleEvent(JSON.parse(e.data) as Record);
+ });
+ source.addEventListener("agent_started", (e) => {
+ handleEvent(JSON.parse(e.data) as Record);
+ });
+ source.addEventListener("agent_completed", (e) => {
+ handleEvent(JSON.parse(e.data) as Record);
+ });
+ source.addEventListener("agent_failed", (e) => {
+ handleEvent(JSON.parse(e.data) as Record);
+ });
+ source.addEventListener("job_completed", (e) => {
+ handleEvent(JSON.parse(e.data) as Record);
+ });
+ source.addEventListener("job_failed", (e) => {
+ handleEvent(JSON.parse(e.data) as Record);
+ });
+
+ source.onerror = () => {
+ setConnected(false);
+ source.close();
+ };
+
+ return () => {
+ source.close();
+ sourceRef.current = null;
+ };
+ }, [jobId, isDemo, handleEvent]);
+
+ return { agents, jobStatus, jobError, connected };
+}
diff --git a/apps/web/src/index.css b/apps/web/src/index.css
new file mode 100644
index 0000000..df5024e
--- /dev/null
+++ b/apps/web/src/index.css
@@ -0,0 +1,143 @@
+@import "tailwindcss";
+
+/* โโโ Design Tokens โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
+:root {
+ --font-sans: "Inter", system-ui, -apple-system, sans-serif;
+
+ /* Dark theme palette */
+ --bg-primary: #0a0a0f;
+ --bg-secondary: #111118;
+ --bg-card: #16161f;
+ --bg-card-hover: #1c1c28;
+ --bg-input: #1a1a26;
+
+ --border-subtle: #23232f;
+ --border-focus: #6366f1;
+
+ --text-primary: #f0f0f5;
+ --text-secondary: #9898a8;
+ --text-muted: #5c5c6f;
+
+ /* Accent colors */
+ --accent-indigo: #6366f1;
+ --accent-indigo-hover: #818cf8;
+ --accent-violet: #8b5cf6;
+ --accent-emerald: #34d399;
+ --accent-sky: #38bdf8;
+ --accent-amber: #fbbf24;
+ --accent-rose: #f43f5e;
+
+ /* Gradients */
+ --gradient-hero: linear-gradient(135deg, #6366f1 0%, #8b5cf6 40%, #a855f7 100%);
+ --gradient-card: linear-gradient(135deg, rgba(99, 102, 241, 0.08) 0%, rgba(139, 92, 246, 0.04) 100%);
+
+ /* Shadows */
+ --shadow-card: 0 1px 3px rgba(0, 0, 0, 0.3), 0 4px 12px rgba(0, 0, 0, 0.2);
+ --shadow-card-hover: 0 4px 16px rgba(99, 102, 241, 0.15), 0 8px 32px rgba(0, 0, 0, 0.3);
+ --shadow-glow: 0 0 20px rgba(99, 102, 241, 0.25);
+
+ /* Spacing */
+ --radius-sm: 6px;
+ --radius-md: 10px;
+ --radius-lg: 16px;
+ --radius-xl: 24px;
+
+ /* Transitions */
+ --transition-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1);
+ --transition-base: 250ms cubic-bezier(0.4, 0, 0.2, 1);
+ --transition-slow: 400ms cubic-bezier(0.4, 0, 0.2, 1);
+}
+
+/* โโโ Reset & Base โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
+*,
+*::before,
+*::after {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+html {
+ font-family: var(--font-sans);
+ background: var(--bg-primary);
+ color: var(--text-primary);
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+body {
+ min-height: 100vh;
+ line-height: 1.6;
+}
+
+#root {
+ min-height: 100vh;
+ display: flex;
+ flex-direction: column;
+}
+
+/* โโโ Scrollbar โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
+::-webkit-scrollbar {
+ width: 6px;
+}
+::-webkit-scrollbar-track {
+ background: var(--bg-primary);
+}
+::-webkit-scrollbar-thumb {
+ background: var(--border-subtle);
+ border-radius: 3px;
+}
+::-webkit-scrollbar-thumb:hover {
+ background: var(--text-muted);
+}
+
+/* โโโ Keyframes โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
+@keyframes spin {
+ to { transform: rotate(360deg); }
+}
+
+@keyframes fadeIn {
+ from { opacity: 0; transform: translateY(8px); }
+ to { opacity: 1; transform: translateY(0); }
+}
+
+@keyframes slideUp {
+ from { opacity: 0; transform: translateY(20px); }
+ to { opacity: 1; transform: translateY(0); }
+}
+
+@keyframes pulse-glow {
+ 0%, 100% { box-shadow: 0 0 8px rgba(99, 102, 241, 0.3); }
+ 50% { box-shadow: 0 0 20px rgba(99, 102, 241, 0.5); }
+}
+
+@keyframes shimmer {
+ 0% { background-position: -200% 0; }
+ 100% { background-position: 200% 0; }
+}
+
+@keyframes toast-in {
+ from { opacity: 0; transform: translateX(100%); }
+ to { opacity: 1; transform: translateX(0); }
+}
+
+@keyframes toast-out {
+ from { opacity: 1; transform: translateX(0); }
+ to { opacity: 0; transform: translateX(100%); }
+}
+
+/* โโโ Utility classes โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
+.animate-fade-in {
+ animation: fadeIn var(--transition-base) ease-out;
+}
+
+.animate-slide-up {
+ animation: slideUp var(--transition-slow) ease-out;
+}
+
+.skeleton {
+ background: linear-gradient(90deg, var(--bg-card) 25%, var(--bg-card-hover) 50%, var(--bg-card) 75%);
+ background-size: 200% 100%;
+ animation: shimmer 1.5s infinite;
+ border-radius: var(--radius-md);
+}
diff --git a/apps/web/src/lib/api.ts b/apps/web/src/lib/api.ts
new file mode 100644
index 0000000..6d5ac8a
--- /dev/null
+++ b/apps/web/src/lib/api.ts
@@ -0,0 +1,111 @@
+const API_BASE = "/api";
+
+export interface GenerateResponse {
+ jobId: string;
+ status: string;
+ projectName: string;
+ createdAt: string;
+ streamUrl: string;
+ jobUrl: string;
+}
+
+export interface RuntimeResponse {
+ provider: "openrouter" | "mock";
+ ready: boolean;
+ reason?: string;
+}
+
+export interface JobResponse {
+ id: string;
+ status: string;
+ projectName: string;
+ createdAt: string;
+ updatedAt: string;
+ completedAt?: string;
+ agentsCompleted: string[];
+ error?: string;
+ blueprint?: Blueprint;
+ tokenUsage?: { inputTokens: number; outputTokens: number; totalTokens: number };
+}
+
+export interface Blueprint {
+ projectName: string;
+ generatedAt: string;
+ stack: {
+ frontend: string;
+ backend: string;
+ database: string;
+ auth: string;
+ hosting: string;
+ packageManager: string;
+ monorepo: boolean;
+ };
+ folderStructure: { path: string; type: "file" | "dir"; description?: string }[];
+ entities: {
+ name: string;
+ tableName: string;
+ fields: { name: string; type: string; nullable: boolean; unique?: boolean; foreignKey?: string }[];
+ indexes?: string[];
+ }[];
+ relationships: { from: string; to: string; type: string; description: string }[];
+ routePlan: {
+ method: string;
+ path: string;
+ description: string;
+ auth: boolean;
+ requestBody?: string;
+ responseType: string;
+ }[];
+ frontendPages: {
+ route: string;
+ name: string;
+ components: string[];
+ auth: boolean;
+ description: string;
+ }[];
+ infraPlan: {
+ ci: string[];
+ docker: boolean;
+ deployment: string[];
+ envVars: string[];
+ };
+ generatedFilesPlan: { path: string; generator: string; description: string }[];
+ reviewerNotes: { severity: "info" | "warning" | "error"; agent: string; note: string }[];
+}
+
+export async function generateProject(prompt: string, projectName?: string): Promise {
+ const res = await fetch(`${API_BASE}/generate`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ prompt, ...(projectName ? { projectName } : {}) }),
+ });
+
+ if (!res.ok) {
+ const body = await res.json().catch(() => ({}));
+ throw new Error((body as { error?: string }).error ?? `Request failed (${res.status})`);
+ }
+
+ return res.json() as Promise;
+}
+
+export async function getJob(jobId: string): Promise {
+ const res = await fetch(`${API_BASE}/jobs/${jobId}`);
+
+ if (!res.ok) {
+ const body = await res.json().catch(() => ({}));
+ throw new Error((body as { error?: string }).error ?? `Request failed (${res.status})`);
+ }
+
+ return res.json() as Promise;
+}
+
+export async function getRuntime(): Promise {
+ const res = await fetch(`${API_BASE}/runtime`);
+
+ if (!res.ok) {
+ const body = await res.json().catch(() => ({}));
+ throw new Error((body as { error?: string }).error ?? `Request failed (${res.status})`);
+ }
+
+ return res.json() as Promise;
+}
diff --git a/apps/web/src/lib/mock-data.ts b/apps/web/src/lib/mock-data.ts
new file mode 100644
index 0000000..3c0f098
--- /dev/null
+++ b/apps/web/src/lib/mock-data.ts
@@ -0,0 +1,217 @@
+import type { Blueprint } from "./api";
+
+export const MOCK_BLUEPRINT: Blueprint = {
+ projectName: "invoice-saas",
+ generatedAt: new Date().toISOString(),
+ stack: {
+ frontend: "react",
+ backend: "express",
+ database: "postgres",
+ auth: "jwt",
+ hosting: "docker + railway",
+ packageManager: "bun",
+ monorepo: true,
+ },
+ folderStructure: [
+ { path: "apps", type: "dir", description: "Application packages" },
+ { path: "apps/web", type: "dir", description: "React frontend" },
+ { path: "apps/web/src", type: "dir" },
+ { path: "apps/web/src/pages", type: "dir" },
+ { path: "apps/web/src/pages/Dashboard.tsx", type: "file", description: "Main dashboard view" },
+ { path: "apps/web/src/pages/Invoices.tsx", type: "file", description: "Invoice list & creation" },
+ { path: "apps/web/src/pages/Login.tsx", type: "file", description: "Auth login page" },
+ { path: "apps/api", type: "dir", description: "Express backend" },
+ { path: "apps/api/src", type: "dir" },
+ { path: "apps/api/src/routes", type: "dir" },
+ { path: "apps/api/src/routes/invoices.ts", type: "file", description: "Invoice CRUD routes" },
+ { path: "apps/api/src/routes/auth.ts", type: "file", description: "Auth endpoints" },
+ { path: "apps/api/src/models", type: "dir" },
+ { path: "apps/api/src/models/User.ts", type: "file" },
+ { path: "apps/api/src/models/Invoice.ts", type: "file" },
+ { path: "packages", type: "dir" },
+ { path: "packages/shared", type: "dir", description: "Shared types & utils" },
+ { path: "docker-compose.yml", type: "file", description: "Docker orchestration" },
+ { path: "Dockerfile", type: "file", description: "Production container" },
+ { path: ".github/workflows/ci.yml", type: "file", description: "CI pipeline" },
+ ],
+ entities: [
+ {
+ name: "User",
+ tableName: "users",
+ fields: [
+ { name: "id", type: "uuid", nullable: false, unique: true },
+ { name: "email", type: "varchar(255)", nullable: false, unique: true },
+ { name: "passwordHash", type: "text", nullable: false },
+ { name: "fullName", type: "varchar(100)", nullable: false },
+ { name: "role", type: "enum('admin','user')", nullable: false },
+ { name: "createdAt", type: "timestamp", nullable: false },
+ ],
+ indexes: ["idx_users_email"],
+ },
+ {
+ name: "Invoice",
+ tableName: "invoices",
+ fields: [
+ { name: "id", type: "uuid", nullable: false, unique: true },
+ { name: "userId", type: "uuid", nullable: false, foreignKey: "users.id" },
+ { name: "clientName", type: "varchar(200)", nullable: false },
+ { name: "amount", type: "decimal(10,2)", nullable: false },
+ { name: "currency", type: "varchar(3)", nullable: false },
+ { name: "status", type: "enum('draft','sent','paid','overdue')", nullable: false },
+ { name: "dueDate", type: "date", nullable: false },
+ { name: "createdAt", type: "timestamp", nullable: false },
+ ],
+ indexes: ["idx_invoices_user_id", "idx_invoices_status"],
+ },
+ {
+ name: "Payment",
+ tableName: "payments",
+ fields: [
+ { name: "id", type: "uuid", nullable: false, unique: true },
+ { name: "invoiceId", type: "uuid", nullable: false, foreignKey: "invoices.id" },
+ { name: "stripePaymentId", type: "varchar(255)", nullable: false },
+ { name: "amount", type: "decimal(10,2)", nullable: false },
+ { name: "paidAt", type: "timestamp", nullable: false },
+ ],
+ },
+ ],
+ relationships: [
+ { from: "User", to: "Invoice", type: "one-to-many", description: "A user can have many invoices" },
+ { from: "Invoice", to: "Payment", type: "one-to-many", description: "An invoice can have multiple payments" },
+ ],
+ routePlan: [
+ { method: "POST", path: "/api/auth/register", description: "Register a new user", auth: false, requestBody: "{ email, password, fullName }", responseType: "{ user, token }" },
+ { method: "POST", path: "/api/auth/login", description: "Login and get JWT", auth: false, requestBody: "{ email, password }", responseType: "{ token }" },
+ { method: "GET", path: "/api/invoices", description: "List user invoices", auth: true, responseType: "Invoice[]" },
+ { method: "POST", path: "/api/invoices", description: "Create a new invoice", auth: true, requestBody: "{ clientName, amount, currency, dueDate }", responseType: "Invoice" },
+ { method: "GET", path: "/api/invoices/:id", description: "Get invoice detail", auth: true, responseType: "Invoice" },
+ { method: "PATCH", path: "/api/invoices/:id", description: "Update invoice status", auth: true, requestBody: "{ status }", responseType: "Invoice" },
+ { method: "DELETE", path: "/api/invoices/:id", description: "Delete a draft invoice", auth: true, responseType: "{ success: boolean }" },
+ { method: "POST", path: "/api/invoices/:id/pay", description: "Process Stripe payment", auth: true, requestBody: "{ paymentMethodId }", responseType: "Payment" },
+ { method: "GET", path: "/api/dashboard/stats", description: "Dashboard summary stats", auth: true, responseType: "{ totalRevenue, pendingInvoices, paidCount }" },
+ ],
+ frontendPages: [
+ { route: "/login", name: "Login", components: ["LoginForm", "AuthLayout"], auth: false, description: "User authentication page" },
+ { route: "/dashboard", name: "Dashboard", components: ["StatsCards", "RevenueChart", "RecentInvoices"], auth: true, description: "Overview with revenue stats and recent activity" },
+ { route: "/invoices", name: "Invoices", components: ["InvoiceTable", "FilterBar", "CreateInvoiceModal"], auth: true, description: "Invoice list with filters and creation" },
+ { route: "/invoices/:id", name: "Invoice Detail", components: ["InvoiceHeader", "LineItems", "PaymentHistory", "PayButton"], auth: true, description: "Single invoice view with payment" },
+ { route: "/settings", name: "Settings", components: ["ProfileForm", "BillingInfo"], auth: true, description: "User profile and billing settings" },
+ ],
+ infraPlan: {
+ ci: ["lint", "typecheck", "test", "build"],
+ docker: true,
+ deployment: ["Railway (API)", "Vercel (Web)", "Neon (Postgres)"],
+ envVars: ["DATABASE_URL", "JWT_SECRET", "STRIPE_SECRET_KEY", "STRIPE_WEBHOOK_SECRET"],
+ },
+ generatedFilesPlan: [
+ { path: "docker-compose.yml", generator: "devops", description: "Local dev with Postgres + API" },
+ { path: "Dockerfile", generator: "devops", description: "Multi-stage production build" },
+ { path: ".github/workflows/ci.yml", generator: "devops", description: "GitHub Actions CI pipeline" },
+ { path: "apps/api/prisma/schema.prisma", generator: "schema", description: "Prisma schema with all entities" },
+ ],
+ reviewerNotes: [
+ { severity: "info", agent: "reviewer", note: "Consider adding rate limiting to auth endpoints to prevent brute force attacks." },
+ { severity: "warning", agent: "reviewer", note: "Stripe webhook endpoint should verify webhook signatures to prevent spoofed events." },
+ { severity: "info", agent: "reviewer", note: "Add pagination to GET /api/invoices for users with large invoice volumes." },
+ { severity: "error", agent: "reviewer", note: "Missing CORS configuration โ frontend and API are on different origins in production." },
+ ],
+};
+
+// โโโ Demo simulation โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+
+interface DemoSSEEvent {
+ type: string;
+ jobId: string;
+ timestamp: string;
+ agent?: string;
+ payload: Record;
+}
+
+const AGENT_SEQUENCE = ["planner", "schema", "api", "frontend", "devops", "reviewer"] as const;
+
+export function runDemoSimulation(
+ jobId: string,
+ onEvent: (event: DemoSSEEvent) => void,
+ onComplete: () => void,
+): () => void {
+ const timers: ReturnType[] = [];
+ let cancelled = false;
+
+ function schedule(fn: () => void, delayMs: number) {
+ const t = setTimeout(() => {
+ if (!cancelled) fn();
+ }, delayMs);
+ timers.push(t);
+ }
+
+ // job_created at t=0
+ schedule(() => {
+ onEvent({
+ type: "job_created",
+ jobId,
+ timestamp: new Date().toISOString(),
+ payload: { prompt: "Demo project", projectName: "invoice-saas" },
+ });
+ }, 200);
+
+ let cumulativeDelay = 500;
+
+ AGENT_SEQUENCE.forEach((agent, i) => {
+ const startDelay = cumulativeDelay;
+ const runDuration = 1200 + Math.random() * 1800; // 1.2s โ 3s per agent
+ const endDelay = startDelay + runDuration;
+
+ // agent_started
+ schedule(() => {
+ onEvent({
+ type: "agent_started",
+ jobId,
+ timestamp: new Date().toISOString(),
+ agent,
+ payload: {},
+ });
+ }, startDelay);
+
+ // agent_completed
+ schedule(() => {
+ onEvent({
+ type: "agent_completed",
+ jobId,
+ timestamp: new Date().toISOString(),
+ agent,
+ payload: {
+ durationMs: Math.round(runDuration),
+ cached: false,
+ inputTokens: 800 + Math.floor(Math.random() * 400),
+ outputTokens: 400 + Math.floor(Math.random() * 300),
+ totalTokens: 1200 + Math.floor(Math.random() * 700),
+ tokensUsed: 1200 + Math.floor(Math.random() * 700),
+ estimatedInputTokens: 900,
+ compressionPasses: 0,
+ providerInputTokens: 800,
+ providerOutputTokens: 400,
+ model: "meta-llama/llama-3.1-8b-instruct",
+ },
+ });
+ }, endDelay);
+
+ cumulativeDelay = endDelay + 300; // small gap between agents
+ });
+
+ // job_completed
+ schedule(() => {
+ onEvent({
+ type: "job_completed",
+ jobId,
+ timestamp: new Date().toISOString(),
+ payload: { durationMs: Math.round(cumulativeDelay) },
+ });
+ onComplete();
+ }, cumulativeDelay + 200);
+
+ // Return a cancel function
+ return () => {
+ cancelled = true;
+ timers.forEach(clearTimeout);
+ };
+}
diff --git a/apps/web/src/main.tsx b/apps/web/src/main.tsx
new file mode 100644
index 0000000..7cc597b
--- /dev/null
+++ b/apps/web/src/main.tsx
@@ -0,0 +1,13 @@
+import React from "react";
+import ReactDOM from "react-dom/client";
+import { App } from "./App";
+import "./index.css";
+
+const rootEl = document.getElementById("root");
+if (!rootEl) throw new Error("Missing #root element");
+
+ReactDOM.createRoot(rootEl).render(
+
+
+ ,
+);
diff --git a/apps/web/src/pages/Home.tsx b/apps/web/src/pages/Home.tsx
new file mode 100644
index 0000000..6a67e52
--- /dev/null
+++ b/apps/web/src/pages/Home.tsx
@@ -0,0 +1,304 @@
+import React, { useEffect, useState } from "react";
+import { useNavigate } from "react-router-dom";
+import { Button, Card, useToast } from "@stackforge/ui";
+import { generateProject, getRuntime, type RuntimeResponse } from "../lib/api";
+
+const FRONTEND_OPTIONS = ["react", "vue", "svelte"] as const;
+const BACKEND_OPTIONS = ["express", "fastify", "hono"] as const;
+const DATABASE_OPTIONS = ["postgres", "mysql", "mongodb"] as const;
+
+export function Home() {
+ const navigate = useNavigate();
+ const { addToast } = useToast();
+
+ const [prompt, setPrompt] = useState("");
+ const [frontend, setFrontend] = useState("react");
+ const [backend, setBackend] = useState("express");
+ const [database, setDatabase] = useState("postgres");
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState(null);
+ const [runtime, setRuntime] = useState(null);
+ const [runtimeError, setRuntimeError] = useState(null);
+
+ const isValid = prompt.trim().length >= 10;
+ const canGenerate = isValid && runtime?.ready !== false;
+
+ useEffect(() => {
+ getRuntime()
+ .then((res) => {
+ setRuntime(res);
+ setRuntimeError(null);
+ })
+ .catch((err) => {
+ setRuntime(null);
+ setRuntimeError(err instanceof Error ? err.message : "Failed to load runtime status");
+ });
+ }, []);
+
+ async function handleSubmit(e: React.FormEvent) {
+ e.preventDefault();
+ if (!canGenerate || loading) return;
+
+ setError(null);
+ setLoading(true);
+
+ const fullPrompt = `${prompt.trim()}\n\nStack preferences: Frontend: ${frontend}, Backend: ${backend}, Database: ${database}`;
+
+ try {
+ const res = await generateProject(fullPrompt);
+ addToast("success", `Project "${res.projectName}" queued!`);
+ navigate(`/jobs/${res.jobId}`);
+ } catch (err) {
+ const msg = err instanceof Error ? err.message : "Something went wrong";
+ setError(msg);
+ addToast("error", msg);
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ return (
+
+ {/* Hero */}
+
+
+ Describe your idea.
+
+
+ We'll build the blueprint.
+
+
+
+ StackForge uses AI agents to plan your full-stack project โ schema, APIs,
+ frontend pages, DevOps โ all generated in seconds.
+
+
+
+ {/* Form */}
+
+
+
+
+ );
+}
+
+/* โโโ Select field helper โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
+function SelectField({
+ id,
+ label,
+ value,
+ onChange,
+ options,
+}: {
+ id: string;
+ label: string;
+ value: string;
+ onChange: (v: string) => void;
+ options: readonly string[];
+}) {
+ return (
+
+
+
+
+ );
+}
diff --git a/apps/web/src/pages/JobPage.tsx b/apps/web/src/pages/JobPage.tsx
new file mode 100644
index 0000000..13f0ca1
--- /dev/null
+++ b/apps/web/src/pages/JobPage.tsx
@@ -0,0 +1,225 @@
+import React, { useEffect, useState } from "react";
+import { useParams, Link, useSearchParams } from "react-router-dom";
+import { Button, Spinner, useToast } from "@stackforge/ui";
+import { AgentTimeline } from "../components/AgentTimeline";
+import { BlueprintView } from "../components/BlueprintView";
+import { useJobStream } from "../hooks/useJobStream";
+import { getJob, type JobResponse } from "../lib/api";
+import { MOCK_BLUEPRINT } from "../lib/mock-data";
+
+export function JobPage() {
+ const { jobId } = useParams<{ jobId: string }>();
+ const [searchParams] = useSearchParams();
+ const isDemo = searchParams.get("demo") === "true";
+
+ const { addToast } = useToast();
+ const stream = useJobStream(jobId, isDemo);
+ const [job, setJob] = useState(null);
+ const [loadError, setLoadError] = useState(null);
+
+ // Fetch job details
+ useEffect(() => {
+ if (!jobId) return;
+ if (isDemo) {
+ setJob({
+ id: jobId,
+ status: "running",
+ projectName: MOCK_BLUEPRINT.projectName,
+ createdAt: new Date().toISOString(),
+ updatedAt: new Date().toISOString(),
+ agentsCompleted: [],
+ });
+ return;
+ }
+
+ getJob(jobId)
+ .then(setJob)
+ .catch((err) => {
+ setLoadError(err instanceof Error ? err.message : "Failed to load job");
+ });
+ }, [jobId, isDemo]);
+
+ // Re-fetch when job completes to get blueprint
+ useEffect(() => {
+ if (!jobId || stream.jobStatus !== "completed") return;
+
+ if (isDemo) {
+ setJob((prev) => prev ? { ...prev, status: "completed", blueprint: MOCK_BLUEPRINT } : null);
+ addToast("success", "Demo blueprint generation complete!");
+ return;
+ }
+
+ getJob(jobId).then(setJob).catch(() => {});
+ addToast("success", "Blueprint generation complete!");
+ }, [stream.jobStatus, jobId, isDemo, addToast]);
+
+ // Notify on failure
+ useEffect(() => {
+ if (stream.jobStatus === "failed") {
+ addToast("error", stream.jobError ?? "Job failed");
+ }
+ }, [stream.jobStatus, stream.jobError, addToast]);
+
+ if (!jobId) {
+ return (
+
+ Invalid job ID
+
+ โ Back to home
+
+
+ );
+ }
+
+ if (loadError) {
+ return (
+
+
+
+ Failed to load job
+
+
{loadError}
+
+ โ Back to home
+
+
+
+ );
+ }
+
+ return (
+
+ {/* Header */}
+
+
+
+ โ Back to home
+
+
+ {job?.projectName ?? "Loading..."}
+
+
+ Job {jobId.slice(0, 8)}...
+
+
+
+
+ {stream.connected && (
+
+
+ Live
+
+ )}
+
+
+
+
+ {/* Two-column layout on large screens */}
+
+ {/* Agent Timeline */}
+
+
+ {/* Blueprint */}
+ {stream.jobStatus === "completed" && job?.blueprint && (
+
+
+ Generated Blueprint
+
+
+
+ )}
+
+
+ {/* Error state */}
+ {stream.jobStatus === "failed" && (
+
+
+ Generation Failed
+
+
+ {stream.jobError ?? "An unexpected error occurred"}
+
+
+
+ )}
+
+ {/* Loading indicator when still running */}
+ {(stream.jobStatus === "queued" || stream.jobStatus === "running") && !job && (
+
+
+
+ )}
+
+ );
+}
+
+function PageShell({ children }: { children: React.ReactNode }) {
+ return (
+
+ {children}
+
+ );
+}
+
+function StatusPill({ status }: { status: string }) {
+ const config: Record = {
+ queued: { bg: "rgba(92, 92, 111, 0.15)", color: "#9898a8" },
+ running: { bg: "rgba(56, 189, 248, 0.12)", color: "#38bdf8" },
+ completed: { bg: "rgba(52, 211, 153, 0.12)", color: "#34d399" },
+ failed: { bg: "rgba(244, 63, 94, 0.12)", color: "#f43f5e" },
+ };
+ const c = config[status] ?? config["queued"]!;
+
+ return (
+
+ {status}
+
+ );
+}
diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json
new file mode 100644
index 0000000..2a31efe
--- /dev/null
+++ b/apps/web/tsconfig.json
@@ -0,0 +1,23 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
+ "strict": true,
+ "jsx": "react-jsx",
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ "noUncheckedIndexedAccess": true,
+ "noImplicitOverride": true,
+ "declaration": true,
+ "declarationMap": true,
+ "sourceMap": true,
+ "outDir": "dist",
+ "rootDir": "src"
+ },
+ "include": ["src", "vite-env.d.ts"],
+ "references": [
+ { "path": "../../packages/ui" }
+ ]
+}
diff --git a/apps/web/vite-env.d.ts b/apps/web/vite-env.d.ts
new file mode 100644
index 0000000..11f02fe
--- /dev/null
+++ b/apps/web/vite-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts
new file mode 100644
index 0000000..4b9bdda
--- /dev/null
+++ b/apps/web/vite.config.ts
@@ -0,0 +1,16 @@
+import { defineConfig } from "vite";
+import react from "@vitejs/plugin-react";
+import tailwindcss from "@tailwindcss/vite";
+
+export default defineConfig({
+ plugins: [react(), tailwindcss()],
+ server: {
+ port: 5173,
+ proxy: {
+ "/api": {
+ target: "http://localhost:3001",
+ changeOrigin: true,
+ },
+ },
+ },
+});
diff --git a/bun.lock b/bun.lock
index 28b0d63..c365814 100644
--- a/bun.lock
+++ b/bun.lock
@@ -30,7 +30,19 @@
"name": "@stackforge/web",
"version": "0.1.0",
"dependencies": {
- "@stackforge/shared": "workspace:*",
+ "@stackforge/ui": "workspace:*",
+ "react": "^19.0.0",
+ "react-dom": "^19.0.0",
+ "react-router-dom": "^7.1.0",
+ },
+ "devDependencies": {
+ "@tailwindcss/vite": "^4.1.0",
+ "@types/react": "^19.0.0",
+ "@types/react-dom": "^19.0.0",
+ "@vitejs/plugin-react": "^4.3.0",
+ "tailwindcss": "^4.1.0",
+ "typescript": "^5.7.3",
+ "vite": "^6.2.0",
},
},
"packages/agents": {
@@ -62,9 +74,170 @@
"packages/ui": {
"name": "@stackforge/ui",
"version": "0.1.0",
+ "devDependencies": {
+ "@types/react": "^19.0.0",
+ "@types/react-dom": "^19.0.0",
+ "typescript": "^5.7.3",
+ },
+ "peerDependencies": {
+ "react": "^19.0.0",
+ "react-dom": "^19.0.0",
+ },
},
},
"packages": {
+ "@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="],
+
+ "@babel/compat-data": ["@babel/compat-data@7.29.0", "", {}, "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg=="],
+
+ "@babel/core": ["@babel/core@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA=="],
+
+ "@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="],
+
+ "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="],
+
+ "@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="],
+
+ "@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="],
+
+ "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="],
+
+ "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="],
+
+ "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
+
+ "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
+
+ "@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="],
+
+ "@babel/helpers": ["@babel/helpers@7.29.2", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.29.0" } }, "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw=="],
+
+ "@babel/parser": ["@babel/parser@7.29.2", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA=="],
+
+ "@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw=="],
+
+ "@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="],
+
+ "@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="],
+
+ "@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="],
+
+ "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
+
+ "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="],
+
+ "@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="],
+
+ "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="],
+
+ "@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="],
+
+ "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="],
+
+ "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="],
+
+ "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="],
+
+ "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="],
+
+ "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="],
+
+ "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="],
+
+ "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="],
+
+ "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="],
+
+ "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="],
+
+ "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="],
+
+ "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="],
+
+ "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="],
+
+ "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="],
+
+ "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="],
+
+ "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="],
+
+ "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="],
+
+ "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="],
+
+ "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="],
+
+ "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="],
+
+ "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="],
+
+ "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="],
+
+ "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="],
+
+ "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
+
+ "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
+
+ "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
+
+ "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
+
+ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
+
+ "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="],
+
+ "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.60.1", "", { "os": "android", "cpu": "arm" }, "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA=="],
+
+ "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.60.1", "", { "os": "android", "cpu": "arm64" }, "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA=="],
+
+ "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.60.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw=="],
+
+ "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.60.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew=="],
+
+ "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.60.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w=="],
+
+ "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.60.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g=="],
+
+ "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.60.1", "", { "os": "linux", "cpu": "arm" }, "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g=="],
+
+ "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.60.1", "", { "os": "linux", "cpu": "arm" }, "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg=="],
+
+ "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.60.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ=="],
+
+ "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.60.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA=="],
+
+ "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.60.1", "", { "os": "linux", "cpu": "none" }, "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ=="],
+
+ "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.60.1", "", { "os": "linux", "cpu": "none" }, "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw=="],
+
+ "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.60.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw=="],
+
+ "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.60.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg=="],
+
+ "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.60.1", "", { "os": "linux", "cpu": "none" }, "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg=="],
+
+ "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.60.1", "", { "os": "linux", "cpu": "none" }, "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg=="],
+
+ "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.60.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ=="],
+
+ "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.60.1", "", { "os": "linux", "cpu": "x64" }, "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg=="],
+
+ "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.60.1", "", { "os": "linux", "cpu": "x64" }, "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w=="],
+
+ "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.60.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw=="],
+
+ "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.60.1", "", { "os": "none", "cpu": "arm64" }, "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA=="],
+
+ "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.60.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g=="],
+
+ "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.60.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg=="],
+
+ "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.60.1", "", { "os": "win32", "cpu": "x64" }, "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg=="],
+
+ "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.60.1", "", { "os": "win32", "cpu": "x64" }, "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ=="],
+
"@stackforge/agents": ["@stackforge/agents@workspace:packages/agents"],
"@stackforge/api": ["@stackforge/api@workspace:apps/api"],
@@ -77,6 +250,36 @@
"@stackforge/web": ["@stackforge/web@workspace:apps/web"],
+ "@tailwindcss/node": ["@tailwindcss/node@4.2.2", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.2.2" } }, "sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA=="],
+
+ "@tailwindcss/oxide": ["@tailwindcss/oxide@4.2.2", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.2.2", "@tailwindcss/oxide-darwin-arm64": "4.2.2", "@tailwindcss/oxide-darwin-x64": "4.2.2", "@tailwindcss/oxide-freebsd-x64": "4.2.2", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.2", "@tailwindcss/oxide-linux-arm64-gnu": "4.2.2", "@tailwindcss/oxide-linux-arm64-musl": "4.2.2", "@tailwindcss/oxide-linux-x64-gnu": "4.2.2", "@tailwindcss/oxide-linux-x64-musl": "4.2.2", "@tailwindcss/oxide-wasm32-wasi": "4.2.2", "@tailwindcss/oxide-win32-arm64-msvc": "4.2.2", "@tailwindcss/oxide-win32-x64-msvc": "4.2.2" } }, "sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg=="],
+
+ "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.2.2", "", { "os": "android", "cpu": "arm64" }, "sha512-dXGR1n+P3B6748jZO/SvHZq7qBOqqzQ+yFrXpoOWWALWndF9MoSKAT3Q0fYgAzYzGhxNYOoysRvYlpixRBBoDg=="],
+
+ "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.2.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-iq9Qjr6knfMpZHj55/37ouZeykwbDqF21gPFtfnhCCKGDcPI/21FKC9XdMO/XyBM7qKORx6UIhGgg6jLl7BZlg=="],
+
+ "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.2.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-BlR+2c3nzc8f2G639LpL89YY4bdcIdUmiOOkv2GQv4/4M0vJlpXEa0JXNHhCHU7VWOKWT/CjqHdTP8aUuDJkuw=="],
+
+ "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.2.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ=="],
+
+ "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.2.2", "", { "os": "linux", "cpu": "arm" }, "sha512-FPdhvsW6g06T9BWT0qTwiVZYE2WIFo2dY5aCSpjG/S/u1tby+wXoslXS0kl3/KXnULlLr1E3NPRRw0g7t2kgaQ=="],
+
+ "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.2.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw=="],
+
+ "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.2.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag=="],
+
+ "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.2.2", "", { "os": "linux", "cpu": "x64" }, "sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg=="],
+
+ "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.2.2", "", { "os": "linux", "cpu": "x64" }, "sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ=="],
+
+ "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.2.2", "", { "dependencies": { "@emnapi/core": "^1.8.1", "@emnapi/runtime": "^1.8.1", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.1.1", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q=="],
+
+ "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.2.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ=="],
+
+ "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.2.2", "", { "os": "win32", "cpu": "x64" }, "sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA=="],
+
+ "@tailwindcss/vite": ["@tailwindcss/vite@4.2.2", "", { "dependencies": { "@tailwindcss/node": "4.2.2", "@tailwindcss/oxide": "4.2.2", "tailwindcss": "4.2.2" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, "sha512-mEiF5HO1QqCLXoNEfXVA1Tzo+cYsrqV7w9Juj2wdUFyW07JRenqMG225MvPwr3ZD9N1bFQj46X7r33iHxLUW0w=="],
+
"@turbo/darwin-64": ["@turbo/darwin-64@2.8.20", "", { "os": "darwin", "cpu": "x64" }, "sha512-FQ9EX1xMU5nbwjxXxM3yU88AQQ6Sqc6S44exPRroMcx9XZHqqppl5ymJF0Ig/z3nvQNwDmz1Gsnvxubo+nXWjQ=="],
"@turbo/darwin-arm64": ["@turbo/darwin-arm64@2.8.20", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Gpyh9ATFGThD6/s9L95YWY54cizg/VRWl2B67h0yofG8BpHf67DFAh9nuJVKG7bY0+SBJDAo5cMur+wOl9YOYw=="],
@@ -89,12 +292,22 @@
"@turbo/windows-arm64": ["@turbo/windows-arm64@2.8.20", "", { "os": "win32", "cpu": "arm64" }, "sha512-voicVULvUV5yaGXo0Iue13BcHGYW3u0VgqSbfQwBaHbpj1zLjYV4KIe+7fYIo6DO8FVUJzxFps3ODCQG/Wy2Qw=="],
+ "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="],
+
+ "@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="],
+
+ "@types/babel__template": ["@types/babel__template@7.4.4", "", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A=="],
+
+ "@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="],
+
"@types/body-parser": ["@types/body-parser@1.19.6", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g=="],
"@types/bun": ["@types/bun@1.3.11", "", { "dependencies": { "bun-types": "1.3.11" } }, "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg=="],
"@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="],
+ "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
+
"@types/express": ["@types/express@4.17.25", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^4.17.33", "@types/qs": "*", "@types/serve-static": "^1" } }, "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw=="],
"@types/express-serve-static-core": ["@types/express-serve-static-core@4.19.8", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA=="],
@@ -109,16 +322,26 @@
"@types/range-parser": ["@types/range-parser@1.2.7", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="],
+ "@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="],
+
+ "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
+
"@types/send": ["@types/send@1.2.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ=="],
"@types/serve-static": ["@types/serve-static@1.15.10", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*", "@types/send": "<1" } }, "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw=="],
+ "@vitejs/plugin-react": ["@vitejs/plugin-react@4.7.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.27", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA=="],
+
"accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="],
"array-flatten": ["array-flatten@1.1.1", "", {}, "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="],
+ "baseline-browser-mapping": ["baseline-browser-mapping@2.10.13", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-BL2sTuHOdy0YT1lYieUxTw/QMtPBC3pmlJC6xk8BBYVv6vcw3SGdKemQ+Xsx9ik2F/lYDO9tqsFQH1r9PFuHKw=="],
+
"body-parser": ["body-parser@1.20.4", "", { "dependencies": { "bytes": "~3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "~1.2.0", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "on-finished": "~2.4.1", "qs": "~6.14.0", "raw-body": "~2.5.3", "type-is": "~1.6.18", "unpipe": "~1.0.0" } }, "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA=="],
+ "browserslist": ["browserslist@4.28.2", "", { "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", "electron-to-chromium": "^1.5.328", "node-releases": "^2.0.36", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg=="],
+
"bun-types": ["bun-types@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="],
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
@@ -127,54 +350,78 @@
"call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
+ "caniuse-lite": ["caniuse-lite@1.0.30001784", "", {}, "sha512-WU346nBTklUV9YfUl60fqRbU5ZqyXlqvo1SgigE1OAXK5bFL8LL9q1K7aap3N739l4BvNqnkm3YrGHiY9sfUQw=="],
+
"content-disposition": ["content-disposition@0.5.4", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ=="],
"content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
+ "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
+
"cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
"cookie-signature": ["cookie-signature@1.0.7", "", {}, "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA=="],
+ "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
+
"debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
"depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
"destroy": ["destroy@1.2.0", "", {}, "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg=="],
+ "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
+
"dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="],
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
+ "electron-to-chromium": ["electron-to-chromium@1.5.330", "", {}, "sha512-jFNydB5kFtYUobh4IkWUnXeyDbjf/r9gcUEXe1xcrcUxIGfTdzPXA+ld6zBRbwvgIGVzDll/LTIiDztEtckSnA=="],
+
"encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
+ "enhanced-resolve": ["enhanced-resolve@5.20.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA=="],
+
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
+ "esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="],
+
+ "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
+
"escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
"etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="],
"express": ["express@4.22.1", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "~1.20.3", "content-disposition": "~0.5.4", "content-type": "~1.0.4", "cookie": "~0.7.1", "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "~1.3.1", "fresh": "~0.5.2", "http-errors": "~2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", "qs": "~6.14.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "~0.19.0", "serve-static": "~1.16.2", "setprototypeof": "1.2.0", "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g=="],
+ "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
+
"finalhandler": ["finalhandler@1.3.2", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "statuses": "~2.0.2", "unpipe": "~1.0.0" } }, "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg=="],
"forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
"fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="],
+ "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
+
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
+ "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
+
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
+ "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
+
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
@@ -189,6 +436,42 @@
"ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
+ "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
+
+ "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
+
+ "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
+
+ "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
+
+ "lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
+
+ "lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="],
+
+ "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="],
+
+ "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="],
+
+ "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="],
+
+ "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="],
+
+ "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="],
+
+ "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="],
+
+ "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="],
+
+ "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="],
+
+ "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="],
+
+ "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="],
+
+ "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
+
+ "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
+
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
"media-typer": ["media-typer@0.3.0", "", {}, "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="],
@@ -205,8 +488,12 @@
"ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
+ "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
+
"negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="],
+ "node-releases": ["node-releases@2.0.36", "", {}, "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA=="],
+
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
"on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
@@ -215,6 +502,12 @@
"path-to-regexp": ["path-to-regexp@0.1.12", "", {}, "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ=="],
+ "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
+
+ "picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
+
+ "postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="],
+
"proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
"qs": ["qs@6.14.2", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q=="],
@@ -223,14 +516,32 @@
"raw-body": ["raw-body@2.5.3", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "unpipe": "~1.0.0" } }, "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA=="],
+ "react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="],
+
+ "react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="],
+
+ "react-refresh": ["react-refresh@0.17.0", "", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="],
+
+ "react-router": ["react-router@7.13.2", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-tX1Aee+ArlKQP+NIUd7SE6Li+CiGKwQtbS+FfRxPX6Pe4vHOo6nr9d++u5cwg+Z8K/x8tP+7qLmujDtfrAoUJA=="],
+
+ "react-router-dom": ["react-router-dom@7.13.2", "", { "dependencies": { "react-router": "7.13.2" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" } }, "sha512-aR7SUORwTqAW0JDeiWF07e9SBE9qGpByR9I8kJT5h/FrBKxPMS6TiC7rmVO+gC0q52Bx7JnjWe8Z1sR9faN4YA=="],
+
+ "rollup": ["rollup@4.60.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.60.1", "@rollup/rollup-android-arm64": "4.60.1", "@rollup/rollup-darwin-arm64": "4.60.1", "@rollup/rollup-darwin-x64": "4.60.1", "@rollup/rollup-freebsd-arm64": "4.60.1", "@rollup/rollup-freebsd-x64": "4.60.1", "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", "@rollup/rollup-linux-arm-musleabihf": "4.60.1", "@rollup/rollup-linux-arm64-gnu": "4.60.1", "@rollup/rollup-linux-arm64-musl": "4.60.1", "@rollup/rollup-linux-loong64-gnu": "4.60.1", "@rollup/rollup-linux-loong64-musl": "4.60.1", "@rollup/rollup-linux-ppc64-gnu": "4.60.1", "@rollup/rollup-linux-ppc64-musl": "4.60.1", "@rollup/rollup-linux-riscv64-gnu": "4.60.1", "@rollup/rollup-linux-riscv64-musl": "4.60.1", "@rollup/rollup-linux-s390x-gnu": "4.60.1", "@rollup/rollup-linux-x64-gnu": "4.60.1", "@rollup/rollup-linux-x64-musl": "4.60.1", "@rollup/rollup-openbsd-x64": "4.60.1", "@rollup/rollup-openharmony-arm64": "4.60.1", "@rollup/rollup-win32-arm64-msvc": "4.60.1", "@rollup/rollup-win32-ia32-msvc": "4.60.1", "@rollup/rollup-win32-x64-gnu": "4.60.1", "@rollup/rollup-win32-x64-msvc": "4.60.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w=="],
+
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
+ "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
+
+ "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
+
"send": ["send@0.19.2", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "~0.5.2", "http-errors": "~2.0.1", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "~2.4.1", "range-parser": "~1.2.1", "statuses": "~2.0.2" } }, "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg=="],
"serve-static": ["serve-static@1.16.3", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "~0.19.1" } }, "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA=="],
+ "set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="],
+
"setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
"side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="],
@@ -241,8 +552,16 @@
"side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
+ "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
+
"statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
+ "tailwindcss": ["tailwindcss@4.2.2", "", {}, "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q=="],
+
+ "tapable": ["tapable@2.3.2", "", {}, "sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA=="],
+
+ "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
+
"toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
"turbo": ["turbo@2.8.20", "", { "optionalDependencies": { "@turbo/darwin-64": "2.8.20", "@turbo/darwin-arm64": "2.8.20", "@turbo/linux-64": "2.8.20", "@turbo/linux-arm64": "2.8.20", "@turbo/windows-64": "2.8.20", "@turbo/windows-arm64": "2.8.20" }, "bin": { "turbo": "bin/turbo" } }, "sha512-Rb4qk5YT8RUwwdXtkLpkVhNEe/lor6+WV7S5tTlLpxSz6MjV5Qi8jGNn4gS6NAvrYGA/rNrE6YUQM85sCZUDbQ=="],
@@ -255,14 +574,42 @@
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
+ "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="],
+
"utils-merge": ["utils-merge@1.0.1", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="],
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
+ "vite": ["vite@6.4.1", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g=="],
+
+ "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
+
"zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
+ "@babel/core/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
+
+ "@babel/traverse/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
+
+ "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.9.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA=="],
+
+ "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.9.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA=="],
+
+ "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg=="],
+
+ "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.2", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" }, "bundled": true }, "sha512-sNXv5oLJ7ob93xkZ1XnxisYhGYXfaG9f65/ZgYuAu3qt7b3NadcOEhLvx28hv31PgX8SZJRYrAIPQilQmFpLVw=="],
+
+ "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
+
+ "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
+
"@types/serve-static/@types/send": ["@types/send@0.17.6", "", { "dependencies": { "@types/mime": "^1", "@types/node": "*" } }, "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og=="],
+ "react-router/cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
+
"send/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
+
+ "@babel/core/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
+
+ "@babel/traverse/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
}
}
diff --git a/packages/ui/package.json b/packages/ui/package.json
index 6f0c1eb..59b4e15 100644
--- a/packages/ui/package.json
+++ b/packages/ui/package.json
@@ -3,9 +3,26 @@
"version": "0.1.0",
"private": true,
"type": "module",
+ "main": "./src/index.ts",
+ "types": "./src/index.ts",
+ "exports": {
+ ".": {
+ "import": "./src/index.ts",
+ "types": "./src/index.ts"
+ }
+ },
"scripts": {
- "dev": "echo 'ui: not implemented yet'",
- "build": "echo 'ui: not implemented yet'",
- "lint": "echo 'ui: not implemented yet'"
+ "dev": "tsc --watch",
+ "build": "tsc",
+ "typecheck": "tsc --noEmit"
+ },
+ "peerDependencies": {
+ "react": "^19.0.0",
+ "react-dom": "^19.0.0"
+ },
+ "devDependencies": {
+ "@types/react": "^19.0.0",
+ "@types/react-dom": "^19.0.0",
+ "typescript": "^5.7.3"
}
}
diff --git a/packages/ui/src/Badge.tsx b/packages/ui/src/Badge.tsx
new file mode 100644
index 0000000..01b4bd4
--- /dev/null
+++ b/packages/ui/src/Badge.tsx
@@ -0,0 +1,54 @@
+import React from "react";
+
+export type BadgeStatus = "waiting" | "running" | "completed" | "failed";
+
+interface BadgeProps {
+ status: BadgeStatus;
+ label?: string;
+}
+
+const statusConfig: Record = {
+ waiting: { bg: "rgba(92, 92, 111, 0.15)", color: "#9898a8", borderColor: "rgba(92, 92, 111, 0.3)", label: "Waiting" },
+ running: { bg: "rgba(56, 189, 248, 0.12)", color: "#38bdf8", borderColor: "rgba(56, 189, 248, 0.3)", label: "Running" },
+ completed: { bg: "rgba(52, 211, 153, 0.12)", color: "#34d399", borderColor: "rgba(52, 211, 153, 0.3)", label: "Completed" },
+ failed: { bg: "rgba(244, 63, 94, 0.12)", color: "#f43f5e", borderColor: "rgba(244, 63, 94, 0.3)", label: "Failed" },
+};
+
+export function Badge({ status, label }: BadgeProps) {
+ const config = statusConfig[status];
+ return (
+
+ {status === "running" && (
+
+ )}
+ {status === "completed" && โ}
+ {status === "failed" && โ}
+ {label ?? config.label}
+
+ );
+}
diff --git a/packages/ui/src/Button.tsx b/packages/ui/src/Button.tsx
new file mode 100644
index 0000000..28dd184
--- /dev/null
+++ b/packages/ui/src/Button.tsx
@@ -0,0 +1,87 @@
+import React from "react";
+
+type ButtonVariant = "primary" | "secondary" | "ghost";
+type ButtonSize = "sm" | "md" | "lg";
+
+interface ButtonProps extends React.ButtonHTMLAttributes {
+ variant?: ButtonVariant;
+ size?: ButtonSize;
+ loading?: boolean;
+ children: React.ReactNode;
+}
+
+const variantStyles: Record = {
+ primary: {
+ background: "linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)",
+ color: "#fff",
+ border: "1px solid rgba(99, 102, 241, 0.3)",
+ boxShadow: "0 2px 12px rgba(99, 102, 241, 0.25)",
+ },
+ secondary: {
+ background: "#1a1a26",
+ color: "#f0f0f5",
+ border: "1px solid #23232f",
+ boxShadow: "none",
+ },
+ ghost: {
+ background: "transparent",
+ color: "#9898a8",
+ border: "1px solid transparent",
+ boxShadow: "none",
+ },
+};
+
+const sizeStyles: Record = {
+ sm: { padding: "6px 14px", fontSize: "13px", borderRadius: "6px" },
+ md: { padding: "10px 22px", fontSize: "14px", borderRadius: "10px" },
+ lg: { padding: "14px 32px", fontSize: "16px", borderRadius: "12px" },
+};
+
+export function Button({
+ variant = "primary",
+ size = "md",
+ loading = false,
+ disabled,
+ children,
+ style,
+ ...props
+}: ButtonProps) {
+ const isDisabled = disabled || loading;
+
+ return (
+
+ );
+}
diff --git a/packages/ui/src/Card.tsx b/packages/ui/src/Card.tsx
new file mode 100644
index 0000000..58eeb0a
--- /dev/null
+++ b/packages/ui/src/Card.tsx
@@ -0,0 +1,37 @@
+import React from "react";
+
+interface CardProps {
+ children: React.ReactNode;
+ hover?: boolean;
+ glow?: boolean;
+ style?: React.CSSProperties;
+ className?: string;
+}
+
+export function Card({ children, hover = true, glow = false, style, className }: CardProps) {
+ const [isHovered, setIsHovered] = React.useState(false);
+
+ return (
+ setIsHovered(true) : undefined}
+ onMouseLeave={hover ? () => setIsHovered(false) : undefined}
+ style={{
+ background: isHovered ? "#1c1c28" : "#16161f",
+ border: "1px solid",
+ borderColor: isHovered && glow ? "rgba(99, 102, 241, 0.3)" : "#23232f",
+ borderRadius: "16px",
+ padding: "24px",
+ transition: "all 250ms cubic-bezier(0.4, 0, 0.2, 1)",
+ boxShadow: isHovered
+ ? glow
+ ? "0 4px 16px rgba(99, 102, 241, 0.15), 0 8px 32px rgba(0, 0, 0, 0.3)"
+ : "0 4px 16px rgba(0, 0, 0, 0.3)"
+ : "0 1px 3px rgba(0, 0, 0, 0.3), 0 4px 12px rgba(0, 0, 0, 0.2)",
+ ...style,
+ }}
+ >
+ {children}
+
+ );
+}
diff --git a/packages/ui/src/Spinner.tsx b/packages/ui/src/Spinner.tsx
new file mode 100644
index 0000000..2bd8ef8
--- /dev/null
+++ b/packages/ui/src/Spinner.tsx
@@ -0,0 +1,25 @@
+import React from "react";
+
+interface SpinnerProps {
+ size?: number;
+ color?: string;
+}
+
+export function Spinner({ size = 20, color = "#6366f1" }: SpinnerProps) {
+ return (
+
+ );
+}
diff --git a/packages/ui/src/Toast.tsx b/packages/ui/src/Toast.tsx
new file mode 100644
index 0000000..42e0c94
--- /dev/null
+++ b/packages/ui/src/Toast.tsx
@@ -0,0 +1,109 @@
+import React, { createContext, useContext, useState, useCallback } from "react";
+
+type ToastType = "success" | "error" | "info";
+
+interface ToastItem {
+ id: number;
+ type: ToastType;
+ message: string;
+}
+
+interface ToastContextValue {
+ addToast: (type: ToastType, message: string) => void;
+}
+
+const ToastContext = createContext(null);
+
+export function useToast(): ToastContextValue {
+ const ctx = useContext(ToastContext);
+ if (!ctx) throw new Error("useToast must be used within ");
+ return ctx;
+}
+
+let nextId = 0;
+
+const typeStyles: Record = {
+ success: { bg: "rgba(52, 211, 153, 0.12)", border: "rgba(52, 211, 153, 0.3)", icon: "โ" },
+ error: { bg: "rgba(244, 63, 94, 0.12)", border: "rgba(244, 63, 94, 0.3)", icon: "โ" },
+ info: { bg: "rgba(56, 189, 248, 0.12)", border: "rgba(56, 189, 248, 0.3)", icon: "โน" },
+};
+
+const typeColor: Record = {
+ success: "#34d399",
+ error: "#f43f5e",
+ info: "#38bdf8",
+};
+
+export function ToastProvider({ children }: { children: React.ReactNode }) {
+ const [toasts, setToasts] = useState([]);
+
+ const addToast = useCallback((type: ToastType, message: string) => {
+ const id = ++nextId;
+ setToasts((prev) => [...prev, { id, type, message }]);
+ setTimeout(() => {
+ setToasts((prev) => prev.filter((t) => t.id !== id));
+ }, 4000);
+ }, []);
+
+ return (
+
+ {children}
+
+ {toasts.map((toast) => {
+ const s = typeStyles[toast.type];
+ return (
+
+
+ {s.icon}
+
+ {toast.message}
+
+ );
+ })}
+
+
+ );
+}
diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts
new file mode 100644
index 0000000..1cf32fa
--- /dev/null
+++ b/packages/ui/src/index.ts
@@ -0,0 +1,6 @@
+export { Button } from "./Button";
+export { Badge } from "./Badge";
+export type { BadgeStatus } from "./Badge";
+export { Card } from "./Card";
+export { Spinner } from "./Spinner";
+export { ToastProvider, useToast } from "./Toast";
diff --git a/packages/ui/tsconfig.json b/packages/ui/tsconfig.json
new file mode 100644
index 0000000..c4fabb8
--- /dev/null
+++ b/packages/ui/tsconfig.json
@@ -0,0 +1,21 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
+ "strict": true,
+ "jsx": "react-jsx",
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ "noUncheckedIndexedAccess": true,
+ "noImplicitOverride": true,
+ "declaration": true,
+ "declarationMap": true,
+ "sourceMap": true,
+ "outDir": "dist",
+ "rootDir": "src",
+ "composite": true
+ },
+ "include": ["src"]
+}