Unified · Split
| @@ -4,6 +4,9 @@ · 3 unmodified lines | ||
| 4 | 4 | <meta charset="UTF-8" /> |
| 5 | 5 | <meta name="viewport" content="width=device-width, initial-scale=1.0" /> |
| 6 | 6 | <title>Executor</title> |
| 7 | + <link rel="preconnect" href="https://fonts.googleapis.com" /> | |
| 8 | + <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> | |
| 9 | + <link href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,300..700;1,9..40,300..700&family=Instrument+Serif&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" /> | |
| 7 | 10 | </head> |
| 8 | 11 | <body> |
| 9 | 12 | <div id="root"></div> |
| @@ -5,6 +5,7 @@ · 4 unmodified lines | ||
| 5 | 5 | "type": "module", |
| 6 | 6 | "dependencies": { |
| 7 | 7 | "@executor/plugin-openapi": "workspace:*", |
| 8 | + "tailwindcss": "^4.1.10", | |
| 8 | 9 | "@executor/react": "workspace:*", |
| 9 | 10 | "@executor/server": "workspace:*", |
| 10 | 11 | "@tanstack/react-router": "^1.168.10", |
| @@ -15,6 +16,7 @@ · 4 unmodified lines | ||
| 15 | 16 | "@types/react": "^19.1.0", |
| 16 | 17 | "@types/react-dom": "^19.1.0", |
| 17 | 18 | "@types/node": "catalog:", |
| 19 | + "@tailwindcss/vite": "^4.1.10", | |
| 18 | 20 | "@vitejs/plugin-react": "^4.5.2", |
| 19 | 21 | "typescript": "^5.9.3", |
| 20 | 22 | "vite": "^6.3.5" |
| @@ -1,5 +1,6 @@ | ||
| 1 | 1 | import React from "react"; |
| 2 | 2 | import { createRoot } from "react-dom/client"; |
| 3 | +import "./styles.css"; | |
| 3 | 4 | import { App } from "./App"; |
| 4 | 5 | |
| 5 | 6 | const root = createRoot(document.getElementById("root")!); |
| @@ -3,25 +3,45 @@ import { useAtomValue, Result, secretsAtom } from "@executor/react"; · 2 unmodified lines | ||
| 3 | 3 | export function SecretsPage() { |
| 4 | 4 | const secrets = useAtomValue(secretsAtom()); |
| 5 | 5 | |
| 6 | − return Result.match(secrets, { | |
| 7 | − onInitial: () => <p>Loading secrets…</p>, | |
| 8 | − onFailure: () => <p style={{ color: "red" }}>Failed to load secrets</p>, | |
| 9 | − onSuccess: ({ value }) => ( | |
| 10 | − <div> | |
| 11 | − <h2>Secrets ({value.length})</h2> | |
| 12 | − {value.length === 0 ? ( | |
| 13 | − <p style={{ color: "#888" }}>No secrets stored yet.</p> | |
| 14 | − ) : ( | |
| 15 | − <ul> | |
| 16 | − {value.map((s) => ( | |
| 17 | − <li key={s.id}> | |
| 18 | − <strong>{s.name}</strong> | |
| 19 | − {s.purpose && <span> — {s.purpose}</span>} | |
| 20 | − </li> | |
| 21 | − ))} | |
| 22 | − </ul> | |
| 23 | − )} | |
| 24 | − </div> | |
| 25 | − ), | |
| 26 | − }); | |
| 6 | + return ( | |
| 7 | + <div> | |
| 8 | + <h1 className="text-xl font-semibold text-foreground">Secrets</h1> | |
| 9 | + | |
| 10 | + {Result.match(secrets, { | |
| 11 | + onInitial: () => ( | |
| 12 | + <p className="mt-4 text-sm text-muted-foreground">Loading secrets…</p> | |
| 13 | + ), | |
| 14 | + onFailure: () => ( | |
| 15 | + <p className="mt-4 text-sm text-destructive">Failed to load secrets</p> | |
| 16 | + ), | |
| 17 | + onSuccess: ({ value }) => | |
| 18 | + value.length === 0 ? ( | |
| 19 | + <p className="mt-4 text-sm text-muted-foreground"> | |
| 20 | + No secrets stored yet. Secrets are created when you configure authentication for a source. | |
| 21 | + </p> | |
| 22 | + ) : ( | |
| 23 | + <div className="mt-4 grid gap-2"> | |
| 24 | + {value.map((s) => ( | |
| 25 | + <div | |
| 26 | + key={s.id} | |
| 27 | + className="flex items-center justify-between rounded-lg border border-border bg-card p-3" | |
| 28 | + > | |
| 29 | + <div className="min-w-0"> | |
| 30 | + <p className="text-sm font-medium text-card-foreground">{s.name}</p> | |
| 31 | + {s.purpose && ( | |
| 32 | + <p className="mt-0.5 text-xs text-muted-foreground">{s.purpose}</p> | |
| 33 | + )} | |
| 34 | + </div> | |
| 35 | + {s.provider && ( | |
| 36 | + <span className="rounded bg-secondary px-1.5 py-0.5 text-[10px] font-medium text-secondary-foreground"> | |
| 37 | + {s.provider} | |
| 38 | + </span> | |
| 39 | + )} | |
| 40 | + </div> | |
| 41 | + ))} | |
| 42 | + </div> | |
| 43 | + ), | |
| 44 | + })} | |
| 45 | + </div> | |
| 46 | + ); | |
| 27 | 47 | } |
| @@ -18,12 +18,10 @@ export function SourcesPage() { · 17 unmodified lines | ||
| 18 | 18 | const tools = useAtomValue(toolsAtom()); |
| 19 | 19 | const refreshTools = useAtomRefresh(toolsAtom()); |
| 20 | 20 | |
| 21 | − // Group tools by namespace (second tag after "openapi") | |
| 22 | 21 | const sources = useMemo(() => { |
| 23 | 22 | if (tools._tag !== "Success") return []; |
| 24 | 23 | const namespaces = new Map<string, number>(); |
| 25 | 24 | for (const tool of tools.value) { |
| 26 | − // Tools are tagged ["openapi", namespace, ...] | |
| 27 | 25 | const ns = tool.tags.find( |
| 28 | 26 | (t) => t !== "openapi" && !sourcePlugins.some((p) => p.key === t), |
| 29 | 27 | ); |
| @@ -44,7 +42,7 @@ export function SourcesPage() { · 14 unmodified lines | ||
| 44 | 42 | if (plugin) { |
| 45 | 43 | const AddComponent = plugin.add; |
| 46 | 44 | return ( |
| 47 | − <Suspense fallback={<p>Loading…</p>}> | |
| 45 | + <Suspense fallback={<p className="text-sm text-muted-foreground">Loading…</p>}> | |
| 48 | 46 | <AddComponent |
| 49 | 47 | onComplete={() => { |
| 50 | 48 | setAdding(null); |
| @@ -58,35 +56,65 @@ export function SourcesPage() { · 7 unmodified lines | ||
| 58 | 56 | |
| 59 | 57 | return ( |
| 60 | 58 | <div> |
| 61 | − <h2>Sources</h2> | |
| 59 | + <div className="flex items-center justify-between"> | |
| 60 | + <h1 className="text-xl font-semibold text-foreground">Sources</h1> | |
| 61 | + <div className="flex gap-2"> | |
| 62 | + {sourcePlugins.map((p) => ( | |
| 63 | + <button | |
| 64 | + key={p.key} | |
| 65 | + onClick={() => setAdding(p.key)} | |
| 66 | + className="inline-flex items-center gap-1.5 rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90" | |
| 67 | + > | |
| 68 | + <svg viewBox="0 0 16 16" fill="none" className="size-3.5"> | |
| 69 | + <path d="M8 3v10M3 8h10" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" /> | |
| 70 | + </svg> | |
| 71 | + Add {p.label} | |
| 72 | + </button> | |
| 73 | + ))} | |
| 74 | + </div> | |
| 75 | + </div> | |
| 62 | 76 | |
| 63 | 77 | {Result.match(tools, { |
| 64 | − onInitial: () => <p>Loading…</p>, | |
| 65 | − onFailure: () => <p style={{ color: "red" }}>Failed to load sources</p>, | |
| 78 | + onInitial: () => ( | |
| 79 | + <p className="mt-4 text-sm text-muted-foreground">Loading…</p> | |
| 80 | + ), | |
| 81 | + onFailure: () => ( | |
| 82 | + <p className="mt-4 text-sm text-destructive">Failed to load sources</p> | |
| 83 | + ), | |
| 66 | 84 | onSuccess: () => |
| 67 | 85 | sources.length === 0 ? ( |
| 68 | − <p style={{ color: "#888" }}>No sources configured yet.</p> | |
| 86 | + <div className="mt-8 flex flex-col items-center justify-center text-center"> | |
| 87 | + <div className="rounded-full bg-muted p-4"> | |
| 88 | + <svg viewBox="0 0 24 24" fill="none" className="size-8 text-muted-foreground/50"> | |
| 89 | + <path d="M12 6v12M6 12h12" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" /> | |
| 90 | + </svg> | |
| 91 | + </div> | |
| 92 | + <p className="mt-3 text-sm font-medium text-foreground">No sources configured</p> | |
| 93 | + <p className="mt-1 text-xs text-muted-foreground"> | |
| 94 | + Add an API source to start discovering tools. | |
| 95 | + </p> | |
| 96 | + </div> | |
| 69 | 97 | ) : ( |
| 70 | − <ul> | |
| 98 | + <div className="mt-4 grid gap-2"> | |
| 71 | 99 | {sources.map((s) => ( |
| 72 | − <li key={s.namespace}> | |
| 73 | − <strong>{s.namespace}</strong> — {s.toolCount} tools | |
| 74 | − </li> | |
| 100 | + <div | |
| 101 | + key={s.namespace} | |
| 102 | + className="flex items-center justify-between rounded-lg border border-border bg-card p-4 transition-colors hover:bg-accent/50" | |
| 103 | + > | |
| 104 | + <div> | |
| 105 | + <p className="text-sm font-medium text-card-foreground">{s.namespace}</p> | |
| 106 | + <p className="mt-0.5 text-xs text-muted-foreground"> | |
| 107 | + {s.toolCount} tool{s.toolCount !== 1 ? "s" : ""} | |
| 108 | + </p> | |
| 109 | + </div> | |
| 110 | + <span className="rounded bg-secondary px-1.5 py-0.5 text-[10px] font-medium text-secondary-foreground"> | |
| 111 | + openapi | |
| 112 | + </span> | |
| 113 | + </div> | |
| 75 | 114 | ))} |
| 76 | − </ul> | |
| 115 | + </div> | |
| 77 | 116 | ), |
| 78 | 117 | })} |
| 79 | − | |
| 80 | − <div style={{ marginTop: "1rem" }}> | |
| 81 | − <h3>Add a source</h3> | |
| 82 | − <div style={{ display: "flex", gap: "0.5rem" }}> | |
| 83 | − {sourcePlugins.map((p) => ( | |
| 84 | − <button key={p.key} onClick={() => setAdding(p.key)}> | |
| 85 | − {p.label} | |
| 86 | − </button> | |
| 87 | − ))} | |
| 88 | − </div> | |
| 89 | − </div> | |
| 90 | 118 | </div> |
| 91 | 119 | ); |
| 92 | 120 | } |
| @@ -3,25 +3,63 @@ import { useAtomValue, Result, toolsAtom } from "@executor/react"; · 2 unmodified lines | ||
| 3 | 3 | export function ToolsPage() { |
| 4 | 4 | const tools = useAtomValue(toolsAtom()); |
| 5 | 5 | |
| 6 | − return Result.match(tools, { | |
| 7 | − onInitial: () => <p>Loading tools…</p>, | |
| 8 | − onFailure: () => <p style={{ color: "red" }}>Failed to load tools</p>, | |
| 9 | − onSuccess: ({ value }) => ( | |
| 10 | − <div> | |
| 11 | − <h2>Tools ({value.length})</h2> | |
| 12 | − {value.length === 0 ? ( | |
| 13 | − <p style={{ color: "#888" }}>No tools registered yet.</p> | |
| 14 | − ) : ( | |
| 15 | − <ul> | |
| 16 | − {value.map((t) => ( | |
| 17 | − <li key={t.id}> | |
| 18 | − <strong>{t.name}</strong> | |
| 19 | − {t.description && <span> — {t.description}</span>} | |
| 20 | − </li> | |
| 21 | − ))} | |
| 22 | − </ul> | |
| 23 | − )} | |
| 24 | − </div> | |
| 25 | − ), | |
| 26 | − }); | |
| 6 | + return ( | |
| 7 | + <div> | |
| 8 | + <h1 className="text-xl font-semibold text-foreground">Tools</h1> | |
| 9 | + | |
| 10 | + {Result.match(tools, { | |
| 11 | + onInitial: () => ( | |
| 12 | + <p className="mt-4 text-sm text-muted-foreground">Loading tools…</p> | |
| 13 | + ), | |
| 14 | + onFailure: () => ( | |
| 15 | + <p className="mt-4 text-sm text-destructive">Failed to load tools</p> | |
| 16 | + ), | |
| 17 | + onSuccess: ({ value }) => | |
| 18 | + value.length === 0 ? ( | |
| 19 | + <p className="mt-4 text-sm text-muted-foreground"> | |
| 20 | + No tools registered yet. Add a source to get started. | |
| 21 | + </p> | |
| 22 | + ) : ( | |
| 23 | + <div className="mt-4"> | |
| 24 | + <p className="text-sm text-muted-foreground mb-3"> | |
| 25 | + {value.length} tool{value.length !== 1 ? "s" : ""} available | |
| 26 | + </p> | |
| 27 | + <div className="grid gap-2"> | |
| 28 | + {value.map((t) => ( | |
| 29 | + <div | |
| 30 | + key={t.id} | |
| 31 | + className="rounded-lg border border-border bg-card p-3 transition-colors hover:bg-accent/50" | |
| 32 | + > | |
| 33 | + <div className="flex items-start justify-between gap-2"> | |
| 34 | + <div className="min-w-0"> | |
| 35 | + <p className="text-sm font-medium text-card-foreground truncate"> | |
| 36 | + {t.name} | |
| 37 | + </p> | |
| 38 | + {t.description && ( | |
| 39 | + <p className="mt-0.5 text-xs text-muted-foreground line-clamp-2"> | |
| 40 | + {t.description} | |
| 41 | + </p> | |
| 42 | + )} | |
| 43 | + </div> | |
| 44 | + {t.tags.length > 0 && ( | |
| 45 | + <div className="flex shrink-0 gap-1"> | |
| 46 | + {t.tags.slice(0, 2).map((tag) => ( | |
| 47 | + <span | |
| 48 | + key={tag} | |
| 49 | + className="rounded bg-secondary px-1.5 py-0.5 text-[10px] font-medium text-secondary-foreground" | |
| 50 | + > | |
| 51 | + {tag} | |
| 52 | + </span> | |
| 53 | + ))} | |
| 54 | + </div> | |
| 55 | + )} | |
| 56 | + </div> | |
| 57 | + </div> | |
| 58 | + ))} | |
| 59 | + </div> | |
| 60 | + </div> | |
| 61 | + ), | |
| 62 | + })} | |
| 63 | + </div> | |
| 64 | + ); | |
| 27 | 65 | } |
| @@ -1,31 +1,141 @@ | ||
| 1 | −import { Link } from "@tanstack/react-router"; | |
| 1 | +import { Link, useLocation } from "@tanstack/react-router"; | |
| 2 | +import { useMemo } from "react"; | |
| 3 | +import { useAtomValue, toolsAtom, Result } from "@executor/react"; | |
| 2 | 4 | import type { ReactNode } from "react"; |
| 3 | 5 | |
| 4 | −export function Shell({ children }: { children: ReactNode }) { | |
| 6 | +// --------------------------------------------------------------------------- | |
| 7 | +// Nav item | |
| 8 | +// --------------------------------------------------------------------------- | |
| 9 | + | |
| 10 | +function NavItem(props: { to: string; label: string; active: boolean }) { | |
| 5 | 11 | return ( |
| 6 | − <div style={{ display: "flex", minHeight: "100vh", fontFamily: "system-ui" }}> | |
| 7 | − <nav | |
| 8 | − style={{ | |
| 9 | − width: 200, | |
| 10 | − padding: "1rem", | |
| 11 | − borderRight: "1px solid #eee", | |
| 12 | − display: "flex", | |
| 13 | − flexDirection: "column", | |
| 14 | − gap: "0.5rem", | |
| 15 | − }} | |
| 16 | − > | |
| 17 | − <h2 style={{ margin: 0, fontSize: "1.2rem" }}>Executor</h2> | |
| 18 | − <Link to="/tools" style={{ textDecoration: "none", color: "#333" }}> | |
| 19 | − Tools | |
| 20 | − </Link> | |
| 21 | − <Link to="/sources" style={{ textDecoration: "none", color: "#333" }}> | |
| 22 | − Sources | |
| 23 | − </Link> | |
| 24 | − <Link to="/secrets" style={{ textDecoration: "none", color: "#333" }}> | |
| 25 | − Secrets | |
| 12 | + <Link | |
| 13 | + to={props.to} | |
| 14 | + className={[ | |
| 15 | + "flex items-center gap-2.5 rounded-md px-2.5 py-1.5 text-[13px] transition-colors", | |
| 16 | + props.active | |
| 17 | + ? "bg-sidebar-active text-foreground font-medium" | |
| 18 | + : "text-sidebar-foreground hover:bg-sidebar-active/60 hover:text-foreground", | |
| 19 | + ].join(" ")} | |
| 20 | + > | |
| 21 | + {props.label} | |
| 22 | + </Link> | |
| 23 | + ); | |
| 24 | +} | |
| 25 | + | |
| 26 | +// --------------------------------------------------------------------------- | |
| 27 | +// Source list in sidebar | |
| 28 | +// --------------------------------------------------------------------------- | |
| 29 | + | |
| 30 | +function SourceList() { | |
| 31 | + const tools = useAtomValue(toolsAtom()); | |
| 32 | + | |
| 33 | + const sources = useMemo(() => { | |
| 34 | + if (tools._tag !== "Success") return []; | |
| 35 | + const namespaces = new Map<string, number>(); | |
| 36 | + for (const tool of tools.value) { | |
| 37 | + const ns = tool.tags.find((t) => t !== "openapi"); | |
| 38 | + if (ns && ns !== tool.tags[0]) { | |
| 39 | + namespaces.set(ns, (namespaces.get(ns) ?? 0) + 1); | |
| 40 | + } | |
| 41 | + } | |
| 42 | + return [...namespaces.entries()].map(([ns, count]) => ({ namespace: ns, toolCount: count })); | |
| 43 | + }, [tools]); | |
| 44 | + | |
| 45 | + if (tools._tag === "Initial" || tools.waiting) { | |
| 46 | + return <div className="px-2.5 py-2 text-[11px] text-muted-foreground/40">Loading…</div>; | |
| 47 | + } | |
| 48 | + | |
| 49 | + if (sources.length === 0) { | |
| 50 | + return <div className="px-2.5 py-2 text-[11px] text-muted-foreground/40">No sources yet</div>; | |
| 51 | + } | |
| 52 | + | |
| 53 | + return ( | |
| 54 | + <div className="flex flex-col gap-px"> | |
| 55 | + {sources.map((s) => ( | |
| 56 | + <Link | |
| 57 | + key={s.namespace} | |
| 58 | + to="/sources" | |
| 59 | + className="group flex items-center gap-2 rounded-md px-2.5 py-1.5 text-[13px] text-sidebar-foreground hover:bg-sidebar-active/60 hover:text-foreground transition-colors" | |
| 60 | + > | |
| 61 | + <span className="flex-1 truncate">{s.namespace}</span> | |
| 62 | + <span className="text-[10px] tabular-nums text-muted-foreground/50">{s.toolCount}</span> | |
| 26 | 63 | </Link> |
| 27 | − </nav> | |
| 28 | − <main style={{ flex: 1, padding: "1.5rem" }}>{children}</main> | |
| 64 | + ))} | |
| 65 | + </div> | |
| 66 | + ); | |
| 67 | +} | |
| 68 | + | |
| 69 | +// --------------------------------------------------------------------------- | |
| 70 | +// Shell | |
| 71 | +// --------------------------------------------------------------------------- | |
| 72 | + | |
| 73 | +export function Shell({ children }: { children: ReactNode }) { | |
| 74 | + const location = useLocation(); | |
| 75 | + const pathname = location.pathname; | |
| 76 | + | |
| 77 | + return ( | |
| 78 | + <div className="flex h-screen overflow-hidden"> | |
| 79 | + {/* Sidebar */} | |
| 80 | + <aside className="hidden w-52 shrink-0 border-r border-sidebar-border bg-sidebar md:flex md:flex-col lg:w-56"> | |
| 81 | + {/* Brand */} | |
| 82 | + <div className="flex h-12 shrink-0 items-center border-b border-sidebar-border px-4"> | |
| 83 | + <Link to="/" className="flex items-center gap-1.5"> | |
| 84 | + <span className="font-display text-base tracking-tight text-foreground"> | |
| 85 | + executor | |
| 86 | + </span> | |
| 87 | + <span className="rounded bg-primary/15 px-1.5 py-0.5 text-[10px] font-medium text-primary"> | |
| 88 | + v4 | |
| 89 | + </span> | |
| 90 | + </Link> | |
| 91 | + </div> | |
| 92 | + | |
| 93 | + {/* Nav */} | |
| 94 | + <nav className="flex flex-1 flex-col overflow-y-auto p-2"> | |
| 95 | + <NavItem to="/" label="Tools" active={pathname === "/" || pathname === "/tools"} /> | |
| 96 | + <NavItem to="/secrets" label="Secrets" active={pathname === "/secrets"} /> | |
| 97 | + | |
| 98 | + {/* Sources section */} | |
| 99 | + <div className="mt-5 mb-1 px-2.5 text-[10px] font-medium uppercase tracking-widest text-muted-foreground/50"> | |
| 100 | + <div className="flex items-center justify-between gap-2"> | |
| 101 | + <span>Sources</span> | |
| 102 | + <Link | |
| 103 | + to="/sources" | |
| 104 | + className="inline-flex items-center gap-1 rounded-md px-1.5 py-1 text-[10px] font-medium normal-case tracking-normal text-primary transition-colors hover:bg-sidebar-active hover:text-foreground" | |
| 105 | + > | |
| 106 | + <svg viewBox="0 0 16 16" fill="none" className="size-3"> | |
| 107 | + <path d="M8 3v10M3 8h10" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" /> | |
| 108 | + </svg> | |
| 109 | + Add | |
| 110 | + </Link> | |
| 111 | + </div> | |
| 112 | + </div> | |
| 113 | + | |
| 114 | + <SourceList /> | |
| 115 | + </nav> | |
| 116 | + | |
| 117 | + {/* Footer */} | |
| 118 | + <div className="shrink-0 border-t border-sidebar-border px-4 py-2.5"> | |
| 119 | + <div className="flex items-center justify-between text-[10px] leading-none"> | |
| 120 | + <span className="text-muted-foreground/70 tabular-nums">v4.0.0-dev</span> | |
| 121 | + <a | |
| 122 | + href="https://github.com" | |
| 123 | + target="_blank" | |
| 124 | + rel="noreferrer" | |
| 125 | + className="text-muted-foreground/70 transition-colors hover:text-foreground" | |
| 126 | + > | |
| 127 | + GitHub | |
| 128 | + </a> | |
| 129 | + </div> | |
| 130 | + </div> | |
| 131 | + </aside> | |
| 132 | + | |
| 133 | + {/* Main content */} | |
| 134 | + <main className="flex min-h-0 flex-1 flex-col min-w-0 overflow-auto"> | |
| 135 | + <div className="flex-1 p-6 max-w-5xl"> | |
| 136 | + {children} | |
| 137 | + </div> | |
| 138 | + </main> | |
| 29 | 139 | </div> |
| 30 | 140 | ); |
| 31 | 141 | } |
| @@ -0,0 +1,128 @@ | ||
| 1 | +@import "tailwindcss"; | |
| 2 | + | |
| 3 | +@source "../../../packages/clients/react/src/**/*.{ts,tsx}"; | |
| 4 | +@source "../../../packages/plugins/openapi/src/react/**/*.{ts,tsx}"; | |
| 5 | + | |
| 6 | +@theme inline { | |
| 7 | + --font-sans: "DM Sans", ui-sans-serif, system-ui, sans-serif; | |
| 8 | + --font-mono: "JetBrains Mono", ui-monospace, monospace; | |
| 9 | + --font-display: "Instrument Serif", Georgia, serif; | |
| 10 | + | |
| 11 | + --color-background: var(--background); | |
| 12 | + --color-foreground: var(--foreground); | |
| 13 | + --color-card: var(--card); | |
| 14 | + --color-card-foreground: var(--card-foreground); | |
| 15 | + --color-primary: var(--primary); | |
| 16 | + --color-primary-foreground: var(--primary-foreground); | |
| 17 | + --color-secondary: var(--secondary); | |
| 18 | + --color-secondary-foreground: var(--secondary-foreground); | |
| 19 | + --color-muted: var(--muted); | |
| 20 | + --color-muted-foreground: var(--muted-foreground); | |
| 21 | + --color-accent: var(--accent); | |
| 22 | + --color-accent-foreground: var(--accent-foreground); | |
| 23 | + --color-destructive: var(--destructive); | |
| 24 | + --color-border: var(--border); | |
| 25 | + --color-input: var(--input); | |
| 26 | + --color-ring: var(--ring); | |
| 27 | + | |
| 28 | + --color-sidebar: var(--sidebar); | |
| 29 | + --color-sidebar-foreground: var(--sidebar-foreground); | |
| 30 | + --color-sidebar-border: var(--sidebar-border); | |
| 31 | + --color-sidebar-active: var(--sidebar-active); | |
| 32 | + | |
| 33 | + --radius-sm: calc(var(--radius) * 0.6); | |
| 34 | + --radius-md: calc(var(--radius) * 0.8); | |
| 35 | + --radius-lg: var(--radius); | |
| 36 | + --radius-xl: calc(var(--radius) * 1.4); | |
| 37 | +} | |
| 38 | + | |
| 39 | +/* ——— Light theme ——— */ | |
| 40 | +:root { | |
| 41 | + color-scheme: light dark; | |
| 42 | + --radius: 0.625rem; | |
| 43 | + | |
| 44 | + --background: oklch(0.985 0.002 260); | |
| 45 | + --foreground: oklch(0.145 0.005 260); | |
| 46 | + --card: oklch(0.99 0.001 260); | |
| 47 | + --card-foreground: oklch(0.145 0.005 260); | |
| 48 | + --primary: oklch(0.55 0.17 175); | |
| 49 | + --primary-foreground: oklch(0.98 0.005 175); | |
| 50 | + --secondary: oklch(0.94 0.005 260); | |
| 51 | + --secondary-foreground: oklch(0.32 0.008 260); | |
| 52 | + --muted: oklch(0.955 0.004 260); | |
| 53 | + --muted-foreground: oklch(0.50 0.01 250); | |
| 54 | + --accent: oklch(0.94 0.006 260); | |
| 55 | + --accent-foreground: oklch(0.145 0.005 260); | |
| 56 | + --destructive: oklch(0.55 0.22 25); | |
| 57 | + --border: oklch(0.88 0.006 260); | |
| 58 | + --input: oklch(0.88 0.006 260); | |
| 59 | + --ring: oklch(0.55 0.17 175); | |
| 60 | + | |
| 61 | + --sidebar: oklch(0.97 0.003 260); | |
| 62 | + --sidebar-foreground: oklch(0.32 0.008 260); | |
| 63 | + --sidebar-border: oklch(0.90 0.005 260); | |
| 64 | + --sidebar-active: oklch(0.935 0.006 260); | |
| 65 | + | |
| 66 | + --scrollbar-thumb: oklch(0.78 0.005 260); | |
| 67 | + --scrollbar-thumb-hover: oklch(0.68 0.005 260); | |
| 68 | +} | |
| 69 | + | |
| 70 | +/* ——— Dark theme ——— */ | |
| 71 | +@media (prefers-color-scheme: dark) { | |
| 72 | + :root { | |
| 73 | + --background: oklch(0.115 0.005 260); | |
| 74 | + --foreground: oklch(0.90 0.008 250); | |
| 75 | + --card: oklch(0.145 0.005 260); | |
| 76 | + --card-foreground: oklch(0.90 0.008 250); | |
| 77 | + --primary: oklch(0.72 0.15 175); | |
| 78 | + --primary-foreground: oklch(0.12 0.01 175); | |
| 79 | + --secondary: oklch(0.195 0.007 260); | |
| 80 | + --secondary-foreground: oklch(0.78 0.008 250); | |
| 81 | + --muted: oklch(0.175 0.005 260); | |
| 82 | + --muted-foreground: oklch(0.56 0.01 250); | |
| 83 | + --accent: oklch(0.195 0.008 260); | |
| 84 | + --accent-foreground: oklch(0.90 0.008 250); | |
| 85 | + --destructive: oklch(0.62 0.22 25); | |
| 86 | + --border: oklch(0.235 0.007 260); | |
| 87 | + --input: oklch(0.195 0.007 260); | |
| 88 | + --ring: oklch(0.72 0.15 175); | |
| 89 | + | |
| 90 | + --sidebar: oklch(0.105 0.005 260); | |
| 91 | + --sidebar-foreground: oklch(0.72 0.008 250); | |
| 92 | + --sidebar-border: oklch(0.20 0.006 260); | |
| 93 | + --sidebar-active: oklch(0.18 0.008 260); | |
| 94 | + | |
| 95 | + --scrollbar-thumb: oklch(0.30 0.005 260); | |
| 96 | + --scrollbar-thumb-hover: oklch(0.38 0.005 260); | |
| 97 | + } | |
| 98 | +} | |
| 99 | + | |
| 100 | +@layer base { | |
| 101 | + * { | |
| 102 | + @apply border-border; | |
| 103 | + } | |
| 104 | + | |
| 105 | + html, body, #root { | |
| 106 | + min-height: 100%; | |
| 107 | + height: 100%; | |
| 108 | + } | |
| 109 | + | |
| 110 | + body { | |
| 111 | + @apply bg-background font-sans text-foreground antialiased; | |
| 112 | + } | |
| 113 | + | |
| 114 | + ::-webkit-scrollbar { | |
| 115 | + width: 6px; | |
| 116 | + height: 6px; | |
| 117 | + } | |
| 118 | + ::-webkit-scrollbar-track { | |
| 119 | + background: transparent; | |
| 120 | + } | |
| 121 | + ::-webkit-scrollbar-thumb { | |
| 122 | + background: var(--scrollbar-thumb); | |
| 123 | + border-radius: 3px; | |
| 124 | + } | |
| 125 | + ::-webkit-scrollbar-thumb:hover { | |
| 126 | + background: var(--scrollbar-thumb-hover); | |
| 127 | + } | |
| 128 | +} | |
| @@ -1,5 +1,6 @@ | ||
| 1 | 1 | import { defineConfig } from "vite"; |
| 2 | 2 | import react from "@vitejs/plugin-react"; |
| 3 | +import tailwindcss from "@tailwindcss/vite"; | |
| 3 | 4 | import { createApiHandler } from "@executor/server"; |
| 4 | 5 | |
| 5 | 6 | const api = createApiHandler(); |
| @@ -7,6 +8,7 @@ const api = createApiHandler(); · 1 unmodified line | ||
| 7 | 8 | export default defineConfig({ |
| 8 | 9 | plugins: [ |
| 9 | 10 | react(), |
| 11 | + tailwindcss(), | |
| 10 | 12 | { |
| 11 | 13 | name: "executor-api", |
| 12 | 14 | configureServer(server) { |
| @@ -37,8 +37,10 @@ · 36 unmodified lines | ||
| 37 | 37 | "@tanstack/react-router": "^1.168.10", |
| 38 | 38 | "react": "^19.1.0", |
| 39 | 39 | "react-dom": "^19.1.0", |
| 40 | + "tailwindcss": "^4.1.10", | |
| 40 | 41 | }, |
| 41 | 42 | "devDependencies": { |
| 43 | + "@tailwindcss/vite": "^4.1.10", | |
| 42 | 44 | "@types/node": "catalog:", |
| 43 | 45 | "@types/react": "^19.1.0", |
| 44 | 46 | "@types/react-dom": "^19.1.0", |
| @@ -521,6 +523,36 @@ · 476 unmodified lines | ||
| 521 | 523 | |
| 522 | 524 | "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], |
| 523 | 525 | |
| 526 | + "@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=="], | |
| 527 | + | |
| 528 | + "@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=="], | |
| 529 | + | |
| 530 | + "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.2.2", "", { "os": "android", "cpu": "arm64" }, "sha512-dXGR1n+P3B6748jZO/SvHZq7qBOqqzQ+yFrXpoOWWALWndF9MoSKAT3Q0fYgAzYzGhxNYOoysRvYlpixRBBoDg=="], | |
| 531 | + | |
| 532 | + "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.2.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-iq9Qjr6knfMpZHj55/37ouZeykwbDqF21gPFtfnhCCKGDcPI/21FKC9XdMO/XyBM7qKORx6UIhGgg6jLl7BZlg=="], | |
| 533 | + | |
| 534 | + "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.2.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-BlR+2c3nzc8f2G639LpL89YY4bdcIdUmiOOkv2GQv4/4M0vJlpXEa0JXNHhCHU7VWOKWT/CjqHdTP8aUuDJkuw=="], | |
| 535 | + | |
| 536 | + "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.2.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ=="], | |
| 537 | + | |
| 538 | + "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.2.2", "", { "os": "linux", "cpu": "arm" }, "sha512-FPdhvsW6g06T9BWT0qTwiVZYE2WIFo2dY5aCSpjG/S/u1tby+wXoslXS0kl3/KXnULlLr1E3NPRRw0g7t2kgaQ=="], | |
| 539 | + | |
| 540 | + "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.2.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw=="], | |
| 541 | + | |
| 542 | + "@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=="], | |
| 543 | + | |
| 544 | + "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.2.2", "", { "os": "linux", "cpu": "x64" }, "sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg=="], | |
| 545 | + | |
| 546 | + "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.2.2", "", { "os": "linux", "cpu": "x64" }, "sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ=="], | |
| 547 | + | |
| 548 | + "@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=="], | |
| 549 | + | |
| 550 | + "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.2.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ=="], | |
| 551 | + | |
| 552 | + "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.2.2", "", { "os": "win32", "cpu": "x64" }, "sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA=="], | |
| 553 | + | |
| 554 | + "@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=="], | |
| 555 | + | |
| 524 | 556 | "@tanstack/history": ["@tanstack/history@1.161.6", "", {}, "sha512-NaOGLRrddszbQj9upGat6HG/4TKvXLvu+osAIgfxPYA+eIvYKv8GKDJOrY2D3/U9MRnKfMWD7bU4jeD4xmqyIg=="], |
| 525 | 557 | |
| 526 | 558 | "@tanstack/react-router": ["@tanstack/react-router@1.168.10", "", { "dependencies": { "@tanstack/history": "1.161.6", "@tanstack/react-store": "^0.9.3", "@tanstack/router-core": "1.168.9", "isbot": "^5.1.22" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-/RmDlOwDkCug609KdPB3U+U1zmrtadJpvsmRg2zEn8TRCKRNri7dYZIjQZbNg8PgUiRL4T6njrZBV1ChzblNaA=="], |
| @@ -623,6 +655,8 @@ · 96 unmodified lines | ||
| 623 | 655 | |
| 624 | 656 | "electron-to-chromium": ["electron-to-chromium@1.5.330", "", {}, "sha512-jFNydB5kFtYUobh4IkWUnXeyDbjf/r9gcUEXe1xcrcUxIGfTdzPXA+ld6zBRbwvgIGVzDll/LTIiDztEtckSnA=="], |
| 625 | 657 | |
| 658 | + "enhanced-resolve": ["enhanced-resolve@5.20.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA=="], | |
| 659 | + | |
| 626 | 660 | "es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="], |
| 627 | 661 | |
| 628 | 662 | "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=="], |
| @@ -647,12 +681,16 @@ · 18 unmodified lines | ||
| 647 | 681 | |
| 648 | 682 | "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], |
| 649 | 683 | |
| 684 | + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], | |
| 685 | + | |
| 650 | 686 | "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], |
| 651 | 687 | |
| 652 | 688 | "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], |
| 653 | 689 | |
| 654 | 690 | "isbot": ["isbot@5.1.37", "", {}, "sha512-5bcicX81xf6NlTEV8rWdg7Pk01LFizDetuYGHx6d/f6y3lR2/oo8IfxjzJqn1UdDEyCcwT9e7NRloj8DwCYujQ=="], |
| 655 | 691 | |
| 692 | + "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], | |
| 693 | + | |
| 656 | 694 | "js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="], |
| 657 | 695 | |
| 658 | 696 | "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], |
| @@ -665,6 +703,30 @@ · 6 unmodified lines | ||
| 665 | 703 | |
| 666 | 704 | "kubernetes-types": ["kubernetes-types@1.30.0", "", {}, "sha512-Dew1okvhM/SQcIa2rcgujNndZwU8VnSapDgdxlYoB84ZlpAD43U6KLAFqYo17ykSFGHNPrg0qry0bP+GJd9v7Q=="], |
| 667 | 705 | |
| 706 | + "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=="], | |
| 707 | + | |
| 708 | + "lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="], | |
| 709 | + | |
| 710 | + "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="], | |
| 711 | + | |
| 712 | + "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="], | |
| 713 | + | |
| 714 | + "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="], | |
| 715 | + | |
| 716 | + "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="], | |
| 717 | + | |
| 718 | + "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="], | |
| 719 | + | |
| 720 | + "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="], | |
| 721 | + | |
| 722 | + "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="], | |
| 723 | + | |
| 724 | + "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="], | |
| 725 | + | |
| 726 | + "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="], | |
| 727 | + | |
| 728 | + "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="], | |
| 729 | + | |
| 668 | 730 | "loupe": ["loupe@3.2.1", "", {}, "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ=="], |
| 669 | 731 | |
| 670 | 732 | "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], |
| @@ -737,6 +799,10 @@ · 66 unmodified lines | ||
| 737 | 799 | |
| 738 | 800 | "strip-literal": ["strip-literal@3.1.0", "", { "dependencies": { "js-tokens": "^9.0.1" } }, "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg=="], |
| 739 | 801 | |
| 802 | + "tailwindcss": ["tailwindcss@4.2.2", "", {}, "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q=="], | |
| 803 | + | |
| 804 | + "tapable": ["tapable@2.3.2", "", {}, "sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA=="], | |
| 805 | + | |
| 740 | 806 | "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], |
| 741 | 807 | |
| 742 | 808 | "tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="], |
| @@ -781,6 +847,18 @@ · 38 unmodified lines | ||
| 781 | 847 | |
| 782 | 848 | "@executor/sdk/typescript": ["typescript@6.0.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ=="], |
| 783 | 849 | |
| 850 | + "@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=="], | |
| 851 | + | |
| 852 | + "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.9.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA=="], | |
| 853 | + | |
| 854 | + "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg=="], | |
| 855 | + | |
| 856 | + "@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=="], | |
| 857 | + | |
| 858 | + "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], | |
| 859 | + | |
| 860 | + "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], | |
| 861 | + | |
| 784 | 862 | "bun-types/@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="], |
| 785 | 863 | |
| 786 | 864 | "vite-node/vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "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-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="], |
| @@ -7,14 +7,14 @@ import { · 6 unmodified lines | ||
| 7 | 7 | Outlet, |
| 8 | 8 | RouterProvider, |
| 9 | 9 | } from "@tanstack/react-router"; |
| 10 | −import { useAtomSet, Result } from "@effect-atom/atom-react"; | |
| 10 | +import { useAtomSet } from "@effect-atom/atom-react"; | |
| 11 | 11 | import { Option } from "effect"; |
| 12 | 12 | |
| 13 | 13 | import { previewOpenApiSpec, addOpenApiSpec } from "./atoms"; |
| 14 | 14 | import type { SpecPreview } from "../sdk/preview"; |
| 15 | 15 | |
| 16 | 16 | // --------------------------------------------------------------------------- |
| 17 | −// Shared state via context | |
| 17 | +// Context | |
| 18 | 18 | // --------------------------------------------------------------------------- |
| 19 | 19 | |
| 20 | 20 | type Navigate = (to: string) => void; |
| @@ -24,9 +24,12 @@ const useNav = () => useContext(NavContext); · 3 unmodified lines | ||
| 24 | 24 | interface AddState { |
| 25 | 25 | specUrl: string; |
| 26 | 26 | setSpecUrl: (v: string) => void; |
| 27 | − preview: Result.Result<SpecPreview, unknown> | null; | |
| 28 | − doPreview: (spec: string) => void; | |
| 29 | − doAdd: (spec: string, baseUrl?: string, headers?: Record<string, unknown>) => void; | |
| 27 | + preview: SpecPreview | null; | |
| 28 | + setPreview: (p: SpecPreview | null) => void; | |
| 29 | + loading: boolean; | |
| 30 | + setLoading: (v: boolean) => void; | |
| 31 | + error: string | null; | |
| 32 | + setError: (v: string | null) => void; | |
| 30 | 33 | onComplete: () => void; |
| 31 | 34 | onCancel: () => void; |
| 32 | 35 | } |
| @@ -39,30 +42,57 @@ const useAddState = () => useContext(AddStateContext); · 6 unmodified lines | ||
| 39 | 42 | // --------------------------------------------------------------------------- |
| 40 | 43 | |
| 41 | 44 | function UrlStep() { |
| 42 | − const { specUrl, setSpecUrl, onCancel, doPreview } = useAddState(); | |
| 45 | + const { specUrl, setSpecUrl, onCancel, setPreview, setLoading, setError, loading } = useAddState(); | |
| 43 | 46 | const nav = useNav(); |
| 47 | + const doPreview = useAtomSet(previewOpenApiSpec, { mode: "promise" }); | |
| 44 | 48 | |
| 45 | − const handleNext = () => { | |
| 46 | − doPreview(specUrl); | |
| 47 | − nav("/auth"); | |
| 49 | + const handleNext = async () => { | |
| 50 | + setLoading(true); | |
| 51 | + setError(null); | |
| 52 | + try { | |
| 53 | + const result = await doPreview({ | |
| 54 | + path: { scopeId: "default" as never }, | |
| 55 | + payload: { spec: specUrl }, | |
| 56 | + }); | |
| 57 | + setPreview(result); | |
| 58 | + nav("/auth"); | |
| 59 | + } catch (e) { | |
| 60 | + setError(e instanceof Error ? e.message : "Failed to parse spec"); | |
| 61 | + } finally { | |
| 62 | + setLoading(false); | |
| 63 | + } | |
| 48 | 64 | }; |
| 49 | 65 | |
| 50 | 66 | return ( |
| 51 | − <div> | |
| 52 | − <label> | |
| 53 | − Spec URL or paste JSON/YAML: | |
| 67 | + <div className="space-y-4"> | |
| 68 | + <div> | |
| 69 | + <label className="block text-sm font-medium text-foreground mb-1.5"> | |
| 70 | + OpenAPI Spec | |
| 71 | + </label> | |
| 54 | 72 | <textarea |
| 55 | 73 | value={specUrl} |
| 56 | 74 | onChange={(e) => setSpecUrl((e.target as HTMLTextAreaElement).value)} |
| 57 | − placeholder={'https://api.example.com/openapi.json\n\nor paste spec content here...'} | |
| 75 | + placeholder={"https://api.example.com/openapi.json\n\nor paste spec content here..."} | |
| 58 | 76 | rows={6} |
| 59 | − style={{ width: "100%", fontFamily: "monospace", fontSize: "0.85rem" }} | |
| 77 | + className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm font-mono text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring resize-y" | |
| 60 | 78 | /> |
| 61 | − </label> | |
| 62 | − <div style={{ marginTop: "0.5rem", display: "flex", gap: "0.5rem" }}> | |
| 63 | − <button onClick={onCancel}>Cancel</button> | |
| 64 | − <button disabled={!specUrl.trim()} onClick={handleNext}> | |
| 65 | − Next | |
| 79 | + <p className="mt-1 text-xs text-muted-foreground"> | |
| 80 | + Paste a URL or the full OpenAPI JSON/YAML content. | |
| 81 | + </p> | |
| 82 | + </div> | |
| 83 | + <div className="flex gap-2"> | |
| 84 | + <button | |
| 85 | + onClick={onCancel} | |
| 86 | + className="rounded-md border border-border bg-background px-3 py-1.5 text-sm font-medium text-foreground transition-colors hover:bg-accent" | |
| 87 | + > | |
| 88 | + Cancel | |
| 89 | + </button> | |
| 90 | + <button | |
| 91 | + disabled={!specUrl.trim() || loading} | |
| 92 | + onClick={handleNext} | |
| 93 | + className="rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90 disabled:opacity-50 disabled:cursor-not-allowed" | |
| 94 | + > | |
| 95 | + {loading ? "Analyzing…" : "Next"} | |
| 66 | 96 | </button> |
| 67 | 97 | </div> |
| 68 | 98 | </div> |
| @@ -70,64 +100,128 @@ function UrlStep() { · 1 unmodified line | ||
| 70 | 100 | } |
| 71 | 101 | |
| 72 | 102 | function AuthStep() { |
| 73 | − const { preview } = useAddState(); | |
| 103 | + const { preview, error: stateError } = useAddState(); | |
| 74 | 104 | const nav = useNav(); |
| 75 | 105 | |
| 76 | 106 | return ( |
| 77 | − <div> | |
| 78 | − {preview && Result.match(preview, { | |
| 79 | − onInitial: () => <p>Analyzing spec…</p>, | |
| 80 | − onSuccess: ({ value }) => ( | |
| 81 | − <div> | |
| 82 | − <h4>{Option.getOrElse(value.title, () => "API")} {Option.getOrElse(value.version, () => "")}</h4> | |
| 83 | − <p>{value.operationCount} operations found</p> | |
| 84 | − {value.headerPresets.length > 0 && ( | |
| 85 | − <div> | |
| 86 | − <p>Authentication options:</p> | |
| 87 | − <ul> | |
| 88 | − {value.headerPresets.map((p, i) => ( | |
| 89 | − <li key={i}>{p.label}</li> | |
| 90 | − ))} | |
| 91 | − </ul> | |
| 92 | − </div> | |
| 93 | − )} | |
| 107 | + <div className="space-y-4"> | |
| 108 | + {preview && ( | |
| 109 | + <div className="rounded-lg border border-border bg-card p-4"> | |
| 110 | + <div className="flex items-start justify-between"> | |
| 111 | + <div> | |
| 112 | + <p className="text-sm font-medium text-card-foreground"> | |
| 113 | + {Option.getOrElse(preview.title, () => "API")} | |
| 114 | + </p> | |
| 115 | + <p className="text-xs text-muted-foreground"> | |
| 116 | + {Option.getOrElse(preview.version, () => "")} · {preview.operationCount} operations | |
| 117 | + </p> | |
| 118 | + </div> | |
| 94 | 119 | </div> |
| 95 | − ), | |
| 96 | − onFailure: () => <p style={{ color: "red" }}>Failed to parse spec</p>, | |
| 97 | − })} | |
| 98 | − <div style={{ marginTop: "0.5rem", display: "flex", gap: "0.5rem" }}> | |
| 99 | − <button onClick={() => nav("/")}>Back</button> | |
| 100 | − <button onClick={() => nav("/confirm")}>Next</button> | |
| 120 | + | |
| 121 | + {preview.tags.length > 0 && ( | |
| 122 | + <div className="mt-3 flex flex-wrap gap-1"> | |
| 123 | + {preview.tags.slice(0, 10).map((tag) => ( | |
| 124 | + <span | |
| 125 | + key={tag} | |
| 126 | + className="rounded bg-secondary px-1.5 py-0.5 text-[10px] font-medium text-secondary-foreground" | |
| 127 | + > | |
| 128 | + {tag} | |
| 129 | + </span> | |
| 130 | + ))} | |
| 131 | + {preview.tags.length > 10 && ( | |
| 132 | + <span className="text-[10px] text-muted-foreground"> | |
| 133 | + +{preview.tags.length - 10} more | |
| 134 | + </span> | |
| 135 | + )} | |
| 136 | + </div> | |
| 137 | + )} | |
| 138 | + | |
| 139 | + {preview.headerPresets.length > 0 && ( | |
| 140 | + <div className="mt-3 border-t border-border pt-3"> | |
| 141 | + <p className="text-xs font-medium text-muted-foreground mb-1.5"> | |
| 142 | + Authentication | |
| 143 | + </p> | |
| 144 | + {preview.headerPresets.map((p, i) => ( | |
| 145 | + <div key={i} className="flex items-center gap-2 py-1"> | |
| 146 | + <span className="size-1.5 rounded-full bg-primary/50" /> | |
| 147 | + <span className="text-xs text-foreground">{p.label}</span> | |
| 148 | + </div> | |
| 149 | + ))} | |
| 150 | + </div> | |
| 151 | + )} | |
| 152 | + </div> | |
| 153 | + )} | |
| 154 | + | |
| 155 | + {stateError && ( | |
| 156 | + <p className="text-sm text-destructive">{stateError}</p> | |
| 157 | + )} | |
| 158 | + | |
| 159 | + <div className="flex gap-2"> | |
| 160 | + <button | |
| 161 | + onClick={() => nav("/")} | |
| 162 | + className="rounded-md border border-border bg-background px-3 py-1.5 text-sm font-medium text-foreground transition-colors hover:bg-accent" | |
| 163 | + > | |
| 164 | + Back | |
| 165 | + </button> | |
| 166 | + <button | |
| 167 | + onClick={() => nav("/confirm")} | |
| 168 | + className="rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90" | |
| 169 | + > | |
| 170 | + Next | |
| 171 | + </button> | |
| 101 | 172 | </div> |
| 102 | 173 | </div> |
| 103 | 174 | ); |
| 104 | 175 | } |
| 105 | 176 | |
| 106 | 177 | function ConfirmStep() { |
| 107 | − const { specUrl, doAdd, onComplete } = useAddState(); | |
| 108 | − const [adding, setAdding] = useState(false); | |
| 109 | − const [error, setError] = useState<string | null>(null); | |
| 178 | + const { specUrl, preview, onComplete, setError, error } = useAddState(); | |
| 110 | 179 | const nav = useNav(); |
| 180 | + const [adding, setAdding] = useState(false); | |
| 181 | + const doAdd = useAtomSet(addOpenApiSpec, { mode: "promise" }); | |
| 111 | 182 | |
| 112 | 183 | const handleAdd = async () => { |
| 113 | 184 | setAdding(true); |
| 114 | 185 | setError(null); |
| 115 | 186 | try { |
| 116 | − doAdd(specUrl); | |
| 187 | + await doAdd({ | |
| 188 | + path: { scopeId: "default" as never }, | |
| 189 | + payload: { spec: specUrl }, | |
| 190 | + }); | |
| 117 | 191 | onComplete(); |
| 118 | 192 | } catch (e) { |
| 119 | − setError(e instanceof Error ? e.message : String(e)); | |
| 193 | + setError(e instanceof Error ? e.message : "Failed to add source"); | |
| 120 | 194 | setAdding(false); |
| 121 | 195 | } |
| 122 | 196 | }; |
| 123 | 197 | |
| 124 | 198 | return ( |
| 125 | − <div> | |
| 126 | − <p>Ready to add spec.</p> | |
| 127 | − {error && <p style={{ color: "red" }}>{error}</p>} | |
| 128 | − <div style={{ marginTop: "0.5rem", display: "flex", gap: "0.5rem" }}> | |
| 129 | − <button onClick={() => nav("/auth")} disabled={adding}>Back</button> | |
| 130 | − <button onClick={handleAdd} disabled={adding}> | |
| 199 | + <div className="space-y-4"> | |
| 200 | + <div className="rounded-lg border border-border bg-card p-4"> | |
| 201 | + <p className="text-sm font-medium text-card-foreground">Ready to add source</p> | |
| 202 | + <p className="mt-1 text-xs text-muted-foreground"> | |
| 203 | + {preview | |
| 204 | + ? `${Option.getOrElse(preview.title, () => "API")} — ${preview.operationCount} operations will be registered as tools.` | |
| 205 | + : `Source from: ${specUrl.slice(0, 60)}${specUrl.length > 60 ? "…" : ""}` | |
| 206 | + } | |
| 207 | + </p> | |
| 208 | + </div> | |
| 209 | + | |
| 210 | + {error && <p className="text-sm text-destructive">{error}</p>} | |
| 211 | + | |
| 212 | + <div className="flex gap-2"> | |
| 213 | + <button | |
| 214 | + onClick={() => nav("/auth")} | |
| 215 | + disabled={adding} | |
| 216 | + className="rounded-md border border-border bg-background px-3 py-1.5 text-sm font-medium text-foreground transition-colors hover:bg-accent disabled:opacity-50" | |
| 217 | + > | |
| 218 | + Back | |
| 219 | + </button> | |
| 220 | + <button | |
| 221 | + onClick={handleAdd} | |
| 222 | + disabled={adding} | |
| 223 | + className="rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90 disabled:opacity-50 disabled:cursor-not-allowed" | |
| 224 | + > | |
| 131 | 225 | {adding ? "Adding…" : "Add Source"} |
| 132 | 226 | </button> |
| 133 | 227 | </div> |
| @@ -144,16 +238,15 @@ export default function AddOpenApiSource(props: { · 10 unmodified lines | ||
| 144 | 238 | onCancel: () => void; |
| 145 | 239 | }) { |
| 146 | 240 | const [specUrl, setSpecUrl] = useState(""); |
| 147 | − const [preview] = useState<Result.Result<SpecPreview, unknown> | null>(null); | |
| 148 | − | |
| 149 | − const doPreviewMutation = useAtomSet(previewOpenApiSpec, { mode: "promise" }); | |
| 150 | − const doAddMutation = useAtomSet(addOpenApiSpec, { mode: "promise" }); | |
| 241 | + const [preview, setPreview] = useState<SpecPreview | null>(null); | |
| 242 | + const [loading, setLoading] = useState(false); | |
| 243 | + const [error, setError] = useState<string | null>(null); | |
| 151 | 244 | |
| 152 | 245 | const router = useMemo(() => { |
| 153 | 246 | const rootRoute = createRootRoute({ |
| 154 | 247 | component: () => ( |
| 155 | 248 | <div> |
| 156 | − <h3>Add OpenAPI Source</h3> | |
| 249 | + <h1 className="text-xl font-semibold text-foreground mb-4">Add OpenAPI Source</h1> | |
| 157 | 250 | <Outlet /> |
| 158 | 251 | </div> |
| 159 | 252 | ), |
| @@ -176,18 +269,10 @@ export default function AddOpenApiSource(props: { · 16 unmodified lines | ||
| 176 | 269 | }; |
| 177 | 270 | |
| 178 | 271 | const state: AddState = { |
| 179 | − specUrl, | |
| 180 | − setSpecUrl, | |
| 181 | − preview, | |
| 182 | − doPreview: (spec) => { | |
| 183 | − void doPreviewMutation({ path: { scopeId: "default" as never }, payload: { spec } }); | |
| 184 | − }, | |
| 185 | − doAdd: (spec, baseUrl, headers) => { | |
| 186 | − void doAddMutation({ | |
| 187 | − path: { scopeId: "default" as never }, | |
| 188 | − payload: { spec, baseUrl, headers }, | |
| 189 | − }); | |
| 190 | − }, | |
| 272 | + specUrl, setSpecUrl, | |
| 273 | + preview, setPreview, | |
| 274 | + loading, setLoading, | |
| 275 | + error, setError, | |
| 191 | 276 | onComplete: props.onComplete, |
| 192 | 277 | onCancel: props.onCancel, |
| 193 | 278 | }; |