feat: fix tv dimension

This commit is contained in:
2026-10-11 14:33:16 +03:00
parent 5c69f6691d
commit a1bb3f1c9d
12 changed files with 76 additions and 14 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 149 KiB

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 123 KiB

After

Width:  |  Height:  |  Size: 76 KiB

View File

@@ -0,0 +1,23 @@
# Читаемость схемы на 960×540 — 11 октября 2026
Проблема воспроизведена на локальном стенде с публичным расписанием Автовокзала и тестовыми часами: для названия «Педагогический колледж» оставалось **9,05 px**, текст высотой **18,08 px** пересекался с временем. Фиксированная строка минут была ниже самих цифр (26 px против 36,44 px).
Изменения:
- Высота строки времени определяется её содержимым. На короткой карточке минуты имеют 26 px, название получает оставшуюся высоту без пересечения.
- При высоте содержимого карточки до 300 px пояснения объединены в подпись «После посадки · по расписанию». При высоте до 260 px повторный верхний блок маршрута скрыт; номер, направление и отправление остаются в нижней панели ближайшего рейса.
- Стрелка схемы удалена из DOM и CSS.
- Кружки увеличены: **16 → 22 px** на короткой карточке, **18 → 24 px** на широкой. Центр линии совпадает с центром маркера.
Проверка в браузере:
- **960×540, ТВ, тёмная тема, расстояние «Близко»**: весь цикл четырёх остановок. Названия — 41,70–41,91 px, высота 47,11–47,35 px при доступных 51,49 px. Зазор до времени — 6,14–6,38 px. Кружки 22 px, стрелок нет.
- **960×540, светлая тема, расстояние «Далеко»**: после окончательного порога 260 px длинное название имеет 43,41 px, две строки высотой 98,11 px при доступных 102,12 px; зазор до времени 6,01 px. Верхний повторный маршрут скрыт, нижний остаётся видимым.
- **960×480, «Далеко»**: длинное название — 33,82 px, высота 38,21 px при доступных 42,12 px.
- **879×593**: длинное название 38,46 px / 43,45 px высоты при доступных 61,30 px; зазор 19,84 px.
- **390×667**: две строки длинного названия 24,94 px / 56,34 px высоты при доступных 60,23 px; зазор 5,89 px.
- **1920×1080, три карточки**: длинное название 34,84 px / 78,72 px высоты при доступных 238,12 px; кружок 24 px.
- Пауза и автоматическая смена остановок сохранены. Ошибок/предупреждений JavaScript нет.
- `npm run build` (TypeScript + Vite) и `git diff --check` прошли. Новые модульные тесты для CSS не добавлялись; основная проверка — воспроизведение и измерение реального DOM до/после.
Снимки: `journey-960x540-before.jpg`, `journey-960x540-fixed.jpg`, `journey-960x540-light-far.jpg`. На стенде — тестовые часы и погода, реальные сохранённые названия остановок. Снимки с отсутствующими статическими фрагментами при скрытом in-app browser перезаписаны после отображения браузера; итоговые показывают полный кадр. Проверка выполнена в браузере, без физического Xiaomi Mi Box.

View File

@@ -0,0 +1,25 @@
# Схема по мотивам «Кванта» — 11 октября 2026
Изучены [описание инфосистемы](https://www.artlebedev.ru/kvant/moscow/) и предоставленное видео `kvant-door-display-new.mp4` (44 секунды). Взяты горизонтальная линия, крупное выделенное название и согласованное движение схемы влево. Цвета, шрифт, фон и карточки сохранены из RideBus Board.
«Дальше по маршруту» теперь последовательно выделяет до четырёх следующих остановок. Новое название появляется с увеличением, выделенная ячейка расширяется, соседние остаются компактными. В конце обзор возвращается к началу через появление, без обратного движения линии. Время — от посадки по расписанию. Подпись «После посадки» обозначает обзор поездки, а не реальное положение транспорта. Данные, ограничения рейса, отмены, кеш и ключ настроек `infoServiceEnabled` сохранены.
## Проверено
- `npm run build` — TypeScript и Vite успешно.
- `npm test` — **81 из 81**, без пропусков и ошибок.
- `git diff --check` — без ошибок.
- Локальный визуальный стенд на 5193 использовал сохранённое публичное расписание Автовокзала, фильтр №4 и тестовые часы/погоду. Рабочая база и сервер не изменялись.
- **879×593**: автоматическая последовательность Поликлиника → Педагогический колледж → Школа №9 → Дом торговли → Поликлиника. Сдвиг линии: −172,54 / −345,08 / −517,62 / 0 / −172,54 px до увеличения минимальной ширины соседних остановок. Возврат в начало отмечен `data-loop=true`.
- На коротком экране после удаления повторных номеров под линией выделенные названия имеют высоту 43–58 px при доступных 62 px; размер пересчитывается для каждого названия, а не наследуется от предыдущего.
- **1079×591**: названия, время пути, схема, подпись и нижний ближайший рейс помещаются без пересечений.
- **1280×800**: сохранён полный нативный снимок широкой карточки; выделенное название и нижняя панель видны целиком.
- **1920×1080**: проверены одна широкая карточка и три карточки вместе с погодой/QR. В окончательном варианте трёх карточек «Дом торговли» — 57,16 px, две строки высотой 129,17 px при доступных 229,35 px. Частично видимые будущие остановки затухают у правого края; выделенное название видно полностью.
- **390×667, светлая тема**: «Педагогический колледж» помещается целиком на двух строках: 28,03 px, высота 63,34 px при доступных 67,40 px. Часы, ближайший рейс и нижняя панель видны.
- Уличный профиль: первая остановка сохраняется после 5,1 секунды, после 6,2 секунды выделена вторая; переход линии — 300 ms. Для ТВ — 4 секунды между остановками и 900 ms движения. Пауза останавливает обзор. Для reduced motion CSS отключает движение и появление.
- При смене ближайшего рейса с 12:15 на 21:45 обзор сброшен на первую остановку; это наблюдалось на стенде после длительного продолжения сессии.
- Ошибок и предупреждений JavaScript в проверенной вкладке нет. Настройки визуального стенда возвращены: ТВ, тёмная тема, все три блока, информация 8 секунд. Основные настройки на 5180 не менялись.
При съёмке большого скрытого in-app viewport некоторые полностраничные снимки содержали только перерисованные фрагменты текста. Нативный `getScreenshot` показывает полный кадр: итоговый широкий снимок сохранён непосредственно через него, итоговые три карточки повторно сняты и просмотрены. Дефектные промежуточные снимки удалены. Это ограничение захвата, а не доказательство поведения на физическом телевизоре. Xiaomi Mi Box в этой проверке не использовался.
Итоговые снимки: `journey-kvant-wide.jpg`, `journey-kvant-three-cards.jpg`, `journey-kvant-879x593.jpg`, `journey-kvant-1079x591.jpg`, `journey-kvant-390x667.jpg`, `journey-kvant-portrait-long.jpg`. Портрет со светлой темой показывает длинное название, широкий — основные приёмы из видео.

View File

@@ -41,9 +41,9 @@ export function NextStops({ departure, journey, loading, now, paused, settings,
<span className="journey-node" aria-hidden="true" />
</li>)}
{journey.more > 0 && <li className="journey-continuation"><span className="journey-continuation-label">Ещё {journey.more} остановок</span><span className="journey-continuation-line" aria-hidden="true" /></li>}
</ol><span className="journey-direction-arrow" aria-hidden="true">›</span></div></div>
</ol></div></div>
</div>
<div className="journey-caption"><span>Время пути по расписанию</span>{pages > 1 && <span className="journey-page" aria-label={`${focus + 1} из ${count} остановок в обзоре`}>{focus + 1} / {count}</span>}</div>
<div className="journey-caption"><span className="journey-caption-full">Время пути по расписанию</span><span className="journey-caption-compact">После посадки · по расписанию</span>{pages > 1 && <span className="journey-page" aria-label={`${focus + 1} из ${count} остановок в обзоре`}>{focus + 1} / {count}</span>}</div>
</>}
</>}
</>;

View File

@@ -118,9 +118,9 @@ h1 { font-size: clamp(30px, 3.8vw, 76px); letter-spacing: -.05em; line-height: 1
.journey-rail-window { width: 100%; height: min(100%, 320px); position: relative; overflow: hidden; }
.journey-rail-window::after { content: ''; position: absolute; inset: 0 0 0 auto; width: 36px; background: linear-gradient(90deg, transparent, var(--background)); pointer-events: none; }
.journey-rail { display: flex; height: 100%; margin: 0; padding: 0 0 0 24px; list-style: none; transform: translateX(calc(-1 * var(--journey-focus) * var(--station-width))); transition: transform 900ms cubic-bezier(.22,.72,.18,1); }
.journey-station { flex: 0 0 var(--station-width); min-width: 0; position: relative; display: grid; grid-template-rows: 34px minmax(0, 1fr) 36px; gap: 10px; padding-right: 28px; transition: flex-basis 900ms cubic-bezier(.22,.72,.18,1); }
.journey-station { flex: 0 0 var(--station-width); min-width: 0; position: relative; display: grid; grid-template-rows: auto minmax(0, 1fr) 40px; gap: 10px; padding-right: 28px; transition: flex-basis 900ms cubic-bezier(.22,.72,.18,1); }
.journey-station[data-focused=true] { flex-basis: var(--focused-width); }
.journey-station::after { content: ''; position: absolute; bottom: 15.5px; left: 0; width: 100%; height: 5px; background: var(--accent); transition: opacity 700ms ease; }
.journey-station::after { content: ''; position: absolute; bottom: 17.5px; left: 0; width: 100%; height: 5px; background: var(--accent); transition: opacity 700ms ease; }
.journey-station:last-child::after { width: calc(var(--station-width) - 36px); }
.journey-metro[data-more=false] .journey-station:last-child::after { display: none; }
.journey-station[data-before=true]::after { opacity: .25; }
@@ -130,18 +130,18 @@ h1 { font-size: clamp(30px, 3.8vw, 76px); letter-spacing: -.05em; line-height: 1
.service-info .journey-station[data-focused=true] .destination-heading { font-size: calc(clamp(48px, 11cqw, 96px) * var(--reading-scale, 1)); font-weight: 650; letter-spacing: -.055em; color: var(--foreground); }
.journey-station[data-focused=true] .destination-current { transform-origin: left bottom; animation: journey-name-in 900ms cubic-bezier(.22,.72,.18,1) both; }
@keyframes journey-name-in { from { opacity: 0; transform: translateX(20px) scale(.84); } to { opacity: 1; transform: translateX(0) scale(1); } }
.journey-duration { align-self: start; display: flex; align-items: baseline; gap: 8px; color: var(--muted); font-size: clamp(18px, 3.4cqw, 26px); font-weight: 550; white-space: nowrap; transition: color 600ms ease, opacity 500ms ease; }
.journey-duration { align-self: start; display: flex; align-items: baseline; gap: 8px; color: var(--muted); font-size: clamp(18px, 3.4cqw, 26px); font-weight: 550; line-height: 1; white-space: nowrap; transition: color 600ms ease, opacity 500ms ease; }
.journey-duration > span { display: inline-flex; align-items: baseline; gap: 5px; }
.journey-duration small { font-size: .72em; font-weight: 500; }
.journey-station[data-focused=true] .journey-duration { color: var(--accent); font-size: clamp(24px, 4.5cqw, 36px); }
.journey-node { align-self: center; display: block; width: 18px; height: 18px; border: 4px solid var(--accent); border-radius: 50%; background: var(--background); margin-left: -9px; z-index: 1; transition: background 600ms ease, box-shadow 600ms ease; }
.journey-node { align-self: center; display: block; width: 24px; height: 24px; border: 4px solid var(--accent); border-radius: 50%; background: var(--background); margin-left: -12px; z-index: 1; transition: background 600ms ease, box-shadow 600ms ease; }
.journey-station[data-focused=true] .journey-node { background: var(--accent); box-shadow: 0 0 0 6px color-mix(in srgb, var(--accent) 18%, transparent); }
.journey-station[data-before=true] .journey-node { border-color: var(--outline); }
.journey-continuation { flex: 0 0 var(--station-width); min-width: 0; display: flex; align-items: center; position: relative; padding: 34px 20px 36px 0; }
.journey-continuation { flex: 0 0 var(--station-width); min-width: 0; display: flex; align-items: center; position: relative; padding: 34px 20px 40px 0; }
.journey-continuation-label { color: var(--muted); font-size: clamp(20px, 4cqw, 28px); line-height: 1.2; max-width: 180px; }
.journey-continuation-line { position: absolute; bottom: 15.5px; left: 0; right: 24px; border-top: 5px dashed var(--accent); opacity: .5; }
.journey-direction-arrow { position: absolute; left: 1px; bottom: 1px; color: var(--accent); font-size: 42px; line-height: 36px; font-weight: 700; }
.journey-continuation-line { position: absolute; bottom: 17.5px; left: 0; right: 24px; border-top: 5px dashed var(--accent); opacity: .5; }
.journey-caption { display: flex; flex-wrap: wrap; gap: 4px 14px; justify-content: space-between; color: var(--muted); font-size: 16px; line-height: 1.3; padding-top: 16px; }
.journey-caption-compact { display: none; }
.journey-page { color: var(--accent); margin-left: auto; }
.journey-metro[data-calm=true] .journey-rail { transition: transform 300ms ease-out; }
.journey-metro[data-calm=true] .journey-station { transition: flex-basis 300ms ease-out; }
@@ -166,14 +166,28 @@ h1 { font-size: clamp(30px, 3.8vw, 76px); letter-spacing: -.05em; line-height: 1
.journey-trip time { font-size: 24px; }
.journey-metro { padding-top: 12px; }
.journey-map-space { margin-top: 10px; }
.journey-station { grid-template-rows: 26px minmax(0, 1fr) 28px; gap: 6px; }
.journey-station { grid-template-rows: auto minmax(0, 1fr) 32px; gap: 6px; }
.journey-station-name { padding-bottom: 4px; }
.journey-station::after { bottom: 12px; height: 4px; }
.journey-direction-arrow { bottom: 0; font-size: 36px; line-height: 28px; }
.journey-node { width: 16px; height: 16px; margin-left: -8px; border-width: 3px; }
.journey-continuation-line { bottom: 12px; border-top-width: 4px; }
.journey-duration { font-size: 22px; }
.journey-station[data-focused=true] .journey-duration { font-size: 26px; }
.journey-station::after { bottom: 14px; height: 4px; }
.journey-node { width: 22px; height: 22px; margin-left: -11px; border-width: 3px; }
.journey-continuation { padding-top: 26px; padding-bottom: 32px; }
.journey-continuation-line { bottom: 14px; border-top-width: 4px; }
.journey-caption { font-size: 14px; padding-top: 10px; }
}
/* Keep the station name readable when the header and next trip use most of the height. */
@container journey-card (max-height: 300px) {
.journey-tour-heading { display: none; }
.journey-metro { padding-top: 10px; }
.journey-map-space { margin-top: 0; }
.journey-caption-full { display: none; }
.journey-caption-compact { display: inline; }
}
@container journey-card (max-height: 260px) {
/* The same route and departure time remain visible in the persistent next-trip bar. */
.journey-route { display: none; }
}
@media (prefers-reduced-motion: reduce) {
.journey-rail, .service-info .journey-station .destination-heading, .journey-station, .journey-station-name, .journey-duration, .journey-station::after, .journey-node { transition: none; }
.journey-metro .destination-current, .journey-metro[data-loop=true] .journey-rail { animation: none; }