Скриншоты
61 кадров, снятых Playwright. Пересобираются одной командой, поэтому не устаревают вместе с интерфейсом.
bash
npm run shots:setup # один раз: playwright + chromium
npm run docs:shots # все кадры
npm run docs:shots -- home # только те, чьё имя начинается с «home»
npm run docs:shots -- --no-build # не пересобирать приложениеРезультат — docs/public/shots/*.png, ссылки в markdown вида /shots/имя.png.
Как это устроено
Приложение собирается и поднимается через vite preview, а не dev-сервером: в dev висит клиент HMR и оверлей ошибок, а при ?mock=1 ещё и водопад из сотни запросов модулей. Снимать надо ровно то, что уедет на прод.
| Настройка | Значение | Зачем |
|---|---|---|
| Вьюпорт | 390 × 844, DPR 2 | Размер, под который сделан макет. Файлы выходят 780 × 1688 |
| Часовой пояс | Europe/Moscow | dayKey() считает локальную дату; без фиксации кадры уехали бы на день |
| Время | 31 июля 2026, 14:20 | Пятница. Совпадает с сидом демо-генератора, а пятница даёт в месячном окне достаточно и будних, и выходных дней |
| Анимации | reducedMotion: 'reduce' | Приложение само схлопывает переходы в @media (prefers-reduced-motion) |
| Готовность страницы | класс app--loading снят | Не networkidle: приложение вообще не ходит в сеть |
Замораживается только Date.now(), но не таймеры: на них висит планировщик React и отсчёт кнопки «Пропустить» в вопросе о разряде.
Каждый кадр получает свежий контекст браузера. Это дороже общей страницы примерно на 100 мс, но снимает целый класс проблем: в прогоне без мока приложение пишет в localStorage, и состояние предыдущего кадра протекало бы в следующий.
Файл меняется, только если изменились байты
Рендер Chromium детерминирован, поэтому неизменившийся экран даёт тот же PNG. Скрипт сравнивает байты и печатает «Изменилось 3 из 61» — в git status попадает только то, что действительно изменилось.
Проверка на флак: два прогона подряд должны дать «Изменилось 0».
Снимайте всегда с одной машины
Веб-шрифт нигде не подключается: приложение просит Inter Tight и падает на системный. На другой машине набор шрифтов другой, и диффы пойдут по всей типографике, а не по тому, что вы меняли.
Селекторы
Локаторы — только по ролям и подписям из ru.ts, плюс рукописные CSS-классы там, где роли не хватает (.prow, .ach__card, .dbars__col).
Атрибутов вроде data-testid в приложении нет и не будет. Такой атрибут — второй, невидимый контракт, который ничто не валидирует: он переживёт переезд элемента в другой блок, и кадр станет правильным по селектору и неправильным по смыслу. Ролевой локатор, наоборот, отваливается ровно тогда, когда пропал aria-label, — то есть сообщает о настоящем баге доступности.
Четыре прогона
Заголовки прогонов стоят статически. Заголовок внутри v-for получил бы якорь из шаблонной подстановки, а не из подставленного текста, — разделы делили бы один и тот же якорь, и в оглавлении справа подряд стояла бы сама подстановка.
Демо-данные
Адрес: /?mock=1. Профиль «Артём»: заполненные экраны, шторки, состояния предела.
| Имя | Что на кадре |
|---|---|
home-today | Вкладка «Приоритеты», период «Сегодня», отметки есть |
home-week | Период «7 дней»: виден лидер и его доля |
home-dots · кроп | Кроп строки: точки отмеченных блоков рядом с «+» |
home-tune-sheet | Шторка счётчика, блоков больше нуля |
home-tune-zero | Та же шторка на нуле: «−» заблокирована |
home-battery-sheet | Выбор заряда из шапки главного экрана |
home-daypicker | Лента четырнадцати дней, точки на заполненных |
home-past-warning | Выбран прошлый день: предупреждение и кнопка «К сегодня» |
edit-list · длинный | Список с ручками перетаскивания и счётчиком в шапке |
edit-form | Шторка «Приоритет»: название и палитра |
edit-add | Шторка «Новый приоритет» — палитры нет, цвет назначается сам |
edit-limit · длинный | Кнопка добралась до предела: «Максимум 10 приоритетов» |
edit-archive · длинный | Архив после удаления приоритета: время не пропало |
charge-hero · кроп | Герой экрана «Заряд»: батарея и сколько вы сегодня в этом состоянии |
charge-list · длинный | Четыре состояния со временем за сегодня и разбивкой дня |
charge-drain-sheet | Вопрос «Что съело всю энергию?»: «Пропустить» ещё отсчитывает |
charge-drain-ready | Та же шторка через три секунды: «Пропустить» разблокирована |
charge-wallpaper-sheet | Генератор обоев: превью, выбор уровня и размера экрана |
skills-list · длинный | Три навыка на разных ступенях лестницы и прибавка за 30 дней |
skills-past | Навыки в режиме прошлого дня: лента, предупреждение и точки у «+» |
skills-sheet | Шторка навыка: ступень, часы, темп за 30 дней и сколько до следующей |
skills-ladder | Лестница развёрнута целиком: все семнадцать ступеней |
skills-link | Связь с приоритетом: список целей и текущая привязка |
skills-add | Новый навык: список или своё название, цвет, уже накопленные часы |
stats-week | Плитки итогов, наблюдения и «Куда уходит время» за неделю |
stats-insights · кроп | Блок «Что видно»: до трёх наблюдений из собственных отметок |
stats-daybars | «По дням»: высота столбца — объём, цвета внутри — из чего сложился |
stats-day-open | День разобран по приоритетам после нажатия на столбик |
stats-charge | Полоса состояний и список уровней со временем и долями |
stats-energy | «Как менялась энергия»: линия среднего заряда и пунктир периода |
stats-drains | «Что сажает батарею»: ответы на вопрос при переходе «на нуле» |
stats-month · длинный | Тот же экран за 30 дней: окно шире, картина спокойнее |
settings-modules | Три тумблера модулей и строка достижений со счётчиком |
settings-block-price | «Цена одного клика»: шесть значений и что даст пересчёт |
settings-data-local | Блок «Данные» вне Telegram: предупреждение о синхронизации |
settings-reset | Две опасные кнопки и приписка о том, что сброс задевает облако |
settings-demo-row | Строка «Демо-аккаунты» в настройках: показать приложение, не показывая себя |
demo-list · длинный | Витрина демо-профилей: пять чужих жизней на выбор |
presets-list · длинный | Экран «Наборы» из настроек: у текущего набора стоит бейдж |
presets-preview | Предпросмотр набора: приоритеты в том порядке, в каком встанут |
achievements-all | Сетка достижений с фильтрами и счётчиком открытых |
achievements-sheet-auto | Карточка автоматического достижения: условие и дата |
achievements-sheet-manual | Достижение из группы «Жизнь»: его отмечают руками |
Пустое приложение
Адрес: /, чистый localStorage. Онбординг и все пустые состояния: в демо отметка «пройден» уже стоит, и туда не попасть.
| Имя | Что на кадре |
|---|---|
onboarding-1 | Первый слайд: перекос, который приложение и показывает |
onboarding-2 | Второй слайд: один клик — полчаса жизни |
onboarding-3 | Третий слайд: заряд — про вас, а не про телефон |
onboarding-presets · длинный | Последний шаг онбординга — выбор стартового набора |
home-empty | Приоритеты есть, отметок ещё нет: объяснение цены клика |
home-battery-unset · кроп | Кроп шапки: заряд ещё не отмечен |
charge-unset | Заряд ни разу не отмечался: батарея приглушена |
skills-empty | Ни одного навыка: что такое навык и зачем вписывать накопленное |
stats-empty · длинный | За период ничего не отмечено — и заряд тоже |
achievements-empty | Фильтр «Открытые» на пустой истории |
Клиент Telegram
Адрес: /?mock=1 плюс стаб SDK. Три вида, недостижимые в голом браузере.
| Имя | Что на кадре |
|---|---|
settings-data-cloud | Тот же блок «Данные» в настоящем клиенте: «Аккаунт Telegram» |
settings-home-screen | Кнопка ярлыка на главный экран — её видно только в клиенте 8.0+ |
charge-wallpaper-manual | Клиент не даёт сохранять файлы: картинку забирают долгим нажатием |
Гостевой режим
Адрес: /?demo=max. То, что видит друг: плашка выхода и настройки без всего, что трогает чужой аккаунт. ?mock= этого не включает — иначе снять обычные настройки было бы нечем.
| Имя | Что на кадре |
|---|---|
demo-bar · кроп | Плашка гостевого режима: чьи данные на экране и как вернуться к своим |
demo-max-home | Профиль «Максимум»: десять приоритетов и тринадцать месяцев за спиной |
demo-settings | Настройки в гостевом режиме: ни входа, ни копии, ни сброса |
Что снять нельзя
Всё, что рисует сам клиент Telegram, в браузер не попадает: системные подтверждения, кнопка «назад», тактильная отдача, диалог добавления ярлыка, меню «Поделиться», настоящая синхронизация между устройствами.
Это документируется текстом на странице «Только в Telegram» и проверяется руками по чек-листу.
Как добавить кадр
- Допишите запись в
tools/shots/scenarios.mjs:name,noteиsetup. npm run docs:shots -- имя-кадра.- Вставьте
в нужную страницу. npm test— тест связности проверит, что кадр существует и на него ссылаются.
Порядок важен: страница пишется по готовому кадру, а не наоборот.
Кадры на лендинге
Часть кадров дублируется в landing/public/shots/ — лендинг деплоится отдельным проектом Vercel и файлы вне своего каталога в сборку не получает. Список дубликатов — tools/landing/shots.mjs, обновляются они npm run landing:sync, совпадение сторожит tools/landing.test.ts.
Брать туда можно только полноэкранные кадры 780×1688: кропы отдельных блоков (charge-hero, stats-insights, home-dots) в рамке телефона на лендинге выглядят поломанной вёрсткой. И только те, что уже описаны в документации, — иначе tools/docs.test.ts уронит тесты на «мёртвом кадре».
Пересняли кадр из списка — выполните npm run landing:sync, иначе тест покажет расхождение.
Иконки приложения
Тем же Chromium, но другим скриптом: npm run brand собирает tools/shots/brand.mjs → иконки PWA, фавиконы и превью ссылки для лендинга. Рисуется всё из геометрии батареи (src/components/batteryGeometry.ts), поэтому иконка на домашнем экране не может разойтись с иконкой в шапке. Подробности — в «Сборке и публикации».
Рядом
Демо-режим · Про эту документацию · Строки интерфейса · Чек-лист проверки