From e9a6bb27112105a28705d39df34900b760432c67 Mon Sep 17 00:00:00 2001 From: "Claude (backend session)" Date: Sun, 2 Aug 2026 14:02:57 +0300 Subject: [PATCH] Land frontend S0 plan: verified version pins, src map, checkable maintainability rules, screenshot-loop protocol, re-measured reference geometry --- .../BACKEND_COLDRUN_DEBUG_SESSION_PROMPT.md | 0 frontend/docs/FRONTEND_PLAN.md | 256 ++++++++++++++++++ 2 files changed, 256 insertions(+) rename docs/{ => archive/prompts}/BACKEND_COLDRUN_DEBUG_SESSION_PROMPT.md (100%) create mode 100644 frontend/docs/FRONTEND_PLAN.md diff --git a/docs/BACKEND_COLDRUN_DEBUG_SESSION_PROMPT.md b/docs/archive/prompts/BACKEND_COLDRUN_DEBUG_SESSION_PROMPT.md similarity index 100% rename from docs/BACKEND_COLDRUN_DEBUG_SESSION_PROMPT.md rename to docs/archive/prompts/BACKEND_COLDRUN_DEBUG_SESSION_PROMPT.md diff --git a/frontend/docs/FRONTEND_PLAN.md b/frontend/docs/FRONTEND_PLAN.md new file mode 100644 index 00000000..c92620aa --- /dev/null +++ b/frontend/docs/FRONTEND_PLAN.md @@ -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 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), светлая тема, мобильная раскладка.