| ... | ... | @@ -12,29 +12,18 @@ import { |
| 12 | 12 | import { readHostedPairingRequest } from "../../hostedPairing"; |
| 13 | 13 | import { Button } from "../ui/button"; |
| 14 | 14 | import { Input } from "../ui/input"; |
| 15 | import { StandalonePage, StandalonePageHeader } from "../ui/standalone-page"; |
| 15 | 16 | import { useAtomCommand } from "../../state/use-atom-command"; |
| 16 | 17 | |
| 17 | 18 | export function PairingPendingSurface() { |
| 18 | 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 | | <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> |
| 38 | 27 | ); |
| 39 | 28 | } |
| 40 | 29 | |
| ... | ... | @@ -97,69 +86,57 @@ export function PairingRouteSurface({ |
| 97 | 86 | }, [submitCredential]); |
| 98 | 87 | |
| 99 | 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"> |
| 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> |
| 136 | 114 | |
| 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} |
| 155 | 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"> |
| 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> |
| 160 | 133 | </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> |
| 163 | 140 | ); |
| 164 | 141 | } |
| 165 | 142 | |
| ... | ... | @@ -233,57 +210,49 @@ export function HostedPairingRouteSurface() { |
| 233 | 210 | const request = hostedPairingRequestRef.current; |
| 234 | 211 | |
| 235 | 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"> |
| 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" |
| 249 | 218 | ? "Backend paired" |
| 250 | 219 | : status === "error" |
| 251 | 220 | ? "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} |
| 255 | 231 | |
| 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> |
| 260 | 248 | ) : 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> |
| 267 | 253 | ) : 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> |
| 287 | 256 | ); |
| 288 | 257 | } |
| 289 | 258 | |