Давай немного усовершенствуем функционал существующего компонента поиска в теме и создадим еще один для дропдауна, который будет полностью повторять логику дефолтного, но работать с нашей таилвинд разметкой. Главным усовершенствованием будет добавление настройки в теме для контроля количества символов после набора которых начинает срабатывать аякс запрос. То есть мы сделаем чтобы стало возможным чтобы аякс срабатывал после набора третьего символа, а не после первого. Хотя эту цифру можно быдет менять теперь из админки в общих настройках редактора темы.

Начнем сначала сделаем несколько дополнений и исправлений в существующем коде:

Шаг.1 Обновление config/settings_shema.json

Открываем файл, находим 1363 строку, увидишь “name”: “t:settings_schema.search_input.name”, и в settings сразу после квадратной скобки вставляем :

 {
        "type": "range",
        "id": "predictive_search_min_chars",
        "min": 1,
        "max": 5,
        "step": 1,
        "label": "Minimum characters to search",
        "default": 2
      },

весь блок будет выглядеть вот так:

 {
    "name": "t:settings_schema.search_input.name",
    "settings": [
       {
        "type": "range",
        "id": "predictive_search_min_chars",
        "min": 1,
        "max": 5,
        "step": 1,
        "label": "Minimum characters to search",
        "default": 2
      },
      {
        "type": "checkbox",
        "id": "predictive_search_enabled",
        "default": true,
        "label": "t:settings_schema.search_input.settings.predictive_search_enabled.label"
      },
      {
        "type": "checkbox",
        "id": "predictive_search_show_vendor",
        "default": false,
        "label": "t:settings_schema.search_input.settings.predictive_search_show_vendor.label",
        "info": "t:settings_schema.search_input.settings.predictive_search_show_vendor.info"
      },
      {
        "type": "checkbox",
        "id": "predictive_search_show_price",
        "default": false,
        "label": "t:settings_schema.search_input.settings.predictive_search_show_price.label",
        "info": "t:settings_schema.search_input.settings.predictive_search_show_price.info"
      },          
    ],
  }, 

После данного действия в админке появится дополнительны рэнж слайдер для изменения значения от 1 до 5. В liquid компоненте мы будем его читать через аттрибут.

Шаг.2 Теперь лечим дефолтный predictive-search.js

Чтобы все заработало нужно в конструктор добавить дополнительное свойство this.minChars примерно на строке 14.

  constructor() {
    super();
    this.cachedResults = {};
    this.predictiveSearchResults = this.querySelector(
      "[data-predictive-search]",
    );
    this.allPredictiveSearchInstances =
      document.querySelectorAll("predictive-search");
    this.isOpen = false;
    this.abortController = new AbortController();
    this.searchTerm = "";

    // ДОБАВЛЕННАЯ СТРОКА ДЛЯ ЧТЕНИЯ АТТРИБУТА ИЗ ВЕРСТКИ
    this.minChars = parseInt(this.getAttribute('data-min-chars')) || 2;

    this.setupEventListeners();
  }

Также на строке +-50 нужно в методе onChange добавить логику срабатывания:

 onChange() {
    super.onChange();
    const newSearchTerm = this.getQuery();
    if (!this.searchTerm || !newSearchTerm.startsWith(this.searchTerm)) {
      // Remove the results when they are no longer relevant for the new search term
      // so they don't show up when the dropdown opens again
      this.querySelector("#predictive-search-results-groups-wrapper")?.remove();
    }

    // Update the term asap, don't wait for the predictive search query to finish loading
    this.updateSearchForTerm(this.searchTerm, newSearchTerm);

    this.searchTerm = newSearchTerm;

    if (!this.searchTerm.length) {
      this.close(true);
      return;
    }

     // НОВОЕ ПРАВИЛО: Проверка на минимальное количество символов
    if (this.searchTerm.length < this.minChars) {
      this.close();
      return;
    }

    this.getSearchResults(this.searchTerm);
  }

В методе отправки , при нажатии на кнопку с лупой тоже нужно проверять кол-во символов. Меняем метод onFormSubmit:

onFormSubmit(event) {
    // Получаем текущий запрос
    const currentQuery = this.getQuery();

    // ПОЯВИЛАСЬ ЛОГИКА ПРОВЕРКИ: Если запрос пустой ИЛИ его длина меньше minChars — отменяем отправку формы
    if (!currentQuery.length || currentQuery.length < this.minChars || this.querySelector('[aria-selected="true"] a')) {
      event.preventDefault();
    }
  }

И при фокусе тоже проверяем:

 onFocus() {
    const currentSearchTerm = this.getQuery();

    if (!currentSearchTerm.length) return;

    if (this.searchTerm !== currentSearchTerm) {
      // Search term was changed from other search input, treat it as a user change
      this.onChange();
    } else if (this.getAttribute("results") === "true") {
      this.open();
    // ИЗМЕНЕННАЯ ЛОГИКА
    } else if (currentSearchTerm.length >= this.minChars) {
      // Запрашиваем результаты только если символов достаточно
      this.getSearchResults(this.searchTerm);
    }
  }

Еще давай закроем недоработку-баг дефолтного поиска при наборе нестандартных символов в поиске, скорректируем метод updateSearchFormTerm:

updateSearchForTerm(previousTerm, newTerm) {
    const searchForTextElement = this.querySelector(
      "[data-predictive-search-search-for-text]",
    );
    const currentButtonText = searchForTextElement?.innerText;
    if (currentButtonText) {

      // ДОБАВЛЕНА ЗАЩИТА ОТ null: сначала проверяем, что match нашел совпадения
      const matchResult = currentButtonText.match(new RegExp(previousTerm, "g"));
      if (matchResult && matchResult.length > 1) {
        // The new term matches part of the button text and not just the search term, do not replace to avoid mistakes
        return;
      }
      const newButtonText = currentButtonText.replace(previousTerm, newTerm);
      searchForTextElement.innerText = newButtonText;
    }
  }

Вот так теперь должен выглядеть весь файл целиком:

class PredictiveSearch extends SearchForm {
  constructor() {
    super();
    this.cachedResults = {};
    this.predictiveSearchResults = this.querySelector(
      "[data-predictive-search]",
    );
    this.allPredictiveSearchInstances =
      document.querySelectorAll("predictive-search");
    this.isOpen = false;
    this.abortController = new AbortController();
    this.searchTerm = "";

    this.minChars = parseInt(this.getAttribute('data-min-chars')) || 2;

    this.setupEventListeners();
  }

  setupEventListeners() {
    this.input.form.addEventListener("submit", this.onFormSubmit.bind(this));

    this.input.addEventListener("focus", this.onFocus.bind(this));
    this.addEventListener("focusout", this.onFocusOut.bind(this));
    this.addEventListener("keyup", this.onKeyup.bind(this));
    this.addEventListener("keydown", this.onKeydown.bind(this));
  }

  getQuery() {
    return this.input.value.trim();
  }

  onChange() {
    super.onChange();
    const newSearchTerm = this.getQuery();
    if (!this.searchTerm || !newSearchTerm.startsWith(this.searchTerm)) {
      // Remove the results when they are no longer relevant for the new search term
      // so they don't show up when the dropdown opens again
      this.querySelector("#predictive-search-results-groups-wrapper")?.remove();
    }

    // Update the term asap, don't wait for the predictive search query to finish loading
    this.updateSearchForTerm(this.searchTerm, newSearchTerm);

    this.searchTerm = newSearchTerm;

    if (!this.searchTerm.length) {
      this.close(true);
      return;
    }

     // ПРАВИЛО: Проверка на минимальное количество символов
    if (this.searchTerm.length < this.minChars) {
      this.close();
      return;
    }

    this.getSearchResults(this.searchTerm);
  }

  onFormSubmit(event) {
    // Получаем текущий запрос
    const currentQuery = this.getQuery();

    // Если запрос пустой ИЛИ его длина меньше minChars — отменяем отправку формы
    if (!currentQuery.length || currentQuery.length < this.minChars || this.querySelector('[aria-selected="true"] a')) {
      event.preventDefault();
    }
  }

  onFormReset(event) {
    super.onFormReset(event);
    if (super.shouldResetForm()) {
      this.searchTerm = "";
      this.abortController.abort();
      this.abortController = new AbortController();
      this.closeResults(true);
    }
  }

  onFocus() {
    const currentSearchTerm = this.getQuery();

    if (!currentSearchTerm.length) return;

    if (this.searchTerm !== currentSearchTerm) {
      // Search term was changed from other search input, treat it as a user change
      this.onChange();
    } else if (this.getAttribute("results") === "true") {
      this.open();
    } else if (currentSearchTerm.length >= this.minChars) {
      // Запрашиваем результаты только если символов достаточно
      this.getSearchResults(this.searchTerm);
    }
  }

  onFocusOut() {
    setTimeout(() => {
      if (!this.contains(document.activeElement)) this.close();
    });
  }

  onKeyup(event) {
    if (!this.getQuery().length) this.close(true);
    event.preventDefault();

    switch (event.code) {
      case "ArrowUp":
        this.switchOption("up");
        break;
      case "ArrowDown":
        this.switchOption("down");
        break;
      case "Enter":
        this.selectOption();
        break;
    }
  }

  onKeydown(event) {
    // Prevent the cursor from moving in the input when using the up and down arrow keys
    if (event.code === "ArrowUp" || event.code === "ArrowDown") {
      event.preventDefault();
    }
  }

  updateSearchForTerm(previousTerm, newTerm) {
    const searchForTextElement = this.querySelector(
      "[data-predictive-search-search-for-text]",
    );
    const currentButtonText = searchForTextElement?.innerText;
    if (currentButtonText) {
      // Защита от null: сначала проверяем, что match нашел совпадения
      const matchResult = currentButtonText.match(new RegExp(previousTerm, "g"));
      if (matchResult && matchResult.length > 1) {
        // The new term matches part of the button text and not just the search term, do not replace to avoid mistakes
        return;
      }
      const newButtonText = currentButtonText.replace(previousTerm, newTerm);
      searchForTextElement.innerText = newButtonText;
    }
  }

  switchOption(direction) {
    if (!this.getAttribute("open")) return;

    const moveUp = direction === "up";
    const selectedElement = this.querySelector('[aria-selected="true"]');

    // Filter out hidden elements (duplicated page and article resources) thanks
    // to this https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetParent
    const allVisibleElements = Array.from(
      this.querySelectorAll("li, button.predictive-search__item"),
    ).filter((element) => element.offsetParent !== null);
    let activeElementIndex = 0;

    if (moveUp && !selectedElement) return;

    let selectedElementIndex = -1;
    let i = 0;

    while (selectedElementIndex === -1 && i <= allVisibleElements.length) {
      if (allVisibleElements[i] === selectedElement) {
        selectedElementIndex = i;
      }
      i++;
    }

    this.statusElement.textContent = "";

    if (!moveUp && selectedElement) {
      activeElementIndex =
        selectedElementIndex === allVisibleElements.length - 1
          ? 0
          : selectedElementIndex + 1;
    } else if (moveUp) {
      activeElementIndex =
        selectedElementIndex === 0
          ? allVisibleElements.length - 1
          : selectedElementIndex - 1;
    }

    if (activeElementIndex === selectedElementIndex) return;

    const activeElement = allVisibleElements[activeElementIndex];

    activeElement.setAttribute("aria-selected", true);
    if (selectedElement) selectedElement.setAttribute("aria-selected", false);

    this.input.setAttribute("aria-activedescendant", activeElement.id);
  }

  selectOption() {
    const selectedOption = this.querySelector(
      '[aria-selected="true"] a, button[aria-selected="true"]',
    );

    if (selectedOption) selectedOption.click();
  }

  getSearchResults(searchTerm) {
    const queryKey = searchTerm.replace(" ", "-").toLowerCase();
    this.setLiveRegionLoadingState();

    if (this.cachedResults[queryKey]) {
      this.renderSearchResults(this.cachedResults[queryKey]);
      return;
    }

    fetch(
      `${routes.predictive_search_url}?q=${encodeURIComponent(searchTerm)}&section_id=predictive-search`,
      {
        signal: this.abortController.signal,
      },
    )
      .then((response) => {
        if (!response.ok) {
          var error = new Error(response.status);
          this.close();
          throw error;
        }

        return response.text();
      })
      .then((text) => {
        const resultsMarkup = new DOMParser()
          .parseFromString(text, "text/html")
          .querySelector("#shopify-section-predictive-search").innerHTML;
        // Save bandwidth keeping the cache in all instances synced
        this.allPredictiveSearchInstances.forEach(
          (predictiveSearchInstance) => {
            predictiveSearchInstance.cachedResults[queryKey] = resultsMarkup;
          },
        );
        this.renderSearchResults(resultsMarkup);
      })
      .catch((error) => {
        if (error?.code === 20) {
          // Code 20 means the call was aborted
          return;
        }
        this.close();
        throw error;
      });
  }

  setLiveRegionLoadingState() {
    this.statusElement =
      this.statusElement || this.querySelector(".predictive-search-status");
    this.loadingText =
      this.loadingText || this.getAttribute("data-loading-text");

    this.setLiveRegionText(this.loadingText);
    this.setAttribute("loading", true);
  }

  setLiveRegionText(statusText) {
    this.statusElement.setAttribute("aria-hidden", "false");
    this.statusElement.textContent = statusText;

    setTimeout(() => {
      this.statusElement.setAttribute("aria-hidden", "true");
    }, 1000);
  }

  renderSearchResults(resultsMarkup) {
    this.predictiveSearchResults.innerHTML = resultsMarkup;
    this.setAttribute("results", true);

    this.setLiveRegionResults();
    this.open();
  }

  setLiveRegionResults() {
    this.removeAttribute("loading");
    this.setLiveRegionText(
      this.querySelector("[data-predictive-search-live-region-count-value]")
        .textContent,
    );
  }

  getResultsMaxHeight() {
    const calculatedHeight = window.innerHeight - (56 + 30 + 30 + 30);

    // высоты экрана - search form
    const maxHeight = calculatedHeight;

    this.resultsMaxHeight = maxHeight;
    return this.resultsMaxHeight;
  }
  open() {
    this.predictiveSearchResults.style.maxHeight =
      this.resultsMaxHeight || `${this.getResultsMaxHeight()}px`;
    this.setAttribute("open", true);
    this.input.setAttribute("aria-expanded", true);
    this.isOpen = true;
  }

  close(clearSearchTerm = false) {
    this.closeResults(clearSearchTerm);
    this.isOpen = false;
  }

  closeResults(clearSearchTerm = false) {
    if (clearSearchTerm) {
      this.input.value = "";
      this.removeAttribute("results");
    }
    const selected = this.querySelector('[aria-selected="true"]');

    if (selected) selected.setAttribute("aria-selected", false);

    this.input.setAttribute("aria-activedescendant", "");
    this.removeAttribute("loading");
    this.removeAttribute("open");
    this.input.setAttribute("aria-expanded", false);
    this.resultsMaxHeight = false;
    this.predictiveSearchResults.removeAttribute("style");
  }
}

customElements.define("predictive-search", PredictiveSearch);

ШАГ.3 Добавляем аттрибуты в дефолтные компоненты.

Давай сначала добавим в существующие компоненты аттрибуты, чтобы связать наши настройки со скриптом. Находим файл section/main-search.liquid и заменяем на строке +- 78:

<predictive-search data-loading-text="{{ 'accessibility.loading' | t }}" 
            data-min-chars="{{ settings.predictive_search_min_chars | default: 2 }}">

Находим файл snippets/header-search.liquid и заменяем на строке +- 39:

 <predictive-search class="search-modal__form" 
            data-loading-text="{{ 'accessibility.loading' | t }}" 
            data-min-chars="{{ settings.predictive_search_min_chars | default: 2 }}">

Вот теперь наш дефолтный поиск (кол-во вводимых символов и срабатывание аякс) темы DAWN будет управляться через глобальные настройки в редакторе темы.

ШАГ.4 Создаем собственный компонент для хедера. Дропдаун-поиск.

Чтобы сделать полноценный компонент который будет стабильно работать с шопифай просто написать обычный fetch не получится, потому что будут проблемы с доступом. Мы повторим логику дефолтного поиска которую разработчики использую в теме DAWN. Для этого нам нужно создать 3 файла. Это будет новый custom-predictive-search.js с основной логикой, custom-predictive-search.liquid с нашей новой разметкой на выдаче, ну и компонент формы custom-header-search.liquid. Что это значит ?

custom-predictive-search.liquid это компонент который формирует разметку ответа для вывода результатов поиска. Мы решили написать новый ,потому что не можем использовать существующий дефолтный компонент, так как он выдает похожую разметку но с другой стилизацией для уже работающей мобильной версии.

custom-header-search.liquid это просто форма – инпут, кнопка и место для вывода custom-predictive-search.liquid

новый custom-predictive-search.js проще написать чем пытаться использовать существующий потому что он будет работать с новыми компонентами и у них свои аттрибуты. Напишем новый чтобы не ломать и не усложнять существующий.

добавь в assets файл custom-predictive-search.js :

(() => {
  function debounce(fn, wait) {
    let t;
    return (...args) => {
      clearTimeout(t);
      t = setTimeout(() => fn.apply(this, args), wait);
    };
  }

  class CustomSearchForm extends HTMLElement {
    constructor() {
      super();
      this.input = this.querySelector('input[type="search"]');
      this.resetButton = this.querySelector('button[type="reset"]');

      if (this.input) {
        this.input.form.addEventListener("reset", this.onFormReset.bind(this));
        this.input.addEventListener(
          "input",
          debounce((event) => {
            this.onChange(event);
          }, 300).bind(this),
        );
      }
    }

    toggleResetButton() {
      const resetIsHidden = this.resetButton.classList.contains("hidden");
      if (this.input.value.length > 0 && resetIsHidden) {
        this.resetButton.classList.remove("hidden");
      } else if (this.input.value.length === 0 && !resetIsHidden) {
        this.resetButton.classList.add("hidden");
      }
    }

    onChange() {
      this.toggleResetButton();
    }

    shouldResetForm() {
      return !document.querySelector('[aria-selected="true"] a');
    }

    onFormReset(event) {
      event.preventDefault();
      if (this.shouldResetForm()) {
        this.input.value = "";
        this.input.focus();
        this.toggleResetButton();
      }
    }
  }

  class CustomPredictiveSearch extends CustomSearchForm {
    constructor() {
      super();
      this.cachedResults = {};
      this.predictiveSearchResults = this.querySelector(
        "[data-predictive-search]",
      );
      this.allPredictiveSearchInstances = document.querySelectorAll(
        "custom-predictive-search",
      );
      this.isOpen = false;
      this.abortController = new AbortController();
      this.searchTerm = "";

      this.minChars = parseInt(this.getAttribute("data-min-chars")) || 2;

      this.setupEventListeners();
    }

    setupEventListeners() {
      this.input.form.addEventListener("submit", this.onFormSubmit.bind(this));
      this.input.addEventListener("focus", this.onFocus.bind(this));
      this.addEventListener("focusout", this.onFocusOut.bind(this));
      this.addEventListener("keyup", this.onKeyup.bind(this));
      this.addEventListener("keydown", this.onKeydown.bind(this));
    }

    getQuery() {
      return this.input.value.trim();
    }

    onChange() {
      super.onChange();
      const newSearchTerm = this.getQuery();
      if (!this.searchTerm || !newSearchTerm.startsWith(this.searchTerm)) {
        this.querySelector(
          "#predictive-search-results-groups-wrapper",
        )?.remove();
      }

      this.updateSearchForTerm(this.searchTerm, newSearchTerm);
      this.searchTerm = newSearchTerm;

      if (!this.searchTerm.length) {
        this.close(true);
        return;
      }

      // Используем динамическое значение из настроек темы
      if (this.searchTerm.length < this.minChars) {
        this.close();
        return;
      }

      this.getSearchResults(this.searchTerm);
    }

    onFormSubmit(event) {
      // Получаем текущий запрос
      const currentQuery = this.getQuery();

      // Если запрос пустой ИЛИ его длина меньше minChars — отменяем отправку формы
      if (
        !currentQuery.length ||
        currentQuery.length < this.minChars ||
        this.querySelector('[aria-selected="true"] a')
      ) {
        event.preventDefault();
      }
    }

    onFormReset(event) {
      super.onFormReset(event);
      if (super.shouldResetForm()) {
        this.searchTerm = "";
        this.abortController.abort();
        this.abortController = new AbortController();
        this.closeResults(true);
      }
    }

    onFocus() {
      const currentSearchTerm = this.getQuery();
      if (!currentSearchTerm.length) return;

      if (this.searchTerm !== currentSearchTerm) {
        this.onChange();
      } else if (this.getAttribute("results") === "true") {
        this.open();
      } else if (currentSearchTerm.length >= this.minChars) {
        // Запрашиваем результаты только если символов достаточно
        this.getSearchResults(this.searchTerm);
      }
    }

    onFocusOut() {
      setTimeout(() => {
        if (!this.contains(document.activeElement)) this.close();
      });
    }

    onKeyup(event) {
      if (!this.getQuery().length) this.close(true);
      event.preventDefault();

      switch (event.code) {
        case "ArrowUp":
          this.switchOption("up");
          break;
        case "ArrowDown":
          this.switchOption("down");
          break;
        case "Enter":
          this.selectOption();
          break;
      }
    }

    onKeydown(event) {
      if (event.code === "ArrowUp" || event.code === "ArrowDown") {
        event.preventDefault();
      }
    }

    updateSearchForTerm(previousTerm, newTerm) {
      const searchForTextElement = this.querySelector(
        "[data-predictive-search-search-for-text]",
      );
      const currentButtonText = searchForTextElement?.innerText;
      if (currentButtonText) {
        // Защита от null: сначала проверяем, что match нашел совпадения
        const matchResult = currentButtonText.match(
          new RegExp(previousTerm, "g"),
        );
        if (matchResult && matchResult.length > 1) {
          // The new term matches part of the button text and not just the search term, do not replace to avoid mistakes
          return;
        }
        const newButtonText = currentButtonText.replace(previousTerm, newTerm);
        searchForTextElement.innerText = newButtonText;
      }
    }

    switchOption(direction) {
    if (!this.getAttribute('open')) return;

    const moveUp = direction === 'up';
    const selectedElement = this.querySelector('[aria-selected="true"]');

    // Ищем все видимые пункты (товары и кнопку See all)
    const allVisibleElements = Array.from(
      this.querySelectorAll('li[role="option"]')
    ).filter((element) => element.offsetParent !== null);
    
    let activeElementIndex = 0;

    if (moveUp && !selectedElement) return;

    let selectedElementIndex = -1;
    let i = 0;

    while (selectedElementIndex === -1 && i <= allVisibleElements.length) {
      if (allVisibleElements[i] === selectedElement) {
        selectedElementIndex = i;
      }
      i++;
    }

    this.statusElement.textContent = '';

    if (!moveUp && selectedElement) {
      activeElementIndex =
        selectedElementIndex === allVisibleElements.length - 1
          ? 0
          : selectedElementIndex + 1;
    } else if (moveUp) {
      activeElementIndex =
        selectedElementIndex === 0
          ? allVisibleElements.length - 1
          : selectedElementIndex - 1;
    }

    if (activeElementIndex === selectedElementIndex) return;

    const activeElement = allVisibleElements[activeElementIndex];
    activeElement.setAttribute('aria-selected', true);
    if (selectedElement) selectedElement.setAttribute('aria-selected', false);

    this.input.setAttribute('aria-activedescendant', activeElement.id);

    // Автоматический скролл к элементу, если он скрыт за пределами видимой области
    activeElement.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
  }

    selectOption() {
      const selectedOption = this.querySelector(
        '[aria-selected="true"] a, button[aria-selected="true"]',
      );
      if (selectedOption) selectedOption.click();
    }

    getSearchResults(searchTerm) {
      const queryKey = searchTerm.replace(" ", "-").toLowerCase();
      this.setLiveRegionLoadingState();

      if (this.cachedResults[queryKey]) {
        this.renderSearchResults(this.cachedResults[queryKey]);
        return;
      }

      fetch(
        `${routes.predictive_search_url}?q=${encodeURIComponent(searchTerm)}&section_id=custom-predictive-search`,
        {
          signal: this.abortController.signal,
        },
      )
        .then((response) => {
          if (!response.ok) {
            var error = new Error(response.status);
            this.close();
            throw error;
          }
          return response.text();
        })
        .then((text) => {
          const resultsMarkup = new DOMParser()
            .parseFromString(text, "text/html")
            .querySelector(
              "#shopify-section-custom-predictive-search",
            ).innerHTML;

          this.allPredictiveSearchInstances.forEach(
            (predictiveSearchInstance) => {
              predictiveSearchInstance.cachedResults[queryKey] = resultsMarkup;
            },
          );
          this.renderSearchResults(resultsMarkup);
        })
        .catch((error) => {
          if (error?.code === 20) return;
          this.close();
          throw error;
        });
    }

    setLiveRegionLoadingState() {
      this.statusElement =
        this.statusElement || this.querySelector(".predictive-search-status");
      this.loadingText =
        this.loadingText || this.getAttribute("data-loading-text");
      this.setLiveRegionText(this.loadingText);
      this.setAttribute("loading", true);
    }

    setLiveRegionText(statusText) {
      this.statusElement.setAttribute("aria-hidden", "false");
      this.statusElement.textContent = statusText;
      setTimeout(() => {
        this.statusElement.setAttribute("aria-hidden", "true");
      }, 1000);
    }

    renderSearchResults(resultsMarkup) {
      this.predictiveSearchResults.innerHTML = resultsMarkup;
      this.setAttribute("results", true);
      this.setLiveRegionResults();
      this.open();
    }

    setLiveRegionResults() {
      this.removeAttribute("loading");
      this.setLiveRegionText(
        this.querySelector("[data-predictive-search-live-region-count-value]")
          .textContent,
      );
    }

    getResultsMaxHeight() {
      return null; // Отключаем JS расчет высоты
    }

    open() {
      this.setAttribute("open", true);
      this.input.setAttribute("aria-expanded", true);
      this.isOpen = true;
    }

    close(clearSearchTerm = false) {
      this.closeResults(clearSearchTerm);
      this.isOpen = false;
    }

    closeResults(clearSearchTerm = false) {
      if (clearSearchTerm) {
        this.input.value = "";
        this.removeAttribute("results");
      }
      const selected = this.querySelector('[aria-selected="true"]');
      if (selected) selected.setAttribute("aria-selected", false);

      this.input.setAttribute("aria-activedescendant", "");
      this.removeAttribute("loading");
      this.removeAttribute("open");
      this.input.setAttribute("aria-expanded", false);
      this.resultsMaxHeight = false;
    }
  }

  customElements.define("custom-predictive-search", CustomPredictiveSearch);
})();

ШАГ.4 Создаем компоненты

Новый компонент для формирования разметки в нашем дроп-дауне. В папку sections создаем custom-predictive-search.liquid:

{% comment %}theme-check-disable ImgLazyLoading{% endcomment %}
{%- if predictive_search.performed -%}
  {% assign first_column_results_size = predictive_search.resources.queries.size
    | plus: predictive_search.resources.collections.size
    | plus: predictive_search.resources.pages.size
    | plus: predictive_search.resources.articles.size
  %}
  
  <div id="custom-predictive-search-results" role="listbox" class="tw:flex tw:flex-col">
    {%- if first_column_results_size > 0 or predictive_search.resources.products.size > 0 -%}
      <div
        id="custom-predictive-search-results-groups-wrapper"
        class="tw:max-h-[60dvh] tw:min-h-[10dvh] tw:overflow-y-auto tw:p-2"
      >
    {%- endif -%}

    {%- if predictive_search.resources.queries.size > 0 or predictive_search.resources.collections.size > 0 -%}
      <div class="tw:py-2">
        <h2 class="tw:px-4 tw:py-2 tw:text-[1.2rem] tw:uppercase tw:text-gray-500 tw:font-semibold tw:tracking-wider">
          {{- 'templates.search.suggestions' | t -}}
        </h2>
        <ul class="tw:flex tw:flex-col" role="group">
          {%- for query in predictive_search.resources.queries -%}
            <li id="custom-predictive-search-option-query-{{ forloop.index }}" role="option" aria-selected="false">
              <a href="{{ query.url }}" class="predictive-search__item tw:block tw:px-4 tw:py-3 tw:text-[1.4rem] tw:text-gray-800 tw:hover:bg-gray-50 tw:rounded-md tw:transition-colors" tabindex="-1">
                {{ query.styled_text }}
              </a>
            </li>
          {%- endfor -%}
          {%- for collection in predictive_search.resources.collections -%}
            <li id="custom-predictive-search-option-collection-{{ forloop.index }}" role="option" aria-selected="false">
              <a href="{{ collection.url }}" class="predictive-search__item tw:block tw:px-4 tw:py-3 tw:text-[1.4rem] tw:text-gray-800 tw:hover:bg-gray-50 tw:rounded-md tw:transition-colors" tabindex="-1">
                {{ collection.title | escape }}
              </a>
            </li>
          {%- endfor -%}
        </ul>
      </div>
    {%- endif -%}

    {%- if predictive_search.resources.products.size > 0 -%}
      <div class="tw:py-2">
        <h2 class="tw:px-4 tw:py-2 tw:text-[1.2rem] tw:uppercase tw:text-gray-500 tw:font-semibold tw:tracking-wider">
          {{- 'templates.search.products' | t -}}
        </h2>
        <ul id="custom-predictive-search-results-products-list" class="tw:flex tw:flex-col" role="group">
          {%- for product in predictive_search.resources.products -%}
            <li id="custom-predictive-search-option-product-{{ forloop.index }}" role="option" aria-selected="false">
              <a
                href="{{ product.url }}"
                class="predictive-search__item tw:flex tw:items-center tw:gap-4 tw:p-4 tw:hover:bg-gray-50 tw:transition-colors tw:rounded-md"
                tabindex="-1"
              >
                {%- if product.featured_media != blank -%}
                  <img
                    class="tw:w-[5rem] tw:h-[5rem] tw:flex-shrink-0 tw:bg-gray-100 tw:rounded-md tw:object-cover"
                    src="{{ product.featured_media | image_url: width: 150 }}"
                    alt="{{ product.featured_media.alt | escape }}"
                    width="50"
                    height="{{ 50 | divided_by: product.featured_media.preview_image.aspect_ratio }}"
                  >
                {%- endif -%}
                <div class="tw:flex-1 tw:min-w-0">
                  {%- if settings.predictive_search_show_vendor -%}
                    <span class="visually-hidden">{{ 'accessibility.vendor' | t }}</span>
                    <div class="tw:text-[1.2rem] tw:text-gray-500 tw:mb-1">
                      {{ product.vendor }}
                    </div>
                  {%- endif -%}
                  <h3 class="tw:text-[1.4rem] tw:font-medium tw:text-grayscale-950 tw:leading-[1.33] tw:line-clamp-2">{{ product.title | escape }}</h3>
                  {%- if settings.predictive_search_show_price -%}
                    <div class="tw:mt-1">
                      {% render 'price', product: product, use_variant: true, show_badges: false %}
                    </div>
                  {%- endif -%}
                </div>
              </a>
            </li>
          {%- endfor -%}
        </ul>
      </div>
    {%- endif -%}

    {%- if predictive_search.resources.pages.size > 0 or predictive_search.resources.articles.size > 0 -%}
      <div class="tw:py-2">
        <h2 class="tw:px-4 tw:py-2 tw:text-[1.2rem] tw:uppercase tw:text-gray-500 tw:font-semibold tw:tracking-wider">
          {{- 'templates.search.pages' | t -}}
        </h2>
        <ul class="tw:flex tw:flex-col" role="group">
          {%- for page in predictive_search.resources.pages -%}
            <li id="custom-predictive-search-option-page-{{ forloop.index }}" role="option" aria-selected="false">
              <a href="{{ page.url }}" class="predictive-search__item tw:block tw:px-4 tw:py-3 tw:text-[1.4rem] tw:text-gray-800 tw:hover:bg-gray-50 tw:rounded-md tw:transition-colors" tabindex="-1">
                {{ page.title | escape }}
              </a>
            </li>
          {%- endfor -%}
          {%- for article in predictive_search.resources.articles -%}
            <li id="custom-predictive-search-option-article-{{ forloop.index }}" role="option" aria-selected="false">
              <a href="{{ article.url }}" class="predictive-search__item tw:block tw:px-4 tw:py-3 tw:text-[1.4rem] tw:text-gray-800 tw:hover:bg-gray-50 tw:rounded-md tw:transition-colors" tabindex="-1">
                {{ article.title | escape }}
              </a>
            </li>
          {%- endfor -%}
        </ul>
      </div>
    {%- endif -%}

    {%- if first_column_results_size > 0 or predictive_search.resources.products.size > 0 -%}
      </div>
    {%- endif -%}

    {%- render 'loading-spinner', class: 'predictive-search__loading-state' -%}

    <div id="custom-predictive-search-option-search-keywords" class="tw:mt-1 tw:border-t tw:border-gray-100">
       <a
        href="{{ routes.search_url }}?q={{ predictive_search.terms | escape }}&type=product&options[prefix]=last"
        class="predictive-search__item predictive-search__item--term tw:!py-8 tw:!block tw:w-full tw:!text-center tw:py-6  tw:text-golden tw:bg-gray-50 tw:hover:!text-goluboy tw:transition-colors tw:text-[1.4rem] tw:font-poppins tw:font-semibold tw:uppercase"      
      >        
          See all products        
      </a>
    </div>
  </div>

  <span class="hidden" data-predictive-search-live-region-count-value>
    {% liquid
      assign total_results = predictive_search.resources.products.size | plus: first_column_results_size
      if total_results == 0
        echo 'templates.search.no_results' | t: terms: predictive_search.terms
      else
        echo 'templates.search.results_with_count' | t: count: total_results | append: ': '
        if predictive_search.resources.queries.size > 0
          assign count = predictive_search.resources.queries.size | plus: predictive_search.resources.collections.size
          echo 'templates.search.results_suggestions_with_count' | t: count: count | append: ', '
        endif
        if predictive_search.resources.pages.size > 0
          assign count = predictive_search.resources.pages.size | plus: predictive_search.resources.articles.size
          echo 'templates.search.results_pages_with_count' | t: count: count | append: ', '
        endif
        if predictive_search.resources.products.size > 0
          echo 'templates.search.results_products_with_count' | t: count: predictive_search.resources.products.size
        endif
      endif
    %}
  </span>
{%- endif -%}

Новый компонент для самой формы. В папку snippets создаем custom-header-search.liquid

{%- comment -%}
  Параллельная реальность поиска.
  Тег: <custom-predictive-search>, Скрипт: custom-predictive-search.js
{%- endcomment -%}

<style>
  /* Убиваем дефолтные браузерные outline и рамки */
  .custom-search-wrapper input,
  .custom-search-wrapper button,
  .custom-search-wrapper a {
    outline: none !important;
    box-shadow: none !important;
  }
  .custom-search-wrapper input[type="search"]::-webkit-search-decoration,
  .custom-search-wrapper input[type="search"]::-webkit-search-cancel-button,
  .custom-search-wrapper input[type="search"]::-webkit-search-results-button,
  .custom-search-wrapper input[type="search"]::-webkit-search-results-decoration {
    -webkit-appearance: none;
  }

  /* МАГИЯ ЗДЕСЬ: Управляем видимостью дропдауна и сбрасываем Dawn */
  .custom-search-wrapper .predictive-search {
    display: none !important;
    position: absolute !important;
    top: 100% !important;
    left: 0 !important;
    right: 0 !important;   
    width: 100% !important;
    border: 0.0625rem solid #f3f4f6 !important; /* gray-100 */
    background-color: #ffffff !important; /* bg-white */
    border-radius: 0.5rem !important;
    box-shadow: 0 0.625rem 1rem -2rem rgba(0, 0, 0, 0.1), 0 0.25rem 0.45rem -0.125rem rgba(0, 0, 0, 0.05) !important;
    overflow: hidden;
    z-index: 50;
  }
  .custom-search-wrapper[open] .predictive-search {
    display: block !important;
  }

  /* Стили для кастомного спиннера (скрываем по умолчанию) */
  .custom-search-wrapper .predictive-search__loading-state {
    display: none;
    align-items: center;
    justify-content: center;
    padding: 3rem 0;
  }
  /* Показываем спиннер только когда есть атрибут loading */
  .custom-search-wrapper[loading="true"] .predictive-search__loading-state {
    display: flex;
  }
  /* Скрываем старые результаты, пока грузится новый запрос */
  .custom-search-wrapper[loading="true"] #custom-predictive-search-results {
    display: none;
  }

  /* Кастомный скроллбар */
  .custom-search-wrapper #custom-predictive-search-results-groups-wrapper::-webkit-scrollbar {
    width: 0.4rem;
  }
  .custom-search-wrapper #custom-predictive-search-results-groups-wrapper::-webkit-scrollbar-track {
    background: transparent;
  }
  .custom-search-wrapper #custom-predictive-search-results-groups-wrapper::-webkit-scrollbar-thumb {
    background: #ccc;
    border-radius: 0.65rem;
  }
    /* Подсветка при навигации с клавиатуры (стрелочки) */
  .custom-search-wrapper #custom-predictive-search-results li[aria-selected="true"] > a {
    background-color: #F9FAFB !important; /* tw:bg-gray-50 */
  }
  .custom-search-wrapper #custom-predictive-search-results li[aria-selected="true"] > .predictive-search__item--term {
    background-color: #F3F4F6 !important; /* tw:bg-gray-100 для кнопки */
    color: #0ea5e9 !important; /* tw:text-goluboy */
  }
</style>

<custom-predictive-search class="custom-search-wrapper tw:relative tw:w-full tw:max-w-[40rem] tw:hidden tw:lg:block" 
  data-loading-text="{{ 'accessibility.loading' | t }}"
  data-min-chars="{{ settings.predictive_search_min_chars | default: 2 }}"
>
  <form action="{{ routes.search_url }}" method="get" role="search" class="tw:flex tw:w-full">
    
    <div class="tw:relative tw:flex-1">
      <input
        class="tw:!w-full tw:px-5 tw:h-[4rem] tw:text-[1.4rem] tw:text-grayscale-950 tw:placeholder-gray-400 tw:outline-none tw:focus:outline-none tw:appearance-none tw:border tw:border-r-0 tw:border-gray-200 tw:focus:border-gray-300 tw:transition-colors"
        id="Header-Search-Input"
        type="search"
        name="q"
        value="{{ search.terms | escape }}"
        placeholder="Search..."
        role="combobox"
        aria-expanded="false"
        aria-owns="predictive-search-results"
        aria-controls="predictive-search-results"
        aria-haspopup="listbox"
        aria-autocomplete="list"
        autocorrect="off"
        autocomplete="off"
        autocapitalize="off"
        spellcheck="false"
      >
      
      <button
        type="reset"
        class="reset__button tw:hidden tw:absolute tw:right-0 tw:top-0 tw:h-[4rem] tw:text-gray-400 tw:hover:text-grayscale-950 tw:transition-colors{% if search.terms == blank %} hidden{% endif %}"
        aria-label="{{ 'general.search.reset' | t }}"
      >
        <svg class="tw:w-5 tw:h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
          <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
        </svg>
      </button>
    </div>

    <input type="hidden" name="type" value="product">
    <input type="hidden" name="options[prefix]" value="last">

    <button type="submit" class="tw:bg-golden tw:text-white tw:px-5 tw:h-[4rem] tw:hover:bg-golden/80 tw:transition-colors tw:shrink-0 tw:outline-none tw:focus:outline-none" aria-label="{{ 'general.search.search' | t }}">
      <svg class="tw:w-7 tw:h-7" fill="none" stroke="currentColor" viewBox="0 0 24 24">
        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
      </svg>
    </button>

    <div class="predictive-search predictive-search--header" tabindex="-1" data-predictive-search>
      <!-- Наш кастомный Tailwind-спиннер -->
      <div class="predictive-search__loading-state">
        <svg class="tw:animate-spin tw:h-10 tw:w-10 tw:text-golden" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
          <circle class="tw:opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
          <path class="tw:opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
        </svg>
      </div>
    </div>

    <span class="predictive-search-status visually-hidden" role="status" aria-hidden="true"></span>
  </form>
</custom-predictive-search>

Компоненты готовы и теперь осталось парочку штрихов чтобы все заработало как следует.

ШАГ.6 Подключаем и наслаждаемся

Добавляем в layout/theme.liquid перед закрывающим тегом body подключение нашего кастомного скрипта

<script src="{{ 'custom-predictive-search.js' | asset_url }}" defer="defer"></script>

Находим место для встаки нашего нового компонента в хедере:

  {% comment %} вставляем после менюшки  {% endcomment %}
    {% render 'custom-header-search' %}      

Учти что у него стоит класс tw:hidden tw:lg:block это значит что тебе нужно скрывать компонент для мобилки на этом брекпоинте чтобы не было сразу 2 формы поиска в хедере.

Открываем наш header-search.liquid или weblegko-header-search.liquid (какой используется в шапке), который лежит в папке snippets и добавляем tw:lg:!hidden примерно на строке 26 чтобы скрыть на десктопе:

<details-modal class="tw:lg:!hidden header__search">

Дроп-даун с настройкой кол-ва символов в редакторе админке готов.