From a1c4b456b917d15f79ec4d48affc40c69724498d Mon Sep 17 00:00:00 2001 From: ashmod Date: Tue, 24 Feb 2026 15:18:05 +0200 Subject: [PATCH 1/5] mount dropdown to searchform --- gcp/website/frontend3/src/search.js | 154 ++++++++++++++++++++------ gcp/website/frontend3/src/styles.scss | 45 +++++--- 2 files changed, 147 insertions(+), 52 deletions(-) diff --git a/gcp/website/frontend3/src/search.js b/gcp/website/frontend3/src/search.js index 55965d8e1cf..c4bb31c9a45 100644 --- a/gcp/website/frontend3/src/search.js +++ b/gcp/website/frontend3/src/search.js @@ -1,6 +1,7 @@ import { submitForm } from './index.js'; const MIN_QUERY_LENGTH = 3; +const NAV_SEARCH_CLOSE_FALLBACK_MS = 380; export class ExpandableSearch { constructor() { @@ -163,11 +164,23 @@ export class ExpandableSearch { export class SearchSuggestionsManager { constructor(inputElement) { this.input = inputElement; + this.suggestionsContainer = this.input?.closest('.search-suggestions-container'); + this.isNavContext = Boolean(this.suggestionsContainer?.closest('.search-container-nav')); + this.mountMode = this.isNavContext ? 'inline' : 'portal'; this.suggestionsElement = null; this.selectedIndex = -1; this.currentSuggestions = []; this.debounceTimer = null; this.isDestroyed = false; + this.inputHandler = null; + this.keydownHandler = null; + this.blurHandler = null; + this.visiblePositionUpdateHandler = () => { + if (this.isDestroyed || !this.suggestionsElement || this.suggestionsElement.classList.contains('search-suggestions--hidden')) { + return; + } + this.updatePosition(); + }; this.init(); } @@ -185,14 +198,10 @@ export class SearchSuggestionsManager { this.suggestionsElement = document.createElement('div'); this.suggestionsElement.classList.add('search-suggestions', 'search-suggestions--hidden'); - // Add a context-specific class based on the input's parent - const container = this.input.closest('.search-suggestions-container'); - if (container) { - if (container.closest('.list-page')) { - this.suggestionsElement.classList.add('search-suggestions--list-page'); - } else if (container.closest('.search-container-nav')) { - this.suggestionsElement.classList.add('search-suggestions--nav'); - } + if (this.isNavContext) { + this.suggestionsElement.classList.add('search-suggestions--nav'); + } else if (this.suggestionsContainer?.closest('.list-page')) { + this.suggestionsElement.classList.add('search-suggestions--list-page'); } // Add a unique identifier to track this element for cleanup @@ -202,13 +211,14 @@ export class SearchSuggestionsManager { this.appendSuggestionsElement(); } - /** - * Appends the suggestions element to the document body. - * If the body is not yet available, it waits and retries. - */ appendSuggestionsElement() { if (this.isDestroyed) return; + if (this.mountMode === 'inline' && this.suggestionsContainer) { + this.suggestionsContainer.appendChild(this.suggestionsElement); + return; + } + if (document.body) { document.body.appendChild(this.suggestionsElement); } else { @@ -219,16 +229,24 @@ export class SearchSuggestionsManager { setupEventListeners() { if (!this.input) return; - - this.input.addEventListener('input', () => { + + this.inputHandler = () => { if (this.isDestroyed) return; this.selectedIndex = -1; clearTimeout(this.debounceTimer); this.debounceTimer = setTimeout(() => this.handleInput(), 300); - }); + }; + this.keydownHandler = (e) => this.handleKeydown(e); + this.blurHandler = () => setTimeout(() => this.hide(), 200); - this.input.addEventListener('keydown', (e) => this.handleKeydown(e)); - this.input.addEventListener('blur', () => setTimeout(() => this.hide(), 200)); // Delay to allow click events on suggestions + this.input.addEventListener('input', this.inputHandler); + this.input.addEventListener('keydown', this.keydownHandler); + this.input.addEventListener('blur', this.blurHandler); + + if (this.mountMode === 'portal') { + window.addEventListener('resize', this.visiblePositionUpdateHandler); + window.addEventListener('scroll', this.visiblePositionUpdateHandler, true); + } } async handleInput() { @@ -304,24 +322,21 @@ export class SearchSuggestionsManager { } updatePosition() { - const rect = this.input.getBoundingClientRect(); - - // Look for the designated suggestions container - const suggestionContainer = this.input.closest('.search-suggestions-container'); - - if (suggestionContainer) { - // Use the designated suggestions container - const containerRect = suggestionContainer.getBoundingClientRect(); - this.suggestionsElement.style.left = `${containerRect.left + window.scrollX}px`; - this.suggestionsElement.style.top = `${containerRect.bottom + window.scrollY}px`; - this.suggestionsElement.style.width = `${containerRect.width}px`; - } else { - console.warn('No .search-suggestions-container found. Add this class to the desired parent element to control suggestions positioning.'); - // Fallback to input element positioning - this.suggestionsElement.style.left = `${rect.left + window.scrollX}px`; - this.suggestionsElement.style.top = `${rect.bottom + window.scrollY}px`; - this.suggestionsElement.style.width = `${rect.width}px`; + if (!this.suggestionsElement) { + return; + } + + if (this.mountMode === 'inline') { + this.suggestionsElement.style.left = ''; + this.suggestionsElement.style.top = ''; + this.suggestionsElement.style.width = ''; + return; } + + const anchorRect = (this.suggestionsContainer || this.input).getBoundingClientRect(); + this.suggestionsElement.style.left = `${anchorRect.left + window.scrollX}px`; + this.suggestionsElement.style.top = `${anchorRect.bottom + window.scrollY}px`; + this.suggestionsElement.style.width = `${anchorRect.width}px`; } render() { @@ -350,12 +365,81 @@ export class SearchSuggestionsManager { selectSuggestion(suggestion) { this.input.value = suggestion; this.hide(); - submitForm(this.input.closest('form')); + + const form = this.input.closest('form'); + if (!form) { + return; + } + + if (this.isNavContext) { + if (window.OSVSearchInstance && typeof window.OSVSearchInstance.closeSearch === 'function') { + window.OSVSearchInstance.closeSearch(); + } else { + form.classList.remove('active'); + const searchContainer = form.closest('.search-container-nav'); + const searchToggle = searchContainer?.querySelector('.search-toggle'); + searchToggle?.classList.remove('active'); + searchToggle?.setAttribute('aria-expanded', 'false'); + } + + this.submitAfterNavClose(form); + return; + } + + submitForm(form); + } + + submitAfterNavClose(form) { + let hasSubmitted = false; + let fallbackTimer = null; + + const submitOnce = () => { + if (hasSubmitted) { + return; + } + + hasSubmitted = true; + form.removeEventListener('transitionend', onTransitionEnd); + + if (fallbackTimer) { + clearTimeout(fallbackTimer); + } + + submitForm(form); + }; + + const onTransitionEnd = (event) => { + if (event.target !== form || event.propertyName !== 'width') { + return; + } + + submitOnce(); + }; + + form.addEventListener('transitionend', onTransitionEnd); + fallbackTimer = setTimeout(submitOnce, NAV_SEARCH_CLOSE_FALLBACK_MS); } destroy() { this.isDestroyed = true; clearTimeout(this.debounceTimer); + + if (this.input) { + if (this.inputHandler) { + this.input.removeEventListener('input', this.inputHandler); + } + if (this.keydownHandler) { + this.input.removeEventListener('keydown', this.keydownHandler); + } + if (this.blurHandler) { + this.input.removeEventListener('blur', this.blurHandler); + } + } + + if (this.mountMode === 'portal') { + window.removeEventListener('resize', this.visiblePositionUpdateHandler); + window.removeEventListener('scroll', this.visiblePositionUpdateHandler, true); + } if (this.suggestionsElement) { this.suggestionsElement.remove(); diff --git a/gcp/website/frontend3/src/styles.scss b/gcp/website/frontend3/src/styles.scss index ded62fd50cd..3365e48b3ad 100644 --- a/gcp/website/frontend3/src/styles.scss +++ b/gcp/website/frontend3/src/styles.scss @@ -1927,6 +1927,12 @@ div.highlight { } /* Navbar search styles */ +$navbar-search-surface: rgba(41, 41, 41, 0.95); +$navbar-search-border: rgba(255, 255, 255, 0.3); +$navbar-search-radius: 19px; +$navbar-search-divider: rgba(255, 255, 255, 0.16); +$navbar-search-control-height: 38px; + .search-container-nav { position: relative; display: flex; @@ -1954,7 +1960,7 @@ div.highlight { display: flex; align-items: center; justify-content: center; - transition: background-color 0.2s ease, opacity 0.3s ease, visibility 0.3s ease; + transition: background-color 0.2s ease-in-out, opacity 0.15s ease-in-out, visibility 0s linear 0.15s; border-radius: 50%; &:hover { @@ -1980,19 +1986,20 @@ div.highlight { .search-form { position: absolute; right: 0; - top: 50%; - transform: translateY(-50%); + top: calc(50% - 19px); + transform: none; display: flex; + flex-wrap: nowrap; align-items: center; width: 0; overflow: hidden; - transition: width 0.3s ease, opacity 0.3s ease, visibility 0.3s ease; - background: rgba(41, 41, 41, 0.95); - border-radius: 24px; + transition: width 0.3s ease-in-out 0.1s, opacity 0.1s ease-in-out, visibility 0s linear 0.1s; + background: $navbar-search-surface; + border-radius: $navbar-search-radius; z-index: 1000; opacity: 0; visibility: hidden; - border: 1px solid rgba(255, 255, 255, 0.3); + border: 1px solid $navbar-search-border; box-sizing: border-box; &.active { @@ -2000,10 +2007,12 @@ div.highlight { box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); opacity: 1; visibility: visible; + transition: width 0.3s ease-in-out, opacity 0.15s ease-in-out 0.1s, visibility 0s linear; } .search-input { flex: 1; + min-width: 0; border: none; padding: 10px 16px; font-size: 14px; @@ -2036,7 +2045,7 @@ div.highlight { align-items: center; justify-content: center; border-radius: 50%; - transition: background-color 0.2s ease; + transition: background-color 0.2s ease-in-out; height: 36px; width: 36px; @@ -2087,8 +2096,8 @@ div.highlight { } .search-form.suggestions-active { - border-bottom-left-radius: 0; - border-bottom-right-radius: 0; + flex-wrap: wrap; + box-shadow: 0 5px 20px rgba(0, 0, 0, 0.4); } @@ -2112,13 +2121,15 @@ div.highlight { /* --- Styles for the Navbar Search Bar --- */ .search-suggestions--nav { - background: rgba(41, 41, 41, 0.95); - border: 1px solid rgba(255, 255, 255, 0.3); - border-top: none; - border-radius: 0 0 24px 24px; - width: 280px; - box-shadow: 0 5px 20px rgba(0, 0, 0, 0.4); - margin-top: -1px; + position: static; + flex-basis: 100%; + width: 100%; + background: transparent; + border: none; + border-top: 1px solid $navbar-search-divider; + border-radius: 0; + box-shadow: none; + margin-top: 0; .search-suggestions__item { &:hover, From 8c6cc910a7f364bd5933a6b34f1f357b8c37cd91 Mon Sep 17 00:00:00 2001 From: ashmod Date: Tue, 24 Feb 2026 15:28:08 +0200 Subject: [PATCH 2/5] =?UTF-8?q?=F0=9F=A7=BC=F0=9F=A7=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gcp/website/frontend3/src/search.js | 213 +++++++++++--------------- gcp/website/frontend3/src/styles.scss | 79 +++++----- 2 files changed, 122 insertions(+), 170 deletions(-) diff --git a/gcp/website/frontend3/src/search.js b/gcp/website/frontend3/src/search.js index c4bb31c9a45..95146bba001 100644 --- a/gcp/website/frontend3/src/search.js +++ b/gcp/website/frontend3/src/search.js @@ -14,24 +14,13 @@ export class ExpandableSearch { } cleanupExistingInstances() { - // If there's a global instance, clean it up - if (window.OSVSearchInstance) { - if (window.OSVSearchInstance.documentClickHandler) { - document.removeEventListener('click', window.OSVSearchInstance.documentClickHandler); - } - if (window.OSVSearchInstance.documentKeydownHandler) { - document.removeEventListener('keydown', window.OSVSearchInstance.documentKeydownHandler); - } - if (window.OSVSearchInstance.cacheHandler) { - document.removeEventListener('turbo:before-cache', window.OSVSearchInstance.cacheHandler); - } - // Cleanup existing suggestions manager - if (window.OSVSearchInstance.suggestionsManager) { - window.OSVSearchInstance.suggestionsManager.destroy(); - } + const prev = window.OSVSearchInstance; + if (prev) { + document.removeEventListener('click', prev.documentClickHandler); + document.removeEventListener('keydown', prev.documentKeydownHandler); + document.removeEventListener('turbo:before-cache', prev.cacheHandler); + prev.suggestionsManager?.destroy(); } - - // Store this instance globally window.OSVSearchInstance = this; } @@ -45,36 +34,30 @@ export class ExpandableSearch { document.addEventListener('turbo:before-cache', this.cacheHandler); } - detachElementHandlers(searchToggle, searchForm) { - if (searchToggle && searchToggle.__osvSearchToggleHandler) { - searchToggle.removeEventListener('click', searchToggle.__osvSearchToggleHandler); - delete searchToggle.__osvSearchToggleHandler; - } - if (searchForm && searchForm.__osvSearchSubmitHandler) { - searchForm.removeEventListener('submit', searchForm.__osvSearchSubmitHandler); - delete searchForm.__osvSearchSubmitHandler; - } + detachElementHandler(element, event, handlerKey) { + if (!element?.[handlerKey]) return; + element.removeEventListener(event, element[handlerKey]); + delete element[handlerKey]; } preCache() { - this.detachElementHandlers(this.container?.toggle, this.container?.form); + this.detachElementHandler(this.container?.toggle, 'click', '__osvSearchToggleHandler'); + this.detachElementHandler(this.container?.form, 'submit', '__osvSearchSubmitHandler'); + } + + isSearchActive() { + return this.container?.form?.classList.contains('active') ?? false; } handleDocumentClick(e) { - if (this.container && this.container.element && - !this.container.element.contains(e.target) && - this.container.form.classList.contains('active')) { + if (this.isSearchActive() && !this.container.element.contains(e.target)) { this.closeSearch(); } } handleDocumentKeydown(e) { - // Close any open search on escape key - if (e.key === 'Escape') { - if (this.container && this.container.form && - this.container.form.classList.contains('active')) { - this.closeSearch(); - } + if (e.key === 'Escape' && this.isSearchActive()) { + this.closeSearch(); } } @@ -99,13 +82,12 @@ export class ExpandableSearch { input: searchInput }; - this.detachElementHandlers(searchToggle, searchForm); + this.detachElementHandler(searchToggle, 'click', '__osvSearchToggleHandler'); + this.detachElementHandler(searchForm, 'submit', '__osvSearchSubmitHandler'); const toggleHandler = (e) => { e.preventDefault(); - const isActive = searchForm.classList.contains('active'); - - if (isActive) { + if (searchForm.classList.contains('active')) { this.closeSearch(); } else { this.openSearch(); @@ -125,37 +107,31 @@ export class ExpandableSearch { } openSearch() { - if (!this.container || !this.container.form) return; - - this.container.form.classList.add('active'); - this.container.toggle.classList.add('active'); - this.container.toggle.setAttribute('aria-expanded', 'true'); - + if (!this.container?.form) return; + const { form, toggle, input } = this.container; + + form.classList.add('active'); + toggle.classList.add('active'); + toggle.setAttribute('aria-expanded', 'true'); + setTimeout(() => { - if (this.container.input) { - this.container.input.focus(); - // Create suggestions only when search is opened - if (!this.suggestionsManager) { - this.suggestionsManager = new SearchSuggestionsManager(this.container.input); - } + input?.focus(); + if (input && !this.suggestionsManager) { + this.suggestionsManager = new SearchSuggestionsManager(input); } }, 100); } closeSearch() { - if (!this.container || !this.container.form) return; - - this.container.form.classList.remove('active'); - this.container.toggle.classList.remove('active'); - this.container.toggle.setAttribute('aria-expanded', 'false'); - - if (this.container.input) { - this.container.input.blur(); - // Hide suggestions when closing search - if (this.suggestionsManager) { - this.suggestionsManager.hide(); - } - } + if (!this.container?.form) return; + const { form, toggle, input } = this.container; + + form.classList.remove('active'); + toggle.classList.remove('active'); + toggle.setAttribute('aria-expanded', 'false'); + + input?.blur(); + this.suggestionsManager?.hide(); } } @@ -176,20 +152,20 @@ export class SearchSuggestionsManager { this.keydownHandler = null; this.blurHandler = null; this.visiblePositionUpdateHandler = () => { - if (this.isDestroyed || !this.suggestionsElement || this.suggestionsElement.classList.contains('search-suggestions--hidden')) { - return; - } + if (!this.isSuggestionsVisible()) return; this.updatePosition(); }; - - this.init(); - } - init() { this.createSuggestionsElement(); this.setupEventListeners(); } + isSuggestionsVisible() { + return !this.isDestroyed + && this.suggestionsElement + && !this.suggestionsElement.classList.contains('search-suggestions--hidden'); + } + createSuggestionsElement() { if (this.suggestionsElement) { return; @@ -204,25 +180,19 @@ export class SearchSuggestionsManager { this.suggestionsElement.classList.add('search-suggestions--list-page'); } - // Add a unique identifier to track this element for cleanup - this.managerId = `suggestions-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; - this.suggestionsElement.dataset.managerId = this.managerId; - this.appendSuggestionsElement(); } appendSuggestionsElement() { if (this.isDestroyed) return; - if (this.mountMode === 'inline' && this.suggestionsContainer) { - this.suggestionsContainer.appendChild(this.suggestionsElement); - return; - } + const parent = (this.mountMode === 'inline' && this.suggestionsContainer) + ? this.suggestionsContainer + : document.body; - if (document.body) { - document.body.appendChild(this.suggestionsElement); + if (parent) { + parent.appendChild(this.suggestionsElement); } else { - // If body is not ready, try again shortly. setTimeout(() => this.appendSuggestionsElement(), 10); } } @@ -273,7 +243,7 @@ export class SearchSuggestionsManager { } handleKeydown(e) { - if (this.isDestroyed || !this.suggestionsElement || this.suggestionsElement.classList.contains('search-suggestions--hidden')) return; + if (!this.isSuggestionsVisible()) return; switch (e.key) { case 'ArrowDown': @@ -307,18 +277,14 @@ export class SearchSuggestionsManager { this.updatePosition(); this.render(); this.suggestionsElement.classList.remove('search-suggestions--hidden'); - // Add active class to container for styling - this.input.closest('.search-suggestions-container')?.classList.add('suggestions-active'); + this.suggestionsContainer?.classList.add('suggestions-active'); } hide() { if (this.isDestroyed) return; - if (this.suggestionsElement) { - this.suggestionsElement.classList.add('search-suggestions--hidden'); - } + this.suggestionsElement?.classList.add('search-suggestions--hidden'); this.selectedIndex = -1; - // Remove active class from container - this.input.closest('.search-suggestions-container')?.classList.remove('suggestions-active'); + this.suggestionsContainer?.classList.remove('suggestions-active'); } updatePosition() { @@ -342,13 +308,11 @@ export class SearchSuggestionsManager { render() { this.suggestionsElement.innerHTML = ''; - this.currentSuggestions.forEach((suggestion, index) => { + this.currentSuggestions.forEach((suggestion) => { const item = document.createElement('div'); item.classList.add('search-suggestions__item'); item.textContent = suggestion; - item.addEventListener('click', () => this.selectSuggestion(suggestion)); - this.suggestionsElement.appendChild(item); }); @@ -367,26 +331,27 @@ export class SearchSuggestionsManager { this.hide(); const form = this.input.closest('form'); - if (!form) { + if (!form) return; + + if (!this.isNavContext) { + submitForm(form); return; } - if (this.isNavContext) { - if (window.OSVSearchInstance && typeof window.OSVSearchInstance.closeSearch === 'function') { - window.OSVSearchInstance.closeSearch(); - } else { - form.classList.remove('active'); - const searchContainer = form.closest('.search-container-nav'); - const searchToggle = searchContainer?.querySelector('.search-toggle'); - searchToggle?.classList.remove('active'); - searchToggle?.setAttribute('aria-expanded', 'false'); - } - - this.submitAfterNavClose(form); - return; + if (typeof window.OSVSearchInstance?.closeSearch === 'function') { + window.OSVSearchInstance.closeSearch(); + } else { + this.closeNavSearchManually(form); } - submitForm(form); + this.submitAfterNavClose(form); + } + + closeNavSearchManually(form) { + form.classList.remove('active'); + const toggle = form.closest('.search-container-nav')?.querySelector('.search-toggle'); + toggle?.classList.remove('active'); + toggle?.setAttribute('aria-expanded', 'false'); } submitAfterNavClose(form) { @@ -424,30 +389,24 @@ export class SearchSuggestionsManager { this.isDestroyed = true; clearTimeout(this.debounceTimer); - if (this.input) { - if (this.inputHandler) { - this.input.removeEventListener('input', this.inputHandler); - } - if (this.keydownHandler) { - this.input.removeEventListener('keydown', this.keydownHandler); - } - if (this.blurHandler) { - this.input.removeEventListener('blur', this.blurHandler); - } - } + this.removeInputListener('input', this.inputHandler); + this.removeInputListener('keydown', this.keydownHandler); + this.removeInputListener('blur', this.blurHandler); if (this.mountMode === 'portal') { window.removeEventListener('resize', this.visiblePositionUpdateHandler); window.removeEventListener('scroll', this.visiblePositionUpdateHandler, true); } - - if (this.suggestionsElement) { - this.suggestionsElement.remove(); - this.suggestionsElement = null; - } - - // Clear references + + this.suggestionsElement?.remove(); + this.suggestionsElement = null; this.input = null; this.currentSuggestions = []; } + + removeInputListener(event, handler) { + if (this.input && handler) { + this.input.removeEventListener(event, handler); + } + } } diff --git a/gcp/website/frontend3/src/styles.scss b/gcp/website/frontend3/src/styles.scss index 3365e48b3ad..ac06fe0da7d 100644 --- a/gcp/website/frontend3/src/styles.scss +++ b/gcp/website/frontend3/src/styles.scss @@ -1931,7 +1931,6 @@ $navbar-search-surface: rgba(41, 41, 41, 0.95); $navbar-search-border: rgba(255, 255, 255, 0.3); $navbar-search-radius: 19px; $navbar-search-divider: rgba(255, 255, 255, 0.16); -$navbar-search-control-height: 38px; .search-container-nav { position: relative; @@ -1963,13 +1962,13 @@ $navbar-search-control-height: 38px; transition: background-color 0.2s ease-in-out, opacity 0.15s ease-in-out, visibility 0s linear 0.15s; border-radius: 50%; - &:hover { + &:hover, + &:focus { background-color: rgba(255, 255, 255, 0.1); } &:focus { outline: none; - background-color: rgba(255, 255, 255, 0.1); } &.active { @@ -2059,7 +2058,7 @@ $navbar-search-control-height: 38px; } } -/* --- Base styles for the suggestions dropdown --- */ +/* --- Suggestions dropdown --- */ .search-suggestions { position: absolute; z-index: 9999; @@ -2072,20 +2071,33 @@ $navbar-search-control-height: 38px; &--hidden { display: none; } -} -/* --- Base styles for a single suggestion item --- */ -.search-suggestions__item { - display: flex; - align-items: center; - gap: 12px; - padding: 10px 16px; - cursor: pointer; - color: $osv-text-color; - transition: background-color 0.15s ease; - - &:last-child { - border-bottom: none; + &__item { + display: flex; + align-items: center; + gap: 12px; + padding: 10px 16px; + cursor: pointer; + color: $osv-text-color; + transition: background-color 0.15s ease; + } + + &::-webkit-scrollbar { + width: 6px; + } + + &::-webkit-scrollbar-track { + background: transparent; + margin: 4px 0; + } + + &::-webkit-scrollbar-thumb { + background-color: $osv-grey-600; + border-radius: 3px; + + &:hover { + background-color: $osv-grey-800; + } } } @@ -2100,26 +2112,25 @@ $navbar-search-control-height: 38px; box-shadow: 0 5px 20px rgba(0, 0, 0, 0.4); } - -/* --- Styles for the Main List Page Search Bar --- */ +/* --- List page suggestions --- */ .search-suggestions--list-page { - background: $osv-background; - border: 2px solid $osv-text-color; + background: $osv-background; + border: 2px solid $osv-text-color; border-top: none; border-radius: 0 0 8px 8px; - margin-top: -2px; + margin-top: -2px; .search-suggestions__item { border-bottom: 1px dashed $osv-grey-600; &:hover, &--selected { - background-color: $osv-grey-800; + background-color: $osv-grey-800; } } } -/* --- Styles for the Navbar Search Bar --- */ +/* --- Navbar suggestions --- */ .search-suggestions--nav { position: static; flex-basis: 100%; @@ -2130,7 +2141,7 @@ $navbar-search-control-height: 38px; border-radius: 0; box-shadow: none; margin-top: 0; - + .search-suggestions__item { &:hover, &--selected { @@ -2138,21 +2149,3 @@ $navbar-search-control-height: 38px; } } } - -.search-suggestions::-webkit-scrollbar { - width: 6px; -} - -.search-suggestions::-webkit-scrollbar-track { - background: transparent; - margin: 4px 0; -} - -.search-suggestions::-webkit-scrollbar-thumb { - background-color: $osv-grey-600; - border-radius: 3px; -} - -.search-suggestions::-webkit-scrollbar-thumb:hover { - background-color: $osv-grey-800; -} From 784a48d6cac1317787c07b16b1652b1f8003c54a Mon Sep 17 00:00:00 2001 From: ashmod Date: Tue, 24 Feb 2026 15:38:25 +0200 Subject: [PATCH 3/5] align fallback with close duration --- gcp/website/frontend3/src/search.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gcp/website/frontend3/src/search.js b/gcp/website/frontend3/src/search.js index 95146bba001..55a38f0b9d2 100644 --- a/gcp/website/frontend3/src/search.js +++ b/gcp/website/frontend3/src/search.js @@ -1,7 +1,7 @@ import { submitForm } from './index.js'; const MIN_QUERY_LENGTH = 3; -const NAV_SEARCH_CLOSE_FALLBACK_MS = 380; +const NAV_SEARCH_CLOSE_FALLBACK_MS = 450; export class ExpandableSearch { constructor() { From 827aa4d511a66938b200c4ce1b36403e5381dc1b Mon Sep 17 00:00:00 2001 From: ashmod Date: Tue, 24 Feb 2026 15:47:40 +0200 Subject: [PATCH 4/5] animate searchbar closing --- gcp/website/frontend3/src/styles.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gcp/website/frontend3/src/styles.scss b/gcp/website/frontend3/src/styles.scss index ac06fe0da7d..9ea9fccb3f5 100644 --- a/gcp/website/frontend3/src/styles.scss +++ b/gcp/website/frontend3/src/styles.scss @@ -1992,7 +1992,7 @@ $navbar-search-divider: rgba(255, 255, 255, 0.16); align-items: center; width: 0; overflow: hidden; - transition: width 0.3s ease-in-out 0.1s, opacity 0.1s ease-in-out, visibility 0s linear 0.1s; + transition: width 0.3s ease-in-out, opacity 0.3s ease-in-out, visibility 0s linear 0.3s; background: $navbar-search-surface; border-radius: $navbar-search-radius; z-index: 1000; From b8fc63ab9bc9152f066b26e662b7ea668609d12c Mon Sep 17 00:00:00 2001 From: ashmod Date: Thu, 5 Mar 2026 11:15:23 +0200 Subject: [PATCH 5/5] the gemini thing --- gcp/website/frontend3/src/styles.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/gcp/website/frontend3/src/styles.scss b/gcp/website/frontend3/src/styles.scss index 9ea9fccb3f5..a4e18711115 100644 --- a/gcp/website/frontend3/src/styles.scss +++ b/gcp/website/frontend3/src/styles.scss @@ -2123,6 +2123,10 @@ $navbar-search-divider: rgba(255, 255, 255, 0.16); .search-suggestions__item { border-bottom: 1px dashed $osv-grey-600; + &:last-child { + border-bottom: none; + } + &:hover, &--selected { background-color: $osv-grey-800;