History

Split auth screens into a guided layout (#1270)

Rhys Sullivan committed 4 months ago

Commit d64365f1 parent 41b75eb4 files · +109 −12Browse files

Unified · Split

4 files
  1. apps/host-selfhost/web/auth-layout.tsx +90 −0
  2. apps/host-selfhost/web/login.tsx +10 −5
  3. apps/host-selfhost/web/setup.tsx +6 −5
  4. packages/react/src/components/mcp-install-card.tsx +3 −2
apps/host-selfhost/web/auth-layout.tsx+90−0
@@ -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+}
apps/host-selfhost/web/login.tsx+10−5
@@ -5,6 +5,7 @@ import { Input } from "@executor-js/react/components/input"; · 4 unmodified lines
55 import { Label } from "@executor-js/react/components/label";
66
77 import { authClient } from "./auth-client";
8+import { AuthLayout } from "./auth-layout";
89 import { mcpAuthorizeResumeTarget, safeReturnTo } from "../src/auth/return-to";
910
1011 // Self-host login: email + password sign-in via Better Auth. On success we
@@ -54,12 +55,16 @@ export const LoginPage = () => { · 43 unmodified lines
5455 };
5556
5657 return (
57− <div className="flex min-h-screen items-center justify-center bg-background p-6">
58+ <AuthLayout>
5859 <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>
6164 <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."}
6368 </p>
6469 </div>
6570
@@ -127,6 +132,6 @@ export const LoginPage = () => { · 61 unmodified lines
127132 </Button>
128133 </div>
129134 </div>
130− </div>
135+ </AuthLayout>
131136 );
132137 };
apps/host-selfhost/web/setup.tsx+6−5
@@ -5,6 +5,7 @@ import { Input } from "@executor-js/react/components/input"; · 4 unmodified lines
55 import { Label } from "@executor-js/react/components/label";
66
77 import { authClient } from "./auth-client";
8+import { AuthLayout } from "./auth-layout";
89
910 // First-run setup. A fresh instance has no users, so the first visitor creates
1011 // the admin account here. The server admits the first signup into the empty org
@@ -32,15 +33,15 @@ export const SetupPage = () => { · 21 unmodified lines
3233 };
3334
3435 return (
35− <div className="flex min-h-screen items-center justify-center bg-background p-6">
36+ <AuthLayout>
3637 <form
3738 onSubmit={submit}
3839 className="w-full max-w-sm space-y-4 rounded-xl border border-border bg-card p-6 shadow-sm"
3940 >
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>
4243 <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.
4445 </p>
4546 </div>
4647
@@ -87,6 +88,6 @@ export const SetupPage = () => { · 40 unmodified lines
8788 {busy ? "Creating…" : "Create admin account"}
8889 </Button>
8990 </form>
90− </div>
91+ </AuthLayout>
9192 );
9293 };
packages/react/src/components/mcp-install-card.tsx+3−2
@@ -170,7 +170,7 @@ export function McpInstallCard(props: { className?: string }) { · 169 unmodified lines
170170 ? isDev
171171 ? "Uses the repo-local dev CLI from any agent working directory."
172172 : "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.";
174174
175175 const advancedControls = (
176176 <Collapsible open={advancedOpen} onOpenChange={setAdvancedOpen}>
@@ -224,9 +224,10 @@ export function McpInstallCard(props: { className?: string }) { · 47 unmodified lines
224224 key={key}
225225 title={label}
226226 aria-label={label}
227+ role="img"
227228 style={{ zIndex: SUPPORTED_AGENTS.length - index }}
228229 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)]",
230231 index > 0 && "-ml-2 group-hover/agents:ml-1",
231232 )}
232233 >