authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-09-17 20:26:01-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-17 20:26:01-07:00
logfa2decfce7fa359551634b4d24a2aecd9cef88c9
treeb3b7ebc9e0027be6015cf178f151a37251552419
parent58fcae64aece8198bcbbf051b02554ea893d859e
signature Signed by PGP key B5690EEEBB952194

refactor(web): share standalone page layout (#12354)


4 files changed, 191 insertions(+), 167 deletions(-)

apps/web/src/components/auth/AuthSurfaceShell.tsx+9-13
......@@ -2,22 +2,18 @@ import type { ReactNode } from "react";
22
33import { APP_DISPLAY_NAME, APP_STAGE_LABEL } from "../../branding";
44import { resolveSidebarStageBackdropVariant, StageBackdropArt } from "../SidebarStageBackdrop";
5import { StandalonePage } from "../ui/standalone-page";
56
67/**
7 * Full-screen card for standalone auth pages, mirroring the pairing surface's
8 * treatment. Used by the CLI-connect authorize and callback surfaces.
8 * Branded masthead for the CLI-connect authorize and callback pages.
99 */
1010export function AuthSurfaceShell({ children }: { readonly children: ReactNode }) {
1111 const stageVariant = resolveSidebarStageBackdropVariant(APP_STAGE_LABEL);
1212
1313 return (
14 <div className="relative flex min-h-screen items-center justify-center overflow-hidden bg-background px-4 py-10 text-foreground sm:px-6">
15 <div className="pointer-events-none absolute inset-0 opacity-80">
16 <div className="absolute inset-x-0 top-0 h-72 bg-[radial-gradient(48rem_20rem_at_top,color-mix(in_srgb,var(--color-blue-500)_12%,transparent),transparent)]" />
17 <div className="absolute inset-0 bg-[linear-gradient(145deg,color-mix(in_srgb,var(--background)_94%,var(--color-black))_0%,var(--background)_62%)]" />
18 </div>
19
20 <section className="relative w-full max-w-xl overflow-hidden rounded-2xl border border-border/80 bg-card/94 shadow-2xl shadow-black/20 backdrop-blur-md">
14 <StandalonePage
15 tone="brand"
16 masthead={
2117 <header className="relative h-24 overflow-hidden bg-[linear-gradient(135deg,#1e61de,#17348e)] text-white">
2218 {stageVariant ? (
2319 <div className="absolute inset-0" aria-hidden>
......@@ -36,9 +32,9 @@ export function AuthSurfaceShell({ children }: { readonly children: ReactNode })
3632 </p>
3733 </div>
3834 </header>
39
40 <div className="p-6 sm:p-8">{children}</div>
41 </section>
42 </div>
35 }
36 >
37 {children}
38 </StandalonePage>
4339 );
4440}
apps/web/src/components/auth/PairingRouteSurface.tsx+92-123
......@@ -12,29 +12,18 @@ import {
1212import { readHostedPairingRequest } from "../../hostedPairing";
1313import { Button } from "../ui/button";
1414import { Input } from "../ui/input";
15import { StandalonePage, StandalonePageHeader } from "../ui/standalone-page";
1516import { useAtomCommand } from "../../state/use-atom-command";
1617
1718export function PairingPendingSurface() {
1819 return (
19 <div className="relative flex min-h-screen items-center justify-center overflow-hidden bg-background px-4 py-10 text-foreground sm:px-6">
20 <div className="pointer-events-none absolute inset-0 opacity-80">
21 <div className="absolute inset-x-0 top-0 h-44 bg-[radial-gradient(44rem_16rem_at_top,color-mix(in_srgb,var(--color-emerald-500)_14%,transparent),transparent)]" />
22 <div className="absolute inset-y-0 left-0 w-72 bg-[radial-gradient(28rem_18rem_at_left,color-mix(in_srgb,var(--color-sky-500)_10%,transparent),transparent)]" />
23 <div className="absolute inset-0 bg-[linear-gradient(145deg,color-mix(in_srgb,var(--background)_90%,var(--color-black))_0%,var(--background)_55%)]" />
24 </div>
25
26 <section className="relative w-full max-w-xl rounded-2xl border border-border/80 bg-card/90 p-6 shadow-2xl shadow-black/20 backdrop-blur-md sm:p-8">
27 <p className="text-[11px] font-semibold tracking-[0.18em] text-muted-foreground uppercase">
28 {APP_DISPLAY_NAME}
29 </p>
30 <h1 className="mt-3 text-2xl font-semibold tracking-tight sm:text-3xl">
31 Pairing with this environment
32 </h1>
33 <p className="mt-2 text-sm leading-relaxed text-muted-foreground">
34 Validating the pairing link and preparing your session.
35 </p>
36 </section>
37 </div>
20 <StandalonePage tone="pairing">
21 <StandalonePageHeader
22 eyebrow={APP_DISPLAY_NAME}
23 title="Pairing with this environment"
24 description="Validating the pairing link and preparing your session."
25 />
26 </StandalonePage>
3827 );
3928}
4029
......@@ -97,69 +86,57 @@ export function PairingRouteSurface({
9786 }, [submitCredential]);
9887
9988 return (
100 <div className="relative flex min-h-screen items-center justify-center overflow-hidden bg-background px-4 py-10 text-foreground sm:px-6">
101 <div className="pointer-events-none absolute inset-0 opacity-80">
102 <div className="absolute inset-x-0 top-0 h-44 bg-[radial-gradient(44rem_16rem_at_top,color-mix(in_srgb,var(--color-emerald-500)_14%,transparent),transparent)]" />
103 <div className="absolute inset-y-0 left-0 w-72 bg-[radial-gradient(28rem_18rem_at_left,color-mix(in_srgb,var(--color-sky-500)_10%,transparent),transparent)]" />
104 <div className="absolute inset-0 bg-[linear-gradient(145deg,color-mix(in_srgb,var(--background)_90%,var(--color-black))_0%,var(--background)_55%)]" />
105 </div>
106
107 <section className="relative w-full max-w-xl rounded-2xl border border-border/80 bg-card/90 p-6 shadow-2xl shadow-black/20 backdrop-blur-md sm:p-8">
108 <p className="text-[11px] font-semibold tracking-[0.18em] text-muted-foreground uppercase">
109 {APP_DISPLAY_NAME}
110 </p>
111 <h1 className="mt-3 text-2xl font-semibold tracking-tight sm:text-3xl">
112 Pair with this environment
113 </h1>
114 <p className="mt-2 text-sm leading-relaxed text-muted-foreground">
115 {describeAuthGate(auth.bootstrapMethods)}
116 </p>
117
118 <form className="mt-6 space-y-4" onSubmit={(event) => void handleSubmit(event)}>
119 <div className="space-y-2">
120 <label className="text-sm font-medium" htmlFor="pairing-token">
121 Pairing token
122 </label>
123 <Input
124 id="pairing-token"
125 autoCapitalize="none"
126 autoComplete="off"
127 autoCorrect="off"
128 disabled={isSubmitting}
129 nativeInput
130 onChange={(event) => setCredential(event.currentTarget.value)}
131 placeholder="Paste a one-time token or pairing secret"
132 spellCheck={false}
133 value={credential}
134 />
135 </div>
89 <StandalonePage tone="pairing">
90 <StandalonePageHeader
91 eyebrow={APP_DISPLAY_NAME}
92 title="Pair with this environment"
93 description={describeAuthGate(auth.bootstrapMethods)}
94 />
95
96 <form className="mt-6 space-y-4" onSubmit={(event) => void handleSubmit(event)}>
97 <div className="space-y-2">
98 <label className="text-sm font-medium" htmlFor="pairing-token">
99 Pairing token
100 </label>
101 <Input
102 id="pairing-token"
103 autoCapitalize="none"
104 autoComplete="off"
105 autoCorrect="off"
106 disabled={isSubmitting}
107 nativeInput
108 onChange={(event) => setCredential(event.currentTarget.value)}
109 placeholder="Paste a one-time token or pairing secret"
110 spellCheck={false}
111 value={credential}
112 />
113 </div>
136114
137 {errorMessage ? (
138 <div className="rounded-lg border border-destructive/30 bg-destructive/6 px-3 py-2 text-sm text-destructive">
139 {errorMessage}
140 </div>
141 ) : null}
142
143 <div className="flex flex-wrap gap-2">
144 <Button disabled={isSubmitting} size="sm" type="submit">
145 {isSubmitting ? "Pairing..." : "Continue"}
146 </Button>
147 <Button
148 disabled={isSubmitting}
149 onClick={() => window.location.reload()}
150 size="sm"
151 variant="outline"
152 >
153 Reload app
154 </Button>
115 {errorMessage ? (
116 <div className="rounded-lg border border-destructive/30 bg-destructive/6 px-3 py-2 text-sm text-destructive">
117 {errorMessage}
155118 </div>
156 </form>
119 ) : null}
157120
158 <div className="mt-6 rounded-lg border border-border/70 bg-background/55 px-3 py-3 text-xs leading-relaxed text-muted-foreground">
159 {describeSupportedMethods(auth.bootstrapMethods)}
121 <div className="flex flex-wrap gap-2">
122 <Button disabled={isSubmitting} size="sm" type="submit">
123 {isSubmitting ? "Pairing..." : "Continue"}
124 </Button>
125 <Button
126 disabled={isSubmitting}
127 onClick={() => window.location.reload()}
128 size="sm"
129 variant="outline"
130 >
131 Reload app
132 </Button>
160133 </div>
161 </section>
162 </div>
134 </form>
135
136 <div className="mt-6 rounded-lg border border-border/70 bg-background/55 px-3 py-3 text-xs leading-relaxed text-muted-foreground">
137 {describeSupportedMethods(auth.bootstrapMethods)}
138 </div>
139 </StandalonePage>
163140 );
164141}
165142
......@@ -233,57 +210,49 @@ export function HostedPairingRouteSurface() {
233210 const request = hostedPairingRequestRef.current;
234211
235212 return (
236 <div className="relative flex min-h-screen items-center justify-center overflow-hidden bg-background px-4 py-10 text-foreground sm:px-6">
237 <div className="pointer-events-none absolute inset-0 opacity-80">
238 <div className="absolute inset-x-0 top-0 h-44 bg-[radial-gradient(44rem_16rem_at_top,color-mix(in_srgb,var(--color-emerald-500)_14%,transparent),transparent)]" />
239 <div className="absolute inset-y-0 left-0 w-72 bg-[radial-gradient(28rem_18rem_at_left,color-mix(in_srgb,var(--color-sky-500)_10%,transparent),transparent)]" />
240 <div className="absolute inset-0 bg-[linear-gradient(145deg,color-mix(in_srgb,var(--background)_90%,var(--color-black))_0%,var(--background)_55%)]" />
241 </div>
242
243 <section className="relative w-full max-w-xl rounded-2xl border border-border/80 bg-card/90 p-6 shadow-2xl shadow-black/20 backdrop-blur-md sm:p-8">
244 <p className="text-[11px] font-semibold tracking-[0.18em] text-muted-foreground uppercase">
245 {APP_DISPLAY_NAME}
246 </p>
247 <h1 className="mt-3 text-2xl font-semibold tracking-tight sm:text-3xl">
248 {status === "paired"
213 <StandalonePage tone="pairing">
214 <StandalonePageHeader
215 eyebrow={APP_DISPLAY_NAME}
216 title={
217 status === "paired"
249218 ? "Backend paired"
250219 : status === "error"
251220 ? "Pairing failed"
252 : "Pairing backend"}
253 </h1>
254 <p className="mt-2 text-sm leading-relaxed text-muted-foreground">{message}</p>
221 : "Pairing backend"
222 }
223 description={message}
224 />
225
226 {request ? (
227 <div className="mt-5 rounded-lg border border-border/70 bg-background/55 px-3 py-3 text-xs leading-relaxed text-muted-foreground">
228 Host: <span className="font-mono text-foreground/80">{request.host}</span>
229 </div>
230 ) : null}
255231
256 {request ? (
257 <div className="mt-5 rounded-lg border border-border/70 bg-background/55 px-3 py-3 text-xs leading-relaxed text-muted-foreground">
258 Host: <span className="font-mono text-foreground/80">{request.host}</span>
259 </div>
232 {status === "error" ? (
233 <div className="mt-5 rounded-lg border border-destructive/30 bg-destructive/6 px-3 py-2 text-sm text-destructive">
234 Verify the backend is reachable from this browser, supports CORS for hosted clients, and
235 is served over HTTPS when opening this page from HTTPS.
236 </div>
237 ) : null}
238
239 <div className="mt-6 flex flex-wrap gap-2">
240 {status === "pairing" ? (
241 <Button disabled size="sm">
242 Pairing...
243 </Button>
244 ) : canRetry ? (
245 <Button size="sm" onClick={() => void submitHostedPairingRequest()}>
246 Try again
247 </Button>
260248 ) : null}
261
262 {status === "error" ? (
263 <div className="mt-5 rounded-lg border border-destructive/30 bg-destructive/6 px-3 py-2 text-sm text-destructive">
264 Verify the backend is reachable from this browser, supports CORS for hosted clients, and
265 is served over HTTPS when opening this page from HTTPS.
266 </div>
249 {status === "paired" ? (
250 <Button size="sm" variant="outline" onClick={() => (window.location.href = "/")}>
251 Open app
252 </Button>
267253 ) : null}
268
269 <div className="mt-6 flex flex-wrap gap-2">
270 {status === "pairing" ? (
271 <Button disabled size="sm">
272 Pairing...
273 </Button>
274 ) : canRetry ? (
275 <Button size="sm" onClick={() => void submitHostedPairingRequest()}>
276 Try again
277 </Button>
278 ) : null}
279 {status === "paired" ? (
280 <Button size="sm" variant="outline" onClick={() => (window.location.href = "/")}>
281 Open app
282 </Button>
283 ) : null}
284 </div>
285 </section>
286 </div>
254 </div>
255 </StandalonePage>
287256 );
288257}
289258
apps/web/src/components/ui/standalone-page.tsx created+67
......@@ -0,0 +1,67 @@
1import type { ReactNode } from "react";
2
3const backdropClassNames = {
4 pairing:
5 "absolute inset-x-0 top-0 h-44 bg-[radial-gradient(44rem_16rem_at_top,color-mix(in_srgb,var(--color-emerald-500)_14%,transparent),transparent)]",
6 error:
7 "absolute inset-x-0 top-0 h-44 bg-[radial-gradient(44rem_16rem_at_top,color-mix(in_srgb,var(--color-red-500)_16%,transparent),transparent)]",
8 brand:
9 "absolute inset-x-0 top-0 h-72 bg-[radial-gradient(48rem_20rem_at_top,color-mix(in_srgb,var(--color-blue-500)_12%,transparent),transparent)]",
10};
11
12/** Shared page and card geometry for entry points outside the app shell. */
13export function StandalonePage({
14 tone,
15 masthead,
16 children,
17}: {
18 readonly tone: keyof typeof backdropClassNames;
19 readonly masthead?: ReactNode;
20 readonly children: ReactNode;
21}) {
22 return (
23 <div className="relative flex min-h-screen items-center justify-center overflow-hidden bg-background px-4 py-10 text-foreground sm:px-6">
24 <div className="pointer-events-none absolute inset-0 opacity-80" aria-hidden>
25 <div className={backdropClassNames[tone]} />
26 {tone === "pairing" ? (
27 <div className="absolute inset-y-0 left-0 w-72 bg-[radial-gradient(28rem_18rem_at_left,color-mix(in_srgb,var(--color-sky-500)_10%,transparent),transparent)]" />
28 ) : null}
29 <div
30 className={
31 tone === "brand"
32 ? "absolute inset-0 bg-[linear-gradient(145deg,color-mix(in_srgb,var(--background)_94%,var(--color-black))_0%,var(--background)_62%)]"
33 : "absolute inset-0 bg-[linear-gradient(145deg,color-mix(in_srgb,var(--background)_90%,var(--color-black))_0%,var(--background)_55%)]"
34 }
35 />
36 </div>
37
38 <section
39 className={`relative w-full max-w-xl rounded-2xl border border-border/80 shadow-2xl shadow-black/20 backdrop-blur-md ${tone === "brand" ? "overflow-hidden bg-card/94" : "bg-card/90"}`}
40 >
41 {masthead}
42 <div className="p-6 sm:p-8">{children}</div>
43 </section>
44 </div>
45 );
46}
47
48/** Entry-page headings keep their typography and spacing together. */
49export function StandalonePageHeader({
50 eyebrow,
51 title,
52 description,
53}: {
54 readonly eyebrow: ReactNode;
55 readonly title: ReactNode;
56 readonly description: ReactNode;
57}) {
58 return (
59 <>
60 <p className="text-[11px] font-semibold tracking-[0.18em] text-muted-foreground uppercase">
61 {eyebrow}
62 </p>
63 <h1 className="mt-3 text-2xl font-semibold tracking-tight sm:text-3xl">{title}</h1>
64 <p className="mt-2 text-sm leading-relaxed text-muted-foreground">{description}</p>
65 </>
66 );
67}
apps/web/src/routes/__root.tsx+23-31
......@@ -35,6 +35,7 @@ import { useCopyToClipboard } from "../hooks/useCopyToClipboard";
3535import { useDefaultThemeAdoption } from "../hooks/useDefaultTheme";
3636import { useEnvironmentThemeSync } from "../hooks/useEnvironmentTheme";
3737import { Button } from "../components/ui/button";
38import { StandalonePage, StandalonePageHeader } from "../components/ui/standalone-page";
3839import {
3940 AnchoredToastProvider,
4041 stackedThreadToast,
......@@ -368,39 +369,30 @@ function RootRouteErrorView({ error }: ErrorComponentProps) {
368369 const report = useMemo(() => errorReport(error, pathname), [error, pathname]);
369370
370371 return (
371 <div className="relative flex min-h-screen items-center justify-center overflow-hidden bg-background px-4 py-10 text-foreground sm:px-6">
372 <div className="pointer-events-none absolute inset-0 opacity-80">
373 <div className="absolute inset-x-0 top-0 h-44 bg-[radial-gradient(44rem_16rem_at_top,color-mix(in_srgb,var(--color-red-500)_16%,transparent),transparent)]" />
374 <div className="absolute inset-0 bg-[linear-gradient(145deg,color-mix(in_srgb,var(--background)_90%,var(--color-black))_0%,var(--background)_55%)]" />
372 <StandalonePage tone="error">
373 <StandalonePageHeader
374 eyebrow={APP_DISPLAY_NAME}
375 title="Something went wrong."
376 description={message}
377 />
378
379 <div className="mt-5 flex flex-wrap gap-2">
380 <Button size="sm" onClick={() => void router.invalidate()}>
381 Try again
382 </Button>
383 <Button size="sm" variant="outline" onClick={() => window.location.reload()}>
384 Reload app
385 </Button>
386 <CopyErrorButton report={report} />
375387 </div>
376388
377 <section className="relative w-full max-w-xl rounded-2xl border border-border/80 bg-card/90 p-6 shadow-2xl shadow-black/20 backdrop-blur-md sm:p-8">
378 <p className="text-[11px] font-semibold tracking-[0.18em] text-muted-foreground uppercase">
379 {APP_DISPLAY_NAME}
380 </p>
381 <h1 className="mt-3 text-2xl font-semibold tracking-tight sm:text-3xl">
382 Something went wrong.
383 </h1>
384 <p className="mt-2 text-sm leading-relaxed text-muted-foreground">{message}</p>
385
386 <div className="mt-5 flex flex-wrap gap-2">
387 <Button size="sm" onClick={() => void router.invalidate()}>
388 Try again
389 </Button>
390 <Button size="sm" variant="outline" onClick={() => window.location.reload()}>
391 Reload app
392 </Button>
393 <CopyErrorButton report={report} />
394 </div>
395
396 <div className="mt-5 overflow-hidden rounded-lg border border-border/70 bg-background/55">
397 <p className="px-3 py-1.5 text-xs font-medium text-muted-foreground">Error report</p>
398 <pre className="max-h-64 overflow-auto border-t border-border/70 bg-background/80 px-3 py-2 text-xs whitespace-pre-wrap text-foreground/85">
399 {report}
400 </pre>
401 </div>
402 </section>
403 </div>
389 <div className="mt-5 overflow-hidden rounded-lg border border-border/70 bg-background/55">
390 <p className="px-3 py-1.5 text-xs font-medium text-muted-foreground">Error report</p>
391 <pre className="max-h-64 overflow-auto border-t border-border/70 bg-background/80 px-3 py-2 text-xs whitespace-pre-wrap text-foreground/85">
392 {report}
393 </pre>
394 </div>
395 </StandalonePage>
404396 );
405397}
406398