diff --git a/_site/styles.css b/_site/styles.css index f5359d688..0175de826 100644 --- a/_site/styles.css +++ b/_site/styles.css @@ -2836,3 +2836,271 @@ a.mobile-nav-tabs-item[href$="/products/clickhouse-private"], a.mobile-nav-tabs-item[href$="/products/clickhouse-private/index"] { display: none !important; } + +/* ============================================ + Google Programmable Search (CSE) — /search + ============================================ + + The CSE widget (search.mdx) ships Google's own dated CSS: a light-grey + `.gsc-control-cse` block, a cramped input with an "Enhanced by Google" + watermark, and result links that our aggressive `.prose a` overrides turn + into underlined yellow/blue. These `.gsc-*` / `.gs-*` classes exist only on + the /search page, so scoping the whole restyle to them (rather than a page + selector) is safe. Goal: a clean full-width input + a readable results list + in the spirit of kubernetes.io/search, in both light and dark mode. All + rules use !important and sit at the end of the file so they win over the + prose link rules on equal specificity via source order. */ + +/* — Container reset: drop Google's grey box, border, padding, and font — */ +.gsc-control-cse { + background: transparent !important; + border: none !important; + padding: 0 !important; + font-family: inherit !important; + width: 100% !important; +} + +/* — Our own search box (rendered by search.mdx; CSE runs results-only, so it + emits no search box of its own). Styled to match the left-sidebar search + #search-bar-entry — 4px pill, gray-400/30 ring, left magnifier, text-sm — + at full content width like the kubernetes.io reference. */ +.ch-cse { width: 100% !important; } +.ch-cse-form { + display: flex !important; + align-items: center !important; + gap: 0.5rem !important; + height: 2.25rem !important; /* h-9, like the sidebar search */ + padding: 0 0.75rem !important; + border: 1px solid rgba(156, 163, 175, 0.3) !important; /* gray-400/30 ring */ + border-radius: 4px !important; + background: #ffffff !important; + transition: border-color 0.12s ease !important; +} +.ch-cse-form:hover, +.ch-cse-form:focus-within { + border-color: rgba(75, 85, 99, 0.3) !important; /* gray-600/30 */ +} +.ch-cse-icon { flex: 0 0 auto !important; color: #374151 !important; } /* gray-700 */ +.ch-cse-input { + flex: 1 1 auto !important; + min-width: 0 !important; + border: none !important; + outline: none !important; + background: transparent !important; + color: #1f1f1c !important; + font-size: 0.875rem !important; /* text-sm */ + line-height: 1.5 !important; + padding: 0 !important; + box-shadow: none !important; +} +.ch-cse-input::placeholder { color: #6b7280 !important; } +/* "Enter ↵" keycap on the right — signals you press Enter to search, and is + itself a clickable submit button. */ +.ch-cse-enter { + flex: 0 0 auto !important; + display: inline-flex !important; + align-items: center !important; + gap: 0.3rem !important; + height: 1.5rem !important; + padding: 0 0.45rem !important; + border: 1px solid rgba(156, 163, 175, 0.4) !important; + border-radius: 4px !important; + background: #f3f4f6 !important; + color: #6b7280 !important; + font-size: 0.75rem !important; + font-weight: 600 !important; + line-height: 1 !important; + white-space: nowrap !important; + cursor: pointer !important; + transition: background-color 0.12s ease, border-color 0.12s ease, color 0.12s ease !important; +} +.ch-cse-enter:hover { + background: #e5e7eb !important; + border-color: rgba(75, 85, 99, 0.4) !important; + color: #374151 !important; +} +.ch-cse-enter-key { font-size: 0.85rem !important; line-height: 1 !important; } + +.dark .ch-cse-enter, +:is(.dark) .ch-cse-enter { + background: rgba(255, 255, 255, 0.08) !important; + border-color: rgba(255, 255, 255, 0.12) !important; + color: rgb(190, 190, 190) !important; +} +.dark .ch-cse-enter:hover, +:is(.dark) .ch-cse-enter:hover { + background: rgba(255, 255, 255, 0.14) !important; + border-color: rgba(255, 255, 255, 0.2) !important; + color: #ffffff !important; +} + +/* Gap between the box and the results block. */ +.ch-cse > div:last-child { margin-top: 1.25rem !important; } +/* results-only mode emits no visible search box, but hide any Google-rendered + input box defensively so it can never flash alongside our own. */ +.ch-cse .gsc-search-box, +.ch-cse .gsc-input-box { display: none !important; } + +/* Dark mode — translucent fill + hairline border, like the dark sidebar search. */ +.dark .ch-cse-form, +:is(.dark) .ch-cse-form { + background: rgba(255, 255, 255, 0.06) !important; + border-color: rgba(255, 255, 255, 0.07) !important; +} +.dark .ch-cse-form:hover, +.dark .ch-cse-form:focus-within, +:is(.dark) .ch-cse-form:hover, +:is(.dark) .ch-cse-form:focus-within { + border-color: rgba(255, 255, 255, 0.14) !important; +} +.dark .ch-cse-icon, :is(.dark) .ch-cse-icon { color: rgb(206, 206, 206) !important; } +.dark .ch-cse-input, :is(.dark) .ch-cse-input { color: rgb(206, 206, 206) !important; } +.dark .ch-cse-input::placeholder, +:is(.dark) .ch-cse-input::placeholder { color: #9ca3af !important; } + +/* — Results toolbar ("About N results", tabs) — */ +.gsc-above-wrapper-area { + border-bottom: 1px solid #e5e7eb !important; + padding: 0.5rem 0 !important; +} +.gsc-result-info { + color: #6b7280 !important; + padding-left: 0 !important; +} +.gsc-tabsArea { border-color: #e5e7eb !important; } + +/* — Results list — */ +.gsc-results-wrapper-overlay, +.gsc-results, +.gsc-webResult .gsc-result { + background: transparent !important; + border: none !important; + box-shadow: none !important; +} +.gsc-webResult .gsc-result { + padding: 0.9rem 0 !important; + border-bottom: 1px solid #f0f0f0 !important; +} +.gsc-table-result { font-family: inherit !important; } + +/* Result title link (beats the .prose a overrides). */ +.gs-webResult .gs-title, +.gs-webResult a.gs-title, +.gs-webResult a.gs-title b, +.gsc-webResult .gs-title a.gs-title { + color: #135be6 !important; + text-decoration: none !important; + font-weight: 600 !important; + font-size: 1.05rem !important; + height: auto !important; +} +.gs-webResult a.gs-title:hover, +.gsc-webResult .gs-title a.gs-title:hover { + text-decoration: underline !important; + color: #135be6 !important; +} + +/* Breadcrumb / visible URL — green like the reference. */ +.gsc-url-top, +.gs-webResult .gs-visibleUrl, +.gs-webResult .gs-visibleUrl-long, +.gs-webResult .gs-visibleUrl-short, +.gs-webResult .gs-visibleUrl-breadcrumb { + color: #0b8043 !important; + font-size: 0.85rem !important; +} + +/* Snippet body text. */ +.gs-webResult .gs-snippet, +.gs-snippet, +.gs-webResult .gs-snippet b { + color: #374151 !important; + font-size: 0.9rem !important; + line-height: 1.5 !important; +} + +/* — Pagination — */ +.gsc-cursor-box { + text-align: center !important; + margin: 1.5rem 0 !important; +} +.gsc-cursor-page { + color: #135be6 !important; + padding: 0 0.4rem !important; + background: transparent !important; + text-decoration: none !important; +} +.gsc-cursor-current-page { + color: #1f1f1c !important; + font-weight: 700 !important; +} + +/* — Branding / find-more ("Search 'x' on Google") — keep it inline: Google + ships the magnifier svg as display:block, which drops it onto its own line + above the text, so force it inline and vertically centred beside the text. */ +.gcsc-find-more-on-google { + color: #135be6 !important; + text-align: center !important; +} +.gcsc-find-more-on-google-magnifier { + display: inline-block !important; + vertical-align: middle !important; + margin-right: 0.35rem !important; + fill: #135be6 !important; +} +.gcsc-find-more-on-google-text, +.gcsc-find-more-on-google-query { vertical-align: middle !important; } + +/* — Hide ads — */ +.gsc-adBlock, +.gsc-webResult.gsc-result.gsc-ad, +.gsc-results .gsc-cursor-box.gs-bidi-start-align.gsc-adBlock { + display: none !important; +} + +/* ============================================ + CSE results — Dark mode + ============================================ */ +.dark .gsc-above-wrapper-area, +:is(.dark) .gsc-above-wrapper-area { border-bottom-color: rgba(255, 255, 255, 0.1) !important; } +.dark .gsc-result-info, +:is(.dark) .gsc-result-info { color: #a1a1aa !important; } + +.dark .gsc-webResult .gsc-result, +:is(.dark) .gsc-webResult .gsc-result { border-bottom-color: rgba(255, 255, 255, 0.08) !important; } + +/* Titles: ClickHouse yellow (matches docs dark links). */ +.dark .gs-webResult .gs-title, +.dark .gs-webResult a.gs-title, +.dark .gs-webResult a.gs-title b, +.dark .gsc-webResult .gs-title a.gs-title, +:is(.dark) .gs-webResult a.gs-title, +:is(.dark) .gsc-webResult .gs-title a.gs-title { + color: #FAFF69 !important; +} +.dark .gs-webResult a.gs-title:hover, +:is(.dark) .gsc-webResult .gs-title a.gs-title:hover { color: #FAFF69 !important; } + +.dark .gsc-url-top, +.dark .gs-webResult .gs-visibleUrl, +.dark .gs-webResult .gs-visibleUrl-long, +.dark .gs-webResult .gs-visibleUrl-short, +.dark .gs-webResult .gs-visibleUrl-breadcrumb, +:is(.dark) .gs-webResult .gs-visibleUrl, +:is(.dark) .gs-webResult .gs-visibleUrl-long { + color: #57bd8a !important; +} +.dark .gs-webResult .gs-snippet, +.dark .gs-snippet, +.dark .gs-webResult .gs-snippet b, +:is(.dark) .gs-webResult .gs-snippet, +:is(.dark) .gs-snippet { color: #d1d5db !important; } + +.dark .gsc-cursor-page, +:is(.dark) .gsc-cursor-page { color: #FAFF69 !important; } +.dark .gsc-cursor-current-page, +:is(.dark) .gsc-cursor-current-page { color: #ffffff !important; } +.dark .gcsc-find-more-on-google, +:is(.dark) .gcsc-find-more-on-google { color: #FAFF69 !important; } +.dark .gcsc-find-more-on-google-magnifier, +:is(.dark) .gcsc-find-more-on-google-magnifier { fill: #FAFF69 !important; } diff --git a/search.mdx b/search.mdx new file mode 100644 index 000000000..e187dae5e --- /dev/null +++ b/search.mdx @@ -0,0 +1,146 @@ +--- +title: "Search" +description: "Search the ClickHouse documentation." +--- + +{/* + Custom search box + Google Programmable Search (CSE) in RESULTS-ONLY mode. + + Rather than embedding the combined CSE widget (which renders its own + hard-to-style search box and drives the URL with an ugly `#gsc.tab=0&gsc.q=…` + hash), we render OUR OWN input — styled like the sidebar search — and a + `searchresults-only` CSE element, then run queries via the CSE API + (`element.execute(q)`). This is the approach kubernetes.io/search uses. + + - Loader runs in `parsetags: 'explicit'` mode so nothing auto-renders; we + render the results element imperatively into a ref'd container (a node React + does not manage, so re-renders/hydration can't wipe Google's injected DOM — + the same reason the container is created in an effect, not authored in MDX). + - The URL is ours: submitting sets `?q=…` via history, and on load we read + `?q=` and run it. No `#gsc.*` hash. + + This page is intentionally omitted from navigation.json (reachable at /search, + not listed in the sidebar). Styling lives in _site/styles.css (.ch-cse*, .gsc*). +*/} + +export const DocSearch = () => { + const CX = 'e11478a2bdeb94dd1'; + const GNAME = 'chsearch'; + const resultsRef = useRef(null); + const inputRef = useRef(null); + const lastQRef = useRef(undefined); // last query handed to the CSE element + + const execute = (q) => { + try { + const cse = window.google && window.google.search && window.google.search.cse; + const el = cse && window.google.search.cse.element.getElement(GNAME); + if (el) el.execute(q); + } catch (e) { /* noop */ } + }; + + // Run a query in place: sync the input, and re-execute only when it actually + // changed. Dedup means an unchanged `?q=` (or a repeat submit) never rebuilds + // the results — it updates rather than destroys. + const runQuery = (q) => { + if (inputRef.current && inputRef.current.value !== q) inputRef.current.value = q; + if (q === lastQRef.current) return; + lastQRef.current = q; + if (q) execute(q); + }; + + const syncFromUrl = () => { + runQuery((new URLSearchParams(window.location.search).get('q') || '').trim()); + }; + + useEffect(() => { + const wrapper = resultsRef.current; + if (!wrapper) return; + + // The results container is stashed on window and re-attached to the current + // wrapper on every (re)mount. React remounts this component on things like a + // theme toggle; if the CSE-rendered results lived in the discarded wrapper + // they'd vanish (the CSE element stays bound to the detached node). Keeping + // one persistent node and moving it into the live wrapper preserves them. + let node = window.__chCseResultsNode; + if (!node) { + node = document.createElement('div'); + node.className = 'ch-cse-results'; + window.__chCseResultsNode = node; + } + if (node.parentNode !== wrapper) wrapper.appendChild(node); + + const initial = (new URLSearchParams(window.location.search).get('q') || '').trim(); + if (initial && inputRef.current) inputRef.current.value = initial; + + const boot = () => { + const cse = window.google && window.google.search && window.google.search.cse; + if (!cse) return; + if (!window.google.search.cse.element.getElement(GNAME)) { + window.google.search.cse.element.render({ + div: node, + tag: 'searchresults-only', + gname: GNAME, + attributes: { enableHistory: false }, + }); + } + runQuery(initial); + }; + + // Update (don't destroy) the search when the `?q=` param changes via + // back/forward or an in-app link to /search?q=… (a full-reload URL edit + // remounts and re-boots instead). runQuery dedups, so this is a no-op when + // the query is unchanged. + window.addEventListener('popstate', syncFromUrl); + + if (!document.getElementById('google-cse-script')) { + // Must be set BEFORE the loader runs. + window.__gcse = { parsetags: 'explicit', callback: boot }; + const s = document.createElement('script'); + s.id = 'google-cse-script'; + s.async = true; + s.src = 'https://cse.google.com/cse.js?cx=' + CX; + document.head.appendChild(s); + } else if (window.google && window.google.search && window.google.search.cse) { + boot(); // loader already ready (remount / SPA return) + } else { + // Script tag present but still loading — chain onto its callback. + const prev = window.__gcse && window.__gcse.callback; + window.__gcse = { parsetags: 'explicit', callback: () => { try { prev && prev(); } catch (e) {} boot(); } }; + } + + return () => window.removeEventListener('popstate', syncFromUrl); + }, []); + + const onSubmit = (e) => { + e.preventDefault(); + const q = (inputRef.current ? inputRef.current.value : '').trim(); + const qs = q ? ('?q=' + encodeURIComponent(q)) : ''; + const target = window.location.pathname + qs; + // pushState (not replace) so each search is a history entry — back/forward + // then moves between searches, handled by the popstate listener above. + if (target !== window.location.pathname + window.location.search) { + window.history.pushState(null, '', target); + } + runQuery(q); + }; + + return ( +
+
+ + + +
+
+
+ ); +}; + + +