Каркас приложения переведён с height: 100% + position: fixed/sticky на 100dvh с единственной прокручиваемой областью (.app-main). Шапка и подвал стали обычными элементами флекс-колонки: fixed-подвалу больше нечем дёргаться при смене высоты layout viewport, а шапка статична по построению — sticky ей не нужен.
Отступы от safe-area убраны полностью, сверху и снизу: подвал и купол FAB прижаты к нижнему краю экрана, шапка — к верхнему. Вслед за этим снят viewport-fit=cover в index.html — именно он заставлял браузер отдавать ненулевые insets, передавая управление безопасной зоной странице; без него env() везде даёт ноль, а контент от системных элементов отделяет сам браузер. Токены --safe-top / --safe-bottom стали мёртвыми и удалены.
Попутно: .top-bar-home защищён от распирания шапки длинным названием на узких экранах, padding-bottom у .app-main уменьшен со 154.5px до 24px (компенсировать fixed-подвал больше не нужно), купол кнопки создания партии увеличен до 140×70 — пропорция полукруга сохранена, выступ над кромкой панели 6px.
Эндпойнты и модели не затронуты: регенерация schema.d.ts и миграции не требовались.
Коммиты
5e4e483 UI: шапка и подвал вне прокрутки — каркас на 100dvh
81ad2a0 UI: подвал и купол FAB вплотную к нижнему краю экрана
2a4c5d9 UI: шапка вплотную к верхнему краю — снят viewport-fit=cover
09be0f5 UI: купол кнопки создания партии — 140x70
534ba64 UI: комментарий у купола FAB — под новую геометрию
Проверки
npm run build (tsc --noEmit && vite build) — зелёный на каждом коммите
pytest (backend) — 65 passed (бэкенд не затрагивался, прогнан для полноты)
Проверка на устройстве через dev-стенд на forbidden-stars.ru: Firefox Focus — чисто; в DuckDuckGo найден и исправлен отступ шапки от адресной строки (коммит 2a4c5d9), подтверждение после него ожидается
Отклонения от плана
План предполагал ограничить safe-area потолком в 24px и не трогать ничего, кроме CSS. По результатам проверки на реальных устройствах пошли дальше: отступы safe-area убраны полностью, снят viewport-fit=cover (правка в index.html), удалены оба токена. Размеры FAB, которые план оставлял как есть, изменены на 140×70 по запросу в ревью — иначе после прижатия базы к нижнему краю купол утапливался в панель.
## Что сделано
Каркас приложения переведён с `height: 100%` + `position: fixed`/`sticky` на `100dvh` с единственной прокручиваемой областью (`.app-main`). Шапка и подвал стали обычными элементами флекс-колонки: `fixed`-подвалу больше нечем дёргаться при смене высоты layout viewport, а шапка статична по построению — `sticky` ей не нужен.
Отступы от safe-area убраны полностью, сверху и снизу: подвал и купол FAB прижаты к нижнему краю экрана, шапка — к верхнему. Вслед за этим снят `viewport-fit=cover` в `index.html` — именно он заставлял браузер отдавать ненулевые insets, передавая управление безопасной зоной странице; без него `env()` везде даёт ноль, а контент от системных элементов отделяет сам браузер. Токены `--safe-top` / `--safe-bottom` стали мёртвыми и удалены.
Попутно: `.top-bar-home` защищён от распирания шапки длинным названием на узких экранах, `padding-bottom` у `.app-main` уменьшен со 154.5px до 24px (компенсировать `fixed`-подвал больше не нужно), купол кнопки создания партии увеличен до `140×70` — пропорция полукруга сохранена, выступ над кромкой панели 6px.
Эндпойнты и модели не затронуты: регенерация `schema.d.ts` и миграции не требовались.
## Коммиты
- `5e4e483` UI: шапка и подвал вне прокрутки — каркас на 100dvh
- `81ad2a0` UI: подвал и купол FAB вплотную к нижнему краю экрана
- `2a4c5d9` UI: шапка вплотную к верхнему краю — снят viewport-fit=cover
- `09be0f5` UI: купол кнопки создания партии — 140x70
- `534ba64` UI: комментарий у купола FAB — под новую геометрию
## Проверки
- `npm run build` (`tsc --noEmit && vite build`) — зелёный на каждом коммите
- `pytest` (backend) — 65 passed (бэкенд не затрагивался, прогнан для полноты)
- Проверка на устройстве через dev-стенд на `forbidden-stars.ru`: **Firefox Focus — чисто**; в DuckDuckGo найден и исправлен отступ шапки от адресной строки (коммит `2a4c5d9`), подтверждение после него ожидается
## Отклонения от плана
План предполагал ограничить safe-area потолком в 24px и не трогать ничего, кроме CSS. По результатам проверки на реальных устройствах пошли дальше: отступы safe-area убраны полностью, снят `viewport-fit=cover` (правка в `index.html`), удалены оба токена. Размеры FAB, которые план оставлял как есть, изменены на `140×70` по запросу в ревью — иначе после прижатия базы к нижнему краю купол утапливался в панель.
Closes #4
Подвал был position: fixed, шапка — position: sticky при html/body с
height: 100%. В мобильных браузерах со скрывающейся адресной строкой
layout viewport меняет высоту при прокрутке: fixed-подвал за ним
дёргается, а у sticky-шапки нет однозначного прокручиваемого предка.
Каркас переведён на 100dvh с единственной прокручиваемой областью
(.app-main) — шапка и подвал стали обычными элементами флекс-колонки и
дёргаться им больше нечем. Safe-area ограничена потолком 24px: при
viewport-fit=cover часть браузеров (DuckDuckGo, Firefox Focus) отдаёт
inset поверх уже смещённого вьюпорта, из-за чего шапка раздувалась вдвое,
а надпись и колокольчик съезжали вниз. Заодно шапка защищена от
распирания длинным названием на узких экранах.
#4
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ps52xzuUXnWrJ5SRnEeaLk
Локальный dev с ветки issue-4-header-footer-layout выставлен через SSH-туннель на слот VPS 9001 (LOCAL_PUBLIC=vps, как это делает run.ps1): uvicorn на :8000, vite на :5173, туннель -R 9001:localhost:5173.
Проверил, что наружу уходит именно правленый код:
GET / — 200, GET /api/health — {"status":"ok"}
tokens.css отдаёт min(env(safe-area-inset-top, 0px), 24px) и то же для bottom
layout.css отдаёт 100dvh, min-height: 0, padding-bottom: 24px и flex: none у шапки/подвала/вкладок админки
На что смотреть в DuckDuckGo и Firefox Focus:
Шапка — обычной высоты, надпись и колокольчик по центру по вертикали, не съезжают вниз.
Прокрутка длинной страницы (справка, список партий) — шапка и подвал неподвижны, подвал не дёргается при сворачивании адресной строки.
Горизонтальной прокрутки нет, шапка не выходит за края.
Заодно: круглая кнопка создания партии на месте, боковое меню открывается на всю высоту, панель уведомлений не обрезается низом шапки.
HMR через домен может не подключиться — это штатно (отмечено комментарием в vite.config.ts), само приложение работает. Стенд живёт, пока запущена моя сессия; после неё поднимается заново через .\run.ps1.
Готово — изменения открыты на https://forbidden-stars.ru
Локальный dev с ветки `issue-4-header-footer-layout` выставлен через SSH-туннель на слот VPS 9001 (`LOCAL_PUBLIC=vps`, как это делает `run.ps1`): uvicorn на :8000, vite на :5173, туннель `-R 9001:localhost:5173`.
Проверил, что наружу уходит именно правленый код:
- `GET /` — 200, `GET /api/health` — `{"status":"ok"}`
- `tokens.css` отдаёт `min(env(safe-area-inset-top, 0px), 24px)` и то же для `bottom`
- `layout.css` отдаёт `100dvh`, `min-height: 0`, `padding-bottom: 24px` и `flex: none` у шапки/подвала/вкладок админки
На что смотреть в DuckDuckGo и Firefox Focus:
1. Шапка — обычной высоты, надпись и колокольчик по центру по вертикали, не съезжают вниз.
2. Прокрутка длинной страницы (справка, список партий) — шапка и подвал неподвижны, подвал не дёргается при сворачивании адресной строки.
3. Горизонтальной прокрутки нет, шапка не выходит за края.
4. Заодно: круглая кнопка создания партии на месте, боковое меню открывается на всю высоту, панель уведомлений не обрезается низом шапки.
HMR через домен может не подключиться — это штатно (отмечено комментарием в `vite.config.ts`), само приложение работает. Стенд живёт, пока запущена моя сессия; после неё поднимается заново через `.\run.ps1`.
Сделано, коммит 81ad2a0 — стенд https://forbidden-stars.ru уже отдаёт новое (перезагрузите страницу, HMR через домен не подключается).
Зазор снизу давали два места:
.bottom-bar — height: calc(var(--bottombar-height) + var(--safe-bottom)) плюс padding-bottom: var(--safe-bottom). Теперь просто height: var(--bottombar-height).
.fab — bottom: calc(var(--safe-bottom) + 8px). Теперь bottom: 0, база купола ровно на нижнем краю.
Токен --safe-bottom после этого нигде не использовался и удалён; --safe-top остаётся у шапки. .toast пересчитан на calc(var(--bottombar-height) + 16px).
Один побочный эффект, посмотрите на него отдельно: купол больше не выступает над верхней кромкой панели. Раньше выступ в 6.5px давали как раз те самые 8px подъёма (--fab-height: 62.5px при --bottombar-height: 64px). Теперь купол вписан в панель заподлицо — на 1.5px ниже её верхней кромки. Если выступ хотелось сохранить при нулевом отступе снизу, это решается высотой купола: --fab-height: 70px даст выступ в 6px, но полукруг перестанет быть ровным (ширина уже не равна двум высотам) — либо --fab-width: 140px вместе с ним, чтобы пропорция сохранилась. Скажите, если нужно, — поправлю.
npm run build — зелёный.
Сделано, коммит `81ad2a0` — стенд https://forbidden-stars.ru уже отдаёт новое (перезагрузите страницу, HMR через домен не подключается).
Зазор снизу давали два места:
- `.bottom-bar` — `height: calc(var(--bottombar-height) + var(--safe-bottom))` плюс `padding-bottom: var(--safe-bottom)`. Теперь просто `height: var(--bottombar-height)`.
- `.fab` — `bottom: calc(var(--safe-bottom) + 8px)`. Теперь `bottom: 0`, база купола ровно на нижнем краю.
Токен `--safe-bottom` после этого нигде не использовался и удалён; `--safe-top` остаётся у шапки. `.toast` пересчитан на `calc(var(--bottombar-height) + 16px)`.
**Один побочный эффект, посмотрите на него отдельно:** купол больше не выступает над верхней кромкой панели. Раньше выступ в 6.5px давали как раз те самые 8px подъёма (`--fab-height: 62.5px` при `--bottombar-height: 64px`). Теперь купол вписан в панель заподлицо — на 1.5px ниже её верхней кромки. Если выступ хотелось сохранить при нулевом отступе снизу, это решается высотой купола: `--fab-height: 70px` даст выступ в 6px, но полукруг перестанет быть ровным (ширина уже не равна двум высотам) — либо `--fab-width: 140px` вместе с ним, чтобы пропорция сохранилась. Скажите, если нужно, — поправлю.
`npm run build` — зелёный.
2a4c5d9 — отступ шапки в DuckDuckGo. Природа та же, что была у подвала: safe-area. Insets браузер отдаёт именно из-за viewport-fit=cover в index.html — этот флаг передаёт управление безопасной зоной самой странице. После того как отступы убраны и сверху, и снизу, компенсировать нечего, поэтому cover снят: вьюпорт снова размещает браузер, env() везде даёт ноль. Следом убран --safe-top и три его использования (.top-bar, .side-menu, .toast-top-wrap) — без cover переменная всегда нулевая. Токенов safe-area в проекте больше не осталось.
Побочный плюс: на устройствах с вырезом шапка теперь не может под него заехать — без cover браузер сам держит контент в безопасной зоне.
09be0f5 — купол 140×70. Пропорция полукруга сохранена (ширина = 2× высоты), выступ над кромкой панели вернулся — 6px при высоте подвала 64px.
npm run build — зелёный.
Проверьте, пожалуйста, в даке: шапка вплотную к адресной строке, и заодно что купол на узком экране не поджимает боковые слоты (при 320px на каждый остаётся по 90px).
Два коммита, стенд https://forbidden-stars.ru обновлён — перезагрузите страницу.
**`2a4c5d9` — отступ шапки в DuckDuckGo.** Природа та же, что была у подвала: safe-area. Insets браузер отдаёт именно из-за `viewport-fit=cover` в `index.html` — этот флаг передаёт управление безопасной зоной самой странице. После того как отступы убраны и сверху, и снизу, компенсировать нечего, поэтому `cover` снят: вьюпорт снова размещает браузер, `env()` везде даёт ноль. Следом убран `--safe-top` и три его использования (`.top-bar`, `.side-menu`, `.toast-top-wrap`) — без `cover` переменная всегда нулевая. Токенов safe-area в проекте больше не осталось.
Побочный плюс: на устройствах с вырезом шапка теперь не может под него заехать — без `cover` браузер сам держит контент в безопасной зоне.
**`09be0f5` — купол 140×70.** Пропорция полукруга сохранена (ширина = 2× высоты), выступ над кромкой панели вернулся — 6px при высоте подвала 64px.
`npm run build` — зелёный.
Проверьте, пожалуйста, в даке: шапка вплотную к адресной строке, и заодно что купол на узком экране не поджимает боковые слоты (при 320px на каждый остаётся по 90px).
Diff починен — обновите страницу, теперь видны все пять коммитов и четыре файла.
Что было. Gitea обновляет служебную ссылку refs/pull/18/head по хуку на push, но только пока PR открыт. Три пуша (81ad2a0, 2a4c5d9, 09be0f5) пришли, пока PR был закрыт, — они легли в саму ветку refs/heads/issue-4-header-footer-layout, но в PR-ссылку не попали, и она осталась на первом коммите 5e4e483. Веб рисует вкладку изменений именно по ней, поэтому показывал только первый коммит. Переоткрытие PR ссылку не пересчитывает — проверил, close/reopen через API её не сдвинул.
Ничего не терялось: коммиты всё это время лежали в ветке на сервере.
Чем починено. Нужен был push-хук в ветку при открытом PR — единственное, что заставляет Gitea пересчитать PR-ссылку. Вместо пустышки взял накопившуюся мелочь: комментарий у .fab описывал прежнюю геометрию («смещён вниз», база у кромки панели), хотя после bottom: 0 и 140×70 база стоит на нижнем краю экрана. Коммит 534ba64. После пуша refs/pull/18/head встал на актуальный HEAD, get_files отдаёт четыре файла с index.html.
Про «не даёт закрыть». Через API PR закрылся и переоткрылся штатно, так что состояние самого PR целое — упиралось в веб-интерфейс на битой PR-ссылке. Сейчас ссылка в порядке, кнопка должна работать; если снова упрётся — скажите, закрою через API.
На будущее: пуши в ветку задачи, пока её PR закрыт, в diff не попадут. Если нужно приостановить ревью — лучше не закрывать PR, а перевести его в черновик или просто оставить открытым.
npm run build — зелёный.
Diff починен — обновите страницу, теперь видны все пять коммитов и четыре файла.
**Что было.** Gitea обновляет служебную ссылку `refs/pull/18/head` по хуку на push, но только пока PR открыт. Три пуша (`81ad2a0`, `2a4c5d9`, `09be0f5`) пришли, пока PR был закрыт, — они легли в саму ветку `refs/heads/issue-4-header-footer-layout`, но в PR-ссылку не попали, и она осталась на первом коммите `5e4e483`. Веб рисует вкладку изменений именно по ней, поэтому показывал только первый коммит. Переоткрытие PR ссылку не пересчитывает — проверил, close/reopen через API её не сдвинул.
Ничего не терялось: коммиты всё это время лежали в ветке на сервере.
**Чем починено.** Нужен был push-хук в ветку при открытом PR — единственное, что заставляет Gitea пересчитать PR-ссылку. Вместо пустышки взял накопившуюся мелочь: комментарий у `.fab` описывал прежнюю геометрию («смещён вниз», база у кромки панели), хотя после `bottom: 0` и `140×70` база стоит на нижнем краю экрана. Коммит `534ba64`. После пуша `refs/pull/18/head` встал на актуальный HEAD, `get_files` отдаёт четыре файла с `index.html`.
**Про «не даёт закрыть».** Через API PR закрылся и переоткрылся штатно, так что состояние самого PR целое — упиралось в веб-интерфейс на битой PR-ссылке. Сейчас ссылка в порядке, кнопка должна работать; если снова упрётся — скажите, закрою через API.
**На будущее:** пуши в ветку задачи, пока её PR закрыт, в diff не попадут. Если нужно приостановить ревью — лучше не закрывать PR, а перевести его в черновик или просто оставить открытым.
`npm run build` — зелёный.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Что сделано
Каркас приложения переведён с
height: 100%+position: fixed/stickyна100dvhс единственной прокручиваемой областью (.app-main). Шапка и подвал стали обычными элементами флекс-колонки:fixed-подвалу больше нечем дёргаться при смене высоты layout viewport, а шапка статична по построению —stickyей не нужен.Отступы от safe-area убраны полностью, сверху и снизу: подвал и купол FAB прижаты к нижнему краю экрана, шапка — к верхнему. Вслед за этим снят
viewport-fit=coverвindex.html— именно он заставлял браузер отдавать ненулевые insets, передавая управление безопасной зоной странице; без негоenv()везде даёт ноль, а контент от системных элементов отделяет сам браузер. Токены--safe-top/--safe-bottomстали мёртвыми и удалены.Попутно:
.top-bar-homeзащищён от распирания шапки длинным названием на узких экранах,padding-bottomу.app-mainуменьшен со 154.5px до 24px (компенсироватьfixed-подвал больше не нужно), купол кнопки создания партии увеличен до140×70— пропорция полукруга сохранена, выступ над кромкой панели 6px.Эндпойнты и модели не затронуты: регенерация
schema.d.tsи миграции не требовались.Коммиты
5e4e483UI: шапка и подвал вне прокрутки — каркас на 100dvh81ad2a0UI: подвал и купол FAB вплотную к нижнему краю экрана2a4c5d9UI: шапка вплотную к верхнему краю — снят viewport-fit=cover09be0f5UI: купол кнопки создания партии — 140x70534ba64UI: комментарий у купола FAB — под новую геометриюПроверки
npm run build(tsc --noEmit && vite build) — зелёный на каждом коммитеpytest(backend) — 65 passed (бэкенд не затрагивался, прогнан для полноты)forbidden-stars.ru: Firefox Focus — чисто; в DuckDuckGo найден и исправлен отступ шапки от адресной строки (коммит2a4c5d9), подтверждение после него ожидаетсяОтклонения от плана
План предполагал ограничить safe-area потолком в 24px и не трогать ничего, кроме CSS. По результатам проверки на реальных устройствах пошли дальше: отступы safe-area убраны полностью, снят
viewport-fit=cover(правка вindex.html), удалены оба токена. Размеры FAB, которые план оставлял как есть, изменены на140×70по запросу в ревью — иначе после прижатия базы к нижнему краю купол утапливался в панель.Closes #4
Открой изменения на девовском forbidden-stars.ru
Готово — изменения открыты на https://forbidden-stars.ru
Локальный dev с ветки
issue-4-header-footer-layoutвыставлен через SSH-туннель на слот VPS 9001 (LOCAL_PUBLIC=vps, как это делаетrun.ps1): uvicorn на :8000, vite на :5173, туннель-R 9001:localhost:5173.Проверил, что наружу уходит именно правленый код:
GET /— 200,GET /api/health—{"status":"ok"}tokens.cssотдаётmin(env(safe-area-inset-top, 0px), 24px)и то же дляbottomlayout.cssотдаёт100dvh,min-height: 0,padding-bottom: 24pxиflex: noneу шапки/подвала/вкладок админкиНа что смотреть в DuckDuckGo и Firefox Focus:
HMR через домен может не подключиться — это штатно (отмечено комментарием в
vite.config.ts), само приложение работает. Стенд живёт, пока запущена моя сессия; после неё поднимается заново через.\run.ps1.Убери у подвала отступ снизу (сфера и сам подвал должны начинаться от нижнего края экрана), после этого можешь переоткрывать запрос.
Сделано, коммит
81ad2a0— стенд https://forbidden-stars.ru уже отдаёт новое (перезагрузите страницу, HMR через домен не подключается).Зазор снизу давали два места:
.bottom-bar—height: calc(var(--bottombar-height) + var(--safe-bottom))плюсpadding-bottom: var(--safe-bottom). Теперь простоheight: var(--bottombar-height)..fab—bottom: calc(var(--safe-bottom) + 8px). Теперьbottom: 0, база купола ровно на нижнем краю.Токен
--safe-bottomпосле этого нигде не использовался и удалён;--safe-topостаётся у шапки..toastпересчитан наcalc(var(--bottombar-height) + 16px).Один побочный эффект, посмотрите на него отдельно: купол больше не выступает над верхней кромкой панели. Раньше выступ в 6.5px давали как раз те самые 8px подъёма (
--fab-height: 62.5pxпри--bottombar-height: 64px). Теперь купол вписан в панель заподлицо — на 1.5px ниже её верхней кромки. Если выступ хотелось сохранить при нулевом отступе снизу, это решается высотой купола:--fab-height: 70pxдаст выступ в 6px, но полукруг перестанет быть ровным (ширина уже не равна двум высотам) — либо--fab-width: 140pxвместе с ним, чтобы пропорция сохранилась. Скажите, если нужно, — поправлю.npm run build— зелёный.Два коммита, стенд https://forbidden-stars.ru обновлён — перезагрузите страницу.
2a4c5d9— отступ шапки в DuckDuckGo. Природа та же, что была у подвала: safe-area. Insets браузер отдаёт именно из-заviewport-fit=coverвindex.html— этот флаг передаёт управление безопасной зоной самой странице. После того как отступы убраны и сверху, и снизу, компенсировать нечего, поэтомуcoverснят: вьюпорт снова размещает браузер,env()везде даёт ноль. Следом убран--safe-topи три его использования (.top-bar,.side-menu,.toast-top-wrap) — безcoverпеременная всегда нулевая. Токенов safe-area в проекте больше не осталось.Побочный плюс: на устройствах с вырезом шапка теперь не может под него заехать — без
coverбраузер сам держит контент в безопасной зоне.09be0f5— купол 140×70. Пропорция полукруга сохранена (ширина = 2× высоты), выступ над кромкой панели вернулся — 6px при высоте подвала 64px.npm run build— зелёный.Проверьте, пожалуйста, в даке: шапка вплотную к адресной строке, и заодно что купол на узком экране не поджимает боковые слоты (при 320px на каждый остаётся по 90px).
Diff починен — обновите страницу, теперь видны все пять коммитов и четыре файла.
Что было. Gitea обновляет служебную ссылку
refs/pull/18/headпо хуку на push, но только пока PR открыт. Три пуша (81ad2a0,2a4c5d9,09be0f5) пришли, пока PR был закрыт, — они легли в саму веткуrefs/heads/issue-4-header-footer-layout, но в PR-ссылку не попали, и она осталась на первом коммите5e4e483. Веб рисует вкладку изменений именно по ней, поэтому показывал только первый коммит. Переоткрытие PR ссылку не пересчитывает — проверил, close/reopen через API её не сдвинул.Ничего не терялось: коммиты всё это время лежали в ветке на сервере.
Чем починено. Нужен был push-хук в ветку при открытом PR — единственное, что заставляет Gitea пересчитать PR-ссылку. Вместо пустышки взял накопившуюся мелочь: комментарий у
.fabописывал прежнюю геометрию («смещён вниз», база у кромки панели), хотя послеbottom: 0и140×70база стоит на нижнем краю экрана. Коммит534ba64. После пушаrefs/pull/18/headвстал на актуальный HEAD,get_filesотдаёт четыре файла сindex.html.Про «не даёт закрыть». Через API PR закрылся и переоткрылся штатно, так что состояние самого PR целое — упиралось в веб-интерфейс на битой PR-ссылке. Сейчас ссылка в порядке, кнопка должна работать; если снова упрётся — скажите, закрою через API.
На будущее: пуши в ветку задачи, пока её PR закрыт, в diff не попадут. Если нужно приостановить ревью — лучше не закрывать PR, а перевести его в черновик или просто оставить открытым.
npm run build— зелёный.