Compare commits

...
Sign in to create a new pull request.

1 commit

Author SHA1 Message Date
5d3a045e86 МЕРА-лэндинг: меню на узком экране + спейсер под липкую панель
Навигация на узком экране была не «спрятана», а потеряна: «Точность»,
«Проверьте себя», «Статьи» и кнопка «Проверить квартиру» отдавали нулевой
getBoundingClientRect (замер на проде, 800px), замены им не было ни в шапке,
ни рядом. При высоте страницы 11 316px это значит, что до раздела можно было
добраться только прокруткой.

Довод прежнего докстринга (липкая шапка на 375px занимала 29% экрана, потому
что четыре пункта переносились) оставлен в силе: пункты вернулись НЕ в строку
шапки, а в родной `<details>`. Закрытое меню — одна кнопка, высота липкой
шапки прежняя (замерено до и после: 53px), открытая панель лежит поверх
страницы (position: absolute) и шапку не раздвигает. Клиентского острова не
завелось — шапка осталась серверной. Пункты описаны одним списком NAV_ITEMS и
рендерятся в оба места, чтобы пункт не мог снова появиться в одном и
потеряться в другом.

Спейсер под липкой нижней панелью был на 2,5px короче самой панели с 1000px,
где у неё растёт паддинг и появляется строка текста (замер на проде: панель
70,5px против спейсера 68px) — нижние пиксели подвала уходили под панель ровно
там, где спейсер и заводили. Второе значение стоит в том же @media-блоке, что
и паддинг, который его меняет.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 12:48:02 +05:00
2 changed files with 193 additions and 27 deletions

View file

@ -17,17 +17,41 @@
* которой выпадашку когда-то читали, теперь стоит текстом в герое (плашка
* «полное покрытие / данных меньше», HeroV3), то есть видна без клика.
*
* НА УЗКОМ ЭКРАНЕ ШАПКА ОДНА СТРОКА: лого и «Для бизнеса». Навигация по
* секциям и CTA скрыты в CSS (`landing-v3.module.css`, блок шапки) и
* возвращаются с той ширины, на которой шапка макета замерена как
* НА УЗКОМ ЭКРАНЕ ШАПКА ОДНА СТРОКА: лого, «Для бизнеса» и кнопка меню.
* Развёрнутый ряд навигации и CTA скрыты в CSS (`landing-v3.module.css`, блок
* шапки) и возвращаются с той ширины, на которой шапка макета замерена как
* однострочная, а не с общего для файла шага в 720px, где она всё ещё
* переносится. На 375px переносилось всё, и липкая шапка занимала 29%
* экрана эти проценты отъедались при любой прокрутке. Куда делись пункты: «Точность»
* и «Проверьте себя» секции этой же страницы, до них доезжают прокруткой
* (ради чего на мобильном и скроллят); «Статьи» и «МЕРА для бизнеса» стоят
* ссылками в подвале (`FooterV3`); «Продажа под ключ» и на десктопе не
* ссылка, а заглушка. CTA не потерян: снизу висит `StickyCtaV3` на экране
* остаётся ОДИН призыв вместо двух одинаковых.
* экрана эти проценты отъедались при любой прокрутке.
*
* ПОЧЕМУ ПОЯВИЛОСЬ МЕНЮ (31.08.2026). Прежняя редакция этого докстринга
* объясняла, куда делись пункты: «Точность» и «Проверьте себя» секции этой
* же страницы, до них доезжают прокруткой; «Статьи» и «МЕРА для бизнеса»
* стоят ссылками в подвале (`FooterV3`). Довод про высоту шапки верен и
* оставлен в силе а вот замена не проверялась замером: страница на 800px
* имеет высоту 11 316px, то есть «доехать прокруткой» до `#accuracy` и
* `#game` значит листать половину лэндинга, а до подвала весь. Навигации на
* узком экране не было вовсе (все четыре пункта и кнопка отдавали
* `getBoundingClientRect` нулевого размера). Поэтому пункты возвращены, но НЕ
* в строку шапки: `<details>` родная разворачивалка браузера, без
* JavaScript и без клиентского острова. Закрытое меню одна кнопка, то есть
* высота липкой шапки прежняя (замер, ради которого её и сужали, сохранён);
* открытая панель лежит ПОВЕРХ страницы (`position: absolute`), а не
* раздвигает шапку. CTA не потерян и на закрытой шапке: снизу висит
* `StickyCtaV3` на экране по-прежнему ОДИН призыв вместо двух одинаковых,
* второй виден только когда человек сам открыл меню.
*
* ПОТОЛОК РОДНОГО `<details>`: после клика по пункту меню остаётся открытым
* закрыть его без JavaScript браузер не умеет, а клик по якорю страницу не
* перезагружает. Человек видит, куда уехал, и закрывает той же кнопкой.
* Апгрейд, если этого окажется мало: клиентский остров с onClick на пунктах
* (`setOpen(false)`) но это первый клиентский компонент в шапке, поэтому
* заводить его стоит по жалобе, а не заранее.
*
* ПУНКТЫ ОПИСАНЫ ОДИН РАЗ (`NAV_ITEMS`) и рендерятся дважды: в ряду для
* широкого экрана и в панели меню. Список в коде, а не две копии разметки,
* чтобы пункт не мог появиться в одном месте и потеряться в другом (ровно
* этим и был дефект: на широком экране навигация есть, на узком её нет).
*
* НЕАКТИВНЫЕ ПУНКТЫ (29.08.2026). Осталась одна «Продажа под ключ»
* партнёрской формы нет ни в коде, ни у владельца, и пункт остаётся видимым,
@ -53,6 +77,47 @@ import { B2B_URL } from "../../content";
import styles from "../../landing-v3.module.css";
import { PublicLink } from "../PublicLink";
/**
* Пункты навигации один список на оба места вывода (ряд шапки и панель
* меню). `href: null` пункт-заглушка: раздела нет ни в коде, ни у владельца,
* поэтому он видим, но некликабелен (см. блок «НЕАКТИВНЫЕ ПУНКТЫ» выше).
* `route` ключ `PUBLIC_ROUTES`: короткие адреса публичного домена
* выразимы только через `PublicLink`, next/link подставил бы basePath.
*/
const NAV_ITEMS = [
{ label: "Точность", href: "#accuracy" },
{ label: "Проверьте себя", href: "#game" },
{ label: "Продажа под ключ", href: null },
{ label: "Статьи", route: "articles" },
] as const;
function renderNavItem(item: (typeof NAV_ITEMS)[number]) {
if ("route" in item) {
return (
<PublicLink key={item.label} to={item.route} className={styles.hdrNavLink}>
{item.label}
</PublicLink>
);
}
if (item.href === null) {
return (
<span
key={item.label}
className={styles.hdrNavDisabled}
aria-disabled="true"
title="Раздел скоро откроется"
>
{item.label}
</span>
);
}
return (
<a key={item.label} href={item.href} className={styles.hdrNavLink}>
{item.label}
</a>
);
}
export function HeaderV3() {
return (
<header className={styles.hdrRoot}>
@ -68,22 +133,7 @@ export function HeaderV3() {
</div>
<nav className={styles.hdrNav} aria-label="Основная навигация">
<a href="#accuracy" className={styles.hdrNavLink}>
Точность
</a>
<a href="#game" className={styles.hdrNavLink}>
Проверьте себя
</a>
<span
className={styles.hdrNavDisabled}
aria-disabled="true"
title="Раздел скоро откроется"
>
Продажа под ключ
</span>
<PublicLink to="articles" className={styles.hdrNavLink}>
Статьи
</PublicLink>
{NAV_ITEMS.map(renderNavItem)}
</nav>
<div className={styles.hdrRight}>
@ -93,6 +143,23 @@ export function HeaderV3() {
<a href="#check" className={styles.hdrCta}>
Проверить квартиру
</a>
<details className={styles.hdrMenu}>
<summary className={styles.hdrMenuBtn}>
<span className={styles.hdrMenuIcon} aria-hidden="true" />
Меню
</summary>
<div className={styles.hdrMenuPanel}>
<nav
className={styles.hdrMenuNav}
aria-label="Навигация по разделам"
>
{NAV_ITEMS.map(renderNavItem)}
</nav>
<a href="#check" className={styles.hdrMenuCta}>
Проверить квартиру
</a>
</div>
</details>
</div>
</div>
</header>

View file

@ -46,6 +46,9 @@
а не режим работы). Навигация и CTA возвращаются в @media-блоке в конце
секции. */
.hdrInner {
/* `position: relative` база для панели меню (`.hdrMenuPanel`): открытая
панель лежит ПОВЕРХ страницы и не увеличивает высоту липкой шапки. */
position: relative;
max-width: 1280px;
margin-inline: auto;
padding: 8px clamp(20px, 5vw, 56px);
@ -156,6 +159,7 @@
}
.hdrCta,
.hdrMenuCta,
.barBtn {
display: inline-flex;
align-items: center;
@ -183,10 +187,87 @@
}
.hdrCta:hover,
.hdrMenuCta:hover,
.barBtn:hover {
filter: brightness(0.92);
}
/* Меню узкого экрана (HeaderV3). Заменяет собой скрытые здесь `.hdrNav` и
`.hdrCta`: до этого на узком экране навигации не было ВООБЩЕ ни ряда, ни
меню, и «Точность»/«Проверьте себя»/«Статьи» отдавали нулевой
getBoundingClientRect. Разворачивалка родной `<details>`: без JavaScript,
то есть шапка остаётся серверной.
Закрытое меню одна кнопка, поэтому довод, ради которого ряд навигации
отсюда убирали (липкая шапка в 29% экрана на 375px), остаётся соблюдён:
высота строки прежняя. Открытая панель позиционируется абсолютно она
лежит ПОВЕРХ страницы, а не раздвигает шапку. */
.hdrMenu {
font-size: 14px;
}
.hdrMenuBtn {
display: inline-flex;
align-items: center;
gap: 8px;
/* Паддинг по вертикали 6px, а не 8: с ним кнопка укладывается в ту же
высоту строки, что и логотип, и липкая шапка на 800px остаётся 53px
ровно как до появления меню (замер до/после). */
padding: 6px 12px;
border: 1px solid var(--b2c-line);
border-radius: 6px;
font-weight: 600;
color: var(--b2c-ink);
cursor: pointer;
/* Родной маркер-треугольник убран, рядом стоит своя иконка: `list-style`
закрывает Firefox, `::-webkit-details-marker` Safari. */
list-style: none;
}
.hdrMenuBtn::-webkit-details-marker {
display: none;
}
/* Три полосы гамбургера одним элементом: сам элемент средняя полоса, две
крайние нарисованы тенями. Дешевле трёх <span> в разметке. */
.hdrMenuIcon {
width: 16px;
height: 2px;
background: currentColor;
box-shadow:
0 -5px 0 currentColor,
0 5px 0 currentColor;
}
.hdrMenuPanel {
position: absolute;
left: 0;
right: 0;
top: 100%;
display: flex;
flex-direction: column;
gap: 16px;
padding: 16px clamp(20px, 5vw, 56px) 20px;
/* Фон непрозрачный (не color-mix, как у самой шапки): под панелью едет
контент страницы, и полупрозрачный слой делал бы пункты нечитаемыми. */
background: var(--b2c-page-bg);
border-bottom: 1px solid var(--b2c-line);
}
.hdrMenuNav {
display: flex;
flex-direction: column;
gap: 14px;
font-size: 15px;
font-weight: 600;
}
.hdrMenuCta {
justify-content: center;
padding: 12px 20px;
font-size: 14.5px;
}
/* Шапка макета целиком с той ширины, на которой она РЕАЛЬНО влезает в одну
строку (замерено: 1000px лого, четыре пункта, «Для бизнеса» и кнопка;
на 720px то же самое переносится в две строки и даёт 109px липкого хрома,
@ -205,6 +286,11 @@
.hdrCta {
display: inline-flex;
}
/* Меню не нужно: с этой ширины все его пункты стоят в строке шапки. */
.hdrMenu {
display: none;
}
}
/* ==========================================================================
@ -363,8 +449,15 @@
/* Компенсация fixed-позиционирования `.barRoot` (тот вырезан из потока
документа): без спейсера панель легла бы поверх последних пикселей
страницы при докрутке до конца (реквизиты в подвале, последний вопрос
FAQ). Высота одна на все ширины: панель везде в одну строку на узком
экране за счёт того, что текст скрыт (см. `.barText`), а не переносится. */
FAQ).
ВЫСОТА НЕ ОДНА НА ВСЕ ШИРИНЫ (31.08.2026). Здесь стояло 68px «на все
ширины», и на узком экране это верно (замер на проде, 800px: панель
66,5px). Но с 1000px у панели растёт вертикальный паддинг и появляется
строка текста замерено 70,5px, то есть спейсер был на 2,5px КОРОЧЕ
панели, и нижние пиксели подвала уходили под неё ровно там, где спейсер и
заводили. Второе значение стоит в том же @media-блоке, что и паддинг,
который его меняет, чтобы правки не разъезжались. */
.barSpacer {
height: 68px;
}
@ -416,6 +509,12 @@
padding-block: 12px;
}
/* Ровно та высота, которую панель занимает с этими паддингами (замер на
проде: 70,5px) плюс запас на округление подпикселей. */
.barSpacer {
height: 72px;
}
.barText {
display: block;
}