Спецификация под реализацию · tazzz.ru
Сайт не является мобильным. Это десктопный сайт, у которого местами включены tailwind-брейкпоинты. Разница принципиальна: адаптив здесь — сужение десктопного макета, а не отдельный мобильный сценарий. Отсюда и отзывы.
Пять фактов, снятых инструментально с боевого сайта — не гипотезы:
/docs/api на 360 px/analytics на 360 pxscrollWidth 388 при clientWidth 360 →
горизонтальная прокрутка всей страницы, авто-зум 93 %.header.scrollWidth 388 при вьюпорте 360 → кнопка
«Профиль» физически обрезана. Гость на телефоне не видит входа в аккаунт.Ни одна из этих проблем не чинится правкой пары классов. Нужен слой мобильных примитивов — лист-шторка, безопасные зоны, тач-цели, единицы вьюпорта, — и уже поверх него переработка шести ключевых экранов. Работа разбита на шесть волн; первые две закрывают около 80 % видимой боли.
Пять волн из шести сделаны 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 |
Параллельная сессия прислала пять замечаний по этим файлам. Все подтвердились чтением кода и исправлены:
| Что было | Чем это грозило |
|---|---|
| Чип снятия фильтра очищал один ключ | Выдача молча оставалась суженной: у марки и модели есть зависимые поля, которые в чипах не показываются |
| Безопасная зона в нижней панели считалась дважды | На телефоне без выреза подписи вкладок срезались снизу |
| Шторка рендерилась без проверки ширины экрана | При смене ширины с открытой шторкой в дереве оказывалась вторая копия формы фильтров — два подписчика одного состояния |
| Граница «телефон / компьютер» в коде и в стилях стояла в разных местах | В зазоре 640–767 px выходил гибрид: карточка компьютера, выезжающая снизу и таскаемая пальцем |
| Пауза ленты пересчитывалась из настроек системы | Смена настроек перезапускала ленту поверх явно нажатой паузы — ломая ровно тот контрол, ради которого пауза и добавлена |
По тому же следу нашлась та же болезнь в карточке автомобиля, где ревью её не искало: в зазоре 640–767 px выходил гибрид — ряд действий как на компьютере, но без закреплённой кнопки «Заказать», и при этом уже свёрнутые разделы. Восемь мест приведены к одной границе.
Одиннадцать мест грузили фото без запасного варианта — при сбое отдачи там появлялась иконка битой картинки. Девять сделал коллега (это его область), два оставшихся в моих файлах добрал я. Теперь в проекте не осталось ни одного изображения, которое при сбое покажет «сломанную картинку» вместо заглушки.
Побочный выигрыш: плиткам списков перестала прилетать картинка в 1600 px — запрашивается размер под место показа.
Две параллельные сессии в одной папке — и мою работу дважды утащили чужие коммиты. Второй раз спровоцировал я сам: добавил файлы в индекс, чтобы они ушли вместе с настройкой выкладки, и следующий же коммит соседа забрал индекс целиком. Правило, к которому пришли: добавление файла в индекс на общем дереве — это не защита, а способ подарить его следующему коммиту.
Третьим заходом коллега нашёл то, что не видно ни в разметке, ни в размерах:
Обе — про поведение, а не про вёрстку: автопроверка видит корректную разметку, правильные размеры и отсутствие переполнения. Это её граница, и её стоит помнить.
Вторым заходом коллега разобрал не код, а инструменты. Это оказалось важнее любой отдельной правки вёрстки: проверка, которая зелёная по недосмотру, хуже отсутствующей.
| Что было | Чем это грозило |
|---|---|
| Шаблон поиска запрещённых единиц не видел записи с пробелом после двоеточия | Самая частая форма записи проходила мимо. После починки шаблон сразу нашёл два настоящих нарушения на странице документации API — то есть прежнее «чисто» было ложным |
| Проверка не была подключена ни к сборке, ни к выкладке | Проверка, которую никто не запускает, ничего не проверяет. Теперь блокирует выкладку |
| Запись эталона стирала маршруты, не опрошенные в этом прогоне | Обычный прогон молча вычёркивал шесть закрытых страниц и выключал для них контроль размера кнопок |
| Токен доступа подхватывался в любом прогоне | Достаточно было переменной в окружении, чтобы публичные страницы измерялись «изнутри» — то есть не так, как их видит гость |
Отдельно: коллега убрал из набора вариантов картинки самый крупный — сервер его не всегда создаёт. Это ломало мой вызов в карточке: при заданном наборе браузер игнорирует основной адрес, и главное фото навсегда осталось бы мелким. Набор с главного фото снят — замер после правки: 1200×900 в слоте 766 px.
Sheet (нижняя шторка со свайпом, ловушкой фокуса
и блокировкой фона), Gallery (свайп на CSS scroll-snap),
useBodyScrollLock, useMediaQuery, carImageSrcSet,
утилита .tap-44. Шторка и галерея созданы, но подключаются в волнах В3–В4.container.padding (+32 % полезной
ширины на 360 px), h-screen→100dvh, безопасные зоны,
переменные --navbar-h и --composer-h вместо магических констант.max-height и прокрутку
(без них контент выше экрана обрезался без возможности прочитать), у тостов крестик
перестал прятаться за hover, кнопки и поля выросли до 44 px на тач.scripts/mobile_audit.py валит сборку на
горизонтальном переполнении, авто-зуме, целях меньше 24 px и мелком тексте;
verify-no-fixed-viewport-units.sh запрещает возврат 100vh.
Первая же версия таб-бара с подписями 10 px была поймана этой проверкой.Sheet со свайпом и блокировкой фона. Карточка авто: зоны нажатия 44 px
поверх фото без изменения рисунка, заголовок в две строки вместо обрезки,
srcset по ярусам, ховер-подъём убран с тач (там он залипал)..tap-44: цель меряется по большему из «рисунок»
и абсолютный ::after. Без этого корректно сделанные цели считались бы
нарушением.content-visibility: auto на секциях лендинга отложено в В6: оно
конфликтует с добавленными якорями разделов — у пропущенного контента позиция
считается по contain-intrinsic-size, и прокрутка попадала бы мимо.?car=<id>, а id берётся из витрины лендинга, так что проверка не
протухает вместе с базой.Скриншоты сняты Chromium в мобильной эмуляции, 360×740, DPR 2, боевой tazzz.ru.
overflow-x-hidden обёртки: она не чинит переполнение, а прячет его./docs/api, 360 px. Сайдбар шириной 300 px занимает
83 % экрана, контентная колонка срезана. Страница публичная и маркетинговая —
и нечитаема с телефона.Уточнение до начала работ, иначе усилия уйдут не туда.
| Каталог | Что это | Статус |
|---|---|---|
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 % при известном качестве мобильной версии — это не спрос, а его отсутствие из-за качества.
Тринадцать системных дефектов. Каждый — с доказательством и следствием для пользователя.
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). Семь неподписанных иконок в верхней, то есть худшей по досягаемости, зоне экрана.
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+ полей.
Проблема на уровне примитивов, поэтому системная: button.tsx —
default h-10, sm h-9, icon h-10 w-10;
input.tsx — h-10. Сердечко «в избранное» и кнопка «Обновить» —
32 px, и обе лежат поверх фото в углах карточки: промах открывает карточку
вместо действия. Чипы под фото — ~20 px. Тумблер «Вход / Регистрация» — 32 px.
components/search/CarModal.tsx (1424 строки)
Это главный экран продажи. На мобильном:
grid-cols-2 без брейкпоинта: на 360 px это две
колонки по 160 px под пары «Первая регистрация / 12.03.2021».h-full, не dvh → на iOS низ уходит под панель браузера.safe-area-inset-bottom, нет липкого бара с ценой и «Заказать»:
чтобы заказать, надо прокрутить всю простыню ТТХ, осмотра и конфигурации.Главное фото — статичный <img>; переключение только тапом по миниатюре
80×56 (максимум 12 кадров из N). В лайтбоксе — стрелки и клавиатура.
Во всём проекте ноль обработчиков свайпа и ни одного scroll-snap.
Это ровно тот жест, который на телефоне ожидается по умолчанию — а фото автомобиля здесь
и есть товар.
classic/components/filters/SearchFiltersPlatforms.tsx — 2577 строк, 2 брейкпоинт-префикса на весь файл
group-hover —
на тач недостижима в принципе; остаётся немаркированная иконка.type="number"; inputMode — в одном файле,
autoComplete — в четырёх, enterKeyHint — нигде.Отраслевой эталон для этого экрана — шторка по кнопке «Фильтры», явное «Показать N» вместо мгновенного применения, липкий триггер, чипы применённых фильтров. Нет ничего.
classic/components/ui/dialog.tsx:33
DialogContent центрируется трансформом, без max-height и
без overflow: содержимое выше экрана обрезается сверху и снизу без
возможности прокрутки. w-full без бокового поля — диалог встык к краям.
В одном месте это починено локально (AlertEditorModal), то есть баг известен,
а примитив остался сломанным. Прокрутку фона блокирует только CarModal, и та наивно.
vh/h-screen вместо dvh — 11 мест,
включая область переписки h-[70vh]. env(safe-area-inset-*) —
два вхождения на весь проект. Плавающая кнопка «Из базы» прибита к
bottom-28 — константе, подобранной под текущую высоту композера.
При этом #root { min-height: 100dvh } в проекте есть: подход известен,
но не доведён до компонентов.
Экран входа — без единого autoComplete, name,
id: менеджеры паролей и автозаполнение не работают. Плюс
autoFocus: клавиатура вылезает сразу и ломает центрирование.
67× type="number" вместо inputMode. Загрузка фото объявления —
без клиентского сжатия: с камеры это 5×4 МБ через мобильную сеть.
pages/ApiDocsPage.tsx — 1460 строк, ноль tailwind-классов
Вся вёрстка — инлайновые style с пиксельными константами: сайдбар
width: 300, контент padding: 28px 40px, таблицы без контейнера
прокрутки, <pre> без overflow-x. Требует 644 px →
зум 56 % на 360, 61 % на 390.
/docs/api, все 8 панелей фильтров и аналитику.full: на 360 CSS-px это 1,5–2× лишних пикселей.
srcset нет ни в одном <img>; 21 <img>
без loading; fetchpriority не используется.<link>
в критическом пути рендера.requestAnimationFrame-лента,
дописывающая scrollLeft каждый кадр. Плюс анимированный градиентный слой на
body::before и backdrop-blur-2xl на каждой стеклянной поверхности —
самое дорогое, что можно попросить у бюджетного Android.246 использований hover:, часть — единственное состояние обратной связи.
Тултипы title= на тач не открываются никогда, а в карточке за ними спрятаны
пояснения к кнопкам без подписей. Боковой рейл разделов лендинга скрыт на мобильном
без замены. Автобегущая лента без кнопки паузы — нарушение WCAG 2.2.2.
aria-label — 23 штуки на 40 000 строк.
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.InfoTip в аналитике: работает по тапу, через портал, с закрытием по скроллу
и Esc, с зажимом позиции в границах экрана. Это эталон для всех подсказок проекта.Контракт, по которому проверяется любой будущий PR к фронту.
sm:/md:. Не «прячем лишнее на мобильном», а «добавляем на десктопе».scrollWidth ≤ clientWidth на 360 px — приёмочный критерий каждой страницы. Горизонтально скроллится только то, что объявлено скроллером.dvh/svh вместо vh; env(safe-area-inset-*) на любом липком крае. 100vh в коде запрещён.hover: — только украшение. Любое состояние и любая подсказка доступны тапом.md:hidden.Без этого слоя переработка экранов превратится в 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 игнорирует последнее) |
useMediaQuery | 5 императивных 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 на каждом месте |
На телефоне верхняя панель ужимается до трёх элементов: бургер · логотип · аватар.
Появляется нижняя навигация — 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 |
Цель — «good» по Core Web Vitals на классе Moto G Power / 4G.
| Метрика | Цель | Сейчас |
|---|---|---|
| LCP | < 2,5 с | не измерялось; 411 KB одним чанком делает достижение маловероятным |
| INP | < 200 мс | не измерялось |
| CLS | < 0,1 | риск: <img> без размеров |
| JS первого маршрута, gzip | ≤ 180 KB | 411 KB |
| CSS, gzip | ≤ 25 KB | 17 KB ✓ |
| Шрифты в критическом пути | ≤ 2 | 7 |
Меры в порядке «эффект / стоимость»:
/docs/api,
весь кабинет перевозчика, аналитика, классические фильтры (2577 строк), сообщения, кабинет.
Лендинг гостя должен тянуть только лендинг. Ожидаемо 411 → 150–180 KB.srcset / sizes / loading / fetchpriority.<link> на fonts.googleapis.com.backdrop-blur-2xl → blur-xl на тач;
дрейф аврора-фона гасить на мобильном (фон остаётся, анимация выключается).content-visibility: auto ниже сгиба; виртуализация списков при N > 100.Репозиторий тестируется ад-хок-скриптами; вписываемся в этот стиль, не притаскивая новый
фреймворк. scripts/mobile-audit.mjs для каждого маршрута × {360×740, 390×844}
валит сборку при нарушении:
scrollWidth ≤ clientWidth + 1 — нет горизонтального переполнения.innerWidth === viewport.width — нет авто-зума (ловит случай /docs/api).<img> с loading и пропорциями.| Устройство | Ширина | Зачем |
|---|---|---|
| Galaxy A-серия | 360 | проектная точка, самый частый Android |
| iPhone SE 2/3 | 375 | самый узкий актуальный iOS, без нижней safe-area |
| iPhone 14/15 | 390 | основная масса iOS в логах |
| iPhone 15 Pro Max | 430 | safe-area, Dynamic Island |
| Android 6 / 8 | 360 | старый WebView: dvh, @property, backdrop-filter |
| Планшет | 768–834 | граница md, переключение парадигмы |
Обязательно: портрет и альбом; с открытой клавиатурой; в Safari с показанной и скрытой нижней панелью; свайп «назад» от левого края iOS не должен конфликтовать со свайпом галереи на первом кадре.
Отсутствие данных — самостоятельный дефект, и он чинится параллельно с волной В1:
viewport_width <768 / ≥768. Разрыв между полосами — прямая цена дефектов./api/chronicle/events — точка «до».| Волна | Содержание | Риск |
|---|---|---|
| В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 наплодят локальных решений одних и тех же задач.
frontend-v2.
Nuxt/Tauri-версия не деплоится, отстала от API и дублирует продукт. Если приложение
понадобится — это отдельное решение с ADR, а не побочный эффект этой работы.tsc --noEmit и селфтест фильтров после каждого шага.pb-safe, проверка на живом iPhone и готовность
откатить бар за флагом.