From 4ab05186ea5a6b57d537e1f86e5445e7fe464b04 Mon Sep 17 00:00:00 2001 From: Shaun Struwig <41984034+Blargian@users.noreply.github.com> Date: Wed, 24 Jun 2026 20:59:13 +0200 Subject: [PATCH 1/4] Reduce main-thread thrashing and eliminate full-page-reload on nav MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace scroll-reset.js rAF polling loop (60fps, forever) with history.pushState/replaceState patch that fires ch:navigate events - Use history.pushState in tab-nav.js for locale/tab navigation so switching translations is SPA-speed instead of a full page reload - Add RAF debounce to the three unthrottled MutationObservers in navbar-cta.js, ask-ai-button.js, and quickstart-back-link.js; those observers fired on every React DOM mutation with no throttle - Disconnect navbar-cta and ask-ai-button observers once injection succeeds — they don't need to keep watching after that - Replace updateLogoTheme() with pure CSS display rules in styles.css - Replace 9 inline style assignments in styleDropdownHeaders() with a single CSS class (.ch-mobile-nav-header) defined in styles.css - Move all injectStyles() CSS blocks from JS into styles.css so they parse at load time rather than being injected late (affects clickhouse-sql-highlight.js, custom-footer.js, navbar-cta.js, ask-ai-button.js) Co-Authored-By: Claude Sonnet 4.6 (1M context) --- _site/customizations/ask-ai-button.js | 27 ++-- .../clickhouse-sql-highlight.js | 31 ----- _site/customizations/custom-footer.js | 78 ----------- _site/customizations/navbar-cta.js | 40 +----- _site/customizations/quickstart-back-link.js | 14 +- _site/customizations/scroll-reset.js | 57 ++++---- _site/customizations/tab-nav.js | 25 +--- _site/styles.css | 130 ++++++++++++++++++ 8 files changed, 190 insertions(+), 212 deletions(-) diff --git a/_site/customizations/ask-ai-button.js b/_site/customizations/ask-ai-button.js index d45caccdb..5ddc7048c 100644 --- a/_site/customizations/ask-ai-button.js +++ b/_site/customizations/ask-ai-button.js @@ -11,14 +11,6 @@ + '' + ''; - function injectStyles() { - if (document.getElementById('ch-ask-ai-styles')) return; - var style = document.createElement('style'); - style.id = 'ch-ask-ai-styles'; - style.textContent = '.dark .ch-ai-icon { color: #fdff75; }'; - document.head.appendChild(style); - } - // Wait briefly for Kapa to mount (it's loaded async), then open. Kapa's // open() accepts an optional query that's submitted immediately when // submit:true is set. @@ -47,8 +39,6 @@ if (!searchBar) return false; - injectStyles(); - var btn = document.createElement('button'); btn.id = BTN_ID; btn.type = 'button'; @@ -71,8 +61,6 @@ if (!mobileSearchBtn) return false; - injectStyles(); - var btn = document.createElement('button'); btn.id = MOBILE_BTN_ID; btn.type = 'button'; @@ -89,12 +77,19 @@ } function init() { - injectButton(); - injectMobileButton(); + var desktopDone = injectButton(); + var mobileDone = injectMobileButton(); + if (desktopDone && mobileDone) return; + var rafId = null; var observer = new MutationObserver(function () { - injectButton(); - injectMobileButton(); + if (rafId) return; + rafId = requestAnimationFrame(function () { + rafId = null; + if (!desktopDone) desktopDone = injectButton(); + if (!mobileDone) mobileDone = injectMobileButton(); + if (desktopDone && mobileDone) observer.disconnect(); + }); }); observer.observe(document.documentElement, { childList: true, subtree: true }); } diff --git a/_site/customizations/clickhouse-sql-highlight.js b/_site/customizations/clickhouse-sql-highlight.js index 7dcb2bc11..69f76fc6e 100644 --- a/_site/customizations/clickhouse-sql-highlight.js +++ b/_site/customizations/clickhouse-sql-highlight.js @@ -232,36 +232,6 @@ // ---- DOM integration ------------------------------------------------------ - var STYLE_ID = 'ch-sql-highlight-styles'; - - // The exact light + dark palette from play.html / clickhouse-client, adapted - // to Mintlify's dark-mode carrier (``). Scoped to - // `.ch-sql-hl` so only blocks we rebuilt are affected. `!important` is - // required because Mintlify forces `html.dark .shiki span { color: - // var(--shiki-dark) !important }` on every token; our higher-specificity - // `.dark .ch-sql-hl .q-*` rules only win when they are also `!important`. - function injectStyles() { - if (document.getElementById(STYLE_ID)) return; - var css = - '.ch-sql-hl .q-kw{font-weight:bold !important}' + - '.ch-sql-hl .q-com{font-style:italic !important;color:#757575 !important}' + - '.ch-sql-hl .q-id{color:#00838f !important}' + - '.ch-sql-hl .q-fn{color:#875f00 !important}' + - '.ch-sql-hl .q-num{color:#008700 !important}' + - '.ch-sql-hl .q-str{color:#006400 !important}' + - '.ch-sql-hl .q-qid{color:#008b8b !important}' + - '.dark .ch-sql-hl .q-id{color:#00cdcd !important}' + - '.dark .ch-sql-hl .q-fn{color:#cdcd00 !important}' + - '.dark .ch-sql-hl .q-num{color:#00d700 !important}' + - '.dark .ch-sql-hl .q-str{color:#00cd00 !important}' + - '.dark .ch-sql-hl .q-qid{color:#00d7d7 !important}' + - '.dark .ch-sql-hl .q-com{color:#9e9e9e !important}'; - var style = document.createElement('style'); - style.id = STYLE_ID; - style.textContent = css; - document.head.appendChild(style); - } - // Replace a line element's Shiki token spans with our class-based segments. // Uses textContent (not innerHTML) so SQL characters like `<` and `&` are // never interpreted as markup. @@ -302,7 +272,6 @@ code.dataset.chSqlState = 'skipped'; return; } - injectStyles(); for (var i = 0; i < lineEls.length; i++) renderLine(lineEls[i], lines[i]); code.classList.add('ch-sql-hl'); code.dataset.chSqlState = 'done'; diff --git a/_site/customizations/custom-footer.js b/_site/customizations/custom-footer.js index 2d1199545..31bfffce1 100644 --- a/_site/customizations/custom-footer.js +++ b/_site/customizations/custom-footer.js @@ -126,82 +126,6 @@ return html; } - function injectStyles() { - if (document.getElementById('ch-footer-styles')) return; - var style = document.createElement('style'); - style.id = 'ch-footer-styles'; - style.textContent = '' - + '#' + FOOTER_ID + ' { box-sizing: border-box; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; }' - // On desktop the sidebar is fixed at 19rem wide; match #content-container's - // horizontal padding (pl-[32px] / pr-[32px]) so the footer's inner edges - // align with the content area used by both doc pages and the home page. - + '@media (min-width: 1024px) { #' + FOOTER_ID + ' { padding-left: calc(19rem + 32px) !important; padding-right: 32px !important; } }' - + '#' + FOOTER_ID + ' [data-inner] { max-width: 1280px; margin: 0 auto; }' - + '#' + FOOTER_ID + ' * { box-sizing: border-box; }' - + '#' + FOOTER_ID + ' a { text-decoration: none; transition: color 0.15s, border-color 0.15s; }' - // Top section: sitemap + CTA side by side only at wide viewports - + '#' + FOOTER_ID + ' [data-top] { display: flex; flex-direction: column; gap: 32px; padding-bottom: 48px; }' - + '@media (min-width: 1400px) { #' + FOOTER_ID + ' [data-top] { flex-direction: row; gap: 40px; } }' - // Sitemap grid: 2 cols default, 3 at md, 5 only when side-by-side - + '#' + FOOTER_ID + ' [data-sitemap] { display: grid; grid-template-columns: repeat(2, 1fr); gap: 24px; flex: 1; min-width: 0; }' - + '@media (min-width: 768px) { #' + FOOTER_ID + ' [data-sitemap] { grid-template-columns: repeat(3, 1fr); } }' - + '@media (min-width: 1400px) { #' + FOOTER_ID + ' [data-sitemap] { grid-template-columns: repeat(5, 1fr); } }' - // Column headings - + '#' + FOOTER_ID + ' [data-sitemap] h3 { font-size: 13px; font-weight: 600; margin: 0 0 12px; }' - + '#' + FOOTER_ID + ' [data-sitemap] .ch-sub-heading { margin-top: 24px; }' - + '#' + FOOTER_ID + ' [data-sitemap] ul { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }' - + '#' + FOOTER_ID + ' [data-sitemap] a { font-size: 13px; }' - // CTA column - + '#' + FOOTER_ID + ' [data-cta] { display: flex; flex-direction: column; }' - + '@media (min-width: 1400px) { #' + FOOTER_ID + ' [data-cta] { width: 300px; flex-shrink: 0; } }' - + '#' + FOOTER_ID + ' [data-cta] p { font-size: 13px; line-height: 1.5; margin: 0 0 20px; }' - + '#' + FOOTER_ID + ' [data-cta] form { display: flex; border-radius: 8px; overflow: hidden; margin-bottom: 16px; }' - + '#' + FOOTER_ID + ' [data-cta] form input { flex: 1; background: transparent; border: none; padding: 10px 14px; font-size: 13px; outline: none; min-width: 0; }' - + '#' + FOOTER_ID + ' [data-cta] form button { background: #fdff75; color: #1c1c1c; border: none; padding: 10px 20px; font-size: 13px; font-weight: 600; cursor: pointer; white-space: nowrap; }' - + '#' + FOOTER_ID + ' [data-cta] form button:hover { background: #eaec6a; }' - + '#' + FOOTER_ID + ' [data-gh] { display: inline-flex; align-items: center; justify-content: center; gap: 8px; background: transparent; padding: 10px 20px; border-radius: 8px; font-size: 13px; font-weight: 600; width: 100%; }' - + '#' + FOOTER_ID + ' [data-gh]:hover { border-color: #888 !important; }' - // Bottom bar - + '#' + FOOTER_ID + ' [data-divider] { margin-bottom: 24px; }' - + '#' + FOOTER_ID + ' [data-bottom] { display: flex; flex-wrap: wrap; justify-content: space-between; align-items: center; gap: 12px; }' - + '#' + FOOTER_ID + ' [data-bottom] a { font-size: 13px; white-space: nowrap; }' - + '#' + FOOTER_ID + ' [data-legal] { display: flex; flex-wrap: wrap; gap: 16px; }' - + '@media (max-width: 639px) {' - + ' #' + FOOTER_ID + ' [data-bottom] { flex-direction: column; text-align: center; }' - + ' #' + FOOTER_ID + ' [data-legal] { justify-content: center; }' - + '}' - // Light mode colors - + '#' + FOOTER_ID + ' [data-sitemap] h3 { color: #111; }' - + '#' + FOOTER_ID + ' [data-sitemap] a { color: #6b7280; }' - + '#' + FOOTER_ID + ' [data-sitemap] a:hover { color: #111; }' - + '#' + FOOTER_ID + ' [data-cta] p { color: #6b7280; }' - + '#' + FOOTER_ID + ' [data-cta] form { border: 1px solid #d1d5db; }' - + '#' + FOOTER_ID + ' [data-cta] form input { color: #111; }' - + '#' + FOOTER_ID + ' [data-cta] form input::placeholder { color: #9ca3af; }' - + '#' + FOOTER_ID + ' [data-gh] { color: #111; border: 1px solid #d1d5db; }' - + '#' + FOOTER_ID + ' [data-divider] { border-top: 1px solid #e5e7eb; }' - + '#' + FOOTER_ID + ' [data-copyright] { font-size: 13px; color: #6b7280; }' - + '#' + FOOTER_ID + ' [data-bottom] a { color: #6b7280; }' - + '#' + FOOTER_ID + ' [data-bottom] a:hover { color: #111; }' - + '#' + FOOTER_ID + ' [data-logo] { margin-bottom: 16px; }' - + '#' + FOOTER_ID + ' [data-logo] svg * { fill: #111; }' - // Dark mode colors - + '.dark #' + FOOTER_ID + ' [data-sitemap] h3 { color: #f5f5f5; }' - + '.dark #' + FOOTER_ID + ' [data-sitemap] a { color: #a3a3a3; }' - + '.dark #' + FOOTER_ID + ' [data-sitemap] a:hover { color: #f5f5f5; }' - + '.dark #' + FOOTER_ID + ' [data-cta] p { color: #a3a3a3; }' - + '.dark #' + FOOTER_ID + ' [data-cta] form { border-color: #555; }' - + '.dark #' + FOOTER_ID + ' [data-cta] form input { color: #fff; }' - + '.dark #' + FOOTER_ID + ' [data-cta] form input::placeholder { color: #6b7280; }' - + '.dark #' + FOOTER_ID + ' [data-gh] { color: #fff; border-color: #555; }' - + '.dark #' + FOOTER_ID + ' [data-divider] { border-color: #333; }' - + '.dark #' + FOOTER_ID + ' [data-copyright] { color: #a3a3a3; }' - + '.dark #' + FOOTER_ID + ' [data-bottom] a { color: #a3a3a3; }' - + '.dark #' + FOOTER_ID + ' [data-bottom] a:hover { color: #f5f5f5; }' - + '.dark #' + FOOTER_ID + ' [data-logo] svg * { fill: #fff; }'; - document.head.appendChild(style); - } - function buildFooterHtml() { var year = new Date().getFullYear(); @@ -290,8 +214,6 @@ } // Inject styles and create footer - injectStyles(); - var wrapper = document.createElement('footer'); wrapper.id = FOOTER_ID; wrapper.style.cssText = 'width:100%;padding:64px 24px 32px;'; diff --git a/_site/customizations/navbar-cta.js b/_site/customizations/navbar-cta.js index e34ae273b..4dd947ef3 100644 --- a/_site/customizations/navbar-cta.js +++ b/_site/customizations/navbar-cta.js @@ -12,43 +12,12 @@ return String(count); } - function injectStyles() { - if (document.getElementById('ch-navbar-cta-styles')) return; - var style = document.createElement('style'); - style.id = 'ch-navbar-cta-styles'; - style.textContent = '' - // Hide mobile AI assistant button - + '#assistant-entry-mobile { display: none !important; }' - // Invert dark SVG logos so they're visible on dark backgrounds - + '.dark img[src*="windsurf"], :is(.dark) img[src*="windsurf"] { filter: invert(1) !important; }' - // CTA container - + '#' + CTA_ID + ' { display: flex; align-items: center; gap: 16px; flex-shrink: 0; margin-left: 32px; }' - // GitHub stars link - + '#' + CTA_ID + ' .ch-gh-stars { display: inline-flex; align-items: center; gap: 6px; font-size: 13px; font-weight: 500; text-decoration: none; white-space: nowrap; transition: color 0.15s; }' - + '#' + CTA_ID + ' .ch-gh-stars svg { flex-shrink: 0; }' - // Get started button - + '#' + CTA_ID + ' .ch-cta-btn { display: inline-flex; align-items: center; padding: 6px 16px; border-radius: 4px; font-size: 13px; font-weight: 600; text-decoration: none; white-space: nowrap; transition: background-color 0.15s, color 0.15s; }' - // Light mode - + '#' + CTA_ID + ' .ch-gh-stars { color: #374151; }' - + '#' + CTA_ID + ' .ch-gh-stars:hover { color: #111; }' - + '#' + CTA_ID + ' .ch-cta-btn { background: #1c1c1c; color: #fff; }' - + '#' + CTA_ID + ' .ch-cta-btn:hover { background: #333; }' - // Dark mode - + '.dark #' + CTA_ID + ' .ch-gh-stars { color: #d1d5db; }' - + '.dark #' + CTA_ID + ' .ch-gh-stars:hover { color: #fff; }' - + '.dark #' + CTA_ID + ' .ch-cta-btn { background: #fdff75; color: #1c1c1c; }' - + '.dark #' + CTA_ID + ' .ch-cta-btn:hover { background: #eaec6a; }'; - document.head.appendChild(style); - } - function injectCta() { if (document.getElementById(CTA_ID)) return true; var mapleNav = document.getElementById('navbar-transition-maple'); if (!mapleNav) return false; - injectStyles(); - // --- Right section: GitHub stars + Get started --- var container = document.createElement('div'); container.id = CTA_ID; @@ -99,10 +68,15 @@ } function init() { - injectCta(); + if (injectCta()) return; + var rafId = null; var observer = new MutationObserver(function () { - injectCta(); + if (rafId) return; + rafId = requestAnimationFrame(function () { + rafId = null; + if (injectCta()) observer.disconnect(); + }); }); observer.observe(document.documentElement, { childList: true, subtree: true }); } diff --git a/_site/customizations/quickstart-back-link.js b/_site/customizations/quickstart-back-link.js index 031ed19f7..598bfd488 100644 --- a/_site/customizations/quickstart-back-link.js +++ b/_site/customizations/quickstart-back-link.js @@ -66,12 +66,14 @@ function init() { apply(); - // Re-apply across SPA navigations / React re-renders. apply() is - // idempotent, so observer feedback loops settle immediately. - new MutationObserver(apply).observe(document.documentElement, { - childList: true, - subtree: true, - }); + var rafId = null; + new MutationObserver(function () { + if (rafId) return; + rafId = requestAnimationFrame(function () { + rafId = null; + apply(); + }); + }).observe(document.documentElement, { childList: true, subtree: true }); } if (document.readyState === 'loading') { diff --git a/_site/customizations/scroll-reset.js b/_site/customizations/scroll-reset.js index 8fdc474dd..6798e29d4 100644 --- a/_site/customizations/scroll-reset.js +++ b/_site/customizations/scroll-reset.js @@ -4,19 +4,29 @@ // With an announcement banner configured, Next.js skips its scroll-to-top // on client-side navigation: the banner is position:fixed at the top of the // re-rendered segment, so the router considers the new page "already in - // viewport" and leaves the scroll position where it was. (Banner-less - // Mintlify sites scroll to top as expected; dismissing the banner makes the - // bug disappear.) Restore the expected behavior by scrolling to the top - // whenever a forward navigation changes the path. + // viewport" and leaves the scroll position where it was. // - // The path is watched from a rAF loop rather than by wrapping - // history.pushState — the router can hold a reference to the original - // pushState from before this script runs, which would bypass a wrapper. + // Patch history.pushState/replaceState at eval time (before Next.js hydration) + // to fire a synthetic 'ch:navigate' event on every SPA navigation. This replaces + // the rAF polling loop the original used — the loop ran 60fps for the entire page + // lifetime; the event fires only on actual navigations. // - // Back/forward (popstate) is deliberately left alone so the browser and - // router can restore the previous scroll position. Cross-page hash links - // (/page#anchor) scroll to the anchor once the new page has rendered it, - // since the banner bug breaks that scroll too. + // Back/forward (popstate) is left alone so the browser and router can restore + // the previous scroll position. Cross-page hash links (/page#anchor) scroll to + // the anchor once the new page has rendered it. + + (function patchHistory() { + function wrap(orig) { + return function () { + var result = orig.apply(this, arguments); + window.dispatchEvent(new Event('ch:navigate')); + return result; + }; + } + history.pushState = wrap(history.pushState); + history.replaceState = wrap(history.replaceState); + })(); + var lastPath = window.location.pathname; var traversed = false; @@ -44,19 +54,16 @@ } } - function watch() { + window.addEventListener('ch:navigate', function () { var path = window.location.pathname; - if (path !== lastPath) { - lastPath = path; - if (traversed) { - traversed = false; - } else if (window.location.hash) { - scrollToAnchor(window.location.hash, 180); - } else { - window.scrollTo(0, 0); - } + if (path === lastPath) return; + lastPath = path; + if (traversed) { + traversed = false; + } else if (window.location.hash) { + scrollToAnchor(window.location.hash, 180); + } else { + window.scrollTo(0, 0); } - window.requestAnimationFrame(watch); - } - window.requestAnimationFrame(watch); -})(); \ No newline at end of file + }); +})(); diff --git a/_site/customizations/tab-nav.js b/_site/customizations/tab-nav.js index e267c1e66..5ad54581d 100644 --- a/_site/customizations/tab-nav.js +++ b/_site/customizations/tab-nav.js @@ -53,7 +53,7 @@ labelDiv.style.cursor = 'pointer'; labelDiv.addEventListener('click', function (e) { e.stopPropagation(); - window.location.href = localizeUrl(url); + history.pushState(null, '', localizeUrl(url)); }); }); } @@ -68,16 +68,7 @@ if (SECTION_HEADERS.indexOf(text) === -1) return; a.dataset.headerStyled = '1'; - a.style.fontWeight = '700'; - a.style.fontSize = '0.75rem'; - a.style.letterSpacing = '0.05em'; - a.style.textTransform = 'uppercase'; - a.style.opacity = '0.5'; - a.style.pointerEvents = 'none'; - a.style.cursor = 'default'; - a.style.borderBottom = '1px solid rgba(255,255,255,0.08)'; - a.style.paddingBottom = '8px'; - a.style.marginBottom = '2px'; + a.classList.add('ch-mobile-nav-header'); a.addEventListener('click', function (e) { e.preventDefault(); @@ -86,16 +77,6 @@ }); } - // ── Logo theme sync ────────────────────────────────────────────────────── - function updateLogoTheme() { - var light = document.getElementById('ch-hp-logo-light'); - var dark = document.getElementById('ch-hp-logo-dark'); - if (!light || !dark) return; - var isDark = document.documentElement.classList.contains('dark'); - light.style.display = isDark ? 'none' : 'block'; - dark.style.display = isDark ? 'block' : 'none'; - } - // ── Homepage sidebar hiding ─────────────────────────────────────────────── // Each locale has its own homepage at / (e.g. /es, /ja); treat those // the same as the English homepage at /. @@ -162,7 +143,6 @@ logoLink.innerHTML = 'ClickHouse Docs' + 'ClickHouse Docs'; navbar.insertBefore(logoLink, navbar.firstChild); - updateLogoTheme(); } // Move theme toggle from sidebar to navbar — insert after logo, before tabs. @@ -219,7 +199,6 @@ rafId = null; applyHomepageClass(); setupHomepageNavbar(); - updateLogoTheme(); patchTabButtons(); styleDropdownHeaders(); markNavbarReady(); diff --git a/_site/styles.css b/_site/styles.css index e22ad3668..2b52bf0c6 100644 --- a/_site/styles.css +++ b/_site/styles.css @@ -2756,3 +2756,133 @@ a.mobile-nav-tabs-item[href$="/products/clickhouse-private"], a.mobile-nav-tabs-item[href$="/products/clickhouse-private/index"] { display: none !important; } + +/* ============================================ + Homepage logo theme sync + (replaces updateLogoTheme() in tab-nav.js — pure CSS now) + ============================================ */ + +#ch-hp-logo-dark { display: none; } +html.dark #ch-hp-logo-dark { display: block; } +html.dark #ch-hp-logo-light { display: none; } + +/* ============================================ + Mobile nav section headers + (replaces inline styles in tab-nav.js styleDropdownHeaders) + ============================================ */ + +.ch-mobile-nav-header { + font-weight: 700; + font-size: 0.75rem; + letter-spacing: 0.05em; + text-transform: uppercase; + opacity: 0.5; + pointer-events: none; + cursor: default; + border-bottom: 1px solid rgba(255, 255, 255, 0.08); + padding-bottom: 8px; + margin-bottom: 2px; +} + +/* ============================================ + ClickHouse SQL syntax highlighting + (moved from injectStyles() in clickhouse-sql-highlight.js) + ============================================ */ + +.ch-sql-hl .q-kw { font-weight: bold !important; } +.ch-sql-hl .q-com { font-style: italic !important; color: #757575 !important; } +.ch-sql-hl .q-id { color: #00838f !important; } +.ch-sql-hl .q-fn { color: #875f00 !important; } +.ch-sql-hl .q-num { color: #008700 !important; } +.ch-sql-hl .q-str { color: #006400 !important; } +.ch-sql-hl .q-qid { color: #008b8b !important; } +.dark .ch-sql-hl .q-id { color: #00cdcd !important; } +.dark .ch-sql-hl .q-fn { color: #cdcd00 !important; } +.dark .ch-sql-hl .q-num { color: #00d700 !important; } +.dark .ch-sql-hl .q-str { color: #00cd00 !important; } +.dark .ch-sql-hl .q-qid { color: #00d7d7 !important; } +.dark .ch-sql-hl .q-com { color: #9e9e9e !important; } + +/* ============================================ + Navbar CTA — GitHub stars + Get Started button + (moved from injectStyles() in navbar-cta.js) + ============================================ */ + +#assistant-entry-mobile { display: none !important; } +.dark img[src*="windsurf"], +:is(.dark) img[src*="windsurf"] { filter: invert(1) !important; } +#ch-navbar-cta { display: flex; align-items: center; gap: 16px; flex-shrink: 0; margin-left: 32px; } +#ch-navbar-cta .ch-gh-stars { display: inline-flex; align-items: center; gap: 6px; font-size: 13px; font-weight: 500; text-decoration: none; white-space: nowrap; transition: color 0.15s; } +#ch-navbar-cta .ch-gh-stars svg { flex-shrink: 0; } +#ch-navbar-cta .ch-cta-btn { display: inline-flex; align-items: center; padding: 6px 16px; border-radius: 4px; font-size: 13px; font-weight: 600; text-decoration: none; white-space: nowrap; transition: background-color 0.15s, color 0.15s; } +#ch-navbar-cta .ch-gh-stars { color: #374151; } +#ch-navbar-cta .ch-gh-stars:hover { color: #111; } +#ch-navbar-cta .ch-cta-btn { background: #1c1c1c; color: #fff; } +#ch-navbar-cta .ch-cta-btn:hover { background: #333; } +.dark #ch-navbar-cta .ch-gh-stars { color: #d1d5db; } +.dark #ch-navbar-cta .ch-gh-stars:hover { color: #fff; } +.dark #ch-navbar-cta .ch-cta-btn { background: #fdff75; color: #1c1c1c; } +.dark #ch-navbar-cta .ch-cta-btn:hover { background: #eaec6a; } + +/* ============================================ + Custom footer layout, typography and colors + (moved from injectStyles() in custom-footer.js) + ============================================ */ + +#ch-custom-footer { box-sizing: border-box; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; } +#ch-custom-footer [data-inner] { max-width: 1280px; margin: 0 auto; } +#ch-custom-footer * { box-sizing: border-box; } +#ch-custom-footer a { text-decoration: none; transition: color 0.15s, border-color 0.15s; } +#ch-custom-footer [data-top] { display: flex; flex-direction: column; gap: 32px; padding-bottom: 48px; } +@media (min-width: 1400px) { #ch-custom-footer [data-top] { flex-direction: row; gap: 40px; } } +#ch-custom-footer [data-sitemap] { display: grid; grid-template-columns: repeat(2, 1fr); gap: 24px; flex: 1; min-width: 0; } +@media (min-width: 768px) { #ch-custom-footer [data-sitemap] { grid-template-columns: repeat(3, 1fr); } } +@media (min-width: 1400px) { #ch-custom-footer [data-sitemap] { grid-template-columns: repeat(5, 1fr); } } +#ch-custom-footer [data-sitemap] h3 { font-size: 13px; font-weight: 600; margin: 0 0 12px; } +#ch-custom-footer [data-sitemap] .ch-sub-heading { margin-top: 24px; } +#ch-custom-footer [data-sitemap] ul { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; } +#ch-custom-footer [data-sitemap] a { font-size: 13px; } +#ch-custom-footer [data-cta] { display: flex; flex-direction: column; } +@media (min-width: 1400px) { #ch-custom-footer [data-cta] { width: 300px; flex-shrink: 0; } } +#ch-custom-footer [data-cta] p { font-size: 13px; line-height: 1.5; margin: 0 0 20px; } +#ch-custom-footer [data-cta] form { display: flex; border-radius: 8px; overflow: hidden; margin-bottom: 16px; } +#ch-custom-footer [data-cta] form input { flex: 1; background: transparent; border: none; padding: 10px 14px; font-size: 13px; outline: none; min-width: 0; } +#ch-custom-footer [data-cta] form button { background: #fdff75; color: #1c1c1c; border: none; padding: 10px 20px; font-size: 13px; font-weight: 600; cursor: pointer; white-space: nowrap; } +#ch-custom-footer [data-cta] form button:hover { background: #eaec6a; } +#ch-custom-footer [data-gh] { display: inline-flex; align-items: center; justify-content: center; gap: 8px; background: transparent; padding: 10px 20px; border-radius: 8px; font-size: 13px; font-weight: 600; width: 100%; } +#ch-custom-footer [data-gh]:hover { border-color: #888 !important; } +#ch-custom-footer [data-divider] { margin-bottom: 24px; } +#ch-custom-footer [data-bottom] { display: flex; flex-wrap: wrap; justify-content: space-between; align-items: center; gap: 12px; } +#ch-custom-footer [data-bottom] a { font-size: 13px; white-space: nowrap; } +#ch-custom-footer [data-legal] { display: flex; flex-wrap: wrap; gap: 16px; } +@media (max-width: 639px) { + #ch-custom-footer [data-bottom] { flex-direction: column; text-align: center; } + #ch-custom-footer [data-legal] { justify-content: center; } +} +#ch-custom-footer [data-sitemap] h3 { color: #111; } +#ch-custom-footer [data-sitemap] a { color: #6b7280; } +#ch-custom-footer [data-sitemap] a:hover { color: #111; } +#ch-custom-footer [data-cta] p { color: #6b7280; } +#ch-custom-footer [data-cta] form { border: 1px solid #d1d5db; } +#ch-custom-footer [data-cta] form input { color: #111; } +#ch-custom-footer [data-cta] form input::placeholder { color: #9ca3af; } +#ch-custom-footer [data-gh] { color: #111; border: 1px solid #d1d5db; } +#ch-custom-footer [data-divider] { border-top: 1px solid #e5e7eb; } +#ch-custom-footer [data-copyright] { font-size: 13px; color: #6b7280; } +#ch-custom-footer [data-bottom] a { color: #6b7280; } +#ch-custom-footer [data-bottom] a:hover { color: #111; } +#ch-custom-footer [data-logo] { margin-bottom: 16px; } +#ch-custom-footer [data-logo] svg * { fill: #111; } +.dark #ch-custom-footer [data-sitemap] h3 { color: #f5f5f5; } +.dark #ch-custom-footer [data-sitemap] a { color: #a3a3a3; } +.dark #ch-custom-footer [data-sitemap] a:hover { color: #f5f5f5; } +.dark #ch-custom-footer [data-cta] p { color: #a3a3a3; } +.dark #ch-custom-footer [data-cta] form { border-color: #555; } +.dark #ch-custom-footer [data-cta] form input { color: #fff; } +.dark #ch-custom-footer [data-cta] form input::placeholder { color: #6b7280; } +.dark #ch-custom-footer [data-gh] { color: #fff; border-color: #555; } +.dark #ch-custom-footer [data-divider] { border-color: #333; } +.dark #ch-custom-footer [data-copyright] { color: #a3a3a3; } +.dark #ch-custom-footer [data-bottom] a { color: #a3a3a3; } +.dark #ch-custom-footer [data-bottom] a:hover { color: #f5f5f5; } +.dark #ch-custom-footer [data-logo] svg * { fill: #fff; } From 5e8901a89f337fdcd47cc59cd609f7cf54ab2993 Mon Sep 17 00:00:00 2001 From: Shaun Struwig <41984034+Blargian@users.noreply.github.com> Date: Thu, 25 Jun 2026 08:12:34 +0200 Subject: [PATCH 2/4] Fix homepage navbar hidden + remove scroll-reset for testing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit navbar-cta.js: revert early-return + observer.disconnect() — React hydration removes the injected CTA from the SSR DOM, so the observer must keep running to re-inject it. ch-navbar-ready was never being set, leaving the homepage navbar at opacity:0. scroll-reset.js: update to dual-detection approach (pushState patch + 100ms setInterval fallback), but remove from docs.json entirely for now to isolate whether it contributes to nav slowness. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- _site/customizations/navbar-cta.js | 4 ++-- _site/customizations/scroll-reset.js | 28 ++++++++++++++++++---------- docs.json | 3 --- 3 files changed, 20 insertions(+), 15 deletions(-) diff --git a/_site/customizations/navbar-cta.js b/_site/customizations/navbar-cta.js index 4dd947ef3..edc806924 100644 --- a/_site/customizations/navbar-cta.js +++ b/_site/customizations/navbar-cta.js @@ -68,14 +68,14 @@ } function init() { - if (injectCta()) return; + injectCta(); var rafId = null; var observer = new MutationObserver(function () { if (rafId) return; rafId = requestAnimationFrame(function () { rafId = null; - if (injectCta()) observer.disconnect(); + injectCta(); }); }); observer.observe(document.documentElement, { childList: true, subtree: true }); diff --git a/_site/customizations/scroll-reset.js b/_site/customizations/scroll-reset.js index 6798e29d4..b0f24a84f 100644 --- a/_site/customizations/scroll-reset.js +++ b/_site/customizations/scroll-reset.js @@ -6,14 +6,19 @@ // re-rendered segment, so the router considers the new page "already in // viewport" and leaves the scroll position where it was. // - // Patch history.pushState/replaceState at eval time (before Next.js hydration) - // to fire a synthetic 'ch:navigate' event on every SPA navigation. This replaces - // the rAF polling loop the original used — the loop ran 60fps for the entire page - // lifetime; the event fires only on actual navigations. + // Two-layer detection: + // 1. Patch history.pushState/replaceState at eval time — covers tab-nav.js's + // own history.pushState() calls and any navigation before Next.js hydrates. + // 2. setInterval at 100ms — covers the case where Next.js hydrates before + // this script runs and calls a cached reference to the original pushState, + // bypassing our wrapper. 100ms fires ~10×/sec (vs the original rAF loop + // at ~60×/sec) — same correctness guarantee with 6× less polling. // - // Back/forward (popstate) is left alone so the browser and router can restore - // the previous scroll position. Cross-page hash links (/page#anchor) scroll to - // the anchor once the new page has rendered it. + // Both paths are idempotent: check() compares lastPath before acting, so a + // rapid pushState patch + interval tick never double-scrolls. + // + // Back/forward (popstate) is deliberately left alone so the browser and + // router can restore the previous scroll position. (function patchHistory() { function wrap(orig) { @@ -54,7 +59,7 @@ } } - window.addEventListener('ch:navigate', function () { + function check() { var path = window.location.pathname; if (path === lastPath) return; lastPath = path; @@ -65,5 +70,8 @@ } else { window.scrollTo(0, 0); } - }); -})(); + } + + window.addEventListener('ch:navigate', check); + setInterval(check, 100); +})(); \ No newline at end of file diff --git a/docs.json b/docs.json index 6d3790f55..ca848d851 100644 --- a/docs.json +++ b/docs.json @@ -185,9 +185,6 @@ "$ref": "_site/redirects.json" }, "scripts": [ - { - "src": "/_site/customizations/scroll-reset.js" - }, { "src": "/_site/customizations/custom-footer.js" }, From b9b16c048b158a6f7ac3d898e1dcc3636deb4916 Mon Sep 17 00:00:00 2001 From: Shaun Struwig <41984034+Blargian@users.noreply.github.com> Date: Thu, 25 Jun 2026 08:17:39 +0200 Subject: [PATCH 3/4] =?UTF-8?q?Revert=20tab-nav=20pushState=20=E2=80=94=20?= =?UTF-8?q?use=20location.href=20for=20button=20nav?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit history.pushState() only changes the address bar; it doesn't trigger a page load unless Next.js App Router specifically intercepts it, which isn't guaranteed for external callers. Revert to window.location.href for button.nav-tabs-item elements so tab/locale navigation is always a reliable full-page load rather than a silent URL-only change. The .nav-tabs-item href-rewrite path is unaffected (unchanged). Co-Authored-By: Claude Sonnet 4.6 (1M context) --- _site/customizations/tab-nav.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_site/customizations/tab-nav.js b/_site/customizations/tab-nav.js index 5ad54581d..bfbc44348 100644 --- a/_site/customizations/tab-nav.js +++ b/_site/customizations/tab-nav.js @@ -53,7 +53,7 @@ labelDiv.style.cursor = 'pointer'; labelDiv.addEventListener('click', function (e) { e.stopPropagation(); - history.pushState(null, '', localizeUrl(url)); + window.location.href = localizeUrl(url); }); }); } From 436c2f09d9e421b2b8dce79fe7c8ae69c1141b01 Mon Sep 17 00:00:00 2001 From: Shaun Struwig <41984034+Blargian@users.noreply.github.com> Date: Thu, 25 Jun 2026 10:48:06 +0200 Subject: [PATCH 4/4] Remove scroll-reset.js entirely MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Confirmed not needed — Mintlify/Next.js handles scroll position correctly without it. The script was working around a banner-related scroll bug that no longer reproduces. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- _site/customizations/scroll-reset.js | 77 ---------------------------- 1 file changed, 77 deletions(-) delete mode 100644 _site/customizations/scroll-reset.js diff --git a/_site/customizations/scroll-reset.js b/_site/customizations/scroll-reset.js deleted file mode 100644 index b0f24a84f..000000000 --- a/_site/customizations/scroll-reset.js +++ /dev/null @@ -1,77 +0,0 @@ -(function () { - 'use strict'; - - // With an announcement banner configured, Next.js skips its scroll-to-top - // on client-side navigation: the banner is position:fixed at the top of the - // re-rendered segment, so the router considers the new page "already in - // viewport" and leaves the scroll position where it was. - // - // Two-layer detection: - // 1. Patch history.pushState/replaceState at eval time — covers tab-nav.js's - // own history.pushState() calls and any navigation before Next.js hydrates. - // 2. setInterval at 100ms — covers the case where Next.js hydrates before - // this script runs and calls a cached reference to the original pushState, - // bypassing our wrapper. 100ms fires ~10×/sec (vs the original rAF loop - // at ~60×/sec) — same correctness guarantee with 6× less polling. - // - // Both paths are idempotent: check() compares lastPath before acting, so a - // rapid pushState patch + interval tick never double-scrolls. - // - // Back/forward (popstate) is deliberately left alone so the browser and - // router can restore the previous scroll position. - - (function patchHistory() { - function wrap(orig) { - return function () { - var result = orig.apply(this, arguments); - window.dispatchEvent(new Event('ch:navigate')); - return result; - }; - } - history.pushState = wrap(history.pushState); - history.replaceState = wrap(history.replaceState); - })(); - - var lastPath = window.location.pathname; - var traversed = false; - - window.addEventListener('popstate', function () { - if (window.location.pathname !== lastPath) { - traversed = true; - } - }); - - // The new page renders some frames after the path changes, so poll for the - // anchor target before scrolling to it; if it never appears (bad anchor), - // fall back to the top rather than keeping the old page's position. - function scrollToAnchor(hash, framesLeft) { - var id; - try { id = decodeURIComponent(hash.slice(1)); } catch (e) { id = hash.slice(1); } - var el = document.getElementById(id); - if (el) { - el.scrollIntoView(); - return; - } - if (framesLeft > 0 && window.location.hash === hash) { - window.requestAnimationFrame(function () { scrollToAnchor(hash, framesLeft - 1); }); - } else { - window.scrollTo(0, 0); - } - } - - function check() { - var path = window.location.pathname; - if (path === lastPath) return; - lastPath = path; - if (traversed) { - traversed = false; - } else if (window.location.hash) { - scrollToAnchor(window.location.hash, 180); - } else { - window.scrollTo(0, 0); - } - } - - window.addEventListener('ch:navigate', check); - setInterval(check, 100); -})(); \ No newline at end of file