| ... | ... | @@ -12,6 +12,7 @@ import { |
| 12 | 12 | type ServerProvider, |
| 13 | 13 | } from "@t3tools/contracts"; |
| 14 | 14 | import { useRef, useState } from "react"; |
| 15 | import { Trash2Icon } from "lucide-react"; |
| 15 | 16 | |
| 16 | 17 | import { writeTextToClipboard } from "../../hooks/useCopyToClipboard"; |
| 17 | 18 | import { ensureLocalApi } from "../../localApi"; |
| ... | ... | @@ -20,6 +21,8 @@ import { serverEnvironment } from "../../state/server"; |
| 20 | 21 | import { useAtomCommand } from "../../state/use-atom-command"; |
| 21 | 22 | import { Button } from "../ui/button"; |
| 22 | 23 | import { Input } from "../ui/input"; |
| 24 | import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; |
| 25 | import { SettingsRow } from "./settingsLayout"; |
| 23 | 26 | |
| 24 | 27 | interface ProviderSetupSectionProps { |
| 25 | 28 | readonly environmentId: EnvironmentId; |
| ... | ... | @@ -68,24 +71,34 @@ export function readAntigravityAuthMethod(config: unknown): AntigravityAuthMetho |
| 68 | 71 | /** Setup state belongs to the selected environment and is never saved in client settings. */ |
| 69 | 72 | export function ProviderSetupSection(props: ProviderSetupSectionProps) { |
| 70 | 73 | return ( |
| 71 | | <section aria-label="Antigravity setup" className="grid gap-3 text-xs"> |
| 72 | | <p>Antigravity runs on {props.environmentLabel}.</p> |
| 73 | | {!props.enabled ? ( |
| 74 | | <div className="flex flex-wrap items-center gap-2"> |
| 75 | | <span className="text-muted-foreground">Enable it to use it in threads.</span> |
| 76 | | {!props.readOnly ? ( |
| 77 | | <Button size="xs" variant="outline" onClick={props.onEnable}> |
| 78 | | Enable Antigravity |
| 79 | | </Button> |
| 80 | | ) : null} |
| 81 | | </div> |
| 82 | | ) : null} |
| 74 | <section |
| 75 | aria-label="Antigravity setup" |
| 76 | className="@container/setup divide-y divide-border/50 text-xs" |
| 77 | > |
| 78 | <SettingsRow |
| 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" |
| 80 | title="Environment" |
| 81 | description="Device that runs this provider." |
| 82 | control={ |
| 83 | <div className="flex min-w-0 flex-col gap-2 sm:items-end"> |
| 84 | <span className="text-muted-foreground [overflow-wrap:anywhere]"> |
| 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 | 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 | 97 | ) : props.provider?.setup === undefined ? ( |
| 86 | | <p className="text-muted-foreground"> |
| 87 | | Update this environment to install Antigravity and sign in with Google here. |
| 88 | | </p> |
| 98 | <SettingsRow |
| 99 | title="Update required" |
| 100 | description="Update this environment to manage Antigravity." |
| 101 | /> |
| 89 | 102 | ) : ( |
| 90 | 103 | <ProviderSetupActions |
| 91 | 104 | key={`${props.environmentId}:${props.instanceId}`} |
| ... | ... | @@ -177,12 +190,14 @@ function ProviderSetupActions({ |
| 177 | 190 | : installation?.phase === "verifying" |
| 178 | 191 | ? "Checking the downloaded runtime." |
| 179 | 192 | : installed |
| 180 | | ? "Antigravity is installed." |
| 193 | ? "Installed." |
| 181 | 194 | : usesCustomBinary |
| 182 | 195 | ? enabled |
| 183 | 196 | ? "The configured Antigravity runtime is unavailable." |
| 184 | 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 | 202 | async function runCommand<A, E>( |
| 188 | 203 | label: string, |
| ... | ... | @@ -262,210 +277,257 @@ function ProviderSetupActions({ |
| 262 | 277 | } |
| 263 | 278 | |
| 264 | 279 | return ( |
| 265 | | <div className="grid gap-3"> |
| 266 | | <div className="grid gap-2"> |
| 267 | | <p className="font-medium">Runtime</p> |
| 268 | | <p role="status" className="text-muted-foreground"> |
| 269 | | {installationStatusMessage} |
| 270 | | </p> |
| 271 | | {installation?.phase === "downloading" && |
| 272 | | installation.totalBytes !== null && |
| 273 | | installation.totalBytes > 0 ? ( |
| 274 | | <progress |
| 275 | | aria-label="Antigravity download" |
| 276 | | className="h-1 w-full accent-foreground" |
| 277 | | value={installation.downloadedBytes} |
| 278 | | max={installation.totalBytes} |
| 279 | | /> |
| 280 | | ) : null} |
| 281 | | {installation?.message && installation.message !== installationStatusMessage ? ( |
| 282 | | <p className="text-muted-foreground [overflow-wrap:anywhere]">{installation.message}</p> |
| 283 | | ) : null} |
| 284 | | {usesCustomBinary ? ( |
| 285 | | <p className="text-muted-foreground"> |
| 286 | | This instance uses the binary path below. Installing a managed runtime does not change |
| 287 | | that path. |
| 288 | | </p> |
| 289 | | ) : null} |
| 290 | | {!installed && !usesCustomBinary && !installActive && installation?.totalBytes ? ( |
| 291 | | <p className="text-muted-foreground"> |
| 292 | | Downloads {Math.ceil(installation.totalBytes / 1_000_000)} MB from Google. |
| 293 | | </p> |
| 294 | | ) : null} |
| 295 | | {!installed && !provider.setup?.canInstall ? ( |
| 296 | | <p className="text-muted-foreground"> |
| 297 | | Automatic installation is unavailable here. Set an existing binary path below or use a |
| 298 | | supported remote environment. |
| 299 | | </p> |
| 300 | | ) : null} |
| 301 | | <div className="flex flex-wrap gap-2"> |
| 302 | | {installActive && installation.operationId ? ( |
| 303 | | <Button |
| 304 | | size="xs" |
| 305 | | variant="outline" |
| 306 | | disabled={actionsDisabled} |
| 307 | | onClick={() => { |
| 308 | | const operationId = installation.operationId; |
| 309 | | if (!operationId) return; |
| 310 | | void runCommand("Cancelling installation", () => |
| 311 | | cancelInstall({ environmentId, input: { instanceId, operationId } }), |
| 312 | | ); |
| 313 | | }} |
| 314 | | > |
| 315 | | Cancel installation |
| 316 | | </Button> |
| 317 | | ) : !installActive && provider.setup?.canInstall ? ( |
| 318 | | <Button |
| 319 | | size="xs" |
| 320 | | variant="outline" |
| 321 | | disabled={actionsDisabled || installation === null || authActive} |
| 322 | | onClick={() => void runCommand("Starting installation", () => startInstall(target))} |
| 323 | | > |
| 324 | | {installation?.installedVersion |
| 325 | | ? installation.version && installation.version !== installation.installedVersion |
| 326 | | ? "Update Antigravity" |
| 327 | | : "Reinstall Antigravity" |
| 328 | | : installation?.phase === "failed" || installation?.phase === "cancelled" |
| 329 | | ? "Retry installation" |
| 330 | | : installed |
| 331 | | ? "Install managed runtime" |
| 332 | | : "Install Antigravity"} |
| 333 | | </Button> |
| 334 | | ) : null} |
| 335 | | {installation?.canRemove && !installActive ? ( |
| 336 | | <Button |
| 337 | | size="xs" |
| 338 | | variant="ghost" |
| 339 | | disabled={actionsDisabled || authActive} |
| 340 | | onClick={() => void removeRuntime()} |
| 341 | | > |
| 342 | | Remove downloaded runtime |
| 343 | | </Button> |
| 344 | | ) : null} |
| 345 | | </div> |
| 346 | | </div> |
| 280 | <div className="divide-y divide-border/50"> |
| 281 | <SettingsRow |
| 282 | title="Runtime" |
| 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" |
| 284 | description="Install and manage Antigravity." |
| 285 | status={ |
| 286 | <div className="space-y-2"> |
| 287 | {usesCustomBinary ? ( |
| 288 | <p className="text-muted-foreground"> |
| 289 | Uses the custom binary path below. Installation keeps that path. |
| 290 | </p> |
| 291 | ) : null} |
| 292 | {!installed && !provider.setup?.canInstall ? ( |
| 293 | <p className="text-muted-foreground"> |
| 294 | Automatic installation unavailable. Set a binary path or use another environment. |
| 295 | </p> |
| 296 | ) : null} |
| 297 | </div> |
| 298 | } |
| 299 | control={ |
| 300 | <div className="flex w-full min-w-0 flex-col gap-2 sm:w-56 sm:text-right"> |
| 301 | <p role="status" className="min-h-4 text-muted-foreground tabular-nums"> |
| 302 | {installationStatusMessage} |
| 303 | </p> |
| 304 | <div className="h-1"> |
| 305 | {installation?.phase === "downloading" && |
| 306 | installation.totalBytes !== null && |
| 307 | installation.totalBytes > 0 ? ( |
| 308 | <progress |
| 309 | aria-label="Antigravity download" |
| 310 | className="block h-1 w-full accent-foreground" |
| 311 | value={installation.downloadedBytes} |
| 312 | max={installation.totalBytes} |
| 313 | /> |
| 314 | ) : null} |
| 315 | </div> |
| 316 | {!installActive && |
| 317 | installation?.message && |
| 318 | installation.message !== installationStatusMessage ? ( |
| 319 | <p className="text-muted-foreground [overflow-wrap:anywhere]"> |
| 320 | {installation.message} |
| 321 | </p> |
| 322 | ) : null} |
| 323 | <div className="grid min-h-7 grid-cols-[1.75rem_minmax(0,1fr)] gap-2"> |
| 324 | <div className="col-start-2 row-start-1 grid"> |
| 325 | {installActive && installation.operationId ? ( |
| 326 | <Button |
| 327 | size="sm" |
| 328 | variant="outline" |
| 329 | disabled={actionsDisabled} |
| 330 | onClick={() => { |
| 331 | const operationId = installation.operationId; |
| 332 | if (!operationId) return; |
| 333 | void runCommand("Cancelling installation", () => |
| 334 | cancelInstall({ environmentId, input: { instanceId, operationId } }), |
| 335 | ); |
| 336 | }} |
| 337 | > |
| 338 | Cancel installation |
| 339 | </Button> |
| 340 | ) : !installActive && provider.setup?.canInstall ? ( |
| 341 | <Button |
| 342 | size="sm" |
| 343 | variant="outline" |
| 344 | disabled={actionsDisabled || installation === null || authActive} |
| 345 | onClick={() => |
| 346 | void runCommand("Starting installation", () => startInstall(target)) |
| 347 | } |
| 348 | > |
| 349 | {installation?.installedVersion |
| 350 | ? installation.version && |
| 351 | installation.version !== installation.installedVersion |
| 352 | ? "Update Antigravity" |
| 353 | : "Reinstall Antigravity" |
| 354 | : installation?.phase === "failed" || installation?.phase === "cancelled" |
| 355 | ? "Retry installation" |
| 356 | : installed |
| 357 | ? "Install managed runtime" |
| 358 | : "Install Antigravity"} |
| 359 | </Button> |
| 360 | ) : null} |
| 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"> |
| 349 | | <p className="font-medium">{methodLabel}</p> |
| 350 | | <p role="status" className="text-muted-foreground [overflow-wrap:anywhere]"> |
| 351 | | {authStatusMessage} |
| 352 | | </p> |
| 353 | | {authorizationUrl ? ( |
| 354 | | <> |
| 355 | | <div className="flex flex-wrap gap-2"> |
| 356 | | <Button size="xs" variant="outline" onClick={() => void openSignInPage()}> |
| 357 | | Open sign-in page |
| 358 | | </Button> |
| 359 | | <Button size="xs" variant="ghost" onClick={() => void copySignInLink()}> |
| 360 | | {copiedFlowId === auth?.flowId ? "Link copied" : "Copy sign-in link"} |
| 361 | | </Button> |
| 386 | <SettingsRow |
| 387 | title={methodLabel} |
| 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" |
| 389 | description={ |
| 390 | usesBrowser ? "Connect your Google account." : "Connect with the credentials below." |
| 391 | } |
| 392 | control={ |
| 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"> |
| 394 | <p |
| 395 | role="status" |
| 396 | className={ |
| 397 | authStatusMessage === phaseLabels.idle |
| 398 | ? "sr-only" |
| 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 | 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 | 512 | <p className="text-muted-foreground"> |
| 365 | | Link expires at{" "} |
| 366 | | <time dateTime={auth.expiresAt}> |
| 367 | | {new Date(auth.expiresAt).toLocaleTimeString([], { |
| 368 | | hour: "numeric", |
| 369 | | minute: "2-digit", |
| 370 | | })} |
| 371 | | </time> |
| 372 | | . |
| 513 | Sign-in is open in another client. Complete or cancel it there. |
| 373 | 514 | </p> |
| 374 | 515 | ) : null} |
| 375 | | <form |
| 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> |
| 516 | </div> |
| 414 | 517 | ) : null} |
| 415 | | <div className="flex flex-wrap gap-2"> |
| 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> |
| 518 | </SettingsRow> |
| 459 | 519 | |
| 460 | | {pendingLabel ? <p role="status">{pendingLabel}.</p> : null} |
| 520 | <p className="sr-only" role="status"> |
| 521 | {pendingLabel ? `${pendingLabel}.` : null} |
| 522 | </p> |
| 461 | 523 | {error || queryError ? ( |
| 462 | | <div className="grid gap-2"> |
| 524 | <div className="grid gap-2 px-3 py-3 sm:px-4"> |
| 463 | 525 | <p role="alert" className="text-destructive [overflow-wrap:anywhere]"> |
| 464 | 526 | {error ?? queryError} |
| 465 | 527 | </p> |
| 466 | 528 | {queryError ? ( |
| 467 | 529 | <Button |
| 468 | | size="xs" |
| 530 | size="sm" |
| 469 | 531 | variant="outline" |
| 470 | 532 | className="w-fit" |
| 471 | 533 | onClick={() => { |