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 @@
1import type { ReactNode } from "react";
2
3import { 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.
10const 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
28export 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
5import { Label } from "@executor-js/react/components/label";5import { Label } from "@executor-js/react/components/label";
6 6
7import { authClient } from "./auth-client";7import { authClient } from "./auth-client";
8import { AuthLayout } from "./auth-layout";
8import { mcpAuthorizeResumeTarget, safeReturnTo } from "../src/auth/return-to";9import { mcpAuthorizeResumeTarget, safeReturnTo } from "../src/auth/return-to";
9 10
10// Self-host login: email + password sign-in via Better Auth. On success we11// 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 return (57 return (
57 <div className="flex min-h-screen items-center justify-center bg-background p-6">58 <AuthLayout>
58 <div className="w-full max-w-sm space-y-4 rounded-xl border border-border bg-card p-6 shadow-sm">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 <div className="space-y-1">
60 <h1 className="font-mono text-2xl tracking-tight text-foreground">Executor</h1>61 <h1 className="text-xl font-semibold tracking-tight text-foreground">
62 {mode === "signin" ? "Sign in" : "Join this instance"}
63 </h1>
61 <p className="text-sm text-muted-foreground">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 </p>68 </p>
64 </div>69 </div>
65 70
@@ -127,6 +132,6 @@ export const LoginPage = () => { · 61 unmodified lines
127 </Button>132 </Button>
128 </div>133 </div>
129 </div>134 </div>
130 </div>135 </AuthLayout>
131 );136 );
132};137};
apps/host-selfhost/web/setup.tsx+6−5
@@ -5,6 +5,7 @@ import { Input } from "@executor-js/react/components/input"; · 4 unmodified lines
5import { Label } from "@executor-js/react/components/label";5import { Label } from "@executor-js/react/components/label";
6 6
7import { authClient } from "./auth-client";7import { authClient } from "./auth-client";
8import { AuthLayout } from "./auth-layout";
8 9
9// First-run setup. A fresh instance has no users, so the first visitor creates10// First-run setup. A fresh instance has no users, so the first visitor creates
10// the admin account here. The server admits the first signup into the empty org11// 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 return (35 return (
35 <div className="flex min-h-screen items-center justify-center bg-background p-6">36 <AuthLayout>
36 <form37 <form
37 onSubmit={submit}38 onSubmit={submit}
38 className="w-full max-w-sm space-y-4 rounded-xl border border-border bg-card p-6 shadow-sm"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 <div className="space-y-1">
41 <h1 className="font-mono text-2xl tracking-tight text-foreground">Set up Executor</h1>42 <h1 className="text-xl font-semibold tracking-tight text-foreground">Set up Executor</h1>
42 <p className="text-sm text-muted-foreground">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 </p>45 </p>
45 </div>46 </div>
46 47
@@ -87,6 +88,6 @@ export const SetupPage = () => { · 40 unmodified lines
87 {busy ? "Creating…" : "Create admin account"}88 {busy ? "Creating…" : "Create admin account"}
88 </Button>89 </Button>
89 </form>90 </form>
90 </div>91 </AuthLayout>
91 );92 );
92};93};
packages/react/src/components/mcp-install-card.tsx+3−2
@@ -170,7 +170,7 @@ export function McpInstallCard(props: { className?: string }) { · 169 unmodified lines
170 ? isDev170 ? isDev
171 ? "Uses the repo-local dev CLI from any agent working directory."171 ? "Uses the repo-local dev CLI from any agent working directory."
172 : "Requires the executor CLI on your PATH."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 const advancedControls = (175 const advancedControls = (
176 <Collapsible open={advancedOpen} onOpenChange={setAdvancedOpen}>176 <Collapsible open={advancedOpen} onOpenChange={setAdvancedOpen}>
@@ -224,9 +224,10 @@ export function McpInstallCard(props: { className?: string }) { · 47 unmodified lines
224 key={key}224 key={key}
225 title={label}225 title={label}
226 aria-label={label}226 aria-label={label}
227 role="img"
227 style={{ zIndex: SUPPORTED_AGENTS.length - index }}228 style={{ zIndex: SUPPORTED_AGENTS.length - index }}
228 className={cn(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 index > 0 && "-ml-2 group-hover/agents:ml-1",231 index > 0 && "-ml-2 group-hover/agents:ml-1",
231 )}232 )}
232 >233 >