47 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.
0.1. Канон: что читать перед чем
Фронт живёт в проекте с ратифицированным контрактом, и половина ответов на вопросы «как правильно» уже написана — не здесь. Опыт S0/S1: два дефекта пришли ровно из непрочитанного канона (мокался не тот уровень контракта; смысл двух вердиктов взят по имени причины вместо доккоммента), оба нашлись бы за десять минут чтения. Ссылки, а не пересказ — пересказ протухает:
| Читать | Зачем фронту | Когда |
|---|---|---|
| ../../CLAUDE.md | зоны, гардрейлы, git-протокол (коммит только pathspec-формой) | первым делом |
| ../../docs/README.md | карта: где что лежит и что чем перекрыто | первым делом |
| ../../docs/product-requirements.md | реестр ПТ-1..ПТ-34; ПТ-33 и ПТ-34 — жёсткие инварианты интерфейса, ПТ-21 задаёт якорь чтения | до первого экрана |
| ../../docs/research/23-engine-platform-seam.md | форма шва движок↔платформа; docs/README.md требует читать его перед любым кодом стыка |
до любого кода данных |
| CURRENT-STATE + единый бэклог в ../../docs/PROGRESS.md | что движок обязан отдать фронту и чего ещё нет: строки 95 (контракт API) · 99–103 (прогресс, манифест, таблица подписи, trace, эмиттер) · 49 (annot-v1) · 54 (масштаб) | перед планированием этапа |
| STACK_DECISIONS.md §5 | транспорт до фронта и правила стрима | до слоя данных |
| ../../docs/glossary.md | жаргон проекта (D-номер, банк, голден, юнит) | при первом непонятном слове |
Правило чтения 05-decisions-log.md: карта актуальности
в шапке + живая голова с хвоста, корпус D1–D38 — grep по D-номеру, целиком не читать.
Фронта касаются D39.81 (SaaS; движок = процесс-на-прогон, сервер в backend/ не пишется),
D39.84 (стек + восемь решений владельца + зонные бэклоги), D39.85 (шов), D39.88 (git).
0.2. Транспорт: с кем фронт разговаривает
С движком — никогда. Это не стилистика, а ратифицированный анти-паттерн: движок — CLI-процесс
на прогон под эксклюзивным локом, его SQLite платформой не читается, HTTP внутрь него не тащится
(research/23 §4, §0). Между фронтом и движком стоит платформа, у которой пока ноль строк кода
(../../platform/BACKLOG.md П-1).
Проводов два, и их легко перепутать:
| Шов | Формат | Где ратифицировано |
|---|---|---|
| движок → платформа | версионированный NDJSON-поток событий (объект на строку, первая строка — version-хендшейк), плюс артефакты границ стадий, плюс tmctl status --json для ре-синка |
D39.85, research/23 §2–§3 |
| платформа → фронт | JSON поверх HTTP для чтений из Postgres read-модели + SSE для живого прогресса; события пушит воркер, фронт read-модель не опрашивает; WebSocket отвергнут | D39.84, STACK_DECISIONS.md §5 |
Фронту принадлежит только вторая строка. NDJSON в коде фронта появиться не может; если появился —
кто-то полез не на тот уровень. Дисциплина стрима, которую обязан выдержать слой данных:
монотонный id + Last-Event-ID (докачка после обрыва), heartbeat ~20 с, EventSource в браузере
и Authorization: Bearer с построчным разбором для десктопа.
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/ |
единственный вход к данным: типы + функции. Сегодня внутри фикстура, на S3 — MSW и асинхронность, потом HTTP и SSE к платформе (§0.2). Единственное место, где легальны импорт из mock/ и любой сетевой вызов |
React-компоненты |
mock/ |
фикстуры. В день появления API папка удаляется целиком, экраны не трогаются | логика; фикстуры — данные |
showcase/ |
витрина примитивов: маршрут /showcase, живой каталог для сверки с references/fleet.png |
продуктовые экраны |
Правило зависимостей — сверху вниз, без обратных рёбер:
features/ → ui/ + api/; shell/ → ui/; ui/ → tokens/. api/ не знает про React.
2.1. Слои каскада
Ловушка STACK_DECISIONS.md §7.4 — тихие расхождения из-за порядка каскада между стилями
сторонних примитивов, токенами и CSS Modules. Порядок объявлен один раз, в шапке reset.css
(он импортируется первым, а заявление слоёв обязано стоять раньше любого слоя):
@layer reset, vendor;
- сброс — самый низ, перебивается чем угодно;
- vendor — стили сторонних примитивов; подключаются только так:
@import 'пакет/styles.css' layer(vendor); - токены и CSS Modules — намеренно ВНЕ слоёв. Неслойное правило выигрывает у любого слоя, поэтому ни сброс, ни чужие стили наши перебить не могут, а модуль экрана при нужде может локально переопределить токен обычным каскадом.
Токены сознательно не завёрнуты в слой: заворачивать нечего (это только объявления переменных
на :root), а лишний слой делает их слабее собственных стилей приложения без всякой выгоды.
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 | Отключить CSS-гейт комментарием нельзя | машинно: reportDisables: true на правилах + reportUnscopedDisables: true в корне конфига. Одного reportDisables НЕ хватает: он ловит только именованное отключение, а голое /* stylelint-disable */ в шапке снимало гейт молча — проверено. В TSX то же самое делают правила @eslint-community/eslint-comments: no-unlimited-disable требует назвать правило, require-description — написать причину, disable-enable-pair — закрыть область. Голое /* eslint-disable */ даёт три ошибки; точечное // eslint-disable-next-line react-hooks/exhaustive-deps -- причина проходит. Бинарный noInlineConfig не берём: он запрещает и то, что рекомендует сам React |
| 3 | Данные только через src/api/ |
машинно, см. правило 14 (было ревью-вопросом — не сработало) |
| 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. Не открывается по прямой ссылке — не снимется |
| 11 | styles.имяКласса ссылается на существующий класс |
машинно: src/cssModules.test.ts. Vite типизирует модуль как { [key: string]: string }, поэтому опечатка даёт className="undefined" тихо — тайпчек и линт её пропускают. Правило заведено не впрок: на витрине такая ссылка уже нашлась |
| 12 | Имя токена в var(--…) и в строках кода объявлено в tokens.css |
машинно: src/tokens/tokens.test.ts. Тот же класс тихой ошибки: stylelint проверяет только форму обёртки var(--…), тайпчек видит обычную строку, браузер отдаёт пустое значение — элемент гаснет в фон, и на скриншоте это 124 пикселя из 3,9 млн. Локальные переменные, задаваемые через style, перечислены в тесте явным списком |
| 14 | Данные — только через src/api/; сеть — только там же |
машинно: no-restricted-imports на **/mock/** плюс no-restricted-globals/no-restricted-properties на ВСЕ транспорты (fetch, EventSource, WebSocket, XMLHttpRequest и они же через window/globalThis/self, плюс navigator.sendBeacon), с исключением для src/api/**. Одного fetch не хватало: живой прогресс по ратифицированному стеку приходит через EventSource (STACK_DECISIONS.md §5) — то есть шов расползся бы именно тем транспортом, который гейт не видел |
| 15 | Доступность проверяется машиной, а не глазами владельца | машинно: axe-core прогоняется в npm run shot по каждому маршруту и валит команду; список маршрутов скрипта сверяется с routes.tsx тестом (src/routes.test.ts) — иначе новый экран просто не попадал в прогон, а команда возвращала успех. Контраст вынесен в отчёт: палитра снята с Fleet замером и принята владельцем, менять её под порог WCAG — отдельное решение (BACKLOG.md Ф-11) |
| 17 | Пара языков живёт в данных, а не в разметке | машинно: src/generality.test.ts запрещает литерал lang="…" в TSX. Это ревью-вопрос канона в исполняемой форме («заработает ли пара, которой в репо ещё нет, без правки кода?» — CLAUDE.md §2). Найдено ревью S1 в живом коде: lang="zh" пережил бы приезд ja→ru молча, и кандзи отрисовались бы китайскими начертаниями |
| 16 | Плавающий промис не проходит | машинно: tseslint.configs.recommendedTypeChecked с projectService. Проверено: load() без await роняет линт |
| 13 | Инлайновый стиль — только литерал объекта прямо в атрибуте, и только с ключами-CSS-переменными | машинно: no-restricted-syntax запрещает сам атрибут style и разрешает единственную форму. Перечислять формы записи оказалось бесполезно: из одиннадцати способов записать то же самое ловилось три. Остаточная дыра — JSX-спред <div {...props} />, где style приезжает внутри объекта; без типовой информации не ловится |
Контрольный вопрос владельца (§5.1, применять к каждому пакету работ):
добавление нового состояния главы или нового вида замечания правит один файл.
Правит три — структура неверна, переделать до движения дальше.
Ответ этой структуры: состояние главы живёт как строка типа в src/api/types.ts и
как одна запись в карте отображения src/features/chapters/. Цвет индикатора — токен.
Новый вид замечания — запись в карте видов, компонент выноски не трогается.
4. Протокол скриншот-цикла
Цель — не «код валиден», а «вид совпал». Смотреть на построенное обязательно, а не предполагать.
Команда: npm run shot [маршрут ...] [--size ШxВ]. Без аргументов снимает /showcase
при 1440×900.
Что делает:
- собирает и поднимает
vite preview(сборка, а не dev — dev-оверлеи не должны попадать в кадр); - запускает Chromium из Playwright, вьюпорт 1440×900 (базовая ширина из §4.5 промта),
deviceScaleFactor: 2— чтобы снимок был сравним с референсом, снятым на macOS при 2x;--size 1280x764даёт вьюпорт самого референса, когда нужно прямое наложение; - ждёт
networkidleи загрузки шрифтов (document.fonts.ready) — иначе в кадр попадает фолбэк-шрифт; - кладёт PNG в
frontend/.shots/<маршрут>.png.
Что делаю я после команды — обязательная часть цикла, а не опция: открываю PNG инструментом
чтения файлов, смотрю на него, кладу рядом references/fleet.png, называю расхождения словами,
правлю, снимаю снова.
Стенд без root. .tooling/ собирается один раз и в репозиторий не едет (вместе с .shots/
он в .gitignore). Chromium не стартует, пока не найдёт libnss3, libnssutil3, libnspr4,
libasound.so.2; в системе их нет, а sudo недоступен, поэтому пакеты выкачиваются и
распаковываются локально. Тем же способом кладётся шрифт CJK: на стенде не установлено ни одного
(fc-list :lang=zh пуст), и без него весь китайский текст в кадре — квадраты, а плотность колонок
проверить нечем. scripts/shot.mjs сам подставляет LD_LIBRARY_PATH и XDG_DATA_HOME, когда
видит .tooling/root — думать об этом больше не нужно.
mkdir -p .tooling && cd .tooling
apt-get download libnss3 libnspr4 libasound2t64 fonts-noto-cjk
for d in *.deb; do dpkg -x "$d" root; done
rm -f *.deb && cd ..
Шрифт нужен только стенду: продукт отдаёт CJK системному стеку (STACK_DECISIONS.md §2),
на Windows у пользователя он есть.
5. Критерий сходимости с референсом
Растровое совпадение не является целью и недостижимо: Fleet закрыт (скачивание прекращено
22.12.2025), референс снят на macOS при 2x, цель — Chromium с другим хинтингом шрифтов.
Эталонных скриншотов в тестах нет — вместо них контракт-тест токенов
(STACK_DECISIONS.md §3).
Сходиться обязаны: цвета · промежутки 8px · радиусы 6px · плотность строк · общее впечатление. Подбираются на глаз: кегли, интерлиньяж, внутренние отступы — померить их больше негде.
5.1. Замеры, перепроверенные в этой сессии
Значения §1.1 промта перепроверены заново по references/fleet.png (PIL: гистограмма всего
изображения, срезы строк и столбцов, детект края скругления по порогу яркости).
Все числа совпали, одна роль разошлась: §1.1 отдаёт #353739 и «выбранной строке», и
«активной вкладке», а в кадре это разные вещи — вкладка открытого документа залита #142f4c,
вкладка панели #27292b, и #353739 под вкладками не встречается вовсе. В токенах роли
разведены. Ниже — сводка с тем, что удалось доснять; это и есть входные числа tokens.css.
Масштаб снимка 2x подтверждён независимо: все четыре промежутка между панелями равны ровно 16 физическим пикселям, что даёт целые 8 CSS-пикселей.
Поверхности (доля площади — по гистограмме всего кадра 2560×1528):
| Роль | Значение | Подтверждение |
|---|---|---|
| фон оболочки | #090909 |
10.45% площади |
| заливка панели | #17191a |
80.48% площади |
| приподнятая поверхность, наведение, активная вкладка панели | #27292b |
18 196 px; вкладки Files, Terminal, AI Assistant |
| выбранная строка дерева, клавиша-чип | #353739 |
37 374 px; ровно три пятна в кадре, и все три — строка дерева и чипы клавиш |
| активная вкладка открытого документа | #142f4c |
поправка: заливка вкладки rpc-node.ts — 7 183 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 не заводим.
5.2. Витрина сведена с референсом — что сошлось и что нет
Витрина снята тем же способом, что и референс (--size 1280x764, 2x), и промерена тем же кодом.
Сошлось до пикселя: промежутки — четыре по 16 физ.; верхний край панелей — 72 физ.; радиус — угол выходит на прямую за 12 физ.; шаг строки дерева — 26 CSS; нижняя полоса — 56 физ.; доли площади поверхностей (панель 83.5% против 80.5%, фон 9.4% против 10.5% — разница от того, что у нас другое наполнение). Высота глифов статус-полосы совпала точно (23 физ.), вкладки — 19 против 20 физ.
Расхождения, названные вслух:
- Субпиксельное сглаживание. У Fleet текст сглажен в серую шкалу (macOS): в статус-полосе
ровно 0% цветных пикселей. У нас в том же месте 1.29%, в дереве 1.35% против 0.37% —
это цветная бахрома Chromium.
-webkit-font-smoothing: antialiasedв сбросе действует только на macOS. Расхождение принято: §8 промта прямо снимает совпадение по хинтингу, а на Windows субпиксельное сглаживание — норма платформы. Гасить его флагом в скриншот-цикле не стали: снимок должен показывать то, что рисует настоящий браузер. - Колонка оригинала приглушена (
--color-text-secondary) — это решение, а не замер: во Fleet на этом месте подсвеченный код. Пересмотреть на S6, когда читалка будет настоящей. +в конце рядов вкладок у Fleet есть, у нас нет — добавляется вместе с действием, которое он будет запускать (S2).- Наведение и прочие интерактивные состояния витриной не проверены: снимок статичен.
Токен
--color-raisedподтверждён только на активной вкладке. - Боковые панели фиксированы 320px (замеренная абсолютная ширина при 1280). Тянущиеся
панели на
react-resizable-panels— S2; тогда же решится, тянуть их долей или пикселями. - Скроллбар в кадр не попадает. Chromium на стенде отдаёт оверлейные полосы
(
offsetWidth - clientWidth = 0), поэтому в статичном снимке их нет. Что стилизация применяется — проверено вычисленными значениями:scrollbar-width: thin,scrollbar-color: rgb(53, 55, 57) rgba(0, 0, 0, 0). На Windows полосы займут место и получат эти цвета.
5.3. Что выяснилось про инструменты — не переоткрывать
eslint-plugin-react-hooks: flat-конфиг лежит вconfigs.flat['recommended-latest']. Одноимённый ключ верхнего уровня — старого формата, ESLint 10 на нём падает с ошибкой про «plugins as array».- Vitest по умолчанию подменяет CSS пустой заглушкой. Без
test.css: trueконтракт-тест токенов сверял бы пустоту и был бы вечно зелёным. Проверено: до включения он падал на пустой строке, а не проходил. - happy-dom не понимает заявление
@layer a, b;— проглатывает весь остаток файла, иgetComputedStyleперестаёт видеть переменные. Поэтому заявление слоёв живёт вreset.css, аtokens.cssостаётся чистым (§2.1). Блочную форму@layer x { }он тоже не поддерживает. @types/nodeдержим на мажоре 22, а не на latest 26: иначе тайпчек разрешает API, которых на стенде нет.import.meta.globпо*.module.cssбрать с?raw, а не?inline:?inlineотдаёт уже скомпилированный CSS с хешированными именами (._shell_1abc_1), и сверять с ним имена из TSX бессмысленно.
5.4. Что именно проверено живым нарушением
Формулировка «гейты проверены живым нарушением» без перечня форм — ловушка: она звучит как машинная гарантия, а покрывает ровно те входы, которые придумал автор. Ниже полный список проверенных форм; каждая роняла проверку, после отката дерево зелёное.
CSS: hex-литерал в модуле · hex в сокращённой записи border · rgb() · color()
в сокращённой записи · font-size числом · именованное отключение правила
(/* stylelint-disable color-no-hex */) · голое /* stylelint-disable */ в шапке файла.
TSX: hex всех четырёх длин (#RGB, #RGBA, #RRGGBB, #RRGGBBAA) · цветовые функции
включая color() и light-dark() · одиннадцать форм инлайнового стиля (литерал, спред,
вынесенная переменная, приведение as, свойство объекта, вызов фабрики, тернарник,
вычисляемый ключ, JSX-спред, смешанный литерал) · импорт глобального CSS мимо main.tsx ·
предупреждение линта при --max-warnings 0.
Сеть (все восемь форм ловятся, в src/api/** все восемь проходят): fetch · EventSource ·
WebSocket · XMLHttpRequest · window.fetch · globalThis.fetch · self.fetch ·
navigator.sendBeacon. Проверялось файлом на диске, а не через --stdin: тип-осведомлённый
линт отказывается разбирать stdin («was not found by the project service»).
Структурные тесты: опечатка в имени класса CSS Module · то же при переименованном импорте
модуля · опечатка в имени токена в CSS · опечатка в имени токена в данных TS · маршрут добавлен
в routes.tsx и забыт в scripts/shot.mjs · литерал lang="zh" возвращён в разметку.
Что НЕ ловится и остаётся известной дырой: /* eslint-disable */ в TSX (см. правило 2) ·
JSX-спред <div {...props} /> со style внутри объекта · именованные цвета CSS в TSX
(fill="red") — в CSS они запрещены, в TSX ратифицированный список STACK_DECISIONS.md §3
их не называет; строка в бэклоге.
6. Что уже известно про движок — учтено в форме данных
Из FRONTEND_SESSION_PROMPT.md §9 и STACK_DECISIONS.md §8. Влияет на типы в src/api/
уже сейчас, поэтому записано в плане, а не откладывается на S3.
- Прогресс — пофазный, не «готово N из M». Юнит становится
doneтолько когда есть и черновые строки всех членов, и строка редактуры: во время черновой волны сквозной счётчик стоит на нуле почти всё время. Тип прогресса — две пары{ done, total }(черновик ∥ редактура), а не одно число. Пользователю всё равно показывается одна полоса без названий фаз (§4.1). ⚠ Это правило было записано здесь ещё в S0 — и витрина S1 всё равно вышла с наивным счётчиком глав, потому что фикстура выдумала полеtranslatedChapters, которого read-модель не отдаёт. Лечение сильнее правила: фикстура поставлена в состояние «идёт черновая волна», где наивный счётчик дал бы ноль, и скриншот-цикл видит трудный случай, а не удобный. 1а. Состояние — у ПРОГОНА над книгой, у главы его нет. Подпись банка — один стоп между черновой и редакторской волнами на всю книгу (mining.go:199), поэтому «глава ждёт подписи, пока соседняя финализируется» — картина, которую движок породить не может. У главы есть выполнение:ChapterPassport(status.go:37-55) несётunits_total/done/in_progress/pendingи вердикт. В интерфейсе: лестница из девяти состояний — на строке книги, полоса выполнения — на строке главы. Девять состояний точкой не различаются (проверено снимком: три пары совпадали) — метка словом, цвет только дублирует и только для внимания и отказа. 1б. Язык — код, а не слово. Движок держитsource_lang/target_langкодами (book.go:26-27), ключ пары —zh-ru; контракт отдаст то же самое. Человеческое имя считается на экране черезIntl.DisplayNames,langна элементе берётся из данных книги (правило 17). - Подпись термина — не 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 → stylelint → tsc --noEmit → vitest run.
Шагов пять, а не четыре: STACK_DECISIONS.md §3 перечисляет четыре, но там же требует, чтобы
обе половины гейта цвета жили в одной команде, а CSS-половину гоняет именно stylelint.
npm run check:full = check → vite build → shot. Отдельного e2e-набора в S1 нет:
единственная браузерная проверка — скриншот-цикл, он и стоит в check:full.
Git-хуков нет.
Не входит в S1 (и не должно появиться раньше срока): экраны, оболочка трёх панелей,
слой данных и MSW, React Compiler (Ф-2), токен-гейт на отступы (Ф-4 — включается, когда
шкала отступов устоится, иначе мешает подбору), эталонные скриншоты в тестах (отложены
решением STACK_DECISIONS.md §3), светлая тема, мобильная раскладка.