| ... | @@ -12,6 +12,7 @@ import { | ... | @@ -12,6 +12,7 @@ import { |
| 12 | type ServerProvider, | 12 | type ServerProvider, |
| 13 | } from "@t3tools/contracts"; | 13 | } from "@t3tools/contracts"; |
| 14 | import { useRef, useState } from "react"; | 14 | import { useRef, useState } from "react"; |
| | 15 | import { Trash2Icon } from "lucide-react"; |
| 15 | | 16 | |
| 16 | import { writeTextToClipboard } from "../../hooks/useCopyToClipboard"; | 17 | import { writeTextToClipboard } from "../../hooks/useCopyToClipboard"; |
| 17 | import { ensureLocalApi } from "../../localApi"; | 18 | import { ensureLocalApi } from "../../localApi"; |
| ... | @@ -20,6 +21,8 @@ import { serverEnvironment } from "../../state/server"; | ... | @@ -20,6 +21,8 @@ import { serverEnvironment } from "../../state/server"; |
| 20 | import { useAtomCommand } from "../../state/use-atom-command"; | 21 | import { useAtomCommand } from "../../state/use-atom-command"; |
| 21 | import { Button } from "../ui/button"; | 22 | import { Button } from "../ui/button"; |
| 22 | import { Input } from "../ui/input"; | 23 | import { Input } from "../ui/input"; |
| | 24 | import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; |
| | 25 | import { SettingsRow } from "./settingsLayout"; |
| 23 | | 26 | |
| 24 | interface ProviderSetupSectionProps { | 27 | interface ProviderSetupSectionProps { |
| 25 | readonly environmentId: EnvironmentId; | 28 | readonly environmentId: EnvironmentId; |
| ... | @@ -68,24 +71,34 @@ export function readAntigravityAuthMethod(config: unknown): AntigravityAuthMetho | ... | @@ -68,24 +71,34 @@ export function readAntigravityAuthMethod(config: unknown): AntigravityAuthMetho |
| 68 | /** Setup state belongs to the selected environment and is never saved in client settings. */ | 71 | /** Setup state belongs to the selected environment and is never saved in client settings. */ |
| 69 | export function ProviderSetupSection(props: ProviderSetupSectionProps) { | 72 | export function ProviderSetupSection(props: ProviderSetupSectionProps) { |
| 70 | return ( | 73 | return ( |
| 71 | <section aria-label="Antigravity setup" className="grid gap-3 text-xs"> | 74 | <section |
| 72 | <p>Antigravity runs on {props.environmentLabel}.</p> | 75 | aria-label="Antigravity setup" |
| 73 | {!props.enabled ? ( | 76 | className="@container/setup divide-y divide-border/50 text-xs" |
| 74 | <div className="flex flex-wrap items-center gap-2"> | 77 | > |
| 75 | <span className="text-muted-foreground">Enable it to use it in threads.</span> | 78 | <SettingsRow |
| 76 | {!props.readOnly ? ( | 79 | className="@max-lg/setup:[&>div:first-child]:flex @max-lg/setup:[&>div:first-child]:items-stretch @max-lg/setup:[&>div:first-child]:gap-3" |
| 77 | <Button size="xs" variant="outline" onClick={props.onEnable}> | 80 | title="Environment" |
| 78 | Enable Antigravity | 81 | description="Device that runs this provider." |
| 79 | </Button> | 82 | control={ |
| 80 | ) : null} | 83 | <div className="flex min-w-0 flex-col gap-2 sm:items-end"> |
| 81 | </div> | 84 | <span className="text-muted-foreground [overflow-wrap:anywhere]"> |
| 82 | ) : null} | 85 | {props.environmentLabel} |
| | 86 | </span> |
| | 87 | {!props.enabled && !props.readOnly ? ( |
| | 88 | <Button size="sm" variant="outline" onClick={props.onEnable}> |
| | 89 | Enable Antigravity |
| | 90 | </Button> |
| | 91 | ) : null} |
| | 92 | </div> |
| | 93 | } |
| | 94 | /> |
| 83 | {props.readOnly ? ( | 95 | {props.readOnly ? ( |
| 84 | <p className="text-muted-foreground">This connection cannot change provider setup.</p> | 96 | <SettingsRow title="Setup unavailable" description="Provider setup is read-only." /> |
| 85 | ) : props.provider?.setup === undefined ? ( | 97 | ) : props.provider?.setup === undefined ? ( |
| 86 | <p className="text-muted-foreground"> | 98 | <SettingsRow |
| 87 | Update this environment to install Antigravity and sign in with Google here. | 99 | title="Update required" |
| 88 | </p> | 100 | description="Update this environment to manage Antigravity." |
| | 101 | /> |
| 89 | ) : ( | 102 | ) : ( |
| 90 | <ProviderSetupActions | 103 | <ProviderSetupActions |
| 91 | key={`${props.environmentId}:${props.instanceId}`} | 104 | key={`${props.environmentId}:${props.instanceId}`} |
| ... | @@ -177,12 +190,14 @@ function ProviderSetupActions({ | ... | @@ -177,12 +190,14 @@ function ProviderSetupActions({ |
| 177 | : installation?.phase === "verifying" | 190 | : installation?.phase === "verifying" |
| 178 | ? "Checking the downloaded runtime." | 191 | ? "Checking the downloaded runtime." |
| 179 | : installed | 192 | : installed |
| 180 | ? "Antigravity is installed." | 193 | ? "Installed." |
| 181 | : usesCustomBinary | 194 | : usesCustomBinary |
| 182 | ? enabled | 195 | ? enabled |
| 183 | ? "The configured Antigravity runtime is unavailable." | 196 | ? "The configured Antigravity runtime is unavailable." |
| 184 | : "The configured Antigravity runtime has not been checked." | 197 | : "The configured Antigravity runtime has not been checked." |
| 185 | : "Install the official Antigravity runtime before signing in."; | 198 | : installation?.totalBytes |
| | 199 | ? `${Math.ceil(installation.totalBytes / 1_000_000)} MB download.` |
| | 200 | : "Not installed."; |
| 186 | | 201 | |
| 187 | async function runCommand<A, E>( | 202 | async function runCommand<A, E>( |
| 188 | label: string, | 203 | label: string, |
| ... | @@ -262,210 +277,257 @@ function ProviderSetupActions({ | ... | @@ -262,210 +277,257 @@ function ProviderSetupActions({ |
| 262 | } | 277 | } |
| 263 | | 278 | |
| 264 | return ( | 279 | return ( |
| 265 | <div className="grid gap-3"> | 280 | <div className="divide-y divide-border/50"> |
| 266 | <div className="grid gap-2"> | 281 | <SettingsRow |
| 267 | <p className="font-medium">Runtime</p> | 282 | title="Runtime" |
| 268 | <p role="status" className="text-muted-foreground"> | 283 | className="@max-lg/setup:[&>div:first-child]:flex @max-lg/setup:[&>div:first-child]:items-stretch @max-lg/setup:[&>div:first-child]:gap-3" |
| 269 | {installationStatusMessage} | 284 | description="Install and manage Antigravity." |
| 270 | </p> | 285 | status={ |
| 271 | {installation?.phase === "downloading" && | 286 | <div className="space-y-2"> |
| 272 | installation.totalBytes !== null && | 287 | {usesCustomBinary ? ( |
| 273 | installation.totalBytes > 0 ? ( | 288 | <p className="text-muted-foreground"> |
| 274 | <progress | 289 | Uses the custom binary path below. Installation keeps that path. |
| 275 | aria-label="Antigravity download" | 290 | </p> |
| 276 | className="h-1 w-full accent-foreground" | 291 | ) : null} |
| 277 | value={installation.downloadedBytes} | 292 | {!installed && !provider.setup?.canInstall ? ( |
| 278 | max={installation.totalBytes} | 293 | <p className="text-muted-foreground"> |
| 279 | /> | 294 | Automatic installation unavailable. Set a binary path or use another environment. |
| 280 | ) : null} | 295 | </p> |
| 281 | {installation?.message && installation.message !== installationStatusMessage ? ( | 296 | ) : null} |
| 282 | <p className="text-muted-foreground [overflow-wrap:anywhere]">{installation.message}</p> | 297 | </div> |
| 283 | ) : null} | 298 | } |
| 284 | {usesCustomBinary ? ( | 299 | control={ |
| 285 | <p className="text-muted-foreground"> | 300 | <div className="flex w-full min-w-0 flex-col gap-2 sm:w-56 sm:text-right"> |
| 286 | This instance uses the binary path below. Installing a managed runtime does not change | 301 | <p role="status" className="min-h-4 text-muted-foreground tabular-nums"> |
| 287 | that path. | 302 | {installationStatusMessage} |
| 288 | </p> | 303 | </p> |
| 289 | ) : null} | 304 | <div className="h-1"> |
| 290 | {!installed && !usesCustomBinary && !installActive && installation?.totalBytes ? ( | 305 | {installation?.phase === "downloading" && |
| 291 | <p className="text-muted-foreground"> | 306 | installation.totalBytes !== null && |
| 292 | Downloads {Math.ceil(installation.totalBytes / 1_000_000)} MB from Google. | 307 | installation.totalBytes > 0 ? ( |
| 293 | </p> | 308 | <progress |
| 294 | ) : null} | 309 | aria-label="Antigravity download" |
| 295 | {!installed && !provider.setup?.canInstall ? ( | 310 | className="block h-1 w-full accent-foreground" |
| 296 | <p className="text-muted-foreground"> | 311 | value={installation.downloadedBytes} |
| 297 | Automatic installation is unavailable here. Set an existing binary path below or use a | 312 | max={installation.totalBytes} |
| 298 | supported remote environment. | 313 | /> |
| 299 | </p> | 314 | ) : null} |
| 300 | ) : null} | 315 | </div> |
| 301 | <div className="flex flex-wrap gap-2"> | 316 | {!installActive && |
| 302 | {installActive && installation.operationId ? ( | 317 | installation?.message && |
| 303 | <Button | 318 | installation.message !== installationStatusMessage ? ( |
| 304 | size="xs" | 319 | <p className="text-muted-foreground [overflow-wrap:anywhere]"> |
| 305 | variant="outline" | 320 | {installation.message} |
| 306 | disabled={actionsDisabled} | 321 | </p> |
| 307 | onClick={() => { | 322 | ) : null} |
| 308 | const operationId = installation.operationId; | 323 | <div className="grid min-h-7 grid-cols-[1.75rem_minmax(0,1fr)] gap-2"> |
| 309 | if (!operationId) return; | 324 | <div className="col-start-2 row-start-1 grid"> |
| 310 | void runCommand("Cancelling installation", () => | 325 | {installActive && installation.operationId ? ( |
| 311 | cancelInstall({ environmentId, input: { instanceId, operationId } }), | 326 | <Button |
| 312 | ); | 327 | size="sm" |
| 313 | }} | 328 | variant="outline" |
| 314 | > | 329 | disabled={actionsDisabled} |
| 315 | Cancel installation | 330 | onClick={() => { |
| 316 | </Button> | 331 | const operationId = installation.operationId; |
| 317 | ) : !installActive && provider.setup?.canInstall ? ( | 332 | if (!operationId) return; |
| 318 | <Button | 333 | void runCommand("Cancelling installation", () => |
| 319 | size="xs" | 334 | cancelInstall({ environmentId, input: { instanceId, operationId } }), |
| 320 | variant="outline" | 335 | ); |
| 321 | disabled={actionsDisabled || installation === null || authActive} | 336 | }} |
| 322 | onClick={() => void runCommand("Starting installation", () => startInstall(target))} | 337 | > |
| 323 | > | 338 | Cancel installation |
| 324 | {installation?.installedVersion | 339 | </Button> |
| 325 | ? installation.version && installation.version !== installation.installedVersion | 340 | ) : !installActive && provider.setup?.canInstall ? ( |
| 326 | ? "Update Antigravity" | 341 | <Button |
| 327 | : "Reinstall Antigravity" | 342 | size="sm" |
| 328 | : installation?.phase === "failed" || installation?.phase === "cancelled" | 343 | variant="outline" |
| 329 | ? "Retry installation" | 344 | disabled={actionsDisabled || installation === null || authActive} |
| 330 | : installed | 345 | onClick={() => |
| 331 | ? "Install managed runtime" | 346 | void runCommand("Starting installation", () => startInstall(target)) |
| 332 | : "Install Antigravity"} | 347 | } |
| 333 | </Button> | 348 | > |
| 334 | ) : null} | 349 | {installation?.installedVersion |
| 335 | {installation?.canRemove && !installActive ? ( | 350 | ? installation.version && |
| 336 | <Button | 351 | installation.version !== installation.installedVersion |
| 337 | size="xs" | 352 | ? "Update Antigravity" |
| 338 | variant="ghost" | 353 | : "Reinstall Antigravity" |
| 339 | disabled={actionsDisabled || authActive} | 354 | : installation?.phase === "failed" || installation?.phase === "cancelled" |
| 340 | onClick={() => void removeRuntime()} | 355 | ? "Retry installation" |
| 341 | > | 356 | : installed |
| 342 | Remove downloaded runtime | 357 | ? "Install managed runtime" |
| 343 | </Button> | 358 | : "Install Antigravity"} |
| 344 | ) : null} | 359 | </Button> |
| 345 | </div> | 360 | ) : null} |
| 346 | </div> | 361 | </div> |
| | 362 | {installation?.canRemove && !installActive ? ( |
| | 363 | <Tooltip> |
| | 364 | <TooltipTrigger |
| | 365 | render={ |
| | 366 | <Button |
| | 367 | size="icon-sm" |
| | 368 | variant="ghost" |
| | 369 | className="col-start-1 row-start-1" |
| | 370 | aria-label="Remove downloaded runtime" |
| | 371 | disabled={actionsDisabled || authActive} |
| | 372 | onClick={() => void removeRuntime()} |
| | 373 | /> |
| | 374 | } |
| | 375 | > |
| | 376 | <Trash2Icon className="size-3.5" /> |
| | 377 | </TooltipTrigger> |
| | 378 | <TooltipPopup>Remove downloaded runtime</TooltipPopup> |
| | 379 | </Tooltip> |
| | 380 | ) : null} |
| | 381 | </div> |
| | 382 | </div> |
| | 383 | } |
| | 384 | /> |
| 347 | | 385 | |
| 348 | <div className="grid gap-2 border-t border-border/60 pt-3"> | 386 | <SettingsRow |
| 349 | <p className="font-medium">{methodLabel}</p> | 387 | title={methodLabel} |
| 350 | <p role="status" className="text-muted-foreground [overflow-wrap:anywhere]"> | 388 | className="@max-lg/setup:[&>div:first-child]:flex @max-lg/setup:[&>div:first-child]:items-stretch @max-lg/setup:[&>div:first-child]:gap-3" |
| 351 | {authStatusMessage} | 389 | description={ |
| 352 | </p> | 390 | usesBrowser ? "Connect your Google account." : "Connect with the credentials below." |
| 353 | {authorizationUrl ? ( | 391 | } |
| 354 | <> | 392 | control={ |
| 355 | <div className="flex flex-wrap gap-2"> | 393 | <div className="flex min-w-0 flex-col gap-2 sm:max-w-56 sm:items-end sm:text-right xl:max-w-72"> |
| 356 | <Button size="xs" variant="outline" onClick={() => void openSignInPage()}> | 394 | <p |
| 357 | Open sign-in page | 395 | role="status" |
| 358 | </Button> | 396 | className={ |
| 359 | <Button size="xs" variant="ghost" onClick={() => void copySignInLink()}> | 397 | authStatusMessage === phaseLabels.idle |
| 360 | {copiedFlowId === auth?.flowId ? "Link copied" : "Copy sign-in link"} | 398 | ? "sr-only" |
| 361 | </Button> | 399 | : "text-muted-foreground [overflow-wrap:anywhere]" |
| | 400 | } |
| | 401 | > |
| | 402 | {authStatusMessage} |
| | 403 | </p> |
| | 404 | {authorizationUrl ? ( |
| | 405 | <div className="flex flex-wrap gap-2 sm:justify-end"> |
| | 406 | <Button size="sm" variant="outline" onClick={() => void openSignInPage()}> |
| | 407 | Open sign-in page |
| | 408 | </Button> |
| | 409 | <Button size="sm" variant="ghost" onClick={() => void copySignInLink()}> |
| | 410 | {copiedFlowId === auth?.flowId ? "Link copied" : "Copy sign-in link"} |
| | 411 | </Button> |
| | 412 | </div> |
| | 413 | ) : null} |
| | 414 | <div className="flex flex-wrap gap-2 sm:justify-end"> |
| | 415 | {authActive && auth?.flowId ? ( |
| | 416 | <Button |
| | 417 | size="sm" |
| | 418 | variant="ghost" |
| | 419 | disabled={actionsDisabled} |
| | 420 | onClick={() => { |
| | 421 | const flowId = auth.flowId; |
| | 422 | if (!flowId) return; |
| | 423 | void runCommand("Cancelling sign-in", () => |
| | 424 | cancelAuth({ environmentId, input: { instanceId, flowId } }), |
| | 425 | ); |
| | 426 | }} |
| | 427 | > |
| | 428 | Cancel sign-in |
| | 429 | </Button> |
| | 430 | ) : !authActive && !authenticated && provider.setup?.canAuthenticate ? ( |
| | 431 | <Button |
| | 432 | size="sm" |
| | 433 | variant="outline" |
| | 434 | disabled={actionsDisabled || !installed || auth === null || installActive} |
| | 435 | onClick={() => void runCommand("Starting sign-in", () => startAuth(target))} |
| | 436 | > |
| | 437 | {usesBrowser |
| | 438 | ? auth?.phase === "failed" || auth?.phase === "cancelled" |
| | 439 | ? "Retry Google sign-in" |
| | 440 | : "Sign in with Google" |
| | 441 | : auth?.phase === "failed" || auth?.phase === "cancelled" |
| | 442 | ? "Retry connection" |
| | 443 | : "Connect"} |
| | 444 | </Button> |
| | 445 | ) : null} |
| | 446 | {!authActive && provider.setup?.canAuthenticate ? ( |
| | 447 | <Button |
| | 448 | size="sm" |
| | 449 | variant={authenticated ? "outline" : "ghost"} |
| | 450 | disabled={actionsDisabled || auth === null} |
| | 451 | onClick={() => void signOut()} |
| | 452 | > |
| | 453 | {usesBrowser ? "Sign out of Google" : "Disconnect"} |
| | 454 | </Button> |
| | 455 | ) : null} |
| 362 | </div> | 456 | </div> |
| 363 | {auth?.expiresAt ? ( | 457 | </div> |
| | 458 | } |
| | 459 | > |
| | 460 | {authorizationUrl || auth?.phase === "waiting" ? ( |
| | 461 | <div className="space-y-2 pb-2"> |
| | 462 | {authorizationUrl ? ( |
| | 463 | <> |
| | 464 | {auth?.expiresAt ? ( |
| | 465 | <p className="text-muted-foreground"> |
| | 466 | Link expires at{" "} |
| | 467 | <time dateTime={auth.expiresAt}> |
| | 468 | {new Date(auth.expiresAt).toLocaleTimeString([], { |
| | 469 | hour: "numeric", |
| | 470 | minute: "2-digit", |
| | 471 | })} |
| | 472 | </time> |
| | 473 | . |
| | 474 | </p> |
| | 475 | ) : null} |
| | 476 | <form |
| | 477 | className="grid gap-2" |
| | 478 | onSubmit={(event) => { |
| | 479 | event.preventDefault(); |
| | 480 | void submitCallback(); |
| | 481 | }} |
| | 482 | > |
| | 483 | <label htmlFor={`provider-callback-${instanceId}`}> |
| | 484 | If the final localhost page does not load, paste its full URL here. |
| | 485 | </label> |
| | 486 | <Input |
| | 487 | id={`provider-callback-${instanceId}`} |
| | 488 | size="sm" |
| | 489 | type="url" |
| | 490 | autoComplete="off" |
| | 491 | spellCheck={false} |
| | 492 | placeholder="http://127.0.0.1:..." |
| | 493 | value={callbackUrl} |
| | 494 | maxLength={16_384} |
| | 495 | disabled={actionsDisabled} |
| | 496 | onChange={(event) => |
| | 497 | setCallbackDraft({ flowId: auth?.flowId ?? null, value: event.target.value }) |
| | 498 | } |
| | 499 | /> |
| | 500 | <Button |
| | 501 | size="sm" |
| | 502 | variant="outline" |
| | 503 | type="submit" |
| | 504 | className="w-fit" |
| | 505 | disabled={actionsDisabled || !callbackUrl.trim()} |
| | 506 | > |
| | 507 | Continue |
| | 508 | </Button> |
| | 509 | </form> |
| | 510 | </> |
| | 511 | ) : auth?.phase === "waiting" ? ( |
| 364 | <p className="text-muted-foreground"> | 512 | <p className="text-muted-foreground"> |
| 365 | Link expires at{" "} | 513 | Sign-in is open in another client. Complete or cancel it there. |
| 366 | <time dateTime={auth.expiresAt}> | | |
| 367 | {new Date(auth.expiresAt).toLocaleTimeString([], { | | |
| 368 | hour: "numeric", | | |
| 369 | minute: "2-digit", | | |
| 370 | })} | | |
| 371 | </time> | | |
| 372 | . | | |
| 373 | </p> | 514 | </p> |
| 374 | ) : null} | 515 | ) : null} |
| 375 | <form | 516 | </div> |
| 376 | className="grid gap-2" | | |
| 377 | onSubmit={(event) => { | | |
| 378 | event.preventDefault(); | | |
| 379 | void submitCallback(); | | |
| 380 | }} | | |
| 381 | > | | |
| 382 | <label htmlFor={`provider-callback-${instanceId}`}> | | |
| 383 | If the final localhost page does not load, paste its full URL here. | | |
| 384 | </label> | | |
| 385 | <Input | | |
| 386 | id={`provider-callback-${instanceId}`} | | |
| 387 | size="sm" | | |
| 388 | type="url" | | |
| 389 | autoComplete="off" | | |
| 390 | spellCheck={false} | | |
| 391 | placeholder="http://127.0.0.1:..." | | |
| 392 | value={callbackUrl} | | |
| 393 | maxLength={16_384} | | |
| 394 | disabled={actionsDisabled} | | |
| 395 | onChange={(event) => | | |
| 396 | setCallbackDraft({ flowId: auth?.flowId ?? null, value: event.target.value }) | | |
| 397 | } | | |
| 398 | /> | | |
| 399 | <Button | | |
| 400 | size="xs" | | |
| 401 | variant="outline" | | |
| 402 | type="submit" | | |
| 403 | className="w-fit" | | |
| 404 | disabled={actionsDisabled || !callbackUrl.trim()} | | |
| 405 | > | | |
| 406 | Continue | | |
| 407 | </Button> | | |
| 408 | </form> | | |
| 409 | </> | | |
| 410 | ) : auth?.phase === "waiting" ? ( | | |
| 411 | <p className="text-muted-foreground"> | | |
| 412 | Sign-in is open in another client. Complete or cancel it there. | | |
| 413 | </p> | | |
| 414 | ) : null} | 517 | ) : null} |
| 415 | <div className="flex flex-wrap gap-2"> | 518 | </SettingsRow> |
| 416 | {authActive && auth?.flowId ? ( | | |
| 417 | <Button | | |
| 418 | size="xs" | | |
| 419 | variant="ghost" | | |
| 420 | disabled={actionsDisabled} | | |
| 421 | onClick={() => { | | |
| 422 | const flowId = auth.flowId; | | |
| 423 | if (!flowId) return; | | |
| 424 | void runCommand("Cancelling sign-in", () => | | |
| 425 | cancelAuth({ environmentId, input: { instanceId, flowId } }), | | |
| 426 | ); | | |
| 427 | }} | | |
| 428 | > | | |
| 429 | Cancel sign-in | | |
| 430 | </Button> | | |
| 431 | ) : !authActive && !authenticated && provider.setup?.canAuthenticate ? ( | | |
| 432 | <Button | | |
| 433 | size="xs" | | |
| 434 | variant="outline" | | |
| 435 | disabled={actionsDisabled || !installed || auth === null || installActive} | | |
| 436 | onClick={() => void runCommand("Starting sign-in", () => startAuth(target))} | | |
| 437 | > | | |
| 438 | {usesBrowser | | |
| 439 | ? auth?.phase === "failed" || auth?.phase === "cancelled" | | |
| 440 | ? "Retry Google sign-in" | | |
| 441 | : "Sign in with Google" | | |
| 442 | : auth?.phase === "failed" || auth?.phase === "cancelled" | | |
| 443 | ? "Retry connection" | | |
| 444 | : "Connect"} | | |
| 445 | </Button> | | |
| 446 | ) : null} | | |
| 447 | {!authActive && provider.setup?.canAuthenticate ? ( | | |
| 448 | <Button | | |
| 449 | size="xs" | | |
| 450 | variant={authenticated ? "outline" : "ghost"} | | |
| 451 | disabled={actionsDisabled || auth === null} | | |
| 452 | onClick={() => void signOut()} | | |
| 453 | > | | |
| 454 | {usesBrowser ? "Sign out of Google" : "Disconnect"} | | |
| 455 | </Button> | | |
| 456 | ) : null} | | |
| 457 | </div> | | |
| 458 | </div> | | |
| 459 | | 519 | |
| 460 | {pendingLabel ? <p role="status">{pendingLabel}.</p> : null} | 520 | <p className="sr-only" role="status"> |
| | 521 | {pendingLabel ? `${pendingLabel}.` : null} |
| | 522 | </p> |
| 461 | {error || queryError ? ( | 523 | {error || queryError ? ( |
| 462 | <div className="grid gap-2"> | 524 | <div className="grid gap-2 px-3 py-3 sm:px-4"> |
| 463 | <p role="alert" className="text-destructive [overflow-wrap:anywhere]"> | 525 | <p role="alert" className="text-destructive [overflow-wrap:anywhere]"> |
| 464 | {error ?? queryError} | 526 | {error ?? queryError} |
| 465 | </p> | 527 | </p> |
| 466 | {queryError ? ( | 528 | {queryError ? ( |
| 467 | <Button | 529 | <Button |
| 468 | size="xs" | 530 | size="sm" |
| 469 | variant="outline" | 531 | variant="outline" |
| 470 | className="w-fit" | 532 | className="w-fit" |
| 471 | onClick={() => { | 533 | onClick={() => { |