24 KiB
План фронта — как пишется интерфейс TextMachine
Этап S0 сессии фронта: план до первой строки кода (
FRONTEND_SESSION_PROMPT.md§7.2). Ландится отдельным коммитом. Правят следующие фронт-сессии; при конфликте сSTACK_DECISIONS.mdпобеждает STACK_DECISIONS (там ратифицированы пины), при конфликте с промтом сессии — промт (там ратифицирован продукт).
0. Границы
Эта сессия делает S0 (план) и S1 (инструменты, скриншот-цикл, tokens.css, витрина) и
останавливается на витрине. Экраны — S2–S7, по одной сессии на этап (BACKLOG.md Ф-1).
Зона записи — только frontend/. backend/, platform/, docs/, eval/ — read-only.
1. Пины
Все версии сверены с npm-регистри 02.08.2026 командой npm view <pkg> version.
Ставятся точными версиями, без ^ и ~. Колонка «дата» — дата публикации именно этой версии.
Строка «= latest» означает: на 02.08.2026 эта версия и есть последняя опубликованная. Единственное намеренное отставание от latest — TypeScript (см. сноску).
Рантайм
| Пакет | Пин | Дата | Зачем именно нам |
|---|---|---|---|
react |
19.2.8 |
2026-07-21 | ядро; = latest, React 20 не существует |
react-dom |
19.2.8 |
2026-07-21 | обязан совпадать с react версия-в-версию |
react-router |
8.3.0 |
2026-07-22 | библиотечный (data) режим: маршрут на экран — несущая правила «каждый экран открывается в изоляции» (§3.10). Требует Node ≥ 22.22 |
@tanstack/react-query |
5.101.4 |
2026-07-21 | всё серверное состояние: прогон, книги, банк. Мажор 5 актуален, «v6» на npm — svelte-адаптер |
zustand |
5.0.14 |
2026-05-28 | состояние интерфейса (свёрнутость панелей, активные вкладки). Только с селектором или useShallow |
react-aria-components |
1.20.0 |
2026-07-31 | ЕДИНСТВЕННАЯ библиотека примитивов: Table+Virtualizer (банк), Tree (главы), Tabs/Menu/Dialog/Popover/Tooltip (оболочка), I18nProvider (русские служебные строки). Стилей не навязывает. Импорт разрешён только внутри src/ui/ |
react-resizable-panels |
4.12.2 |
2026-07-12 | три панели Fleet. API v4: Group/Panel/Separator — старые имена PanelGroup/PanelResizeHandle мертвы |
lucide-react |
1.28.0 |
2026-07-30 | тонкие линейные монохромные иконки как во Fleet: size 16, strokeWidth 1.5, без absoluteStrokeWidth |
@fontsource-variable/inter |
5.3.0 |
2026-07-19 | интерфейсный гротеск; локальный woff2, а не CDN — вид не зависит от сети и одинаков в скриншот-цикле |
@fontsource-variable/jetbrains-mono |
5.3.0 |
2026-07-19 | моноширинный для содержимого |
CJK-шрифт не ставим: иероглифы отдаём системному стеку, lang на элементе приходит из данных
языковой пары (STACK_DECISIONS.md §2).
Сборка и типы
| Пакет | Пин | Дата | Зачем именно нам |
|---|---|---|---|
vite |
8.2.0 |
2026-07-30 | внутри Rolldown вместо Rollup+esbuild; = latest |
@vitejs/plugin-react |
6.0.5 |
2026-07-30 | без Babel; старый рецепт подключения React Compiler в нём удалён |
typescript |
6.0.3 |
2026-04-16 | намеренно не latest (latest = 7.0.2): у TS 7 нет программного API до 7.1, значит нет typescript-eslint и плагинов языкового сервера. Пересмотр — BACKLOG.md Ф-3 |
@types/react |
19.2.18 |
2026-07-30 | |
@types/react-dom |
19.2.4 |
2026-07-30 | |
@types/node |
22.20.1 |
мажор типов держим на мажоре рантайма (Node 22), а не на latest 26 — иначе тайпчек разрешит API, которых на стенде нет | |
lightningcss |
1.33.0 |
2026-07-20 | минификация CSS в бою; в dev не участвует |
Контроль качества
| Пакет | Пин | Дата | Зачем именно нам |
|---|---|---|---|
eslint |
10.8.0 |
2026-07-24 | ради no-restricted-syntax — на нём стоит половина гейта «одно место для цвета»; в oxlint такого правила нет вовсе |
@eslint/js |
10.0.1 |
2026-02-06 | базовый набор для flat-config |
typescript-eslint |
8.65.0 |
2026-07-20 | единый пакет (парсер+плагин+config) |
eslint-plugin-react-hooks |
7.1.1 |
2026-04-17 | держит совместимость с React Compiler, пока сам компилятор выключен (BACKLOG.md Ф-2) |
eslint-plugin-react-refresh |
0.5.3 |
ловит экспорты, ломающие HMR | |
eslint-config-prettier |
10.1.8 |
2025-07-18 | гасит форматные правила ESLint, чтобы формат был ровно один |
stylelint |
17.14.1 |
2026-07-20 | вторая половина гейта: declaration-property-value-allowed-list |
stylelint-config-standard |
40.0.0 |
2026-01-15 | база |
prettier |
3.9.6 |
2026-07-21 | точный пин: разные патчи форматируют по-разному и создают шум в диффах |
vitest |
4.1.10 |
2026-07-06 | тесты на том же конфиге Vite |
happy-dom |
20.11.1 |
2026-07-22 | DOM для контракт-теста токенов: в отличие от jsdom считает каскад CSS-переменных |
@testing-library/react |
16.3.2 |
2026-01-19 | рендер компонентов в тестах |
@playwright/test |
1.62.1 |
2026-07-30 | скриншот-цикл и сверка вычисленных токенов в настоящем Chromium |
msw |
2.15.0 |
2026-07-08 | моки сети; пин ставим сейчас, включает S3 |
globals |
17.8.0 |
2026-07-26 | наборы глобалов для flat-config |
Стенд
node -v = 22.22.3 — проходит нижнюю границу react-router (engines.node: >=22.22.0),
она самая жёсткая из всех. Проверять первым действием на любой новой машине.
Пакетный менеджер — npm из поставки Node, один package.json в frontend/, воркспейсов нет.
2. Карта src/
| Папка | Что внутри | Что НЕ внутри |
|---|---|---|
tokens/ |
tokens.css — единственный источник цвета, размера, радиуса, шрифта. reset.css |
ничего кроме двух глобальных файлов |
ui/ |
глупые примитивы на токенах: кнопка, поле, вкладки, строка дерева, таблица, выноска. Единственное место, где разрешён импорт react-aria-components |
запросы, знание о доменных сущностях |
shell/ |
оболочка: три панели, верхняя полоса, статус-полоса, вкладки | доменная логика экранов |
features/ |
books, chapters, bank, reader, settings — по экрану на папку; запрос живёт здесь, на уровне экрана |
цвета и размеры мимо токенов |
api/ |
единственный вход к данным: типы контракта + функции. Сейчас внутри моки, потом HTTP | React-компоненты |
mock/ |
фикстуры: книга, главы, пары оригинал/перевод, банк, состояния прогона | логика; фикстуры — данные |
showcase/ |
витрина примитивов: маршрут /showcase, живой каталог для сверки с references/fleet.png |
продуктовые экраны |
Правило зависимостей — сверху вниз, без обратных рёбер:
features/ → ui/ + api/; shell/ → ui/; ui/ → tokens/. api/ не знает про React.
3. Правила поддерживаемости — в проверяемой форме
Из FRONTEND_SESSION_PROMPT.md §5.1. Слева — правило, справа — чем оно проверяется.
Правило без проверки — лозунг, поэтому у каждого либо машинный гейт, либо ревью-вопрос
с однозначным ответом.
| # | Правило | Проверка |
|---|---|---|
| 1 | Цвет и размер только из tokens.css |
машинно: stylelint declaration-property-value-allowed-list (/^var\(--/ на color, background-color, border-color, fill, stroke, font-size, z-index) + ESLint no-restricted-syntax на #hex/rgb(/hsl(/oklch( в TSX. Исключение по пути — только tokens/ |
| 2 | Отключить гейт комментарием нельзя | машинно: reportDisables: true в конфиге stylelint — /* stylelint-disable */ сам становится ошибкой |
| 3 | Данные только через src/api/ |
ревью-вопрос: есть ли fetch/axios/импорт из mock/ вне src/api/? Должно быть «нет» |
| 4 | Файл = один компонент + свой .module.css, больше ~150 строк — делить |
ревью глазами при лендинге пакета |
| 5 | Состояние ровно в двух местах: TanStack Query (серверное), Zustand (интерфейсное) | ревью-вопрос: есть ли useState с копией серверных данных? Должно быть «нет» |
| 6 | Глобальных стилей два файла | машинно: любой .css вне src/tokens/, не являющийся *.module.css, — ошибка сборки правилом ESLint на импорт |
| 7 | Никаких UI-китов | ревью-вопрос: в package.json кроме react-aria-components библиотек компонентов нет |
| 8 | Компоненты глупые: данные пропсами, запросы на уровне экрана | ревью-вопрос: есть ли useQuery внутри src/ui/? Должно быть «нет» |
| 9 | Комментарии — одна-две строки «почему» | ревью глазами; проектная норма |
| 10 | Каждый экран открывается в изоляции: свой маршрут, своя фикстура | машинно косвенно: скриншот-скрипт снимает экран по URL. Не открывается по прямой ссылке — не снимется |
Контрольный вопрос владельца (§5.1, применять к каждому пакету работ):
добавление нового состояния главы или нового вида замечания правит один файл.
Правит три — структура неверна, переделать до движения дальше.
Ответ этой структуры: состояние главы живёт как строка типа в src/api/types.ts и
как одна запись в карте отображения src/features/chapters/. Цвет индикатора — токен.
Новый вид замечания — запись в карте видов, компонент выноски не трогается.
4. Протокол скриншот-цикла
Цель — не «код валиден», а «вид совпал». Смотреть на построенное обязательно, а не предполагать.
Команда: npm run shot [маршрут ...]. Без аргументов снимает все известные маршруты.
Что делает:
- поднимает
vite previewна фиксированном порту (сборка, а не dev — dev-оверлеи не должны попадать в кадр); - запускает Chromium из Playwright, вьюпорт 1440×900 (базовая ширина из §4.5 промта),
deviceScaleFactor: 2— чтобы снимок был сравним с референсом, снятым на macOS при 2x; - ждёт
networkidleи загрузки шрифтов (document.fonts.ready) — иначе в кадр попадает фолбэк-шрифт; - кладёт PNG в
frontend/.shots/<маршрут>.png.
Что делаю я после команды — обязательная часть цикла, а не опция: открываю PNG инструментом
чтения файлов, смотрю на него, кладу рядом references/fleet.png, называю расхождения словами,
правлю, снимаю снова.
.tooling/ (системные библиотеки Chromium, ставятся без sudo) и .shots/ — в .gitignore.
Бинарники и снимки в репозиторий не едут.
5. Критерий сходимости с референсом
Растровое совпадение не является целью и недостижимо: Fleet закрыт (скачивание прекращено
22.12.2025), референс снят на macOS при 2x, цель — Chromium с другим хинтингом шрифтов.
Эталонных скриншотов в тестах нет — вместо них контракт-тест токенов
(STACK_DECISIONS.md §3).
Сходиться обязаны: цвета · промежутки 8px · радиусы 6px · плотность строк · общее впечатление. Подбираются на глаз: кегли, интерлиньяж, внутренние отступы — померить их больше негде.
5.1. Замеры, перепроверенные в этой сессии
Значения §1.1 промта перепроверены заново по references/fleet.png (PIL: гистограмма всего
изображения, срезы строк и столбцов, детект края скругления по порогу яркости). Все совпали.
Ниже — сводка с тем, что удалось доснять; это и есть входные числа tokens.css.
Масштаб снимка 2x подтверждён независимо: все четыре промежутка между панелями равны ровно 16 физическим пикселям, что даёт целые 8 CSS-пикселей.
Поверхности (доля площади — по гистограмме всего кадра 2560×1528):
| Роль | Значение | Подтверждение |
|---|---|---|
| фон оболочки | #090909 |
10.45% площади |
| заливка панели | #17191a |
80.48% площади |
| приподнятая поверхность / наведение | #27292b |
18 196 px |
| выбранная строка, активная вкладка | #353739 |
37 374 px |
| подсветка текущей строки | #152945 |
36 800 px |
| выделение | #164e8d |
476 px |
| текст основной | #dfe1e3 |
0.40% площади |
| текст вторичный | #8a8e91 |
доснято: статус-полоса |
| текст приглушённый | #707479 |
доснято: колонка номеров строк |
| акцент | #746deb |
1 258 px, рамка поля ввода |
| красный индикатора | #b82e45 |
829 px |
Геометрия (физические пиксели / 2):
| Величина | CSS | Как замерено |
|---|---|---|
| промежуток между панелями | 8 | четыре среза по 16 физ. без исключений |
| поле оболочки по краям | 8 | тот же срез: слева и справа те же 16 физ. |
| радиус панели | 6 | угол выходит на прямую за 12 физ. |
| верхняя полоса | 28 | верхняя полоса фона 72 физ. = 8 поля + 28 полосы |
| статус-полоса | 20 | нижняя полоса фона 56 физ. = 20 полосы + 8 поля |
| шапка панели (вкладки) | 26 | заливка вкладки Files 86..137 физ. |
| шаг строки дерева | 26 | шаг текстовых полос: 52 физ., 15 строк подряд |
| подложка выбранной строки | 24 | заливка #353739 — 48 физ. при шаге 52 |
| межстрочный интервал содержимого | 21 | подсветка текущей строки 42 физ. |
| ширина боковой панели при 1280 | 320 (25%) | границы панелей: 8‥328 · 336‥944 · 952‥1272 |
| внутренний отступ панели | 6 | подложка строки 14‥321.5 при панели 8‥328 |
Модель раскладки, вытекающая из замеров и сходящаяся точно (764 = 8+28 + 700 + 20+8): поле оболочки 8 · промежуток 8 · верхняя полоса 28 · статус-полоса 20.
Типографика (оценка по высоте глифов, точнее не измеряется): интерфейс ~13px (высота выносных элементов 10px), статус-полоса ~12px, содержимое ~13px моноширинным при интерлиньяже 21px.
Из antigravity_chat.png — модель выноски замечания (§3.8 промта):
левая граница 3px, синяя #2964ad (заметка) / зелёная #3c7d4f (подсказка),
заливки текста нет. Палитра Antigravity (нейтральный серый, без холодного оттенка):
#101010 фон · #161616 боковая панель · #1c1c1c приподнятая карточка ·
#252525 кнопка основного действия · #323232 разделитель · #cecece текст.
⚠ Две палитры в одном приложении не смешиваются. Базовая — Fleet; из Antigravity берётся
форма пустого состояния и выноски, не цвета. Токенов Antigravity в tokens.css не заводим.
6. Что уже известно про движок — учтено в форме данных
Из FRONTEND_SESSION_PROMPT.md §9 и STACK_DECISIONS.md §8. Влияет на типы в src/api/
уже сейчас, поэтому записано в плане, а не откладывается на S3.
- Прогресс — пофазный, не «готово N из M». Юнит становится
doneтолько когда есть и черновые строки всех членов, и строка редактуры: во время черновой волны сквозной счётчик стоит на нуле почти всё время. Тип прогресса — две пары{ done, total }(черновик ∥ редактура), а не одно число. Пользователю всё равно показывается одна полоса без названий фаз (§4.1). - Подпись термина — не UPDATE строки. Банк пересобирается из файлов на каждом прогоне, прямая запись стирается следующим прогоном. Контракт подписи — отправка набора решений, а не правка записи; фикстуры S3 моделируют именно это.
- Байтовых смещений у замечаний нет. Замечание адресует блок или главу — и только.
Тип замечания не имеет полей
offset/length; подчёркиваний внутри текста не будет. - Единица пары — edit-unit, ~1.9 на главу. Местами вся глава окажется одним блоком. Читалка обязана нормально выглядеть в этом случае; выравнивание грубое, по единицам экспорта.
- Одна вертикальная прокрутка на читалку. Список строк-пар, внутри строки
grid-template-columns: 1fr 1fr. Синхронизации двух прокруток нет и не будет — колонки физически не могут разъехаться.
Денежных полей в интерфейсе нет нигде (§4.8): ни сумм, ни потолков, ни оценки, ни остатка.
7. Порядок S1 и что в него НЕ входит
Порядок работ S1: каркас → npm run check → скриншот-цикл и проверка, что он живой →
tokens.css → витрина → контракт-тест токенов → сверка витрины с референсом.
npm run check = prettier --check → eslint → tsc --noEmit → vitest run.
npm run check:full = check → vite build → shot. Отдельного e2e-набора в S1 нет:
единственная браузерная проверка — скриншот-цикл, он и стоит в check:full.
Git-хуков нет.
Не входит в S1 (и не должно появиться раньше срока): экраны, оболочка трёх панелей,
слой данных и MSW, React Compiler (Ф-2), токен-гейт на отступы (Ф-4 — включается, когда
шкала отступов устоится, иначе мешает подбору), эталонные скриншоты в тестах (отложены
решением STACK_DECISIONS.md §3), светлая тема, мобильная раскладка.