diff --git a/src/app/LauncherApp.tsx b/src/app/LauncherApp.tsx index 79187d8..f3a667c 100644 --- a/src/app/LauncherApp.tsx +++ b/src/app/LauncherApp.tsx @@ -587,6 +587,10 @@ export function LauncherApp() { state={inviteFlow ?? { status: "loading" }} currentEmail={authSession.user.email} onAccept={() => void handleAcceptInvite()} + onSwitchAccount={() => { + const returnTo = `${window.location.pathname}${window.location.search}${window.location.hash}`; + window.location.replace(`/auth/logout?global=1&returnTo=${encodeURIComponent(returnTo)}`); + }} onGoHome={() => { window.history.replaceState(null, "", "/"); window.location.replace("/"); @@ -740,23 +744,16 @@ function InviteFlowScreen({ state, currentEmail, onAccept, + onSwitchAccount, onGoHome, }: { state: InviteFlowState; currentEmail: string; onAccept: () => void; + onSwitchAccount: () => void; onGoHome: () => void; }) { const payload = "payload" in state ? state.payload : undefined; - const title = - state.status === "accepted" - ? "Доступ подключён" - : state.status === "error" - ? "Инвайт недоступен" - : "Приглашение в NODE.DC"; - const description = payload - ? `Клиент: ${payload.client.name}. Роль: ${membershipRoleLabel(payload.invite.role)}.` - : "Проверяем приглашение и платформенную сессию."; const emailMismatch = payload && payload.invite.email.toLowerCase() !== currentEmail.toLowerCase(); const inviteStatus = payload?.invite.status; const isAccepting = state.status === "accepting"; @@ -767,52 +764,46 @@ function InviteFlowScreen({ inviteStatus !== "expired" && inviteStatus !== "revoked" ); + const isTerminalInvite = inviteStatus === "accepted" || inviteStatus === "expired" || inviteStatus === "revoked"; + const details = payload + ? [ + `Рабочая область: ${payload.client.name}`, + `Роль: ${membershipRoleLabel(payload.invite.role)}`, + `Почта инвайта: ${payload.invite.email}`, + ] + : ["Проверяем приглашение и платформенную сессию"]; + const statusMessage = resolveInviteStatusMessage(state, Boolean(emailMismatch), inviteStatus); return ( -
-
-
- NODE.DC -

- Invite flow -

-

{title}

-

{description}

- {payload ? ( -

- Инвайт: {payload.invite.email}. Текущий вход: {currentEmail}. -

- ) : null} +
+ +
+
+
+

Работайте во всех измерениях.

+

Приглашение в NODE.DC.

+
+ +
+ {details.map((detail) => ( + {detail} + ))} +
+ + {statusMessage ?

{statusMessage}

: null} + {state.status === "error" ?

{state.message}

: null} + {emailMismatch ? ( -

- Нужно войти под почтой, на которую выписан инвайт. -

- ) : null} - {state.status === "error" ?

{state.message}

: null} - {state.status === "accepted" ? ( + + ) : state.status === "error" || state.status === "accepted" || isTerminalInvite ? ( ) : ( )}
@@ -821,6 +812,26 @@ function InviteFlowScreen({ ); } +function NodeDcAuthBrandHeader() { + return ( +
+ + NODE DC + +
+ ); +} + +function resolveInviteStatusMessage(state: InviteFlowState, emailMismatch: boolean, inviteStatus?: Invite["status"]) { + if (state.status === "loading") return "Проверяем приглашение."; + if (state.status === "accepting") return "Подключаем доступ к рабочей области."; + if (state.status === "accepted" || inviteStatus === "accepted") return "Доступ уже подключён."; + if (inviteStatus === "expired") return "Срок действия приглашения истёк."; + if (inviteStatus === "revoked") return "Приглашение отозвано."; + if (emailMismatch) return "Нужно войти под почтой, на которую выписан инвайт."; + return null; +} + function AuthStateScreen({ title, description, diff --git a/src/styles/globals.css b/src/styles/globals.css index c9360d4..15d72b8 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -125,6 +125,122 @@ code { background: #050506; } +.nodedc-auth-page { + --nodedc-auth-bg: #0e0f10; + --nodedc-auth-card-bg: rgba(9, 9, 12, 0.84); + --nodedc-auth-primary: rgb(195, 255, 102); + --nodedc-auth-primary-hover: rgb(218, 255, 139); + --nodedc-auth-on-primary: rgb(11, 17, 23); + --nodedc-auth-text-primary: #e4e6e7; + --nodedc-auth-text-secondary: #cacdce; + --nodedc-auth-text-placeholder: #959a9d; + background: var(--nodedc-auth-bg); + color: var(--nodedc-auth-text-primary); + font-synthesis: none; + -webkit-font-smoothing: antialiased; + text-rendering: geometricPrecision; +} + +.nodedc-auth-brand-shell { + position: absolute; + z-index: 80; + top: 0; + left: 50%; + width: min(100%, calc(100vw - var(--nodedc-shell-frame-gutter-x))); + padding: var(--nodedc-shell-padding-top) var(--nodedc-shell-padding-x) var(--nodedc-shell-padding-bottom); + transform: translateX(-50%); +} + +.nodedc-auth-page__main { + display: grid; + min-height: 100vh; + place-items: center; + padding: 0 1rem; +} + +.nodedc-auth-card { + display: grid; + width: min(100%, 28rem); + gap: 1.05rem; + overflow: hidden; + padding: 2.2rem; + border: 0; + border-radius: 1.9rem; + background: + linear-gradient(180deg, rgba(255, 255, 255, 0.048) 0%, rgba(255, 255, 255, 0.015) 100%), + var(--nodedc-auth-card-bg); + box-shadow: none; + backdrop-filter: blur(40px); + -webkit-backdrop-filter: blur(40px); +} + +.nodedc-auth-card__copy { + display: grid; + gap: 0.75rem; +} + +.nodedc-auth-card__copy h1, +.nodedc-auth-card__copy p { + max-width: 22rem; + margin: 0; + letter-spacing: -0.045em; + text-transform: none; + text-wrap: balance; +} + +.nodedc-auth-card__copy h1 { + color: var(--nodedc-auth-text-primary); + font-size: 2rem; + font-weight: 650; + line-height: 2.5rem; +} + +.nodedc-auth-card__copy p { + color: var(--nodedc-auth-text-placeholder); + font-size: 1.9rem; + font-weight: 650; + line-height: 2.5rem; +} + +.nodedc-invite-card__details { + display: grid; + gap: 0.45rem; + margin-top: 0.45rem; + color: var(--nodedc-auth-text-secondary); + font-size: 0.875rem; + line-height: 1.4; +} + +.nodedc-auth-card__status { + margin: 0; + color: var(--nodedc-auth-primary); + font-size: 0.75rem; + font-weight: 600; + line-height: 1rem; +} + +.nodedc-auth-card .button { + width: 100%; + min-height: 3.25rem; + margin-top: 0.45rem; + border-radius: 1.25rem; + background: var(--nodedc-auth-primary); + color: var(--nodedc-auth-on-primary); + font-size: 0.95rem; + font-weight: 600; + letter-spacing: -0.02em; +} + +.nodedc-auth-card .button:hover:not(:disabled), +.nodedc-auth-card .button:focus-visible:not(:disabled) { + background: var(--nodedc-auth-primary-hover); + color: var(--nodedc-auth-on-primary); +} + +.nodedc-auth-card .button:disabled { + opacity: 0.58; +} + .eyebrow { color: var(--text-muted); font-size: 0.72rem;