Files
2026-07-13 12:22:33 +08:00

738 lines
27 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { localeCatalog, localeMessages } from './site-locales.mjs';
const root = document.documentElement;
let activeMessages = localeMessages.en;
const HERO_MOBILE_MAX = 768;
const HERO_TABLET_MAX = 1023;
const HERO_DESKTOP_MIN = 1024;
const MOCKUP_SCREEN_MARGIN_RIGHT = 50;
const MOCKUP_SCREEN_MARGIN_RIGHT_TABLET = 15;
const MOCKUP_ARTBOARD_RATIO = 1.18;
/* Tablet MacBook X tune @ 1001px, scale 1.2 */
const HERO_TABLET_MOCKUP_X_CORRECTION = -9;
const HERO_MOBILE_MOCKUP_TUNE_SCALE = 0.99;
/* Mobile MacBook X tune @ 490px, right-anchored full-bleed stage */
const HERO_MOBILE_MOCKUP_X_CORRECTION = -32;
const HERO_ARTBOARD = { width: 6111, height: 3239 };
/* Figma @ 6111×3239 — menubar anchor 130 top / 150 right; scale 0.94 (desktop) / 0.82 (mobile) */
const HERO_MENUBAR_BASE = {
figmaTop: 130,
figmaRight: 150,
scale: 0.94,
mobileScale: 0.82,
iconScale: 1.28,
};
/* Visual correction vs computed base (tune @ 1920 → 5.25%, 3.8%, scale ×1.55) */
const HERO_MENUBAR_CORRECTION = {
top: 5.25 / ((HERO_MENUBAR_BASE.figmaTop / HERO_ARTBOARD.height) * 100),
right: 3.8 / ((HERO_MENUBAR_BASE.figmaRight / HERO_ARTBOARD.width) * 100),
scale: 1.55,
};
function getMenubarComputed() {
const topPx = HERO_MENUBAR_BASE.figmaTop * HERO_MENUBAR_CORRECTION.top;
const rightPx = HERO_MENUBAR_BASE.figmaRight * HERO_MENUBAR_CORRECTION.right;
const scale = HERO_MENUBAR_BASE.scale * HERO_MENUBAR_CORRECTION.scale;
const mobileScale = HERO_MENUBAR_BASE.mobileScale * HERO_MENUBAR_CORRECTION.scale;
return {
topPx,
rightPx,
anchorTop: `${artboardPxToTopPercent(topPx).toFixed(2)}%`,
marginRight: `${artboardPxToRightPercent(rightPx).toFixed(2)}%`,
scale: Math.round(scale * 1000) / 1000,
mobileScale: Math.round(mobileScale * 1000) / 1000,
};
}
const HERO_FIGMA_SCREEN = { paddingTop: 125, paddingRight: 141 };
const HERO_FIGMA_PANEL = { gapBelowMenubar: 20, insetBeyondPaddingRight: 30 };
const HERO_PANEL_WIDTH_ON_SCREEN = 0.191;
function getMenubarRenderedHeightPx() {
const emPx = 0.52 * (HERO_ARTBOARD.width / 100);
return emPx * HERO_MENUBAR_BASE.iconScale * getMenubarComputed().scale;
}
function getPanelBasePx() {
const menubar = getMenubarComputed();
const menubarHeight = (menubar.topPx - HERO_FIGMA_SCREEN.paddingTop) + getMenubarRenderedHeightPx();
return {
topPx: HERO_FIGMA_SCREEN.paddingTop + menubarHeight + HERO_FIGMA_PANEL.gapBelowMenubar,
rightPx: HERO_FIGMA_SCREEN.paddingRight + HERO_FIGMA_PANEL.insetBeyondPaddingRight,
};
}
/* SVG inner-screen geometry (panel width still % of inner screen) */
const HERO_SCREEN_GEOMETRY = {
top: 124.92 / 3239,
width: (5970.48 - 140.89) / 6111,
height: (3239 - 124.92) / 3239,
rightInset: (6111 - 5970.48) / 6111,
};
function artboardPxToTopPercent(px) {
return (px / HERO_ARTBOARD.height) * 100;
}
function artboardPxToRightPercent(px) {
return (px / HERO_ARTBOARD.width) * 100;
}
const HERO_PANEL_BASE = { popoverScale: 0.91, mobilePopoverScale: 1.01 };
/* Visual correction vs computed base (tune @ 1920 → 8.5%, 3.7%, scale 0.85) */
const HERO_PANEL_CORRECTION = (() => {
const base = getPanelBasePx();
return {
top: 8.5 / artboardPxToTopPercent(base.topPx),
right: 3.7 / artboardPxToRightPercent(base.rightPx),
scale: 0.85 / HERO_PANEL_BASE.popoverScale,
};
})();
/* Tablet-only correction (tune @ 1020 → top 9.5%, scale 1.35) */
const HERO_PANEL_TABLET_CORRECTION = (() => {
const base = getPanelBasePx();
return {
top: 9.5 / artboardPxToTopPercent(base.topPx),
scale: 1.35,
};
})();
function getPanelComputed() {
const base = getPanelBasePx();
const topPx = base.topPx * HERO_PANEL_CORRECTION.top;
const rightPx = base.rightPx * HERO_PANEL_CORRECTION.right;
const scale = Math.round(HERO_PANEL_BASE.popoverScale * HERO_PANEL_CORRECTION.scale * 1000) / 1000;
const mobileScale = Math.round(HERO_PANEL_BASE.mobilePopoverScale * HERO_PANEL_CORRECTION.scale * 1000) / 1000;
return {
topPx,
rightPx,
panelTop: `${artboardPxToTopPercent(topPx).toFixed(2)}%`,
panelMarginRight: `${artboardPxToRightPercent(rightPx).toFixed(2)}%`,
panelWidth: `${(HERO_PANEL_WIDTH_ON_SCREEN * HERO_SCREEN_GEOMETRY.width * 100).toFixed(2)}%`,
scale,
mobileScale,
};
}
/* Mobile-only correction (tune @ 490 → top/right/scale; @ 444 → width 20%) */
const HERO_PANEL_MOBILE_CORRECTION = (() => {
const panel = getPanelComputed();
return {
top: 9.15 / parseFloat(panel.panelTop),
right: 6 / parseFloat(panel.panelMarginRight),
width: 20 / parseFloat(panel.panelWidth),
scale: 1.39 / panel.mobileScale,
};
})();
function getMobilePanelComputed() {
const base = getPanelBasePx();
const panel = getPanelComputed();
const topPx = base.topPx * HERO_PANEL_MOBILE_CORRECTION.top;
return {
panelTop: `${artboardPxToTopPercent(topPx).toFixed(2)}%`,
panelMarginRight: `${(parseFloat(panel.panelMarginRight) * HERO_PANEL_MOBILE_CORRECTION.right).toFixed(2)}%`,
panelWidth: `${(parseFloat(panel.panelWidth) * HERO_PANEL_MOBILE_CORRECTION.width).toFixed(2)}%`,
scale: Math.round(panel.mobileScale * HERO_PANEL_MOBILE_CORRECTION.scale * 1000) / 1000,
};
}
function getTabletPanelComputed() {
const base = getPanelBasePx();
const panel = getPanelComputed();
const topPx = base.topPx * HERO_PANEL_TABLET_CORRECTION.top;
return {
panelTop: `${artboardPxToTopPercent(topPx).toFixed(2)}%`,
panelMarginRight: panel.panelMarginRight,
panelWidth: panel.panelWidth,
scale: HERO_PANEL_TABLET_CORRECTION.scale,
};
}
function getHudDefaultsFromGeometry() {
const menubar = getMenubarComputed();
const panel = getPanelComputed();
const tabletPanel = getTabletPanelComputed();
const mobilePanel = getMobilePanelComputed();
return {
'--hud-anchor-top': menubar.anchorTop,
'--hud-margin-right': menubar.marginRight,
'--menubar-scale': String(menubar.scale),
'--mobile-hud-anchor-top': menubar.anchorTop,
'--mobile-hud-margin-right': menubar.marginRight,
'--mobile-menubar-scale': String(menubar.mobileScale),
'--hud-panel-top': panel.panelTop,
'--hud-panel-margin-right': panel.panelMarginRight,
'--hud-panel-width': panel.panelWidth,
'--popover-scale': String(panel.scale),
'--tablet-hud-panel-top': tabletPanel.panelTop,
'--tablet-hud-panel-margin-right': tabletPanel.panelMarginRight,
'--tablet-hud-panel-width': tabletPanel.panelWidth,
'--tablet-popover-scale': String(tabletPanel.scale),
'--mobile-hud-panel-top': mobilePanel.panelTop,
'--mobile-hud-panel-margin-right': mobilePanel.panelMarginRight,
'--mobile-hud-panel-width': mobilePanel.panelWidth,
'--mobile-popover-scale': String(mobilePanel.scale),
};
}
const HERO_DESKTOP_ANCHOR = {
width: 1920,
fluidMax: 1.22,
tabletTuneWidth: 1022,
tabletFluidMax: 1.18,
mockupY: 10,
mockupScale: 1.15,
tabletMockupScale: 1.2,
};
const HERO_LAYOUT_VARS = ['--mockup-x', '--mockup-y', '--tablet-mockup-x', '--tablet-mockup-stage-height'];
function readRootPx(name, fallback) {
const raw = getComputedStyle(document.documentElement).getPropertyValue(name).trim();
if (!raw) return fallback;
if (raw.endsWith('rem')) {
const rootFont = Number.parseFloat(getComputedStyle(document.documentElement).fontSize);
const rem = Number.parseFloat(raw);
return Number.isFinite(rem) && Number.isFinite(rootFont) ? rem * rootFont : fallback;
}
const value = Number.parseFloat(raw);
return Number.isFinite(value) ? value : fallback;
}
function computeTabletStageHeight(width) {
const headerPx = readRootPx('--header-height', 64);
const tuneWidth = HERO_DESKTOP_ANCHOR.tabletTuneWidth;
const baseWidth = getMockupBaseWidth(width, tuneWidth);
const artboardHeight = baseWidth * (3239 / 6111);
const scale = getMockupEffectiveScale(width, { tablet: true });
const scaledHeight = artboardHeight * scale;
const mockupY = readRootNumber('--tablet-mockup-y', 13);
const mockupBottom = headerPx + mockupY + scaledHeight;
const panelTop = readRootPercent('--tablet-hud-panel-top', 9.5);
const panelScale = readRootNumber('--tablet-popover-scale', 1.35);
const panelApproxHeight = scaledHeight * 0.36 * panelScale;
const panelBottom = headerPx + mockupY + scaledHeight * panelTop + panelApproxHeight;
return Math.ceil(Math.max(mockupBottom, panelBottom) + 12);
}
function getHeroFluidFactor(width, tuneWidth = HERO_DESKTOP_ANCHOR.width, fluidMax = HERO_DESKTOP_ANCHOR.fluidMax) {
return Math.min(Math.max(1, tuneWidth / width), fluidMax);
}
function getMockupBaseWidth(width, tuneWidth = HERO_DESKTOP_ANCHOR.width) {
const layoutWidth = width >= tuneWidth ? tuneWidth : width;
return layoutWidth * MOCKUP_ARTBOARD_RATIO;
}
function readRootNumber(name, fallback) {
const raw = getComputedStyle(document.documentElement).getPropertyValue(name).trim();
const value = Number.parseFloat(raw);
return Number.isFinite(value) ? value : fallback;
}
function readRootPercent(name, fallbackPercent) {
const raw = getComputedStyle(document.documentElement).getPropertyValue(name).trim();
if (!raw) return fallbackPercent / 100;
if (raw.endsWith('%')) {
const value = Number.parseFloat(raw);
return Number.isFinite(value) ? value / 100 : fallbackPercent / 100;
}
const value = Number.parseFloat(raw);
return Number.isFinite(value) ? value / 100 : fallbackPercent / 100;
}
function getMockupEffectiveScale(width, { tablet = false } = {}) {
const tuneWidth = tablet ? HERO_DESKTOP_ANCHOR.tabletTuneWidth : HERO_DESKTOP_ANCHOR.width;
const fluidMax = tablet ? HERO_DESKTOP_ANCHOR.tabletFluidMax : HERO_DESKTOP_ANCHOR.fluidMax;
const scale = tablet
? readRootNumber('--tablet-mockup-scale', HERO_DESKTOP_ANCHOR.tabletMockupScale)
: readRootNumber('--mockup-scale', HERO_DESKTOP_ANCHOR.mockupScale);
if (width >= tuneWidth) return scale;
return scale * getHeroFluidFactor(width, tuneWidth, fluidMax);
}
function computeMockupX(width, { tablet = false } = {}) {
const tuneWidth = tablet ? HERO_DESKTOP_ANCHOR.tabletTuneWidth : HERO_DESKTOP_ANCHOR.width;
const scaledWidth = getMockupBaseWidth(width, tuneWidth) * getMockupEffectiveScale(width, { tablet });
const marginRight = tablet ? MOCKUP_SCREEN_MARGIN_RIGHT_TABLET : MOCKUP_SCREEN_MARGIN_RIGHT;
const baseX = (width / 2) - marginRight - (scaledWidth / 2);
return tablet ? baseX + HERO_TABLET_MOCKUP_X_CORRECTION : baseX;
}
function getMobileStageWidth(viewportWidth) {
return viewportWidth;
}
function computeMobileMockupOffsetX(viewportWidth) {
const stageWidth = getMobileStageWidth(viewportWidth);
const scale = readRootNumber('--mobile-mockup-scale', HERO_MOBILE_MOCKUP_TUNE_SCALE);
const widthPct = readRootNumber('--mobile-mockup-width', 251) / 100;
const scaledWidth = stageWidth * widthPct * scale;
const scaleNudge = (HERO_MOBILE_MOCKUP_TUNE_SCALE - scale) * scaledWidth * 0.5;
return Math.round((HERO_MOBILE_MOCKUP_X_CORRECTION + scaleNudge) * 100) / 100;
}
function applyHudGeometryDefaults() {
const defaults = getHudDefaultsFromGeometry();
const root = document.documentElement;
Object.entries(defaults).forEach(([name, value]) => {
if (!root.style.getPropertyValue(name)) {
root.style.setProperty(name, value);
}
});
}
function clearHeroDesktopLayout() {
HERO_LAYOUT_VARS.forEach((name) => document.documentElement.style.removeProperty(name));
}
function applyHeroDesktopLayout() {
const width = window.innerWidth;
const root = document.documentElement;
const isTablet = width > HERO_MOBILE_MAX && width <= HERO_TABLET_MAX;
const marginRight = isTablet ? MOCKUP_SCREEN_MARGIN_RIGHT_TABLET : MOCKUP_SCREEN_MARGIN_RIGHT;
root.style.setProperty('--mockup-screen-margin-right', `${marginRight}px`);
if (width <= HERO_MOBILE_MAX) {
clearHeroDesktopLayout();
root.style.setProperty('--mobile-mockup-offset-x', `${computeMobileMockupOffsetX(width)}px`);
return;
}
root.style.removeProperty('--mobile-mockup-offset-x');
const mockupX = Math.round(computeMockupX(width, { tablet: isTablet }) * 100) / 100;
if (width <= HERO_TABLET_MAX) {
root.style.setProperty('--tablet-mockup-x', `${mockupX}px`);
root.style.setProperty('--tablet-mockup-y', `${readRootNumber('--tablet-mockup-y', 13)}px`);
root.style.setProperty('--tablet-mockup-stage-height', `${computeTabletStageHeight(width)}px`);
root.style.removeProperty('--mockup-x');
root.style.removeProperty('--mockup-y');
return;
}
root.style.setProperty('--mockup-x', `${mockupX}px`);
root.style.setProperty('--mockup-y', `${HERO_DESKTOP_ANCHOR.mockupY}px`);
root.style.removeProperty('--tablet-mockup-x');
root.style.removeProperty('--tablet-mockup-y');
root.style.removeProperty('--tablet-mockup-stage-height');
}
applyHudGeometryDefaults();
applyHeroDesktopLayout();
window.addEventListener('resize', applyHeroDesktopLayout);
window.visualViewport?.addEventListener('resize', applyHeroDesktopLayout);
const themeMedia = window.matchMedia('(prefers-color-scheme: dark)');
const themeToggle = document.querySelector('#theme-toggle');
const themeNames = { system: 'System', light: 'Light', dark: 'Dark' };
function applyThemePreference(preference, persist = true) {
const resolvedTheme = preference === 'system'
? (themeMedia.matches ? 'dark' : 'light')
: preference;
root.dataset.theme = resolvedTheme;
root.dataset.themePreference = preference;
themeToggle.dataset.themeValue = preference;
themeToggle.title = `${themeNames[preference]} theme`;
updateThemeToggleLabel();
if (persist) {
try {
localStorage.setItem('codexbar-theme', preference);
} catch (_) {}
}
}
function updateThemeToggleLabel() {
const nextTheme = root.dataset.theme === 'dark' ? 'light' : 'dark';
const label = message(nextTheme === 'dark' ? 'theme.toDark' : 'theme.toLight');
themeToggle.setAttribute('aria-label', label);
themeToggle.title = label;
}
themeToggle.addEventListener('click', () => {
const nextPreference = root.dataset.theme === 'dark' ? 'light' : 'dark';
applyThemePreference(nextPreference);
});
themeMedia.addEventListener('change', () => {
if (root.dataset.themePreference === 'system') applyThemePreference('system', false);
});
applyThemePreference(root.dataset.themePreference || 'system', false);
const languageShortNames = {
en: 'EN',
'zh-CN': '简',
'zh-TW': '繁',
'ja-JP': 'JA',
es: 'ES',
'pt-BR': 'PT',
ko: 'KO',
de: 'DE',
fr: 'FR',
ar: 'AR',
it: 'IT',
vi: 'VI',
nl: 'NL',
tr: 'TR',
uk: 'UK',
id: 'ID',
pl: 'PL',
fa: 'FA',
th: 'TH',
ca: 'CA',
sv: 'SV',
};
const languages = localeCatalog.map((language) => ({
...language,
id: language.code,
short: languageShortNames[language.code] || language.code.slice(0, 2).toUpperCase(),
label: language.name,
}));
const supportedLanguages = new Set(languages.map((language) => language.id));
const rtlLanguages = new Set(localeCatalog.filter((language) => language.direction === 'rtl').map((language) => language.code));
const localeAliases = {
'zh-cn': 'zh-CN',
'zh-hans': 'zh-CN',
'zh-hant': 'zh-TW',
'zh-hk': 'zh-TW',
'zh-tw': 'zh-TW',
ja: 'ja-JP',
pt: 'pt-BR',
'pt-br': 'pt-BR',
};
const languageStorageKey = 'codexbar-language';
const languagePicker = document.querySelector('#language-picker');
const languageTrigger = document.querySelector('#language-picker-trigger');
const languageMenu = document.querySelector('#language-picker-menu');
const languageList = document.querySelector('#language-picker-list');
const languageShort = document.querySelector('[data-lang-short]');
function normalizeLocale(value) {
if (!value) return null;
const lower = value.toLowerCase();
if (localeAliases[lower]) return localeAliases[lower];
return languages.find((language) => language.id.toLowerCase() === lower || lower.startsWith(`${language.id.toLowerCase()}-`))?.id || null;
}
function selectedLocale() {
try {
const queryLocale = normalizeLocale(new URLSearchParams(location.search).get('lang'));
if (queryLocale) return queryLocale;
const storedLocale = normalizeLocale(localStorage.getItem(languageStorageKey));
if (storedLocale) return storedLocale;
} catch (_) {}
for (const language of navigator.languages || [navigator.language]) {
const locale = normalizeLocale(language);
if (locale) return locale;
}
return 'en';
}
let activeLanguage = selectedLocale();
if (!supportedLanguages.has(activeLanguage)) activeLanguage = 'en';
function message(key) {
return activeMessages[key] || localeMessages.en[key] || key;
}
function applyAttributeMessages(dataAttribute, targetAttribute) {
document.querySelectorAll(`[${dataAttribute}]`).forEach((element) => {
element.setAttribute(targetAttribute, message(element.getAttribute(dataAttribute)));
});
}
function richToken(name) {
const codeTokens = {
cask: 'brew install --cask steipete/tap/codexbar',
codexbar: 'codexbar',
linuxCommand: 'brew install steipete/tap/codexbar',
upgrade: 'brew upgrade',
};
if (codeTokens[name]) {
const code = document.createElement('code');
code.className = 'inline-code';
code.textContent = codeTokens[name];
return code;
}
if (name === 'releases') {
const link = document.createElement('a');
link.className = 'text-link';
link.href = 'https://github.com/steipete/CodexBar/releases';
link.textContent = 'GitHub Releases';
return link;
}
if (name === 'issue') {
const link = document.createElement('a');
link.className = 'text-link';
link.href = 'https://github.com/steipete/CodexBar/issues/12';
link.textContent = 'issue #12';
return link;
}
if (name === 'mobileBreak') {
return document.createElement('br');
}
if (name === 'break') {
const wrapper = document.createDocumentFragment();
const space = document.createElement('span');
space.className = 'inline sm:hidden';
space.textContent = ' ';
const br = document.createElement('br');
br.className = 'hidden sm:block';
wrapper.append(space, br);
return wrapper;
}
return document.createTextNode(`{${name}}`);
}
function renderRichMessage(element, value) {
const fragment = document.createDocumentFragment();
const tokenPattern = /\{([a-zA-Z][a-zA-Z0-9]*)\}/g;
let cursor = 0;
let match;
while ((match = tokenPattern.exec(value)) !== null) {
fragment.append(document.createTextNode(value.slice(cursor, match.index)));
fragment.append(richToken(match[1]));
cursor = match.index + match[0].length;
}
fragment.append(document.createTextNode(value.slice(cursor)));
element.replaceChildren(fragment);
}
function applyLanguageMessages() {
activeMessages = { ...localeMessages.en, ...(localeMessages[activeLanguage] || {}) };
root.lang = activeLanguage;
root.dir = rtlLanguages.has(activeLanguage) ? 'rtl' : 'ltr';
root.dataset.locale = activeLanguage;
document.title = message('meta.title');
document.querySelector('meta[name="description"]')?.setAttribute('content', message('meta.description'));
document.querySelector('meta[property="og:description"]')?.setAttribute('content', message('meta.ogDescription'));
document.querySelectorAll('[data-i18n]').forEach((element) => {
element.textContent = message(element.dataset.i18n);
});
document.querySelectorAll('[data-i18n-rich]').forEach((element) => {
renderRichMessage(element, message(element.dataset.i18nRich));
});
applyAttributeMessages('data-i18n-aria-label', 'aria-label');
applyAttributeMessages('data-i18n-title', 'title');
applyAttributeMessages('data-i18n-alt', 'alt');
updateThemeToggleLabel();
}
function renderLanguageMenu() {
languageList.replaceChildren();
languages.forEach((language) => {
const option = document.createElement('button');
option.type = 'button';
option.className = 'language-option button-press';
option.role = 'option';
option.dataset.lang = language.id;
option.setAttribute('aria-selected', String(language.id === activeLanguage));
option.tabIndex = language.id === activeLanguage ? 0 : -1;
option.innerHTML = `
<svg viewBox="0 0 18 18" fill="none" aria-hidden="true">
<path d="M4.5 9.25 7.5 12.25 13.5 6.25" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" />
</svg>
<span>${language.label}</span>
`;
option.addEventListener('click', () => selectLanguage(language.id));
languageList.append(option);
});
}
function updateLanguageUi() {
const current = languages.find((language) => language.id === activeLanguage) || languages[0];
languageShort.textContent = current.short;
root.dataset.language = current.id;
applyLanguageMessages();
languageList.querySelectorAll('[role="option"]').forEach((option) => {
const selected = option.dataset.lang === activeLanguage;
option.setAttribute('aria-selected', String(selected));
option.tabIndex = selected ? 0 : -1;
});
}
function setLanguageMenuOpen(open, { focusOption = false, restoreFocus = false } = {}) {
languageMenu.hidden = !open;
languageTrigger.setAttribute('aria-expanded', String(open));
if (open && focusOption) {
languageList.querySelector('[aria-selected="true"]')?.focus();
} else if (!open && restoreFocus) {
languageTrigger.focus();
}
}
function selectLanguage(languageId) {
activeLanguage = languageId;
try {
localStorage.setItem(languageStorageKey, languageId);
const url = new URL(location.href);
url.searchParams.set('lang', languageId);
history.replaceState(null, '', url);
} catch (_) {}
updateLanguageUi();
setLanguageMenuOpen(false, { restoreFocus: true });
}
renderLanguageMenu();
updateLanguageUi();
languageTrigger.addEventListener('click', () => {
setLanguageMenuOpen(languageMenu.hidden, { focusOption: languageMenu.hidden });
});
languageTrigger.addEventListener('keydown', (event) => {
if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp') return;
event.preventDefault();
setLanguageMenuOpen(true, { focusOption: true });
});
languageList.addEventListener('keydown', (event) => {
const options = [...languageList.querySelectorAll('[role="option"]')];
const currentIndex = Math.max(0, options.indexOf(document.activeElement));
let nextIndex;
if (event.key === 'ArrowDown') nextIndex = (currentIndex + 1) % options.length;
if (event.key === 'ArrowUp') nextIndex = (currentIndex - 1 + options.length) % options.length;
if (event.key === 'Home') nextIndex = 0;
if (event.key === 'End') nextIndex = options.length - 1;
if (nextIndex === undefined) return;
event.preventDefault();
options[nextIndex]?.focus();
});
document.addEventListener('click', (event) => {
if (!languagePicker.contains(event.target)) setLanguageMenuOpen(false);
});
document.addEventListener('keydown', (event) => {
if (event.key === 'Escape' && !languageMenu.hidden) {
event.preventDefault();
setLanguageMenuOpen(false, { restoreFocus: true });
}
});
const providerSection = document.querySelector('[data-provider-section]');
const providerCards = providerSection.querySelectorAll('.provider-card:not([hidden])');
providerCards.forEach((card, index) => {
card.classList.add('provider-reveal');
card.style.setProperty('--reveal-delay', `${80 + Math.min(index, 12) * 24}ms`);
});
const revealSection = (section) => section.classList.add('is-revealed');
if ('IntersectionObserver' in window) {
const revealObserver = new IntersectionObserver((entries, observer) => {
entries.forEach((entry) => {
if (!entry.isIntersecting) return;
revealSection(entry.target);
observer.unobserve(entry.target);
});
}, { threshold: 0.01, rootMargin: '0px 0px -8% 0px' });
[providerSection, ...document.querySelectorAll('[data-scroll-section]')].forEach((section) => {
revealObserver.observe(section);
});
} else {
revealSection(providerSection);
document.querySelectorAll('[data-scroll-section]').forEach(revealSection);
}
document.querySelectorAll('.mac-widget-gallery-wrap').forEach((wrap) => {
const gallery = wrap.querySelector('.mac-widget-gallery');
if (!gallery) return;
const syncScrollFade = () => {
const overflows = gallery.scrollHeight > gallery.clientHeight + 1;
const atBottom = gallery.scrollTop + gallery.clientHeight >= gallery.scrollHeight - 2;
wrap.classList.toggle('has-scroll-fade', overflows && !atBottom);
};
gallery.addEventListener('scroll', syncScrollFade, { passive: true });
window.addEventListener('resize', syncScrollFade);
if ('ResizeObserver' in window) {
new ResizeObserver(syncScrollFade).observe(gallery);
}
syncScrollFade();
});
const mockupStage = document.querySelector('#mockup-stage');
const menubarItems = mockupStage.querySelectorAll('.system-menubar > *');
const heroRootStyles = getComputedStyle(document.documentElement);
const readHeroMs = (name) => Number.parseFloat(heroRootStyles.getPropertyValue(name)) || 0;
const menubarStartDelay = readHeroMs('--hero-menubar-start');
const menubarWaveGap = readHeroMs('--hero-menubar-wave');
const reducedHeroMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
const lastMenubarItemDelay = Math.max(0, menubarItems.length - 1) * menubarWaveGap;
const markStartDelay = menubarStartDelay + lastMenubarItemDelay + readHeroMs('--hero-mark-after-menubar');
const popoverStartDelay = markStartDelay + readHeroMs('--hero-popover-after-mark');
menubarItems.forEach((item, index) => {
item.style.setProperty('--menubar-enter-delay', `${reducedHeroMotion ? 0 : index * menubarWaveGap}ms`);
});
const animatePopoverDetails = () => {
mockupStage.querySelectorAll('.usage-meter').forEach((meter, index) => {
meter.style.setProperty('--meter-delay', `${reducedHeroMotion ? 0 : 70 + index * 70}ms`);
});
mockupStage.querySelectorAll('.usage-chart i').forEach((bar, index) => {
bar.style.setProperty('--bar-delay', `${reducedHeroMotion ? 0 : 120 + index * 6}ms`);
});
mockupStage.classList.add('is-animated');
};
if (reducedHeroMotion) {
mockupStage.classList.add('is-menubar-entered', 'is-mark-animated');
animatePopoverDetails();
} else {
window.setTimeout(() => mockupStage.classList.add('is-menubar-entered'), menubarStartDelay);
window.setTimeout(() => mockupStage.classList.add('is-mark-animated'), markStartDelay);
window.setTimeout(animatePopoverDetails, popoverStartDelay);
}
const brewCopyButton = document.querySelector('#brew-copy');
const brewCopyLabel = brewCopyButton?.querySelector('[data-brew-label]');
const brewCopyIcon = brewCopyButton?.querySelector('[data-brew-icon]');
const brewCopyStatus = brewCopyButton?.querySelector('[data-brew-status]');
let brewCopyResetTimer;
async function copyBrewCommand() {
const command = brewCopyLabel?.textContent?.trim();
if (!command || !navigator.clipboard?.writeText) return;
try {
await navigator.clipboard.writeText(command);
} catch (_) {
return;
}
window.clearTimeout(brewCopyResetTimer);
brewCopyIcon.dataset.copied = 'true';
brewCopyStatus.textContent = message('clipboard.copied');
brewCopyButton.setAttribute('aria-label', message('clipboard.copied'));
brewCopyResetTimer = window.setTimeout(() => {
delete brewCopyIcon.dataset.copied;
brewCopyStatus.textContent = '';
brewCopyButton.setAttribute('aria-label', message('clipboard.copyBrew'));
}, 1500);
}
brewCopyButton?.addEventListener('click', copyBrewCommand);