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 = ` ${language.label} `; 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);