diff --git a/apps/server/src/cloud/cliAuthHtml.ts b/apps/server/src/cloud/cliAuthHtml.ts index 69d3b471ae32..642348415e4d 100644 --- a/apps/server/src/cloud/cliAuthHtml.ts +++ b/apps/server/src/cloud/cliAuthHtml.ts @@ -6,16 +6,19 @@ function resolveLoopbackAuthorizationStage(): LoopbackAuthorizationStage { return typeof __T3CODE_BUILD_CHANNEL__ === "undefined" ? "dev" : __T3CODE_BUILD_CHANNEL__; } -const stageBrands = { - dev: "T3 Code (Dev)", - nightly: "T3 Code (Nightly)", - latest: "T3 Code", -} as const satisfies Record; +const stagePillLabels = { + dev: "Dev", + nightly: "Nightly", + latest: null, +} as const satisfies Record; + +const wordmarkPath = + "M33.4509 93V47.56H15.5309V37H64.3309V47.56H46.4109V93H33.4509ZM86.7253 93.96C82.832 93.96 78.9653 93.4533 75.1253 92.44C71.2853 91.3733 68.032 89.88 65.3653 87.96L70.4053 78.04C72.5386 79.5867 75.0186 80.8133 77.8453 81.72C80.672 82.6267 83.5253 83.08 86.4053 83.08C89.6586 83.08 92.2186 82.44 94.0853 81.16C95.952 79.88 96.8853 78.12 96.8853 75.88C96.8853 73.7467 96.0586 72.0667 94.4053 70.84C92.752 69.6133 90.0853 69 86.4053 69H80.4853V60.44L96.0853 42.76L97.5253 47.4H68.1653V37H107.365V45.4L91.8453 63.08L85.2853 59.32H89.0453C95.9253 59.32 101.125 60.8667 104.645 63.96C108.165 67.0533 109.925 71.0267 109.925 75.88C109.925 79.0267 109.099 81.9867 107.445 84.76C105.792 87.48 103.259 89.6933 99.8453 91.4C96.432 93.1067 92.0586 93.96 86.7253 93.96Z"; export function renderLoopbackAuthorizationCompleteHtml( stage: LoopbackAuthorizationStage = resolveLoopbackAuthorizationStage(), ): string { - const stageBrand = stageBrands[stage]; + const stagePill = stagePillLabels[stage]; return ` @@ -27,9 +30,23 @@ export function renderLoopbackAuthorizationCompleteHtml( -
-
-
-

${stageBrand}

-
-
-
-

Browser authorization complete

-

You're connected

-

Return to your terminal to finish setting up T3 Connect. You can close this window.

-
+
+
+ + + Code + + ${stagePill ? `${stagePill}` : ""} +
+

You're connected

+

Return to your terminal to finish. You can close this window.

`; diff --git a/apps/web/src/components/T3Wordmark.tsx b/apps/web/src/components/T3Wordmark.tsx index d0882a5dd61e..1ad70cba2851 100644 --- a/apps/web/src/components/T3Wordmark.tsx +++ b/apps/web/src/components/T3Wordmark.tsx @@ -1,5 +1,7 @@ import type { SVGProps } from "react"; +import { cn } from "~/lib/utils"; + export function T3Wordmark(props: SVGProps) { return ( @@ -10,3 +12,25 @@ export function T3Wordmark(props: SVGProps) { ); } + +export function T3CodeWordmark({ onBackdrop = false }: { readonly onBackdrop?: boolean }) { + return ( + // Center the visible capitals, without the font's ascender/descender space. + + + + Code + + + ); +} diff --git a/apps/web/src/components/auth/AuthSurfaceShell.tsx b/apps/web/src/components/auth/AuthSurfaceShell.tsx index de8939dce372..dbc430a946d8 100644 --- a/apps/web/src/components/auth/AuthSurfaceShell.tsx +++ b/apps/web/src/components/auth/AuthSurfaceShell.tsx @@ -1,40 +1,40 @@ import type { ReactNode } from "react"; -import { APP_DISPLAY_NAME, APP_STAGE_LABEL } from "../../branding"; -import { resolveSidebarStageBackdropVariant, StageBackdropArt } from "../SidebarStageBackdrop"; +import { APP_STAGE_LABEL } from "../../branding"; +import { resolveEnvironmentIdentificationPillLabel } from "../SidebarStageBackdrop"; +import { T3CodeWordmark } from "../T3Wordmark"; +import { Badge } from "../ui/badge"; import { StandalonePage } from "../ui/standalone-page"; -/** - * Branded masthead for the CLI-connect authorize and callback pages. - */ export function AuthSurfaceShell({ children }: { readonly children: ReactNode }) { - const stageVariant = resolveSidebarStageBackdropVariant(APP_STAGE_LABEL); + const stageLabel = resolveEnvironmentIdentificationPillLabel(APP_STAGE_LABEL); return ( - - {stageVariant ? ( -
- -
- ) : ( -
- )} -
-
-

- {APP_DISPLAY_NAME} -

-
- - } - > + +
+ + {stageLabel ? ( + + {stageLabel} + + ) : null} +
{children}
); } + +export function AuthSurfaceMessage({ + title, + description, +}: { + readonly title: string; + readonly description: ReactNode; +}) { + return ( + <> +

{title}

+

{description}

+ + ); +} diff --git a/apps/web/src/components/auth/PairingRouteSurface.tsx b/apps/web/src/components/auth/PairingRouteSurface.tsx index cad11af57fc6..13946667cbda 100644 --- a/apps/web/src/components/auth/PairingRouteSurface.tsx +++ b/apps/web/src/components/auth/PairingRouteSurface.tsx @@ -2,7 +2,6 @@ import type { AuthSessionState } from "@t3tools/contracts"; import { squashAtomCommandFailure } from "@t3tools/client-runtime/state/runtime"; import React, { startTransition, useEffect, useRef, useState, useCallback } from "react"; -import { APP_DISPLAY_NAME } from "../../branding"; import { connectPairing } from "../../connection/onboarding"; import { peekPairingTokenFromUrl, @@ -12,18 +11,17 @@ import { import { readHostedPairingRequest } from "../../hostedPairing"; import { Button } from "../ui/button"; import { Input } from "../ui/input"; -import { StandalonePage, StandalonePageHeader } from "../ui/standalone-page"; import { useAtomCommand } from "../../state/use-atom-command"; +import { AuthSurfaceMessage, AuthSurfaceShell } from "./AuthSurfaceShell"; export function PairingPendingSurface() { return ( - - + - + ); } @@ -86,9 +84,8 @@ export function PairingRouteSurface({ }, [submitCredential]); return ( - - + @@ -100,43 +97,41 @@ export function PairingRouteSurface({ setCredential(event.currentTarget.value)} - placeholder="Paste a one-time token or pairing secret" + placeholder="One-time token or pairing secret" spellCheck={false} value={credential} /> -
- - {errorMessage ? ( -
+

{errorMessage} -

- ) : null} +

+
-
- -
- {describeSupportedMethods(auth.bootstrapMethods)} -
-
+ ); } @@ -150,8 +145,8 @@ export function HostedPairingRouteSurface() { ); const [message, setMessage] = useState(() => hostedPairingRequestRef.current - ? "Connecting to this backend." - : "This pairing link is missing its backend host or token.", + ? "Connecting to this environment." + : "This pairing link is missing its host or token.", ); const [canRetry, setCanRetry] = useState(false); const submitAttemptedRef = useRef(false); @@ -162,7 +157,7 @@ export function HostedPairingRouteSurface() { if (!request) { setStatus("error"); - setMessage("This pairing link is missing its backend host or token."); + setMessage("This pairing link is missing its host or token."); setCanRetry(false); return; } @@ -175,7 +170,7 @@ export function HostedPairingRouteSurface() { } setStatus("pairing"); - setMessage("Connecting to this backend."); + setMessage("Connecting to this environment."); setCanRetry(false); tokenSubmittedRef.current = true; @@ -193,7 +188,7 @@ export function HostedPairingRouteSurface() { setStatus("error"); setCanRetry(true); setMessage( - `${errorMessageFromUnknown(squashAtomCommandFailure(result))} If the backend accepted this one-time token, request a new pairing link before retrying.`, + `${errorMessageFromUnknown(squashAtomCommandFailure(result))} If the environment accepted this one-time token, request a new pairing link before retrying.`, ); }, [connectPairingEnvironment]); @@ -210,49 +205,43 @@ export function HostedPairingRouteSurface() { const request = hostedPairingRequestRef.current; return ( - - + {request ? ( -
- Host: {request.host} +
+ {request.host}
) : null} {status === "error" ? ( -
- Verify the backend is reachable from this browser, supports CORS for hosted clients, and - is served over HTTPS when opening this page from HTTPS. -
+

+ Check that the environment is reachable from this browser and served over HTTPS. +

) : null}
{status === "pairing" ? ( - + ) : canRetry ? ( - + ) : null} {status === "paired" ? ( - ) : null}
- + ); } @@ -269,24 +258,14 @@ function errorMessageFromUnknown(error: unknown): string { } function describeAuthGate(bootstrapMethods: ReadonlyArray): string { - if (bootstrapMethods.includes("desktop-bootstrap")) { - return "This environment expects a trusted pairing credential before the app can connect."; - } - - return "Enter a pairing token to start a session with this environment."; -} - -function describeSupportedMethods(bootstrapMethods: ReadonlyArray): string { - if ( - bootstrapMethods.includes("desktop-bootstrap") && - bootstrapMethods.includes("one-time-token") - ) { - return "Desktop-managed pairing and one-time pairing tokens are both accepted for this environment."; + const desktopManaged = bootstrapMethods.includes("desktop-bootstrap"); + if (desktopManaged && bootstrapMethods.includes("one-time-token")) { + return "Paste a pairing token, or open this environment from the desktop app."; } - if (bootstrapMethods.includes("desktop-bootstrap")) { - return "This environment is desktop-managed. Open it from the desktop app or paste a bootstrap credential if one was issued explicitly."; + if (desktopManaged) { + return "This environment is managed by the desktop app. Open it there, or paste a credential the desktop app issued."; } - return "This environment accepts one-time pairing tokens. Pairing links can open this page directly, or you can paste the token here."; + return "Paste a pairing token to connect this browser."; } diff --git a/apps/web/src/components/cloud/ConnectCliAuthSurface.tsx b/apps/web/src/components/cloud/ConnectCliAuthSurface.tsx index 4bae618a81e0..65a3e915f8aa 100644 --- a/apps/web/src/components/cloud/ConnectCliAuthSurface.tsx +++ b/apps/web/src/components/cloud/ConnectCliAuthSurface.tsx @@ -1,4 +1,4 @@ -import { useAuth, useClerk } from "@clerk/react"; +import { useAuth, useClerk, useUser } from "@clerk/react"; import { readConnectAuthorizeRequest } from "@t3tools/shared/connectAuth"; import { useCallback, useEffect, useRef, useState } from "react"; @@ -7,47 +7,28 @@ import { connectCliSignInRedirectUrl, } from "../../cloud/connectCliAuth"; import { isElectron } from "../../env"; -import { AuthSurfaceShell } from "../auth/AuthSurfaceShell"; +import { AuthSurfaceMessage, AuthSurfaceShell } from "../auth/AuthSurfaceShell"; import { resolveClerkSignInProps } from "../clerk/authRedirect"; import { Button } from "../ui/button"; -function ConnectCliAuthMessage({ - eyebrow, - title, - description, -}: { - readonly eyebrow?: string; - readonly title: string; - readonly description: string; -}) { - return ( - <> - {eyebrow ? ( -

{eyebrow}

- ) : null} -

{title}

-

{description}

- - ); +function ConnectCommand() { + return t3 connect; } -const invalidLinkMessage = { - eyebrow: "Authorization request", - title: "This connect link is incomplete", - description: - "The link is missing its authorization request. Re-run `t3 connect` in your terminal and open the freshly printed URL.", -} as const; +function useClerkAccountLabel(): string | null { + const { user } = useUser(); + return user?.primaryEmailAddress?.emailAddress ?? user?.username ?? null; +} /** - * /connect: the URL the CLI prints for the loopback flow. Waits for a Clerk - * session, then forwards the CLI's PKCE request to Clerk's authorize endpoint - * with the loopback redirect URI so the code returns straight to the waiting - * CLI. Headless hosts use Clerk's device authorization page instead. + * /connect forwards the CLI's PKCE request to Clerk with its loopback redirect. + * Headless hosts use Clerk's device authorization page instead. */ export function ConnectCliAuthorizeSurface() { const [request] = useState(() => readConnectAuthorizeRequest(new URL(window.location.href))); const clerk = useClerk(); const { isLoaded, isSignedIn } = useAuth(); + const accountLabel = useClerkAccountLabel(); const signInOpened = useRef(false); const redirecting = useRef(false); @@ -85,20 +66,41 @@ export function ConnectCliAuthorizeSurface() { if (!request) { return ( - + + The authorization request is missing or invalid. Copy the whole link, or run{" "} + again. + + } + /> ); } return ( - + Sending you to authorize T3 Connect + {accountLabel ? ( + <> + {" "} + as {accountLabel} + + ) : null} + . + + ) : ( + <> + Your terminal ran and is waiting. Sign in here and it finishes on + its own. + + ) } /> {isLoaded && !isSignedIn ? ( diff --git a/apps/web/src/components/sidebar/SidebarChrome.tsx b/apps/web/src/components/sidebar/SidebarChrome.tsx index af4bd8a1f5cb..16a1feac56a1 100644 --- a/apps/web/src/components/sidebar/SidebarChrome.tsx +++ b/apps/web/src/components/sidebar/SidebarChrome.tsx @@ -6,7 +6,7 @@ import { Link, useLocation, useNavigate } from "@tanstack/react-router"; import { useEnvironmentIdentificationMode } from "../../hooks/useSettings"; import { cn } from "../../lib/utils"; import { useEnvironments } from "../../state/environments"; -import { T3Wordmark } from "../T3Wordmark"; +import { T3CodeWordmark } from "../T3Wordmark"; import { resolveEnvironmentIdentificationPillLabel, resolveSidebarStageBackdropVariant, @@ -79,24 +79,10 @@ function SidebarBrand({ onBackdrop }: { onBackdrop: boolean }) { return ( - {/* Center the visible capitals, without the font's ascender/descender space. */} - - - - Code - - + ); } diff --git a/apps/web/src/components/ui/standalone-page.tsx b/apps/web/src/components/ui/standalone-page.tsx index f44ec18f6825..4a08127df8d1 100644 --- a/apps/web/src/components/ui/standalone-page.tsx +++ b/apps/web/src/components/ui/standalone-page.tsx @@ -1,44 +1,29 @@ import type { ReactNode } from "react"; -const backdropClassNames = { - pairing: - "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)]", - error: - "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)]", - brand: - "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)]", -}; - /** Shared page and card geometry for entry points outside the app shell. */ export function StandalonePage({ tone, - masthead, children, }: { - readonly tone: keyof typeof backdropClassNames; - readonly masthead?: ReactNode; + readonly tone: "plain" | "error"; readonly children: ReactNode; }) { return (
-
-
- {tone === "pairing" ? ( -
- ) : null} -
-
+ {tone === "error" ? ( +
+
+
+
+ ) : null}
- {masthead}
{children}