Land frontend S0 plan: verified version pins, src map, checkable maintainability rules, screenshot-loop protocol, re-measured reference geometry

This commit is contained in:
Claude (backend session) 2026-08-02 14:02:57 +03:00
parent a66af279d1
commit e9a6bb2711
2 changed files with 256 additions and 0 deletions

View file

@ -0,0 +1,256 @@
# План фронта — как пишется интерфейс TextMachine
> Этап **S0** сессии фронта: план до первой строки кода (`FRONTEND_SESSION_PROMPT.md` §7.2).
> Ландится отдельным коммитом. Правят следующие фронт-сессии; при конфликте с
> `STACK_DECISIONS.md` побеждает STACK_DECISIONS (там ратифицированы пины), при конфликте
> с промтом сессии — промт (там ратифицирован продукт).
## 0. Границы
Эта сессия делает **S0 (план) и S1 (инструменты, скриншот-цикл, `tokens.css`, витрина)** и
останавливается на витрине. Экраны — S2S7, по одной сессии на этап (`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 [маршрут ...]`. Без аргументов снимает все известные маршруты.
**Что делает:**
1. поднимает `vite preview` на фиксированном порту (сборка, а не dev — dev-оверлеи не должны попадать в кадр);
2. запускает Chromium из Playwright, вьюпорт **1440×900** (базовая ширина из §4.5 промта), `deviceScaleFactor: 2` — чтобы снимок был сравним с референсом, снятым на macOS при 2x;
3. ждёт `networkidle` и загрузки шрифтов (`document.fonts.ready`) — иначе в кадр попадает фолбэк-шрифт;
4. кладёт 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.
1. **Прогресс — пофазный, не «готово N из M».** Юнит становится `done` только когда есть и
черновые строки всех членов, и строка редактуры: во время черновой волны сквозной счётчик
стоит на нуле почти всё время. Тип прогресса — две пары `{ done, total }` (черновик ∥ редактура),
а не одно число. Пользователю всё равно показывается одна полоса без названий фаз (§4.1).
2. **Подпись термина — не UPDATE строки.** Банк пересобирается из файлов на каждом прогоне,
прямая запись стирается следующим прогоном. Контракт подписи — отправка набора решений, а не
правка записи; фикстуры S3 моделируют именно это.
3. **Байтовых смещений у замечаний нет.** Замечание адресует блок или главу — и только.
Тип замечания не имеет полей `offset`/`length`; подчёркиваний внутри текста не будет.
4. **Единица пары — edit-unit, ~1.9 на главу.** Местами вся глава окажется одним блоком.
Читалка обязана нормально выглядеть в этом случае; выравнивание грубое, по единицам экспорта.
5. **Одна вертикальная прокрутка на читалку.** Список строк-пар, внутри строки
`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), светлая тема, мобильная раскладка.