Unified · Split
| @@ -0,0 +1,90 @@ | ||
| 1 | +import type { ReactNode } from "react"; | |
| 2 | + | |
| 3 | +import { Wordmark } from "@executor-js/react/components/wordmark"; | |
| 4 | + | |
| 5 | +// Split auth layout for the chromeless pages (setup, login, join): a promo | |
| 6 | +// panel on the left, the form on the right. The panel follows design.md's | |
| 7 | +// registry-minimal rules — graph-paper texture, mono eyebrow + index numerals, | |
| 8 | +// grayscale only — so the first screen a person ever sees speaks the same | |
| 9 | +// language as the app behind it. Collapses to form-only below lg. | |
| 10 | +const PANEL_POINTS: ReadonlyArray<{ index: string; title: string; body: string }> = [ | |
| 11 | + { | |
| 12 | + index: "01", | |
| 13 | + title: "Connect", | |
| 14 | + body: "OpenAPI, GraphQL, and MCP sources become tools your agent can call.", | |
| 15 | + }, | |
| 16 | + { | |
| 17 | + index: "02", | |
| 18 | + title: "Control", | |
| 19 | + body: "Policies decide which tools run, which ask first, and which are blocked.", | |
| 20 | + }, | |
| 21 | + { | |
| 22 | + index: "03", | |
| 23 | + title: "Audit", | |
| 24 | + body: "Every invocation is recorded, with approvals where they matter.", | |
| 25 | + }, | |
| 26 | +]; | |
| 27 | + | |
| 28 | +export function AuthLayout(props: { readonly children: ReactNode }) { | |
| 29 | + return ( | |
| 30 | + <div className="grid min-h-screen bg-background lg:grid-cols-[1.1fr_1fr]"> | |
| 31 | + <aside className="relative hidden flex-col justify-between overflow-hidden border-r border-border bg-sidebar p-12 lg:flex"> | |
| 32 | + {/* Graph-paper texture, faded toward the bottom (design.md signature). */} | |
| 33 | + <div | |
| 34 | + aria-hidden | |
| 35 | + className="pointer-events-none absolute inset-0 text-foreground opacity-[0.05]" | |
| 36 | + style={{ | |
| 37 | + backgroundImage: | |
| 38 | + "linear-gradient(currentColor 1px, transparent 1px), linear-gradient(90deg, currentColor 1px, transparent 1px)", | |
| 39 | + backgroundSize: "32px 32px", | |
| 40 | + maskImage: "linear-gradient(to bottom, black 30%, transparent 85%)", | |
| 41 | + }} | |
| 42 | + /> | |
| 43 | + | |
| 44 | + <div className="relative"> | |
| 45 | + <Wordmark /> | |
| 46 | + </div> | |
| 47 | + | |
| 48 | + <div className="relative max-w-md space-y-10"> | |
| 49 | + <div className="space-y-4"> | |
| 50 | + <p className="font-mono text-[11px] font-medium uppercase tracking-[0.18em] text-muted-foreground"> | |
| 51 | + The integration layer for AI agents | |
| 52 | + </p> | |
| 53 | + <h2 className="text-4xl font-semibold tracking-[-0.04em] text-foreground"> | |
| 54 | + Every tool your agent needs, behind one endpoint. | |
| 55 | + </h2> | |
| 56 | + <p className="text-sm leading-6 text-muted-foreground"> | |
| 57 | + Connect your APIs once. Any MCP-compatible agent gets the whole catalog, governed by | |
| 58 | + your policies. | |
| 59 | + </p> | |
| 60 | + </div> | |
| 61 | + | |
| 62 | + <ul className="space-y-5"> | |
| 63 | + {PANEL_POINTS.map((point) => ( | |
| 64 | + <li key={point.index} className="flex gap-4"> | |
| 65 | + <span className="font-mono text-[11px] font-medium tracking-[0.08em] text-muted-foreground/70 pt-0.5"> | |
| 66 | + {point.index} | |
| 67 | + </span> | |
| 68 | + <div> | |
| 69 | + <p className="text-sm font-medium text-foreground">{point.title}</p> | |
| 70 | + <p className="text-sm leading-6 text-muted-foreground">{point.body}</p> | |
| 71 | + </div> | |
| 72 | + </li> | |
| 73 | + ))} | |
| 74 | + </ul> | |
| 75 | + </div> | |
| 76 | + | |
| 77 | + <p className="relative font-mono text-[11px] tracking-[0.08em] text-muted-foreground"> | |
| 78 | + self-hosted | |
| 79 | + </p> | |
| 80 | + </aside> | |
| 81 | + | |
| 82 | + <main className="flex flex-col items-center justify-center gap-6 p-6"> | |
| 83 | + <div className="lg:hidden"> | |
| 84 | + <Wordmark /> | |
| 85 | + </div> | |
| 86 | + {props.children} | |
| 87 | + </main> | |
| 88 | + </div> | |
| 89 | + ); | |
| 90 | +} | |
| @@ -5,6 +5,7 @@ import { Input } from "@executor-js/react/components/input"; · 4 unmodified lines | ||
| 5 | 5 | import { Label } from "@executor-js/react/components/label"; |
| 6 | 6 | |
| 7 | 7 | import { authClient } from "./auth-client"; |
| 8 | +import { AuthLayout } from "./auth-layout"; | |
| 8 | 9 | import { mcpAuthorizeResumeTarget, safeReturnTo } from "../src/auth/return-to"; |
| 9 | 10 | |
| 10 | 11 | // Self-host login: email + password sign-in via Better Auth. On success we |
| @@ -54,12 +55,16 @@ export const LoginPage = () => { · 43 unmodified lines | ||
| 54 | 55 | }; |
| 55 | 56 | |
| 56 | 57 | return ( |
| 57 | − <div className="flex min-h-screen items-center justify-center bg-background p-6"> | |
| 58 | + <AuthLayout> | |
| 58 | 59 | <div className="w-full max-w-sm space-y-4 rounded-xl border border-border bg-card p-6 shadow-sm"> |
| 59 | − <div className="space-y-1 text-center"> | |
| 60 | − <h1 className="font-mono text-2xl tracking-tight text-foreground">Executor</h1> | |
| 60 | + <div className="space-y-1"> | |
| 61 | + <h1 className="text-xl font-semibold tracking-tight text-foreground"> | |
| 62 | + {mode === "signin" ? "Sign in" : "Join this instance"} | |
| 63 | + </h1> | |
| 61 | 64 | <p className="text-sm text-muted-foreground"> |
| 62 | − {mode === "signin" ? "Sign in to your instance" : "Join with your invite code"} | |
| 65 | + {mode === "signin" | |
| 66 | + ? "Welcome back. Use your instance account." | |
| 67 | + : "Enter the invite code you were given."} | |
| 63 | 68 | </p> |
| 64 | 69 | </div> |
| 65 | 70 | |
| @@ -127,6 +132,6 @@ export const LoginPage = () => { · 61 unmodified lines | ||
| 127 | 132 | </Button> |
| 128 | 133 | </div> |
| 129 | 134 | </div> |
| 130 | − </div> | |
| 135 | + </AuthLayout> | |
| 131 | 136 | ); |
| 132 | 137 | }; |
| @@ -5,6 +5,7 @@ import { Input } from "@executor-js/react/components/input"; · 4 unmodified lines | ||
| 5 | 5 | import { Label } from "@executor-js/react/components/label"; |
| 6 | 6 | |
| 7 | 7 | import { authClient } from "./auth-client"; |
| 8 | +import { AuthLayout } from "./auth-layout"; | |
| 8 | 9 | |
| 9 | 10 | // First-run setup. A fresh instance has no users, so the first visitor creates |
| 10 | 11 | // the admin account here. The server admits the first signup into the empty org |
| @@ -32,15 +33,15 @@ export const SetupPage = () => { · 21 unmodified lines | ||
| 32 | 33 | }; |
| 33 | 34 | |
| 34 | 35 | return ( |
| 35 | − <div className="flex min-h-screen items-center justify-center bg-background p-6"> | |
| 36 | + <AuthLayout> | |
| 36 | 37 | <form |
| 37 | 38 | onSubmit={submit} |
| 38 | 39 | className="w-full max-w-sm space-y-4 rounded-xl border border-border bg-card p-6 shadow-sm" |
| 39 | 40 | > |
| 40 | − <div className="space-y-1 text-center"> | |
| 41 | − <h1 className="font-mono text-2xl tracking-tight text-foreground">Set up Executor</h1> | |
| 41 | + <div className="space-y-1"> | |
| 42 | + <h1 className="text-xl font-semibold tracking-tight text-foreground">Set up Executor</h1> | |
| 42 | 43 | <p className="text-sm text-muted-foreground"> |
| 43 | − Create the admin account for this instance. | |
| 44 | + Create the admin account for this instance. You can invite your team once you're in. | |
| 44 | 45 | </p> |
| 45 | 46 | </div> |
| 46 | 47 | |
| @@ -87,6 +88,6 @@ export const SetupPage = () => { · 40 unmodified lines | ||
| 87 | 88 | {busy ? "Creating…" : "Create admin account"} |
| 88 | 89 | </Button> |
| 89 | 90 | </form> |
| 90 | − </div> | |
| 91 | + </AuthLayout> | |
| 91 | 92 | ); |
| 92 | 93 | }; |
| @@ -170,7 +170,7 @@ export function McpInstallCard(props: { className?: string }) { · 169 unmodified lines | ||
| 170 | 170 | ? isDev |
| 171 | 171 | ? "Uses the repo-local dev CLI from any agent working directory." |
| 172 | 172 | : "Requires the executor CLI on your PATH." |
| 173 | − : "Connect to executor as a remote MCP server over streamable HTTP."; | |
| 173 | + : "Paste this into Claude Code, Cursor, or any MCP client, and your agent gets every tool you connect here."; | |
| 174 | 174 | |
| 175 | 175 | const advancedControls = ( |
| 176 | 176 | <Collapsible open={advancedOpen} onOpenChange={setAdvancedOpen}> |
| @@ -224,9 +224,10 @@ export function McpInstallCard(props: { className?: string }) { · 47 unmodified lines | ||
| 224 | 224 | key={key} |
| 225 | 225 | title={label} |
| 226 | 226 | aria-label={label} |
| 227 | + role="img" | |
| 227 | 228 | style={{ zIndex: SUPPORTED_AGENTS.length - index }} |
| 228 | 229 | className={cn( |
| 229 | − "flex h-6 items-center justify-center rounded-md border border-border/60 bg-background px-1.5 transition-[margin] duration-200 ease-[cubic-bezier(0.23,1,0.32,1)]", | |
| 230 | + "flex h-6 items-center justify-center rounded-md border border-border/60 bg-background px-1.5 text-muted-foreground transition-[margin] duration-200 ease-[cubic-bezier(0.23,1,0.32,1)]", | |
| 230 | 231 | index > 0 && "-ml-2 group-hover/agents:ml-1", |
| 231 | 232 | )} |
| 232 | 233 | > |