mirror of
https://github.com/nexu-io/open-design.git
synced 2026-10-27 09:51:12 +01:00
[codex] Add AMR sign-in to landing header (#4934)
* feat(landing): add AMR sign-in header * feat(landing): center sign-in popup, scrim the page, return home after login Improve the AMR sign-in popup flow on the landing header: - Center the vela login popup over the current window (multi-monitor aware) instead of letting the browser place it arbitrarily. - Dim the page with a focus scrim while the popup is open so it reads as a modal step; remove it on success, cancel, or timeout. - After a successful login, close the popup and return the main window to the localized Open Design homepage (in place when already there). - Fall back to a same-tab navigation when the popup is blocked. Applied to both the sub-page enhancer (header-enhancer.astro) and the homepage's inlined copy (index.astro). * fix(landing): normalize AMR home redirect --------- Co-authored-by: NJUHua <113895241+NJUHua@users.noreply.github.com>
This commit is contained in:
@@ -190,6 +190,191 @@
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
// Open Design Cloud (AMR) header account. Detects an existing cloud
|
||||
// session via the vela API, swaps the "Sign in" link for an avatar menu,
|
||||
// drives popup login + polling, and handles sign-out. All endpoints/URLs
|
||||
// arrive as `data-*` on `[data-amr-account]` (header.tsx) since this inline
|
||||
// script is not processed by Vite. Every network path fails closed to the
|
||||
// signed-out state: on a non-apex origin (preview/staging) or with
|
||||
// third-party cookies blocked, the credentialed call is rejected and we
|
||||
// simply keep showing "Sign in" rather than erroring.
|
||||
const account = document.querySelector('[data-amr-account]');
|
||||
if (account) {
|
||||
const api = (account.getAttribute('data-amr-api') || '').replace(/\/$/, '');
|
||||
const loginUrl = account.getAttribute('data-amr-login') || '';
|
||||
// Localized Open Design homepage — where the user lands after a
|
||||
// successful popup login ("回到官网首页"). Empty disables the redirect.
|
||||
const homeUrl = account.getAttribute('data-amr-home') || '';
|
||||
const signinEl = account.querySelector('[data-amr-signin]');
|
||||
const menuEl = account.querySelector('[data-amr-menu]');
|
||||
const avatarImg = account.querySelector('[data-amr-avatar]');
|
||||
const avatarFallback = account.querySelector('[data-amr-avatar-fallback]');
|
||||
const nameEl = account.querySelector('[data-amr-name]');
|
||||
const emailEl = account.querySelector('[data-amr-email]');
|
||||
const signoutBtn = account.querySelector('[data-amr-signout]');
|
||||
|
||||
const sessionUrl = api + '/api/auth/get-session';
|
||||
const signoutUrl = api + '/api/auth/sign-out';
|
||||
|
||||
const fetchSession = () =>
|
||||
fetch(sessionUrl, {
|
||||
credentials: 'include',
|
||||
headers: { Accept: 'application/json' },
|
||||
})
|
||||
.then((r) => (r.ok ? r.json() : null))
|
||||
.then((data) => (data && data.user ? data.user : null))
|
||||
.catch(() => null);
|
||||
|
||||
const initials = (user) => {
|
||||
const base = (user.name || user.email || '').trim();
|
||||
return base ? base[0].toUpperCase() : '·';
|
||||
};
|
||||
|
||||
const showSignedIn = (user) => {
|
||||
if (nameEl) nameEl.textContent = user.name || '';
|
||||
if (emailEl) emailEl.textContent = user.email || '';
|
||||
if (avatarImg && user.image) {
|
||||
avatarImg.setAttribute('src', user.image);
|
||||
avatarImg.removeAttribute('hidden');
|
||||
if (avatarFallback) avatarFallback.setAttribute('hidden', '');
|
||||
} else {
|
||||
if (avatarImg) avatarImg.setAttribute('hidden', '');
|
||||
if (avatarFallback) {
|
||||
avatarFallback.textContent = initials(user);
|
||||
avatarFallback.removeAttribute('hidden');
|
||||
}
|
||||
}
|
||||
if (signinEl) signinEl.setAttribute('hidden', '');
|
||||
if (menuEl) menuEl.removeAttribute('hidden');
|
||||
};
|
||||
|
||||
const showSignedOut = () => {
|
||||
if (menuEl) {
|
||||
menuEl.setAttribute('hidden', '');
|
||||
menuEl.removeAttribute('open');
|
||||
}
|
||||
if (signinEl) signinEl.removeAttribute('hidden');
|
||||
};
|
||||
|
||||
// Focus scrim shown on the OD page while the login popup is open, so
|
||||
// the popup reads as a modal step (the iframe-modal alternative is ruled
|
||||
// out by vela's X-Frame-Options + cross-site cookie + OAuth-in-iframe
|
||||
// limits). Mounted on demand and faded via `.is-active`.
|
||||
let overlayEl = null;
|
||||
const showOverlay = () => {
|
||||
if (overlayEl) return;
|
||||
overlayEl = document.createElement('div');
|
||||
overlayEl.className = 'nav-login-overlay';
|
||||
overlayEl.setAttribute('aria-hidden', 'true');
|
||||
document.body.appendChild(overlayEl);
|
||||
requestAnimationFrame(() => {
|
||||
if (overlayEl) overlayEl.classList.add('is-active');
|
||||
});
|
||||
};
|
||||
const hideOverlay = () => {
|
||||
if (!overlayEl) return;
|
||||
const el = overlayEl;
|
||||
overlayEl = null;
|
||||
el.classList.remove('is-active');
|
||||
const drop = () => el.remove();
|
||||
el.addEventListener('transitionend', drop, { once: true });
|
||||
setTimeout(drop, 400); // fallback if transitionend never fires
|
||||
};
|
||||
|
||||
// Send the main window back to the localized OD homepage after login.
|
||||
// Skipped when already on it so a homepage sign-in just swaps in place;
|
||||
// from a sub-page we navigate (the post-load probe re-renders the avatar).
|
||||
const goHome = () => {
|
||||
if (!homeUrl) return;
|
||||
const targetUrl = new URL(homeUrl, window.location.href);
|
||||
const here = window.location.pathname.replace(/\/+$/, '');
|
||||
const target = targetUrl.pathname.replace(/\/+$/, '');
|
||||
if (here !== target) window.location.assign(homeUrl);
|
||||
};
|
||||
|
||||
// One terminal success path for every login completion route.
|
||||
const completeSignIn = (user) => {
|
||||
hideOverlay();
|
||||
showSignedIn(user);
|
||||
goHome();
|
||||
};
|
||||
|
||||
// Initial silent probe for an existing session.
|
||||
fetchSession().then((user) => {
|
||||
if (user) showSignedIn(user);
|
||||
});
|
||||
|
||||
// Popup login + poll. `window.open` MUST run synchronously in the click
|
||||
// handler or the browser blocks the popup. The link keeps a real href so
|
||||
// that with JS disabled the click still navigates to the login page.
|
||||
if (signinEl && loginUrl) {
|
||||
signinEl.addEventListener('click', (ev) => {
|
||||
ev.preventDefault();
|
||||
// Center the popup over the current browser window (multi-monitor
|
||||
// aware: screenLeft/screenTop give the window's screen offset).
|
||||
const W = 480;
|
||||
const H = 720;
|
||||
const dx = window.screenLeft ?? window.screenX ?? 0;
|
||||
const dy = window.screenTop ?? window.screenY ?? 0;
|
||||
const vw = window.innerWidth || document.documentElement.clientWidth || screen.width;
|
||||
const vh = window.innerHeight || document.documentElement.clientHeight || screen.height;
|
||||
const left = Math.round(dx + (vw - W) / 2);
|
||||
const top = Math.round(dy + (vh - H) / 2);
|
||||
const popup = window.open(
|
||||
loginUrl,
|
||||
'od-amr-login',
|
||||
`width=${W},height=${H},left=${left},top=${top},menubar=no,toolbar=no,location=yes`,
|
||||
);
|
||||
// Popup blocked (or returned null): fall back to a same-tab navigation
|
||||
// to the login page so the click never dead-ends.
|
||||
if (!popup) {
|
||||
window.location.assign(loginUrl);
|
||||
return;
|
||||
}
|
||||
showOverlay();
|
||||
const started = Date.now();
|
||||
const POLL_MS = 2000;
|
||||
const MAX_MS = 5 * 60 * 1000;
|
||||
const timer = setInterval(() => {
|
||||
if (Date.now() - started > MAX_MS) {
|
||||
clearInterval(timer);
|
||||
hideOverlay();
|
||||
return;
|
||||
}
|
||||
if (popup.closed) {
|
||||
clearInterval(timer);
|
||||
// Re-check once: login may have completed just before close.
|
||||
fetchSession().then((user) => {
|
||||
if (user) completeSignIn(user);
|
||||
else hideOverlay();
|
||||
});
|
||||
return;
|
||||
}
|
||||
fetchSession().then((user) => {
|
||||
if (!user) return;
|
||||
clearInterval(timer);
|
||||
try {
|
||||
if (!popup.closed) popup.close();
|
||||
} catch {}
|
||||
completeSignIn(user);
|
||||
});
|
||||
}, POLL_MS);
|
||||
});
|
||||
}
|
||||
|
||||
if (signoutBtn) {
|
||||
signoutBtn.addEventListener('click', () => {
|
||||
fetch(signoutUrl, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: { Accept: 'application/json' },
|
||||
})
|
||||
.catch(() => {})
|
||||
.then(() => showSignedOut());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Helper used by every clipboard-write button (link copy, share
|
||||
// text copy). Tries the Clipboard API; falls back to a `prompt()`
|
||||
// window the user can manually copy from on browsers that block
|
||||
|
||||
@@ -28,6 +28,18 @@ const X_PROFILE = 'https://x.com/OpenDesignHQ';
|
||||
// dropdown entry, and the footer Partners column.
|
||||
const AMR_URL = 'https://open-design.ai/amr/';
|
||||
|
||||
// Open Design Cloud (AMR / vela) endpoints for the header sign-in module.
|
||||
// Production defaults; overridable at build time via PUBLIC_* env so a
|
||||
// preview/staging build can point at a non-prod cloud. These are surfaced to
|
||||
// the runtime via `data-*` on `.nav-account` because the auth logic lives in
|
||||
// `header-enhancer.astro`'s `<script is:inline>` (NOT processed by Vite, so it
|
||||
// cannot read `import.meta.env` itself).
|
||||
const env = import.meta.env as Record<string, string | undefined>;
|
||||
const AMR_API_BASE = env.PUBLIC_AMR_API_BASE ?? 'https://amr-api.open-design.ai';
|
||||
const AMR_LOGIN_URL = env.PUBLIC_AMR_LOGIN_URL ?? 'https://open-design.ai/amr/login';
|
||||
const AMR_CONSOLE_URL =
|
||||
env.PUBLIC_AMR_CONSOLE_URL ?? 'https://open-design.ai/amr?source=open_design';
|
||||
|
||||
// Solution → Use cases / Roles. Hrefs mirror upstream main's header 1:1 and
|
||||
// pair positionally with the localized `useCaseItems` / `roleItems` tuples.
|
||||
const USE_CASE_HREFS = [
|
||||
@@ -574,6 +586,65 @@ export function Header({
|
||||
>
|
||||
{headerCopy.download}
|
||||
</a>
|
||||
{/*
|
||||
Open Design Cloud (AMR) account entry. Renders BOTH states up front
|
||||
and lets `header-enhancer.astro` toggle them at runtime: the
|
||||
signed-out "Sign in" link is visible by default (so no-JS / pre-hydration
|
||||
shows a working login link), and the signed-in avatar menu stays
|
||||
`hidden` until the enhancer confirms a live cloud session via
|
||||
`GET {api}/api/auth/get-session`. Config flows through `data-*`
|
||||
because the enhancer script cannot read `import.meta.env`.
|
||||
*/}
|
||||
<div
|
||||
className='nav-account'
|
||||
data-amr-account
|
||||
data-amr-api={AMR_API_BASE}
|
||||
data-amr-login={AMR_LOGIN_URL}
|
||||
data-amr-console={AMR_CONSOLE_URL}
|
||||
data-amr-home={href('/')}
|
||||
>
|
||||
<a className='nav-signin' href={AMR_LOGIN_URL} data-amr-signin>
|
||||
{headerCopy.signIn}
|
||||
</a>
|
||||
<details className='nav-account-menu' data-amr-menu hidden>
|
||||
<summary
|
||||
className='nav-account-trigger'
|
||||
aria-label={headerCopy.accountAria}
|
||||
title={headerCopy.accountAria}
|
||||
>
|
||||
<img className='nav-avatar' alt='' data-amr-avatar />
|
||||
<span
|
||||
className='nav-avatar-fallback'
|
||||
data-amr-avatar-fallback
|
||||
aria-hidden='true'
|
||||
/>
|
||||
</summary>
|
||||
<div className='nav-account-dropdown' role='menu'>
|
||||
<div className='nav-account-id'>
|
||||
<span className='nav-account-name' data-amr-name />
|
||||
<span className='nav-account-email' data-amr-email />
|
||||
</div>
|
||||
<a
|
||||
className='nav-account-item'
|
||||
role='menuitem'
|
||||
href={AMR_CONSOLE_URL}
|
||||
target='_blank'
|
||||
rel='noreferrer noopener'
|
||||
data-amr-console-link
|
||||
>
|
||||
{headerCopy.menuConsole}
|
||||
</a>
|
||||
<button
|
||||
type='button'
|
||||
className='nav-account-item nav-account-signout'
|
||||
role='menuitem'
|
||||
data-amr-signout
|
||||
>
|
||||
{headerCopy.menuSignOut}
|
||||
</button>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/*
|
||||
|
||||
@@ -452,6 +452,182 @@ body::before {
|
||||
.locale-menu { animation: none; }
|
||||
.locale-trigger-caret { transition: none; }
|
||||
}
|
||||
/* ---------- Open Design Cloud (AMR) header account ---------- */
|
||||
/*
|
||||
* Two mutually-exclusive states share this slot: the signed-out "Sign in"
|
||||
* link and the signed-in avatar menu. Both render in the markup; the avatar
|
||||
* `<details>` carries `hidden` until `header-enhancer.astro` confirms a live
|
||||
* cloud session, then toggles `hidden` on each. `[hidden]` on either element
|
||||
* fully removes it from layout, so only one is ever visible.
|
||||
*/
|
||||
.nav-account {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
/* Sign in — an outlined pill so it sits beside the filled Download CTA
|
||||
without competing with it as a second solid button. */
|
||||
.nav-signin {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 7px 15px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 999px;
|
||||
background: transparent;
|
||||
color: var(--ink);
|
||||
font-family: var(--sans);
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
transition: border-color 160ms ease, color 160ms ease;
|
||||
}
|
||||
.nav-signin:hover,
|
||||
.nav-signin:focus-visible {
|
||||
border-color: var(--ink);
|
||||
color: var(--ink);
|
||||
}
|
||||
.nav-account [hidden] { display: none !important; }
|
||||
/* Sign-in focus scrim — dims the page (but not the header chrome at z-index
|
||||
50/60) while the vela login popup is open, so the popup reads as a modal
|
||||
step. Mounted/removed by the account block in `header-enhancer.astro`;
|
||||
`.is-active` drives the fade. */
|
||||
.nav-login-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 40;
|
||||
background: rgba(8, 10, 14, 0.55);
|
||||
-webkit-backdrop-filter: blur(2px);
|
||||
backdrop-filter: blur(2px);
|
||||
opacity: 0;
|
||||
transition: opacity 200ms cubic-bezier(0.23, 1, 0.32, 1);
|
||||
}
|
||||
.nav-login-overlay.is-active { opacity: 1; }
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.nav-login-overlay { transition: none; }
|
||||
}
|
||||
/* Avatar menu — a borderless <details> whose summary is the avatar disc. */
|
||||
.nav-account-menu {
|
||||
position: relative;
|
||||
}
|
||||
.nav-account-trigger {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
list-style: none;
|
||||
border-radius: 999px;
|
||||
}
|
||||
.nav-account-trigger::-webkit-details-marker { display: none; }
|
||||
.nav-account-trigger:focus-visible {
|
||||
outline: 2px solid var(--coral);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
/* Circular avatar — a disc reads unmistakably as a user avatar. Dark tile +
|
||||
single initial mirrors Open Design Cloud's identity styling. */
|
||||
.nav-avatar,
|
||||
.nav-avatar-fallback {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
object-fit: cover;
|
||||
}
|
||||
.nav-avatar-fallback {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #1f1f1f;
|
||||
color: #fafafa;
|
||||
font-family: var(--sans);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
}
|
||||
/* Dropdown panel — mirrors `.locale-menu` so both header menus read as one
|
||||
panel style (same border, radius, shadow, and entrance). */
|
||||
.nav-account-dropdown {
|
||||
position: absolute;
|
||||
top: calc(100% + var(--nav-flyout-gap, 12px));
|
||||
right: 0;
|
||||
z-index: 60;
|
||||
min-width: 240px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
padding: 8px;
|
||||
background: var(--paper);
|
||||
border: 1px solid rgba(26, 26, 26, 0.12);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 12px 36px rgba(26, 26, 26, 0.08);
|
||||
font-family: var(--sans);
|
||||
letter-spacing: normal;
|
||||
text-transform: none;
|
||||
animation: locale-menu-in 180ms cubic-bezier(0.23, 1, 0.32, 1);
|
||||
}
|
||||
/* Hover bridge across the gap, like `.locale-menu::before`. */
|
||||
.nav-account-dropdown::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: calc(-1 * var(--nav-flyout-gap, 12px));
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: var(--nav-flyout-gap, 12px);
|
||||
}
|
||||
.nav-account-id {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
padding: 8px 12px 10px;
|
||||
margin-bottom: 4px;
|
||||
border-bottom: 1px solid var(--line-soft);
|
||||
}
|
||||
.nav-account-name {
|
||||
color: var(--ink);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.nav-account-name:empty { display: none; }
|
||||
.nav-account-email {
|
||||
color: var(--ink-faint);
|
||||
font-size: 11.5px;
|
||||
line-height: 1.2;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.nav-account-email:empty { display: none; }
|
||||
.nav-account-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
color: var(--ink);
|
||||
font-family: var(--sans);
|
||||
font-size: 12.5px;
|
||||
line-height: 1.2;
|
||||
text-align: left;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
transition: background 140ms ease, color 140ms ease;
|
||||
}
|
||||
.nav-account-item:hover,
|
||||
.nav-account-item:focus-visible {
|
||||
background: var(--coral-tint);
|
||||
color: var(--ink);
|
||||
}
|
||||
.nav-account-signout {
|
||||
color: var(--ink-mute);
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.nav-account-dropdown { animation: none; }
|
||||
}
|
||||
.topbar .pulse {
|
||||
width: 6px; height: 6px;
|
||||
border-radius: 50%;
|
||||
|
||||
@@ -226,6 +226,14 @@ export interface HeaderCopy {
|
||||
starAria: string;
|
||||
starTitle: string;
|
||||
starPrefix: string;
|
||||
/** Open Design Cloud (AMR) account entry — see header-enhancer.astro. */
|
||||
signIn: string;
|
||||
/** aria-label for the signed-in avatar / account menu trigger. */
|
||||
accountAria: string;
|
||||
/** Avatar dropdown: open the Cloud console (AMR app). */
|
||||
menuConsole: string;
|
||||
/** Avatar dropdown: sign out of the Cloud session. */
|
||||
menuSignOut: string;
|
||||
}
|
||||
|
||||
export interface HeaderProductMenuCopy {
|
||||
@@ -1635,6 +1643,10 @@ const COMMON_COPY: Record<LandingLocaleCode, CommonCopy> = {
|
||||
starAria: 'Star Open Design on GitHub',
|
||||
starTitle: 'Click to star us on GitHub',
|
||||
starPrefix: 'Star',
|
||||
signIn: 'Sign in',
|
||||
accountAria: 'Account menu',
|
||||
menuConsole: 'Console',
|
||||
menuSignOut: 'Sign out',
|
||||
},
|
||||
},
|
||||
zh: {
|
||||
@@ -1677,6 +1689,10 @@ const COMMON_COPY: Record<LandingLocaleCode, CommonCopy> = {
|
||||
starAria: '在 GitHub 为 Open Design 点 Star',
|
||||
starTitle: '去 GitHub 点 Star',
|
||||
starPrefix: 'Star',
|
||||
signIn: '登录',
|
||||
accountAria: '账户菜单',
|
||||
menuConsole: '控制台',
|
||||
menuSignOut: '退出登录',
|
||||
},
|
||||
},
|
||||
'zh-tw': {
|
||||
@@ -1719,6 +1735,10 @@ const COMMON_COPY: Record<LandingLocaleCode, CommonCopy> = {
|
||||
starAria: '在 GitHub 為 Open Design 按 Star',
|
||||
starTitle: '去 GitHub 按 Star',
|
||||
starPrefix: '點星',
|
||||
signIn: '登入',
|
||||
accountAria: '帳戶選單',
|
||||
menuConsole: '控制台',
|
||||
menuSignOut: '登出',
|
||||
},
|
||||
},
|
||||
ja: {
|
||||
@@ -1761,6 +1781,10 @@ const COMMON_COPY: Record<LandingLocaleCode, CommonCopy> = {
|
||||
starAria: 'GitHub で Open Design にスター',
|
||||
starTitle: 'GitHub でスターする',
|
||||
starPrefix: 'スター',
|
||||
signIn: 'ログイン',
|
||||
accountAria: 'アカウントメニュー',
|
||||
menuConsole: 'コンソール',
|
||||
menuSignOut: 'ログアウト',
|
||||
},
|
||||
},
|
||||
ko: {
|
||||
@@ -1803,6 +1827,10 @@ const COMMON_COPY: Record<LandingLocaleCode, CommonCopy> = {
|
||||
starAria: 'GitHub에서 Open Design에 스타 주기',
|
||||
starTitle: 'GitHub에서 스타 주기',
|
||||
starPrefix: '스타',
|
||||
signIn: '로그인',
|
||||
accountAria: '계정 메뉴',
|
||||
menuConsole: '콘솔',
|
||||
menuSignOut: '로그아웃',
|
||||
},
|
||||
},
|
||||
de: {
|
||||
@@ -1845,6 +1873,10 @@ const COMMON_COPY: Record<LandingLocaleCode, CommonCopy> = {
|
||||
starAria: 'Open Design auf GitHub mit Stern markieren',
|
||||
starTitle: 'Auf GitHub sternen',
|
||||
starPrefix: 'Stern',
|
||||
signIn: 'Anmelden',
|
||||
accountAria: 'Kontomenü',
|
||||
menuConsole: 'Konsole',
|
||||
menuSignOut: 'Abmelden',
|
||||
},
|
||||
},
|
||||
fr: {
|
||||
@@ -1887,6 +1919,10 @@ const COMMON_COPY: Record<LandingLocaleCode, CommonCopy> = {
|
||||
starAria: 'Ajouter une étoile à Open Design sur GitHub',
|
||||
starTitle: 'Mettre une étoile sur GitHub',
|
||||
starPrefix: 'Étoile',
|
||||
signIn: 'Se connecter',
|
||||
accountAria: 'Menu du compte',
|
||||
menuConsole: 'Console',
|
||||
menuSignOut: 'Se déconnecter',
|
||||
},
|
||||
},
|
||||
ru: {
|
||||
@@ -1929,6 +1965,10 @@ const COMMON_COPY: Record<LandingLocaleCode, CommonCopy> = {
|
||||
starAria: 'Поставить звезду Open Design на GitHub',
|
||||
starTitle: 'Поставить звезду на GitHub',
|
||||
starPrefix: 'Звезда',
|
||||
signIn: 'Войти',
|
||||
accountAria: 'Меню аккаунта',
|
||||
menuConsole: 'Консоль',
|
||||
menuSignOut: 'Выйти',
|
||||
},
|
||||
},
|
||||
es: {
|
||||
@@ -1971,6 +2011,10 @@ const COMMON_COPY: Record<LandingLocaleCode, CommonCopy> = {
|
||||
starAria: 'Dar Star a Open Design en GitHub',
|
||||
starTitle: 'Dar Star en GitHub',
|
||||
starPrefix: 'Estrella',
|
||||
signIn: 'Iniciar sesión',
|
||||
accountAria: 'Menú de cuenta',
|
||||
menuConsole: 'Consola',
|
||||
menuSignOut: 'Cerrar sesión',
|
||||
},
|
||||
},
|
||||
'pt-br': {
|
||||
@@ -2013,6 +2057,10 @@ const COMMON_COPY: Record<LandingLocaleCode, CommonCopy> = {
|
||||
starAria: 'Dar Star no Open Design no GitHub',
|
||||
starTitle: 'Dar Star no GitHub',
|
||||
starPrefix: 'Estrela',
|
||||
signIn: 'Entrar',
|
||||
accountAria: 'Menu da conta',
|
||||
menuConsole: 'Console',
|
||||
menuSignOut: 'Sair',
|
||||
},
|
||||
},
|
||||
it: {
|
||||
@@ -2055,6 +2103,10 @@ const COMMON_COPY: Record<LandingLocaleCode, CommonCopy> = {
|
||||
starAria: 'Metti una Star a Open Design su GitHub',
|
||||
starTitle: 'Metti una Star su GitHub',
|
||||
starPrefix: 'Stella',
|
||||
signIn: 'Accedi',
|
||||
accountAria: 'Menu account',
|
||||
menuConsole: 'Console',
|
||||
menuSignOut: 'Esci',
|
||||
},
|
||||
},
|
||||
vi: {
|
||||
@@ -2097,6 +2149,10 @@ const COMMON_COPY: Record<LandingLocaleCode, CommonCopy> = {
|
||||
starAria: 'Star Open Design trên GitHub',
|
||||
starTitle: 'Star trên GitHub',
|
||||
starPrefix: 'Sao',
|
||||
signIn: 'Đăng nhập',
|
||||
accountAria: 'Menu tài khoản',
|
||||
menuConsole: 'Bảng điều khiển',
|
||||
menuSignOut: 'Đăng xuất',
|
||||
},
|
||||
},
|
||||
pl: {
|
||||
@@ -2139,6 +2195,10 @@ const COMMON_COPY: Record<LandingLocaleCode, CommonCopy> = {
|
||||
starAria: 'Daj gwiazdkę Open Design na GitHubie',
|
||||
starTitle: 'Daj gwiazdkę na GitHubie',
|
||||
starPrefix: 'Gwiazdka',
|
||||
signIn: 'Zaloguj się',
|
||||
accountAria: 'Menu konta',
|
||||
menuConsole: 'Konsola',
|
||||
menuSignOut: 'Wyloguj się',
|
||||
},
|
||||
},
|
||||
id: {
|
||||
@@ -2181,6 +2241,10 @@ const COMMON_COPY: Record<LandingLocaleCode, CommonCopy> = {
|
||||
starAria: 'Beri Star Open Design di GitHub',
|
||||
starTitle: 'Beri Star di GitHub',
|
||||
starPrefix: 'Bintang',
|
||||
signIn: 'Masuk',
|
||||
accountAria: 'Menu akun',
|
||||
menuConsole: 'Konsol',
|
||||
menuSignOut: 'Keluar',
|
||||
},
|
||||
},
|
||||
nl: {
|
||||
@@ -2223,6 +2287,10 @@ const COMMON_COPY: Record<LandingLocaleCode, CommonCopy> = {
|
||||
starAria: 'Geef Open Design een Star op GitHub',
|
||||
starTitle: 'Star op GitHub',
|
||||
starPrefix: 'Ster',
|
||||
signIn: 'Inloggen',
|
||||
accountAria: 'Accountmenu',
|
||||
menuConsole: 'Console',
|
||||
menuSignOut: 'Uitloggen',
|
||||
},
|
||||
},
|
||||
ar: {
|
||||
@@ -2265,6 +2333,10 @@ const COMMON_COPY: Record<LandingLocaleCode, CommonCopy> = {
|
||||
starAria: 'ضع نجمة لـ Open Design على GitHub',
|
||||
starTitle: 'ضع نجمة على GitHub',
|
||||
starPrefix: 'نجمة',
|
||||
signIn: 'تسجيل الدخول',
|
||||
accountAria: 'قائمة الحساب',
|
||||
menuConsole: 'لوحة التحكم',
|
||||
menuSignOut: 'تسجيل الخروج',
|
||||
},
|
||||
},
|
||||
tr: {
|
||||
@@ -2307,6 +2379,10 @@ const COMMON_COPY: Record<LandingLocaleCode, CommonCopy> = {
|
||||
starAria: "GitHub'da Open Design'a Star ver",
|
||||
starTitle: "GitHub'da Star ver",
|
||||
starPrefix: 'Yıldız',
|
||||
signIn: 'Giriş yap',
|
||||
accountAria: 'Hesap menüsü',
|
||||
menuConsole: 'Konsol',
|
||||
menuSignOut: 'Çıkış yap',
|
||||
},
|
||||
},
|
||||
uk: {
|
||||
@@ -2349,6 +2425,10 @@ const COMMON_COPY: Record<LandingLocaleCode, CommonCopy> = {
|
||||
starAria: 'Поставити зірку Open Design на GitHub',
|
||||
starTitle: 'Поставити зірку на GitHub',
|
||||
starPrefix: 'Зірка',
|
||||
signIn: 'Увійти',
|
||||
accountAria: 'Меню облікового запису',
|
||||
menuConsole: 'Консоль',
|
||||
menuSignOut: 'Вийти',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1428,6 +1428,181 @@ const matterRuntime = readFileSync(
|
||||
if (ev.key === 'Escape') setNavOpen(false);
|
||||
});
|
||||
})();
|
||||
// Open Design Cloud (AMR) header account. Same as `enhanceMobileNav`
|
||||
// above, this mirrors `header-enhancer.astro` because the homepage does
|
||||
// not include that enhancer. Detect a live cloud session, swap the
|
||||
// "Sign in" link for the avatar menu, drive popup login + polling, and
|
||||
// sign out. Everything fails closed to signed-out (non-apex origin or
|
||||
// blocked third-party cookies just keep "Sign in" showing).
|
||||
(function enhanceAmrAccount() {
|
||||
const account = document.querySelector('[data-amr-account]');
|
||||
if (!account) return;
|
||||
const api = (account.getAttribute('data-amr-api') || '').replace(/\/$/, '');
|
||||
const loginUrl = account.getAttribute('data-amr-login') || '';
|
||||
// Localized OD homepage — post-login destination ("回到官网首页").
|
||||
const homeUrl = account.getAttribute('data-amr-home') || '';
|
||||
const signinEl = account.querySelector('[data-amr-signin]');
|
||||
const menuEl = account.querySelector('[data-amr-menu]');
|
||||
const avatarImg = account.querySelector('[data-amr-avatar]');
|
||||
const avatarFallback = account.querySelector('[data-amr-avatar-fallback]');
|
||||
const nameEl = account.querySelector('[data-amr-name]');
|
||||
const emailEl = account.querySelector('[data-amr-email]');
|
||||
const signoutBtn = account.querySelector('[data-amr-signout]');
|
||||
|
||||
const sessionUrl = api + '/api/auth/get-session';
|
||||
const signoutUrl = api + '/api/auth/sign-out';
|
||||
|
||||
const fetchSession = () =>
|
||||
fetch(sessionUrl, {
|
||||
credentials: 'include',
|
||||
headers: { Accept: 'application/json' },
|
||||
})
|
||||
.then((r) => (r.ok ? r.json() : null))
|
||||
.then((data) => (data && data.user ? data.user : null))
|
||||
.catch(() => null);
|
||||
|
||||
const initials = (user) => {
|
||||
const base = (user.name || user.email || '').trim();
|
||||
return base ? base[0].toUpperCase() : '·';
|
||||
};
|
||||
|
||||
const showSignedIn = (user) => {
|
||||
if (nameEl) nameEl.textContent = user.name || '';
|
||||
if (emailEl) emailEl.textContent = user.email || '';
|
||||
if (avatarImg && user.image) {
|
||||
avatarImg.setAttribute('src', user.image);
|
||||
avatarImg.removeAttribute('hidden');
|
||||
if (avatarFallback) avatarFallback.setAttribute('hidden', '');
|
||||
} else {
|
||||
if (avatarImg) avatarImg.setAttribute('hidden', '');
|
||||
if (avatarFallback) {
|
||||
avatarFallback.textContent = initials(user);
|
||||
avatarFallback.removeAttribute('hidden');
|
||||
}
|
||||
}
|
||||
if (signinEl) signinEl.setAttribute('hidden', '');
|
||||
if (menuEl) menuEl.removeAttribute('hidden');
|
||||
};
|
||||
|
||||
const showSignedOut = () => {
|
||||
if (menuEl) {
|
||||
menuEl.setAttribute('hidden', '');
|
||||
menuEl.removeAttribute('open');
|
||||
}
|
||||
if (signinEl) signinEl.removeAttribute('hidden');
|
||||
};
|
||||
|
||||
// Focus scrim shown while the vela login popup is open (popup is the
|
||||
// chosen modal mechanism; iframe is ruled out by X-Frame-Options +
|
||||
// cross-site cookies + OAuth-in-iframe). Faded via `.is-active`.
|
||||
let overlayEl = null;
|
||||
const showOverlay = () => {
|
||||
if (overlayEl) return;
|
||||
overlayEl = document.createElement('div');
|
||||
overlayEl.className = 'nav-login-overlay';
|
||||
overlayEl.setAttribute('aria-hidden', 'true');
|
||||
document.body.appendChild(overlayEl);
|
||||
requestAnimationFrame(() => {
|
||||
if (overlayEl) overlayEl.classList.add('is-active');
|
||||
});
|
||||
};
|
||||
const hideOverlay = () => {
|
||||
if (!overlayEl) return;
|
||||
const el = overlayEl;
|
||||
overlayEl = null;
|
||||
el.classList.remove('is-active');
|
||||
const drop = () => el.remove();
|
||||
el.addEventListener('transitionend', drop, { once: true });
|
||||
setTimeout(drop, 400);
|
||||
};
|
||||
|
||||
// Return the main window to the localized OD homepage after login;
|
||||
// a homepage sign-in just swaps in place (already on it).
|
||||
const goHome = () => {
|
||||
if (!homeUrl) return;
|
||||
const targetUrl = new URL(homeUrl, window.location.href);
|
||||
const here = window.location.pathname.replace(/\/+$/, '');
|
||||
const target = targetUrl.pathname.replace(/\/+$/, '');
|
||||
if (here !== target) window.location.assign(homeUrl);
|
||||
};
|
||||
|
||||
// Single terminal success path for every login completion route.
|
||||
const completeSignIn = (user) => {
|
||||
hideOverlay();
|
||||
showSignedIn(user);
|
||||
goHome();
|
||||
};
|
||||
|
||||
// Initial silent probe for an existing session.
|
||||
fetchSession().then((user) => {
|
||||
if (user) showSignedIn(user);
|
||||
});
|
||||
|
||||
if (signinEl && loginUrl) {
|
||||
signinEl.addEventListener('click', (ev) => {
|
||||
ev.preventDefault();
|
||||
// Center the popup over the current browser window (multi-monitor
|
||||
// aware via screenLeft/screenTop).
|
||||
const W = 480;
|
||||
const H = 720;
|
||||
const dx = window.screenLeft ?? window.screenX ?? 0;
|
||||
const dy = window.screenTop ?? window.screenY ?? 0;
|
||||
const vw = window.innerWidth || document.documentElement.clientWidth || screen.width;
|
||||
const vh = window.innerHeight || document.documentElement.clientHeight || screen.height;
|
||||
const left = Math.round(dx + (vw - W) / 2);
|
||||
const top = Math.round(dy + (vh - H) / 2);
|
||||
const popup = window.open(
|
||||
loginUrl,
|
||||
'od-amr-login',
|
||||
`width=${W},height=${H},left=${left},top=${top},menubar=no,toolbar=no,location=yes`,
|
||||
);
|
||||
// Popup blocked: fall back to a same-tab navigation.
|
||||
if (!popup) {
|
||||
window.location.assign(loginUrl);
|
||||
return;
|
||||
}
|
||||
showOverlay();
|
||||
const started = Date.now();
|
||||
const POLL_MS = 2000;
|
||||
const MAX_MS = 5 * 60 * 1000;
|
||||
const timer = setInterval(() => {
|
||||
if (Date.now() - started > MAX_MS) {
|
||||
clearInterval(timer);
|
||||
hideOverlay();
|
||||
return;
|
||||
}
|
||||
if (popup.closed) {
|
||||
clearInterval(timer);
|
||||
fetchSession().then((user) => {
|
||||
if (user) completeSignIn(user);
|
||||
else hideOverlay();
|
||||
});
|
||||
return;
|
||||
}
|
||||
fetchSession().then((user) => {
|
||||
if (!user) return;
|
||||
clearInterval(timer);
|
||||
try {
|
||||
if (!popup.closed) popup.close();
|
||||
} catch {}
|
||||
completeSignIn(user);
|
||||
});
|
||||
}, POLL_MS);
|
||||
});
|
||||
}
|
||||
|
||||
if (signoutBtn) {
|
||||
signoutBtn.addEventListener('click', () => {
|
||||
fetch(signoutUrl, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: { Accept: 'application/json' },
|
||||
})
|
||||
.catch(() => {})
|
||||
.then(() => showSignedOut());
|
||||
});
|
||||
}
|
||||
})();
|
||||
// Product and Resources are category-label <button class="nav-trigger">
|
||||
// triggers (not links, so they never navigate) whose dropdown is
|
||||
// revealed by the same pure-CSS :hover / :focus-within rule as the hub
|
||||
|
||||
Reference in New Issue
Block a user