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";...@@ -2,22 +2,18 @@ import type { ReactNode } from "react";
22
3import { APP_DISPLAY_NAME, APP_STAGE_LABEL } from "../../branding";3import { APP_DISPLAY_NAME, APP_STAGE_LABEL } from "../../branding";
4import { resolveSidebarStageBackdropVariant, StageBackdropArt } from "../SidebarStageBackdrop";4import { resolveSidebarStageBackdropVariant, StageBackdropArt } from "../SidebarStageBackdrop";
5import { StandalonePage } from "../ui/standalone-page";
56
6/**7/**
7 * Full-screen card for standalone auth pages, mirroring the pairing surface's8 * Branded masthead for the CLI-connect authorize and callback pages.
8 * treatment. Used by the CLI-connect authorize and callback surfaces.
9 */9 */
10export function AuthSurfaceShell({ children }: { readonly children: ReactNode }) {10export function AuthSurfaceShell({ children }: { readonly children: ReactNode }) {
11 const stageVariant = resolveSidebarStageBackdropVariant(APP_STAGE_LABEL);11 const stageVariant = resolveSidebarStageBackdropVariant(APP_STAGE_LABEL);
1212
13 return (13 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">14 <StandalonePage
15 <div className="pointer-events-none absolute inset-0 opacity-80">15 tone="brand"
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)]" />16 masthead={
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">
21 <header className="relative h-24 overflow-hidden bg-[linear-gradient(135deg,#1e61de,#17348e)] text-white">17 <header className="relative h-24 overflow-hidden bg-[linear-gradient(135deg,#1e61de,#17348e)] text-white">
22 {stageVariant ? (18 {stageVariant ? (
23 <div className="absolute inset-0" aria-hidden>19 <div className="absolute inset-0" aria-hidden>
...@@ -36,9 +32,9 @@ export function AuthSurfaceShell({ children }: { readonly children: ReactNode })...@@ -36,9 +32,9 @@ export function AuthSurfaceShell({ children }: { readonly children: ReactNode })
36 </p>32 </p>
37 </div>33 </div>
38 </header>34 </header>
3935 }
40 <div className="p-6 sm:p-8">{children}</div>36 >
41 </section>37 {children}
42 </div>38 </StandalonePage>
43 );39 );
44}40}
apps/web/src/components/auth/PairingRouteSurface.tsx+92-123
...@@ -12,29 +12,18 @@ import {...@@ -12,29 +12,18 @@ import {
12import { readHostedPairingRequest } from "../../hostedPairing";12import { readHostedPairingRequest } from "../../hostedPairing";
13import { Button } from "../ui/button";13import { Button } from "../ui/button";
14import { Input } from "../ui/input";14import { Input } from "../ui/input";
15import { StandalonePage, StandalonePageHeader } from "../ui/standalone-page";
15import { useAtomCommand } from "../../state/use-atom-command";16import { useAtomCommand } from "../../state/use-atom-command";
1617
17export function PairingPendingSurface() {18export function PairingPendingSurface() {
18 return (19 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 <StandalonePage tone="pairing">
20 <div className="pointer-events-none absolute inset-0 opacity-80">21 <StandalonePageHeader
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 eyebrow={APP_DISPLAY_NAME}
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 title="Pairing with this environment"
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 description="Validating the pairing link and preparing your session."
24 </div>25 />
2526 </StandalonePage>
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>
38 );27 );
39}28}
4029
...@@ -97,69 +86,57 @@ export function PairingRouteSurface({...@@ -97,69 +86,57 @@ export function PairingRouteSurface({
97 }, [submitCredential]);86 }, [submitCredential]);
9887
99 return (88 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">89 <StandalonePage tone="pairing">
101 <div className="pointer-events-none absolute inset-0 opacity-80">90 <StandalonePageHeader
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)]" />91 eyebrow={APP_DISPLAY_NAME}
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)]" />92 title="Pair with this environment"
104 <div className="absolute inset-0 bg-[linear-gradient(145deg,color-mix(in_srgb,var(--background)_90%,var(--color-black))_0%,var(--background)_55%)]" />93 description={describeAuthGate(auth.bootstrapMethods)}
105 </div>94 />
10695
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">96 <form className="mt-6 space-y-4" onSubmit={(event) => void handleSubmit(event)}>
108 <p className="text-[11px] font-semibold tracking-[0.18em] text-muted-foreground uppercase">97 <div className="space-y-2">
109 {APP_DISPLAY_NAME}98 <label className="text-sm font-medium" htmlFor="pairing-token">
110 </p>99 Pairing token
111 <h1 className="mt-3 text-2xl font-semibold tracking-tight sm:text-3xl">100 </label>
112 Pair with this environment101 <Input
113 </h1>102 id="pairing-token"
114 <p className="mt-2 text-sm leading-relaxed text-muted-foreground">103 autoCapitalize="none"
115 {describeAuthGate(auth.bootstrapMethods)}104 autoComplete="off"
116 </p>105 autoCorrect="off"
117106 disabled={isSubmitting}
118 <form className="mt-6 space-y-4" onSubmit={(event) => void handleSubmit(event)}>107 nativeInput
119 <div className="space-y-2">108 onChange={(event) => setCredential(event.currentTarget.value)}
120 <label className="text-sm font-medium" htmlFor="pairing-token">109 placeholder="Paste a one-time token or pairing secret"
121 Pairing token110 spellCheck={false}
122 </label>111 value={credential}
123 <Input112 />
124 id="pairing-token"113 </div>
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>
136114
137 {errorMessage ? (115 {errorMessage ? (
138 <div className="rounded-lg border border-destructive/30 bg-destructive/6 px-3 py-2 text-sm text-destructive">116 <div className="rounded-lg border border-destructive/30 bg-destructive/6 px-3 py-2 text-sm text-destructive">
139 {errorMessage}117 {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>
155 </div>118 </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">121 <div className="flex flex-wrap gap-2">
159 {describeSupportedMethods(auth.bootstrapMethods)}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>
160 </div>133 </div>
161 </section>134 </form>
162 </div>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>
163 );140 );
164}141}
165142
...@@ -233,57 +210,49 @@ export function HostedPairingRouteSurface() {...@@ -233,57 +210,49 @@ export function HostedPairingRouteSurface() {
233 const request = hostedPairingRequestRef.current;210 const request = hostedPairingRequestRef.current;
234211
235 return (212 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">213 <StandalonePage tone="pairing">
237 <div className="pointer-events-none absolute inset-0 opacity-80">214 <StandalonePageHeader
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)]" />215 eyebrow={APP_DISPLAY_NAME}
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)]" />216 title={
240 <div className="absolute inset-0 bg-[linear-gradient(145deg,color-mix(in_srgb,var(--background)_90%,var(--color-black))_0%,var(--background)_55%)]" />217 status === "paired"
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"
249 ? "Backend paired"218 ? "Backend paired"
250 : status === "error"219 : status === "error"
251 ? "Pairing failed"220 ? "Pairing failed"
252 : "Pairing backend"}221 : "Pairing backend"
253 </h1>222 }
254 <p className="mt-2 text-sm leading-relaxed text-muted-foreground">{message}</p>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 ? (232 {status === "error" ? (
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">233 <div className="mt-5 rounded-lg border border-destructive/30 bg-destructive/6 px-3 py-2 text-sm text-destructive">
258 Host: <span className="font-mono text-foreground/80">{request.host}</span>234 Verify the backend is reachable from this browser, supports CORS for hosted clients, and
259 </div>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>
260 ) : null}248 ) : null}
261249 {status === "paired" ? (
262 {status === "error" ? (250 <Button size="sm" variant="outline" onClick={() => (window.location.href = "/")}>
263 <div className="mt-5 rounded-lg border border-destructive/30 bg-destructive/6 px-3 py-2 text-sm text-destructive">251 Open app
264 Verify the backend is reachable from this browser, supports CORS for hosted clients, and252 </Button>
265 is served over HTTPS when opening this page from HTTPS.
266 </div>
267 ) : null}253 ) : null}
268254 </div>
269 <div className="mt-6 flex flex-wrap gap-2">255 </StandalonePage>
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>
287 );256 );
288}257}
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";...@@ -35,6 +35,7 @@ import { useCopyToClipboard } from "../hooks/useCopyToClipboard";
35import { useDefaultThemeAdoption } from "../hooks/useDefaultTheme";35import { useDefaultThemeAdoption } from "../hooks/useDefaultTheme";
36import { useEnvironmentThemeSync } from "../hooks/useEnvironmentTheme";36import { useEnvironmentThemeSync } from "../hooks/useEnvironmentTheme";
37import { Button } from "../components/ui/button";37import { Button } from "../components/ui/button";
38import { StandalonePage, StandalonePageHeader } from "../components/ui/standalone-page";
38import {39import {
39 AnchoredToastProvider,40 AnchoredToastProvider,
40 stackedThreadToast,41 stackedThreadToast,
...@@ -368,39 +369,30 @@ function RootRouteErrorView({ error }: ErrorComponentProps) {...@@ -368,39 +369,30 @@ function RootRouteErrorView({ error }: ErrorComponentProps) {
368 const report = useMemo(() => errorReport(error, pathname), [error, pathname]);369 const report = useMemo(() => errorReport(error, pathname), [error, pathname]);
369370
370 return (371 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 <StandalonePage tone="error">
372 <div className="pointer-events-none absolute inset-0 opacity-80">373 <StandalonePageHeader
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 eyebrow={APP_DISPLAY_NAME}
374 <div className="absolute inset-0 bg-[linear-gradient(145deg,color-mix(in_srgb,var(--background)_90%,var(--color-black))_0%,var(--background)_55%)]" />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} />
375 </div>387 </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">389 <div className="mt-5 overflow-hidden rounded-lg border border-border/70 bg-background/55">
378 <p className="text-[11px] font-semibold tracking-[0.18em] text-muted-foreground uppercase">390 <p className="px-3 py-1.5 text-xs font-medium text-muted-foreground">Error report</p>
379 {APP_DISPLAY_NAME}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">
380 </p>392 {report}
381 <h1 className="mt-3 text-2xl font-semibold tracking-tight sm:text-3xl">393 </pre>
382 Something went wrong.394 </div>
383 </h1>395 </StandalonePage>
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>
404 );396 );
405}397}
406398