Усі записи

Вісь, яка перемикалася сама на себе

Коло 900px горизонтальна вісь інколи вмикала й вимикала сама себе, без кінця: власна прокрутка рамки забирала кілька пікселів у тієї самої ширини, за якою вісь вирішує, горизонтальна вона чи ні. Знайдено зі скріна «6 в 1» на ширині 902–905px.

Що показав скрін

«6 в 1» надіслав скрін часової лінії на ширині десь 902–905px: підписи декад і кружечки міток лежали одне на одному, ніби вісь намальована двічі — раз горизонтально, раз вертикально — в тих самих пікселях.

Що це виявилося не тим

Перша здогадка — зіткнення з view transition (document.startViewTransition, яким рухається зум) під живе перетягування вікна мишею: старий кадр застигає знімком, поки вікно вже перемалювалось під нову ширину. «6 в 1» підтвердив, що тягнув вікно саме тоді — але й сказав, що звичайний reload на тій самій ширині ламав вісь так само, без жодного перетягування чи зуму. View transition тут ні до чого: на завантаженні сторінки він не запускається.

Друга здогадка — подвійний рендер компонента (два <main class="density-app"> чи два .density-marks, скажімо, від застарілого чанка з кешу service worker’а). Перевірено в консолі просто на живому проді (document.querySelectorAll('.density-app').length і .density-marks): обидва — 1. DOM не дублюється.

Що виявилося тим

.density-frame (рамка, куди вписана вісь) — overflow: auto. Її власна виміряна ширина (ResizeObserver, size.width у Timeline.tsx) вирішує, чи вісь горизонтальна (>= 900). Але на ширині, де горизонтальний малюнок потребує трохи більше висоти, ніж дає рамка (вузькі колонки на 903px загортають підписи декад в один рядок більше), у рамки з’являється власна вертикальна прокрутка — і саме вона забирає кілька пікселів від наступного виміру ширини.

Виміряно наживо на проді (консоль, document.querySelector('.density-frame')): innerWidth 903, clientWidth рамки — 894. Дев’ять пікселів, які забирає тонка прокрутка (scrollbar-width: thin у CSS). 894 менше за 900 — вісь мала б переключитись на вертикальну. Вертикальному малюнку менше висоти не треба — прокрутка зникає — ширина повертається до 903 — вісь знову горизонтальна — знову потрібна прокрутка — знову 894. Цикл, який сам себе годує, без жодної паузи, щоб на екрані щось встигло усталитись: .mark-dot має transition: transform 0.12s, і на кожному витку мітки їдуть у зовсім інші координати (горизонтальна й вертикальна формули міняють x та y місцями), тому в проміжку видно суміш обох прочитань одразу.

Виправлення

Одне число, яке сам перемикач може перетнути в обидва боки, — це і є петля. Замість одного порогу 900px — два, залежно від того, на якому боці вісь уже стояла: 900, щоб УВІЙТИ в горизонтальний режим із вертикального, і 880, щоб ВИЙТИ з нього назад. Дев’ять пікселів прокрутки більше не перетинають нижчий поріг, поки вісь уже горизонтальна, — і цикл не запускається.

src/ui/Timeline.tsx: wasHorizontal — реф, який запам’ятовує попереднє прочитання й оновлюється в useLayoutEffect після кожного коміту; horizontal читає його, а не голий size.width >= 900.

Що перевірено

Змінено лише один файл, тож npm run dev у спільному робочому каталозі значення не мало — перевірка йшла в окремому worktree в тимчасовій теці сесії (node_modules — junction на спільний, щоб не чекати npm ci; перша спроба через astro dev впала на власному ж лок-файлі Astro й на @fs/-імпорті крізь junction, тому перевірено на зібраному npm run build + npm run preview, з окремим ATLAS_PREVIEW_PORT).

На 903px живий вимір: clientWidth рамки й далі 894, вісь і далі horizontal — десять прочитань поспіль з паузою по 200мс, жодного перемикання. Скрін чистий, підписи й мітки на своїх місцях. npm run verify:code пройшов повністю (формат, лінт, типи, тести, дані, публічний контракт, бюджети — без змін у бюджетах, зміна суто в JS). Повний atlas.spec.ts у chromium, 99 тестів, включно з усіма перевірками ширини панелі — без регресій.

Запис зроблено 29 вересня 2026 р.

Коміти, про які цей запис

  • 7de44db