| ... | @@ -12,29 +12,18 @@ import { | ... | @@ -12,29 +12,18 @@ import { |
| 12 | import { readHostedPairingRequest } from "../../hostedPairing"; | 12 | import { readHostedPairingRequest } from "../../hostedPairing"; |
| 13 | import { Button } from "../ui/button"; | 13 | import { Button } from "../ui/button"; |
| 14 | import { Input } from "../ui/input"; | 14 | import { Input } from "../ui/input"; |
| | 15 | import { StandalonePage, StandalonePageHeader } from "../ui/standalone-page"; |
| 15 | import { useAtomCommand } from "../../state/use-atom-command"; | 16 | import { useAtomCommand } from "../../state/use-atom-command"; |
| 16 | | 17 | |
| 17 | export function PairingPendingSurface() { | 18 | export 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 | /> |
| 25 | | 26 | </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 | } |
| 40 | | 29 | |
| ... | @@ -97,69 +86,57 @@ export function PairingRouteSurface({ | ... | @@ -97,69 +86,57 @@ export function PairingRouteSurface({ |
| 97 | }, [submitCredential]); | 86 | }, [submitCredential]); |
| 98 | | 87 | |
| 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 | /> |
| 106 | | 95 | |
| 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 environment | 101 | <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" |
| 117 | | 106 | 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 token | 110 | spellCheck={false} |
| 122 | </label> | 111 | value={credential} |
| 123 | <Input | 112 | /> |
| 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> | | |
| 136 | | 114 | |
| 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} |
| 157 | | 120 | |
| 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 | } |
| 165 | | 142 | |
| ... | @@ -233,57 +210,49 @@ export function HostedPairingRouteSurface() { | ... | @@ -233,57 +210,49 @@ export function HostedPairingRouteSurface() { |
| 233 | const request = hostedPairingRequestRef.current; | 210 | const request = hostedPairingRequestRef.current; |
| 234 | | 211 | |
| 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} |
| 255 | | 231 | |
| 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} |
| 261 | | 249 | {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, and | 252 | </Button> |
| 265 | is served over HTTPS when opening this page from HTTPS. | | |
| 266 | </div> | | |
| 267 | ) : null} | 253 | ) : null} |
| 268 | | 254 | </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 | } |
| 289 | | 258 | |