Спецификация под реализацию · tazzz.ru

Мобильный фронтенд Что замерено на боевом сайте, почему это ломается и в каком порядке чинить

31.08.2026 область — frontend-agent/ ветка — full-update замеры — Chromium mobile, 360 / 390 / 430 px

00Итог

Сайт не является мобильным. Это десктопный сайт, у которого местами включены tailwind-брейкпоинты. Разница принципиальна: адаптив здесь — сужение десктопного макета, а не отдельный мобильный сценарий. Отсюда и отзывы.

Пять фактов, снятых инструментально с боевого сайта — не гипотезы:

01
/docs/api на 360 px
Вёрстка требует 644 px → браузер сжимает страницу до 56 %. Подписи 10–11 px рендерятся как ~6 px.
02
/analytics на 360 px
scrollWidth 388 при clientWidth 360 → горизонтальная прокрутка всей страницы, авто-зум 93 %.
03
Шапка сайта
header.scrollWidth 388 при вьюпорте 360 → кнопка «Профиль» физически обрезана. Гость на телефоне не видит входа в аккаунт.
04
Тач-цели, лендинг 390 px
Из 263 интерактивных элементов 162 (62 %) меньше 44 px по короткой стороне; 6 меньше 24 px — прямое нарушение WCAG 2.5.8 AA.
05
Главный JS-бандл
411 KB gzip (1 370 KB сырых) одним чанком, без разбиения по маршрутам. Brotli на проде выключен.

Ни одна из этих проблем не чинится правкой пары классов. Нужен слой мобильных примитивов — лист-шторка, безопасные зоны, тач-цели, единицы вьюпорта, — и уже поверх него переработка шести ключевых экранов. Работа разбита на шесть волн; первые две закрывают около 80 % видимой боли.

▸Статус: волны В1–В5 сделаны

Пять волн из шести сделаны 31.08.2026: фундамент, навигация, первое касание, ядро продажи (карточка и фильтры) и кабинет с данными. Осталась последняя — вес страницы и документация API. Цифры ниже сняты тем же скриптом, что и цифры «до», на сборке с боевым API — то есть с реальными карточками, а не на пустой странице.

ПроверкаБылоСтало
/ 360 px — шапка header.scrollWidth 388, «Профиль» обрезан 360 = 360, все точки входа доступны
/analytics 360 px горизонтальная прокрутка, авто-зум 93 % зум 100 %
/ 390 px — целей < 44 px 162 из 263 0
Текст мельче 11 px на / 81 узел < 12 px 0
Высота лендинга, 360 px 10 121 px 8 688 px (−14 %)
Целей < 24 px (WCAG AA) 6 0
/login 6 нарушений, автозаполнение не работало 0, автозаполнение работает
Карточка авто, 360 px «Заказать» в правом верхнем углу поверх фото; ряд действий занимал 73 % ширины; фото не листались липкий низ с ценой и «Заказать», свайп-галерея, 0 нарушений
/docs/apiзум 56 % без изменений — ждёт волны В6
Карточка автомобиля на экране 360 пикселей: сверху крестик, название и меню, затем фото со счётчиком 1/20, характеристики по одной в строке, снизу закреплена цена и кнопка Заказать
Карточка авто, 360 px. Цена и «Заказать» закреплены снизу — под большим пальцем. Раньше единственная кнопка заказа висела в правом верхнем углу поверх фото, а вторая пряталась под всей простынёй характеристик, осмотра и конфигурации. Фото листаются пальцем.

Правки по код-ревью

Параллельная сессия прислала пять замечаний по этим файлам. Все подтвердились чтением кода и исправлены:

Что былоЧем это грозило
Чип снятия фильтра очищал один ключ Выдача молча оставалась суженной: у марки и модели есть зависимые поля, которые в чипах не показываются
Безопасная зона в нижней панели считалась дважды На телефоне без выреза подписи вкладок срезались снизу
Шторка рендерилась без проверки ширины экрана При смене ширины с открытой шторкой в дереве оказывалась вторая копия формы фильтров — два подписчика одного состояния
Граница «телефон / компьютер» в коде и в стилях стояла в разных местах В зазоре 640–767 px выходил гибрид: карточка компьютера, выезжающая снизу и таскаемая пальцем
Пауза ленты пересчитывалась из настроек системы Смена настроек перезапускала ленту поверх явно нажатой паузы — ломая ровно тот контрол, ради которого пауза и добавлена

По тому же следу нашлась та же болезнь в карточке автомобиля, где ревью её не искало: в зазоре 640–767 px выходил гибрид — ряд действий как на компьютере, но без закреплённой кнопки «Заказать», и при этом уже свёрнутые разделы. Восемь мест приведены к одной границе.

Битые картинки: закрыто до конца

Одиннадцать мест грузили фото без запасного варианта — при сбое отдачи там появлялась иконка битой картинки. Девять сделал коллега (это его область), два оставшихся в моих файлах добрал я. Теперь в проекте не осталось ни одного изображения, которое при сбое покажет «сломанную картинку» вместо заглушки.

Побочный выигрыш: плиткам списков перестала прилетать картинка в 1600 px — запрашивается размер под место показа.

Урок про общее рабочее дерево

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

Два бага, которые не ловит ни одна моя проверка

Третьим заходом коллега нашёл то, что не видно ни в разметке, ни в размерах:

Обе — про поведение, а не про вёрстку: автопроверка видит корректную разметку, правильные размеры и отсутствие переполнения. Это её граница, и её стоит помнить.

Дыры в самих проверках

Вторым заходом коллега разобрал не код, а инструменты. Это оказалось важнее любой отдельной правки вёрстки: проверка, которая зелёная по недосмотру, хуже отсутствующей.

Что былоЧем это грозило
Шаблон поиска запрещённых единиц не видел записи с пробелом после двоеточия Самая частая форма записи проходила мимо. После починки шаблон сразу нашёл два настоящих нарушения на странице документации API — то есть прежнее «чисто» было ложным
Проверка не была подключена ни к сборке, ни к выкладке Проверка, которую никто не запускает, ничего не проверяет. Теперь блокирует выкладку
Запись эталона стирала маршруты, не опрошенные в этом прогоне Обычный прогон молча вычёркивал шесть закрытых страниц и выключал для них контроль размера кнопок
Токен доступа подхватывался в любом прогоне Достаточно было переменной в окружении, чтобы публичные страницы измерялись «изнутри» — то есть не так, как их видит гость

Отдельно: коллега убрал из набора вариантов картинки самый крупный — сервер его не всегда создаёт. Это ломало мой вызов в карточке: при заданном наборе браузер игнорирует основной адрес, и главное фото навсегда осталось бы мелким. Набор с главного фото снят — замер после правки: 1200×900 в слоте 766 px.

Что появилось в коде

Оговорки к замеру

  • Критерий «−25 % высоты лендинга» не достигнут: вышло −14 %. Замер по секциям показал, где высота — 17 контентных карточек. Уплотнение композиции и ритма выбрало всё, что можно взять вёрсткой; оставшееся достижимо только сокращением самого контента, а это продуктовое решение. Критерий стоило формулировать после замера, а не до.
  • Аудит научен видеть .tap-44: цель меряется по большему из «рисунок» и абсолютный ::after. Без этого корректно сделанные цели считались бы нарушением.
  • content-visibility: auto на секциях лендинга отложено в В6: оно конфликтует с добавленными якорями разделов — у пропущенного контента позиция считается по contain-intrinsic-size, и прокрутка попадала бы мимо.
  • Карточка авто теперь В автопроверке: она публична по ссылке вида ?car=<id>, а id берётся из витрины лендинга, так что проверка не протухает вместе с базой.
  • Чат, поиск, кабинет, переписка и содержимое аналитики в автопроверку по-прежнему не входят — нужен тестовый аккаунт. Это уже третья волна подряд, где половина работы остаётся без инструментальной проверки.
  • Проверка сжатия фото в живом браузере нашла ошибку в первой версии порога: он пропускал файлы легче 400 КБ, а ровный кадр 4000×3000 сжимается в 230 КБ — то есть картинка в 4000 px ушла бы на сервер как есть. Исправлено до выкатки.
  • Из плана В4 не сделаны аккордеон-секции внутри формы фильтров и диапазоны «от/до» в одну строку: и то, и другое требует прохода по файлу на 2577 строк с восемью независимыми наборами полей. Шторка с закреплённым счётчиком уже снимает главную боль.

02Доказательства

Скриншоты сняты Chromium в мобильной эмуляции, 360×740, DPR 2, боевой tazzz.ru.

Лендинг tazzz.ru на экране 360 пикселей: в шапке видно шесть иконок, седьмая обрезана правым краем
Лендинг, 360 px. В шапке шесть иконок — седьмая, «Профиль», за краем экрана. Спасает только overflow-x-hidden обёртки: она не чинит переполнение, а прячет его.
Страница документации API на экране 360 пикселей: сайдбар занимает почти весь экран, контент срезан справа
/docs/api, 360 px. Сайдбар шириной 300 px занимает 83 % экрана, контентная колонка срезана. Страница публичная и маркетинговая — и нечитаема с телефона.

01Что именно называется «мобильной версией»

Уточнение до начала работ, иначе усилия уйдут не туда.

КаталогЧто этоСтатус
frontend-agent/React 18 + Vite + TS, Zustand, Tailwind, Radix Боевой, весь сайт. Мобильная версия — это его адаптив
frontend-v2/Nuxt 3 + Vue + Tauri, «мобильное приложение» Не деплоится вообще. Обманчивое имя — работать не здесь
frontend/классика Выведен, страницы перенесены в frontend-agent/src/classic/* (ADR 0017)
frontend-admin/админка Вне области: внутренний десктопный инструмент

Отдельного мобильного шаблона, маршрута или приложения нет и не планируется. Продукт — адаптивный SPA: одна кодовая база, одни маршруты.

Что известно об аудитории

Данных мало, и это само по себе дефект. Хроника покупателя пишет viewport_width, но только для залогиненных и только с 27.08 — выборка непригодна. По логам nginx за 27–31.08, среди уникальных IP, реально загрузивших бандл: 24 десктоп против 6 мобильных (~20 %). Мобильные — iOS Safari 16–26 и Android 6–15: значит критичны dvh, безопасные зоны, правило 16 px против авто-зума и бюджет JS для слабого железа.

Вывод, которого делать нельзя: «мобильных мало, значит неважно». 20 % при известном качестве мобильной версии — это не спрос, а его отсутствие из-за качества.

03Диагноз

Тринадцать системных дефектов. Каждый — с доказательством и следствием для пользователя.

Д1 Шапка не помещается в телефон и молча теряет вход в профиль критично

components/layout/TopBar.tsx:88–96

Центральный nav — 7 кнопок (Чат, Поиск, История, Радар, Сообщения, Аналитика, Профиль), подписи скрыты hidden sm:inline. Замер: nav.scrollWidth = 276, логотип ≈82, паддинги 32, зазоры 16 → header.scrollWidth = 388 px при вьюпорте 360.

  • На / обёртка с overflow-x-hidden обрезает седьмую кнопку — вход в аккаунт.
  • На /analytics, /chat, /search обёртки нет → едет вся страница, Chrome сжимает её до 93 %.
  • В /chat слева добавляется бургер 40 px — дефицит ещё +40.

Сами кнопки — 36×32 px: ниже 44 (Apple), ниже 48 (Material), на грани 24 (WCAG). Семь неподписанных иконок в верхней, то есть худшей по досягаемости, зоне экрана.

Д2 Контент душится двойным паддингом критично

tailwind.config.js · classic/components/ui/card.tsx · layout/ClassicShell.tsx:31

container.padding задан как 2rem на всех брейкпоинтах, внутри — Card с p-6. Арифметика для 360: 360 − 2×32 − 2×24 = 248 px под контент. 69 % экрана съедено полями, и в эти 248 px кладётся форма из 20+ полей.

Д3 Тач-цели системно меньше нормы критично

Проблема на уровне примитивов, поэтому системная: button.tsx — default h-10, sm h-9, icon h-10 w-10; input.tsx — h-10. Сердечко «в избранное» и кнопка «Обновить» — 32 px, и обе лежат поверх фото в углах карточки: промах открывает карточку вместо действия. Чипы под фото — ~20 px. Тумблер «Вход / Регистрация» — 32 px.

Д4 Карточка авто — десктопный экран, показанный на телефоне критично

components/search/CarModal.tsx (1424 строки)

Это главный экран продажи. На мобильном:

  • Кластер действий поверх фото: Сравнить 40 + Сердце 40 + «Заказать» ≈120 + Закрыть 40 + зазоры = ≈264 px из 360 (73 % ширины), без подложки. Главный CTA продукта — в правом верхнем углу, вне зоны большого пальца.
  • Шесть сеток grid-cols-2 без брейкпоинта: на 360 px это две колонки по 160 px под пары «Первая регистрация / 12.03.2021».
  • Оболочка h-full, не dvh → на iOS низ уходит под панель браузера.
  • Нет safe-area-inset-bottom, нет липкого бара с ценой и «Заказать»: чтобы заказать, надо прокрутить всю простыню ТТХ, осмотра и конфигурации.
Д5 Галерея не листается пальцем критично

Главное фото — статичный <img>; переключение только тапом по миниатюре 80×56 (максимум 12 кадров из N). В лайтбоксе — стрелки и клавиатура. Во всём проекте ноль обработчиков свайпа и ни одного scroll-snap. Это ровно тот жест, который на телефоне ожидается по умолчанию — а фото автомобиля здесь и есть товар.

Д6 Форма фильтров — бесконечная простыня критично

classic/components/filters/SearchFiltersPlatforms.tsx — 2577 строк, 2 брейкпоинт-префикса на весь файл

  • Каждая граница диапазона — отдельная ячейка: «Цена от» и «Цена до» становятся двумя полноширинными полями друг под другом. 20+ полей × 66 px = свыше 1300 px вертикали.
  • Кнопка «Найти · N» — в самом низу, не липкая. Счётчик результатов — главная обратная связь фильтра — уезжает при первом же скролле.
  • Секций и аккордеонов нет: раскрыто всё и всегда.
  • Подсказка «Поставьте на радар» показывается только по group-hover — на тач недостижима в принципе; остаётся немаркированная иконка.
  • 67 полей type="number"; inputMode — в одном файле, autoComplete — в четырёх, enterKeyHint — нигде.

Отраслевой эталон для этого экрана — шторка по кнопке «Фильтры», явное «Показать N» вместо мгновенного применения, липкий триггер, чипы применённых фильтров. Нет ничего.

Д7 Диалоги не приспособлены к телефону важно

classic/components/ui/dialog.tsx:33

DialogContent центрируется трансформом, без max-height и без overflow: содержимое выше экрана обрезается сверху и снизу без возможности прокрутки. w-full без бокового поля — диалог встык к краям. В одном месте это починено локально (AlertEditorModal), то есть баг известен, а примитив остался сломанным. Прокрутку фона блокирует только CarModal, и та наивно.

Д8 Единицы вьюпорта и безопасные зоны — точечно важно

vh/h-screen вместо dvh — 11 мест, включая область переписки h-[70vh]. env(safe-area-inset-*) — два вхождения на весь проект. Плавающая кнопка «Из базы» прибита к bottom-28 — константе, подобранной под текущую высоту композера. При этом #root { min-height: 100dvh } в проекте есть: подход известен, но не доведён до компонентов.

Д9 Формы не знают, что они на телефоне важно

Экран входа — без единого autoComplete, name, id: менеджеры паролей и автозаполнение не работают. Плюс autoFocus: клавиатура вылезает сразу и ломает центрирование. 67× type="number" вместо inputMode. Загрузка фото объявления — без клиентского сжатия: с камеры это 5×4 МБ через мобильную сеть.

Д10 /docs/api полностью неадаптивна критично

pages/ApiDocsPage.tsx — 1460 строк, ноль tailwind-классов

Вся вёрстка — инлайновые style с пиксельными константами: сайдбар width: 300, контент padding: 28px 40px, таблицы без контейнера прокрутки, <pre> без overflow-x. Требует 644 px → зум 56 % на 360, 61 % на 390.

Д11 Производительность мобильного входа критично
  • Бандл 411 KB gzip одним чанком. Разбиения по маршрутам нет. Гость, открывший лендинг с телефона, скачивает и парсит кабинет перевозчика, /docs/api, все 8 панелей фильтров и аналитику.
  • Brotli выключен — не берётся 15–20 % экономии (gzip и HTTP/2 есть).
  • Картинки. Ярусы (full 1600 / thumb 480 / shakal 320) сделаны грамотно, но модалка всегда просит full: на 360 CSS-px это 1,5–2× лишних пикселей. srcset нет ни в одном <img>; 21 <img> без loading; fetchpriority не используется.
  • Шрифты. Семь файлов с внешнего хоста блокирующим <link> в критическом пути рендера.
  • Анимации. На лендинге постоянно крутится requestAnimationFrame-лента, дописывающая scrollLeft каждый кадр. Плюс анимированный градиентный слой на body::before и backdrop-blur-2xl на каждой стеклянной поверхности — самое дорогое, что можно попросить у бюджетного Android.
Д12 Интерфейс рассчитан на курсор важно

246 использований hover:, часть — единственное состояние обратной связи. Тултипы title= на тач не открываются никогда, а в карточке за ними спрятаны пояснения к кнопкам без подписей. Боковой рейл разделов лендинга скрыт на мобильном без замены. Автобегущая лента без кнопки паузы — нарушение WCAG 2.2.2. aria-label — 23 штуки на 40 000 строк.

Д13 Нет PWA-манифеста важно

manifest.json отсутствует — запрос отдаёт SPA-фолбэк с кодом 200, ложноположительно. apple-touch-icon указывает на непропорциональный PNG 350×250. «Добавить на главный экран» даёт обрезанную иконку.

Что уже сделано хорошо и ломать нельзя

Это не список «всё плохо». Отдельные места сделаны на уровне:

  • Правило font-size: 16px для полей на <768px — верное решение проблемы авто-зума iOS, с объясняющим комментарием. И viewport-fit=cover, и #root { min-height: 100dvh }.
  • prefers-reduced-motion уважается в трёх местах.
  • В лентах карточек тач-листание явно отдано нативному overflow-x, ручной drag оставлен только мыши — «иначе лента крадёт вертикальную прокрутку страницы». Ровно правильно.
  • Контекстное меню карточки отключено на pointer: coarse, чтобы не ломать long-press.
  • В «Сообщениях» — настоящий master-detail: на мобильном список скрывается при открытом треде.
  • InfoTip в аналитике: работает по тапу, через портал, с закрытием по скроллу и Esc, с зажимом позиции в границах экрана. Это эталон для всех подсказок проекта.
  • Блок услуг перевозчика — единственная настоящая нижняя шторка. База для общего примитива.

04Принципы

Контракт, по которому проверяется любой будущий PR к фронту.

П1Мобильный — базовый Пишем стили без префикса под узкий экран, расширяем через sm:/md:. Не «прячем лишнее на мобильном», а «добавляем на десктопе».
П2Мобильный ≠ узкий десктоп У экрана меньше 768 px есть право на другую компоновку: шторка вместо колонки, нижняя навигация вместо верхней, один экран вместо двух панелей.
П3Ни одного горизонтального переполнения scrollWidth ≤ clientWidth на 360 px — приёмочный критерий каждой страницы. Горизонтально скроллится только то, что объявлено скроллером.
П4Тач-цель — 44×44 24×24 с зазором 24 px — потолок исключений, а не норма. Увеличиваем область попадания, а не рисунок.
П5Главное действие — в нижней трети «Заказать», «Найти · N», «Отправить» живут в липком нижнем баре, а не в правом верхнем углу.
П6Динамический вьюпорт и безопасные зоны — всегда dvh/svh вместо vh; env(safe-area-inset-*) на любом липком крае. 100vh в коде запрещён.
П7Ничего важного не прячется за наведением hover: — только украшение. Любое состояние и любая подсказка доступны тапом.
П8Бюджет мобильного входа — закон Новый вес — только с обоснованием и компенсацией.
П9Жест дополняет, а не заменяет Свайп-закрытие шторки — плюс к «✕». Свайп галереи — плюс к точкам. Одиночный жест никогда не единственный путь.
П10Один набор компонентов Мобильное поведение живёт внутри примитива, а не рассыпается по 30 страницам условиями md:hidden.

05Фундамент — волна В1

Без этого слоя переработка экранов превратится в 30 локальных заплаток.

Семантика брейкпоинтов

Значения оставляем дефолтные, но фиксируем смысл: сейчас sm: используется и как «не телефон», и как «широкий телефон», из-за чего поведение непредсказуемо. md — единственная граница смены парадигмы; sm меняет только число колонок. Нижняя граница поддержки — 320 px, проектная точка — 360.

Токены отступов

container: { padding: { DEFAULT:'1rem', sm:'1.5rem', lg:'2rem' } }   // было '2rem' везде
CardHeader / CardContent / CardFooter:  p-4 md:p-6

Эффект на 360 px: полезная ширина 248 → 328 px (+32 %).

Безопасные зоны и вьюпорт

.pb-safe   { padding-bottom: max(0.75rem, env(safe-area-inset-bottom)); }
.pt-safe   { padding-top:    max(0.75rem, env(safe-area-inset-top)); }
.pb-navbar { padding-bottom: calc(3.5rem + env(safe-area-inset-bottom)); }

height: { screen:'100dvh', 'screen-s':'100svh' }    // + minHeight, maxHeight

svh — для оболочек, которые не должны «дышать» при скрытии панели браузера; dvh — для того, что обязано занимать реально видимую область. Защита от регресса — грит в CI: литералы vh и h-screen в src/** запрещены.

Новые примитивы

ПримитивЧто закрываетКлючевые требования
SheetД7, шторки чата и ленты, будущие фильтры Ручка-хват, свайп вниз (гейт по scrollTop === 0), «✕» всегда, Esc, focus trap, inert фона, max-height: 92dvh, overscroll-contain, .pb-safe; на md: — центрированное окно
useBodyScrollLockПрокрутка фона под любым оверлеем Фиксация позиции, а не overflow:hidden (iOS игнорирует последнее)
useMediaQuery5 императивных matchMedia в коде Подписка на change — компоненты перерисовываются при повороте экрана
GalleryД5 CSS scroll-snap (нативный жест и инерция, без библиотек), scroll-snap-stop: always, счётчик «3 / 24», стрелки только на md:
.tap-44Д3 для оверлейных иконок на фото Псевдоэлемент 44×44 — растёт зона попадания, не рисунок
carImageSrcSetД11 srcset по существующим ярусам 320/480/1600 + явный sizes на каждом месте

06Поверхности

S1 · Навигация — самый важный пункт спецификации

На телефоне верхняя панель ужимается до трёх элементов: бургер · логотип · аватар. Появляется нижняя навигация — fixed bottom-0 h-14 pb-safe, пять пунктов с иконкой и подписью: Чат · Поиск · Радар · Сообщения · Профиль. «История» и «Аналитика» уходят во второй уровень: первая — разрез профиля, вторая — вход из карточки авто. Бар скрывается при открытой шторке и при фокусе в композере. На десктопе всё как сейчас.

Почему нижняя, а не бургер: видимая навигация обнаруживается ~48 % пользователей против ~21 % у скрытой; точность нажатия в нижней зоне ~96 % против ~61 % вверху.

ЭкранЧто меняетсяВолна
ЛендингГерой text-3xl на базе, CTA полноширинными блоками вместо рваной лесенки, ритм py-10 md:py-16, кнопка паузы ленты (WCAG 2.2.2) и выключение авто-бега на pointer: coarse, чипы-якоря разделов вместо скрытого рейлаВ3
ЧатОболочка 100svh; FAB «Из базы» — calc(safe-area + var(--composer-h) + 12px) вместо bottom-28; drawer истории и шторка ленты → общий Sheet; чипы 44 pxВ3
Карточка в ленте.tap-44 на трёх оверлейных кнопках с разводкой по углам; whileHover → whileTap на тач; заголовок line-clamp-2; srcsetВ3
Модалка автоНа <md — полноэкранный экран: липкая шапка «✕ · название · ⋯», галерея со свайпом, липкий нижний бар «цена + Заказать», все grid-cols-2 → grid-cols-1 sm:grid-cols-2, длинные секции — свёрнутые аккордеоны с числом пунктовВ4
ФильтрыЭкран показывает площадку, ссылку и кнопку «Фильтры · N» + чипы применённых. Кнопка открывает Sheet с аккордеон-секциями; диапазоны «от/до» — в одну строку; липкий низ «Показать N автомобилей» (счётчик уже считается, просто переезжает в видимую зону)В4
ВыдачаЛипкая полоса «Фильтры · N» + «Сортировка»; второстепенные действия в «⋯»; прогресс — компактная полоса вместо карточки; gap-3 md:gap-6В4
КабинетПодписи вкладок видимы всегда; grid-cols-[130px,1fr] → метка над значением; липкий «Сохранить»; клиентское уменьшение фото до 1600 px перед отправкойВ5
СообщенияТред полноэкранный, композер sticky bottom-0 pb-safe, h-[70vh] → dvh, аппаратная «назад» закрывает тредВ5
АналитикаЛучшая поверхность проекта. Точечно: CompareTable на телефоне — две карточки-колонки вместо grid-cols-[140px_1fr_1fr] (94 px под цену «4 250 000 ₽»)В5
/docs/apiИнлайновые стили → tailwind (иначе адаптив невозможен); сайдбар → шторка; таблицы и <pre> в overflow-x-auto; минимальный кегль 12 pxВ6
ВходautoComplete, name, id, enterKeyHint; убрать autoFocus; цели 44 px. Дёшево и заметноВ2

07Бюджет производительности

Цель — «good» по Core Web Vitals на классе Moto G Power / 4G.

МетрикаЦельСейчас
LCP< 2,5 сне измерялось; 411 KB одним чанком делает достижение маловероятным
INP< 200 мсне измерялось
CLS< 0,1риск: <img> без размеров
JS первого маршрута, gzip≤ 180 KB411 KB
CSS, gzip≤ 25 KB17 KB ✓
Шрифты в критическом пути≤ 27

Меры в порядке «эффект / стоимость»:

  1. Разбиение по маршрутам. В отдельные чанки: /docs/api, весь кабинет перевозчика, аналитика, классические фильтры (2577 строк), сообщения, кабинет. Лендинг гостя должен тянуть только лендинг. Ожидаемо 411 → 150–180 KB.
  2. manualChunks: react/router — отдельный долгоживущий вендорный чанк, framer-motion — отдельно (нужен не всем маршрутам).
  3. Brotli на nginx: −15–20 % к каждому текстовому ответу.
  4. srcset / sizes / loading / fetchpriority.
  5. Самохостинг шрифтов с подмножеством кириллицы; убрать блокирующий <link> на fonts.googleapis.com.
  6. Композитинг: backdrop-blur-2xl → blur-xl на тач; дрейф аврора-фона гасить на мобильном (фон остаётся, анимация выключается).
  7. content-visibility: auto ниже сгиба; виртуализация списков при N > 100.

09Приёмка

Автопроверка в CI — часть работы, не «потом»

Репозиторий тестируется ад-хок-скриптами; вписываемся в этот стиль, не притаскивая новый фреймворк. scripts/mobile-audit.mjs для каждого маршрута × {360×740, 390×844} валит сборку при нарушении:

  1. scrollWidth ≤ clientWidth + 1 — нет горизонтального переполнения.
  2. innerWidth === viewport.width — нет авто-зума (ловит случай /docs/api).
  3. Нет интерактивных элементов меньше 24 px.
  4. Число элементов меньше 44 px не растёт относительно baseline-файла в репозитории — улучшать постепенно можно, ухудшать нельзя.
  5. Нет текста меньше 12 px; все <img> с loading и пропорциями.

Матрица устройств

УстройствоШиринаЗачем
Galaxy A-серия360проектная точка, самый частый Android
iPhone SE 2/3375самый узкий актуальный iOS, без нижней safe-area
iPhone 14/15390основная масса iOS в логах
iPhone 15 Pro Max430safe-area, Dynamic Island
Android 6 / 8360старый WebView: dvh, @property, backdrop-filter
Планшет768–834граница md, переключение парадигмы

Обязательно: портрет и альбом; с открытой клавиатурой; в Safari с показанной и скрытой нижней панелью; свайп «назад» от левого края iOS не должен конфликтовать со свайпом галереи на первом кадре.

Что нужно измерить до начала работ

Отсутствие данных — самостоятельный дефект, и он чинится параллельно с волной В1:

  • Писать хронику и для гостя — сейчас гейт по токену, а именно гость и уходит с лендинга.
  • Разложить воронки (лендинг → регистрация, поиск → карточка, карточка → заявка) по viewport_width <768 / ≥768. Разрыв между полосами — прямая цена дефектов.
  • RUM Core Web Vitals по устройствам в тот же /api/chronicle/events — точка «до».
  • Тестовый аккаунт для CI: без него половина маршрутов остаётся вне автопроверки.

10План работ

ВолнаСодержаниеРиск
В1 · Фундамент Брейкпоинт-семантика, паддинги, safe-area-утилиты, dvh-токены и замена 11 мест, размеры в button/input, .tap-44, Sheet, useBodyScrollLock, useMediaQuery, Gallery, carImageSrcSet низкий — аддитивно
В2 · Навигация и вход Нижняя навигация, урезание шапки на мобильном, отступы под баром, экран входа средний — касается всех страниц
В3 · Первое касание Лендинг, чат, карточка в лентесредний
В4 · Ядро продажи Модалка авто, шторка фильтров, выдача высокий — самые крупные и ценные файлы
В5 · Кабинет и данные Кабинет, сообщения, аналитика, радар и историянизкий
В6 · Хвосты и перф /docs/api, кабинет перевозчика, разбиение бандла, brotli, шрифты, PWA-манифест, композитингсредний — трогает сборку и деплой

Внутри волны порядок свободен. В1 обязана быть первой — иначе В2–В6 наплодят локальных решений одних и тех же задач.

11Решения и риски

Открытые вопросы к владельцу

  1. Состав пяти пунктов нижней навигации (предложение: Чат · Поиск · Радар · Сообщения · Профиль).
  2. Ярус фото 800 px между thumb-480 и full-1600: стоимость диска против качества на телефоне.
  3. PWA — ограничиться манифестом и иконками или доводить до устанавливаемого приложения.
  4. Тестовый аккаунт в CI: без него половина маршрутов вне автопроверки.