Land frontend S0 plan: verified version pins, src map, checkable maintainability rules, screenshot-loop protocol, re-measured reference geometry
This commit is contained in:
parent
a66af279d1
commit
e9a6bb2711
2 changed files with 256 additions and 0 deletions
256
frontend/docs/FRONTEND_PLAN.md
Normal file
256
frontend/docs/FRONTEND_PLAN.md
Normal file
|
|
@ -0,0 +1,256 @@
|
|||
# План фронта — как пишется интерфейс 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 [маршрут ...]`. Без аргументов снимает все известные маршруты.
|
||||
|
||||
**Что делает:**
|
||||
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), светлая тема, мобильная раскладка.
|
||||
Loading…
Add table
Reference in a new issue