Rework showcase layout on owner feedback: bank moves to right panel for vertical parallel reading, single settings entry, full-height reader, fixtures now use real engine field names
This commit is contained in:
parent
c87d66c355
commit
8d6b3f731a
10 changed files with 322 additions and 137 deletions
|
|
@ -33,6 +33,8 @@ npm run shot # снимок витрины в .shots/ — открыть
|
|||
- [`docs/FRONTEND_PLAN.md`](docs/FRONTEND_PLAN.md) — план S0: сверенные пины, карта `src/`,
|
||||
слои каскада, правила поддерживаемости в проверяемой форме, протокол скриншот-цикла,
|
||||
перепроверенные замеры референса и список расхождений витрины.
|
||||
- [`docs/PROGRESS.md`](docs/PROGRESS.md) — журнал зоны: текущее состояние, решения владельца
|
||||
по продукту, открытые вопросы, хроника сессий.
|
||||
- [`docs/BACKLOG.md`](docs/BACKLOG.md) — зонный бэклог фронта.
|
||||
- `references/` — три скриншота-референса (Fleet, Antigravity).
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
| Ф-2 | **React Compiler** — включить ОТДЕЛЬНЫМ шагом после заморозки интерфейса, с CI-проверкой, что вставки реально попали в бандл (в MVP выключен; совместимость держит линт eslint-plugin-react-hooks) | после заморозки UI | STACK_DECISIONS §1 |
|
||||
| Ф-3 | **Пересмотр TS 6 → TS 7** после выхода TS 7.1 с программным API (~октябрь 2026); решение обратимо — тайпчек не участвует в сборке | триггер: релиз TS 7.1 | STACK_DECISIONS §1 |
|
||||
| Ф-4 | **Токен-гейт на отступы** (`padding`/`margin`/`gap`/`border-radius`) — вторым шагом, когда шкала токенов зафиксирована (иначе гейт мешает подбору) | **триггер наступил**: витрина S1 снята, шкала `--space-1…6` заведена; включать, когда S2 обкатает её на настоящей оболочке | STACK_DECISIONS §3 |
|
||||
| Ф-8 | **Вопрос владельцу: закрывать ли `/* eslint-disable */` в TSX.** CSS-половина гейта цвета обхода комментарием больше не допускает (`reportUnscopedDisables`), TSX-половина — допускает. Закрывается одной строкой `linterOptions: { noInlineConfig: true }`, но той же строкой запрещаются легитимные точечные подавления (`react-hooks/exhaustive-deps`), которые рекомендует сам React. Выбор не технический, а владельческий | решение до S2 | адверсариальное ревью S1 |
|
||||
| ~~Ф-8~~ | ~~Вопрос владельцу: закрывать ли `/* eslint-disable */` в TSX~~ — **закрыт 02.08 без решения владельца.** Вопрос был поставлен как выбор из двух плохих вариантов, а у него есть отраслевой третий: `@eslint-community/eslint-plugin-eslint-comments` 4.7.2 (`no-unlimited-disable` + `require-description` + `disable-enable-pair`). Голое отключение падает тремя ошибками, точечное подавление с причиной проходит. Урок: не выносить владельцу технический вопрос, не поискав отраслевую практику | закрыт | адверсариальное ревью S1 |
|
||||
| Ф-9 | **Хвосты гейтов после адверсариального ревью S1** (все воспроизведены исполнением, ни один не в боевом коде): именованные цвета CSS в TSX (`fill="red"`, `<Icon color="dodgerblue"/>`) не ловятся — в CSS запрещены, асимметрия; санкционированное исключение `style={{'--x': …}}` пропускает любое значение, включая литеральный цвет · JSX-спред `<div {...props}/>` со `style` внутри · системные цветовые ключевые слова (`Highlight`, `ButtonText`) проходят в `box-shadow`/`outline`/`caret-color` и в сокращённых записях | до S2 или в ней | адверсариальное ревью S1 |
|
||||
| Ф-10 | **Хвосты структурных тестов**: `cssModules.test.ts` строит пары по имени файла (компонент, импортирующий соседний модуль, из проверки выпадает), не видит деструктуризацию `const { shell } = styles`, а `classesOf` тянет в «объявленные классы» расширения из `url(...)`. `tokens.test.ts` сверяет 23 токена из 38 и не имеет замка полноты — новый токен приезжает без гейта по построению | до S2 | адверсариальное ревью S1 |
|
||||
| Ф-7 | **Хвосты сведения витрины с референсом** (`FRONTEND_PLAN.md` §5.2): `+` в конце рядов вкладок — вместе с действием, которое он запускает (S2) · трактовка колонки оригинала приглушённым цветом — решение, а не замер, пересмотреть на настоящей читалке (S6) · состояния наведения витриной не проверены, снимок статичен | S2 · S6 | S1, сверка с fleet.png |
|
||||
|
|
|
|||
|
|
@ -126,7 +126,7 @@ CJK-шрифт не ставим: иероглифы отдаём системн
|
|||
| # | Правило | Проверка |
|
||||
|---|---|---|
|
||||
| 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-disable */` работает, `noInlineConfig` сознательно не включён (он же запретит легитимные точечные подавления `react-hooks/exhaustive-deps`). Решение владельца — `BACKLOG.md` Ф-8 |
|
||||
| 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/` | ревью-вопрос: есть ли `fetch`/`axios`/импорт из `mock/` вне `src/api/`? Должно быть «нет» |
|
||||
| 4 | Файл = один компонент + свой `.module.css`, больше ~150 строк — делить | ревью глазами при лендинге пакета |
|
||||
| 5 | Состояние ровно в двух местах: TanStack Query (серверное), Zustand (интерфейсное) | ревью-вопрос: есть ли `useState` с копией серверных данных? Должно быть «нет» |
|
||||
|
|
|
|||
112
frontend/docs/PROGRESS.md
Normal file
112
frontend/docs/PROGRESS.md
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
# Журнал зоны «Фронт»
|
||||
|
||||
> Зонный журнал: сессии фронта пишут сюда сами, работают независимо, в `docs/PROGRESS.md`
|
||||
> оркестратора не пишут (решение владельца 02.08). Хвосты — `BACKLOG.md`, план и замеры —
|
||||
> `FRONTEND_PLAN.md`, ратифицированные пины — `STACK_DECISIONS.md`.
|
||||
> Сверху — текущее состояние, ниже — хроника по сессиям.
|
||||
|
||||
## Текущее состояние
|
||||
|
||||
- **Пройдено:** S0 (план) и S1 (инструменты, скриншот-цикл, `tokens.css`, витрина).
|
||||
- **Дальше:** S2 — оболочка трёх панелей и слой `src/ui/`.
|
||||
- **Ждёт владельца:** решений нет, все вопросы закрыты (см. хронику 02.08).
|
||||
- **Ждёт движка:** ничего до S4. Затем — манифест глав и пофазный прогресс (строки 99–100
|
||||
единого бэклога), к S5 — машиночитаемая таблица подписи банка (строка 101).
|
||||
- `npm run check` зелёный (5 шагов, 27 тестов), `npm run check:full` зелёный.
|
||||
|
||||
## Решения владельца по продукту
|
||||
|
||||
Записываю здесь, потому что они меняют раскладку из §2 промта, а промт правит не фронт-сессия.
|
||||
|
||||
### 02.08 — раскладка: банк памяти переезжает в правую панель
|
||||
|
||||
Владелец, глядя на витрину: **«банк памяти перенёс бы в правую вкладку, чтоб можно было
|
||||
параллельно вертикально смотреть и оригинал, и главу, и банк памяти, а не горизонтально»**.
|
||||
Плюс три дефекта структуры в той же реплике: две кнопки настроек, банк продублирован
|
||||
(вкладка слева + отдельная карточка внизу), окно главы слишком мало.
|
||||
|
||||
Что из этого следует и сделано:
|
||||
|
||||
| Было | Стало | Почему |
|
||||
|---|---|---|
|
||||
| Настройки в верхней полосе И внизу левой панели | только внизу левой панели | промт §3.12: настройки — отдельный раздел из низа левой панели, второй вход лишний |
|
||||
| Банк во вкладке слева И карточкой в центре снизу | одна вкладка в правой панели | одна вещь — одно место; слева осталась только навигация |
|
||||
| Центр разрезан на две карточки | одна панель во всю высоту | нижняя карточка была скопирована с терминала Fleet и не несла задачи |
|
||||
| Кнопки сворачивания трёх панелей | двух | кнопка есть у той панели, которая существует |
|
||||
|
||||
**Вертикальный параллелизм — суть решения:** рабочий цикл читателя это «читаю перевод → спотыкаюсь
|
||||
о термин → проверяю его в банке». Все три вещи обязаны быть видны одновременно, поэтому колонки:
|
||||
навигация · оригинал+перевод · справочное о читаемой главе. Правая панель становится панелью
|
||||
контекста, и её вкладки — `О книге · Замечания · Банк`.
|
||||
|
||||
**Что при этом надо развести (иначе §3.5 ломается):** у банка две разные работы.
|
||||
Справочная — «что это за термин», ей хватает 320px правой панели. Подписная — сотни терминов
|
||||
подряд, клавиатура важнее мыши, массовые действия — в 320px не помещается. Поэтому подписной
|
||||
экран открывается **вкладкой в центре**, как документ, а правая панель ведёт в него действием
|
||||
«Подписать банк». В витрине эта связка показана.
|
||||
|
||||
### 02.08 — данные: моки берут форму от движка
|
||||
|
||||
Владелец: **«всё на диске лежит, и главы, и банки памяти»**. Разбор бэкенда подтвердил: моя
|
||||
первая фикстура выдумывала поля (`kind: 'имя'`, `state: 'подписан'`), а промт прямо запрещает
|
||||
изобретать API. Формы переписаны по настоящим:
|
||||
|
||||
- банк — таблица `glossary` (`backend/internal/store/migrate.go:176`): `src · dst · type · sense ·
|
||||
status · source · since_ch/until_ch`. Типы: `name|term|title|place|org|nickname|fullname`,
|
||||
статусы подписи ровно три: `auto|draft|approved`, провенанс `seed|ruby|auto|mined`;
|
||||
- пары — `ChunkExport` из `tmctl export --pairs` (`backend/internal/pipeline/export.go:25`):
|
||||
`chapter · chunk_idx · disposition · flag_reason · detail · final_text · source`;
|
||||
- причины замечаний — enum из `backend/internal/pipeline/disposition.go:56-104`.
|
||||
|
||||
**Следствие для интерфейса, которое из этого выросло:** движок говорит своим языком —
|
||||
`sanitizer_stripped`, «CJK leak in the ru output: 第一节». Промт §4.1 запрещает показывать это
|
||||
пользователю. Значит между движком и экраном обязан стоять **слой перевода вердиктов
|
||||
в продуктовые понятия**, и он заведён одной картой в данных: причина → человеческая фраза;
|
||||
поле `detail` не показывается никогда. Это же ПТ-33 реестра требований.
|
||||
|
||||
## Открытые вопросы к владельцу
|
||||
|
||||
| # | Вопрос | Почему это его вопрос |
|
||||
|---|---|---|
|
||||
| В-1 | **Данные привязаны к одной машине.** Вся фактура — прогоны, экспорты, банки — лежит в `/home/ubuntu/books/gu-zhenren/` вне git; оба `backend/example/sample-*.db` пустые (только схема); пути стенда зашиты в тесты (`miner_parity_test.go:33-35`, `labelharness_test.go:43`) и в `book.yaml` абсолютными путями. Владелец сказал: «мне не нравится, что это живёт конкретно на моём диске». Лечение — платформа (`platform/README.md:20-29`: библиотека книг, хранение исходников и экспортов), но платформы ещё нет ни строки. Фронт от этого не зависит (работает на моках), но **вопрос на движок/платформу, а не на фронт** | скоуп-решение и порядок работ |
|
||||
| В-2 | **Авторское право на текст фикстур.** Витрина использует короткую выдержку из настоящего прогона (蛊真人). Правило проекта уже есть: `eval/.gitignore:4` держит копирайтные тексты вне git. Сейчас взят объём под проверку плотности и не больше; нужно ли убрать совсем и заменить синтетикой — решать владельцу | юридика |
|
||||
|
||||
## Хроника
|
||||
|
||||
### 02.08 — сессия S0+S1 (первая фронт-сессия)
|
||||
|
||||
**Заленжено:** `e9a6bb2` план S0 · `b98afb5` весь код S1 · `d8437d6` закрытие дыр после ревью.
|
||||
|
||||
**Построено:** каркас Vite 8.2.0 + React 19.2.8 + TS 6.0.3 точными пинами (22 из 23 совпали
|
||||
с latest на 02.08; TS 6 при latest 7.0.2 — намеренно, у TS 7 нет программного API);
|
||||
`npm run check` из пяти шагов; скриншот-цикл на Playwright без sudo, с локальными системными
|
||||
библиотеками и шрифтом CJK в `.tooling/`; `tokens.css` по замерам референса; витрина.
|
||||
|
||||
**Поправка к замерам промта §1.1:** роль `#353739` там смешана. В кадре это разные вещи —
|
||||
вкладка открытого документа залита `#142f4c`, вкладка панели `#27292b`, а `#353739` лежит
|
||||
только под строкой дерева и клавишами-чипами. В токенах роли разведены.
|
||||
|
||||
**Сведение с референсом:** промежутки, верхний край панелей, радиус, шаг строки 26px, подложка
|
||||
выделения 24px и нижняя полоса сошлись с `fleet.png` до пикселя (перемерено тем же кодом).
|
||||
Расхождения названы в `FRONTEND_PLAN.md` §5.2, вид принят владельцем.
|
||||
|
||||
**Адверсариальное агентское ревью диффа** (6 ревьюеров по дименсиям + по 2 опровергателя
|
||||
на находку; 30 находок, 18 выживших). Пять дефектов были в самих гейтах — то есть в главной
|
||||
поставке S1 — и соло-самопроверка их не видела: голое `/* stylelint-disable */` снимало гейт
|
||||
цвета молча; опечатка в имени токена не ловилась ничем; `shot.mjs` проглатывал первый маршрут;
|
||||
запрет инлайнового стиля ловил 3 формы из 11; `check` был зелёным при warning. Всё починено
|
||||
и перепроверено живыми нарушениями, перечень форм — `FRONTEND_PLAN.md` §5.4.
|
||||
|
||||
**Урок, вписанный в §5.4:** формулировка «проверено живым нарушением» без перечня форм — ловушка.
|
||||
Она звучит как машинная гарантия, а покрывает ровно те входы, которые придумал автор.
|
||||
|
||||
**Ф-8 закрыт технически, решение владельца не потребовалось.** Вопрос был «запрещать ли
|
||||
`/* eslint-disable */` в TSX». Отраслевая практика — не бинарный `noInlineConfig`, а
|
||||
`@eslint-community/eslint-plugin-eslint-comments` 4.7.2: `no-unlimited-disable` требует называть
|
||||
правило, `require-description` — писать причину, `disable-enable-pair` — закрывать область.
|
||||
Легитимное точечное подавление `react-hooks/exhaustive-deps` с причиной проходит, голое
|
||||
отключение падает тремя ошибками. Симметрично `reportUnscopedDisables` на стороне CSS.
|
||||
|
||||
**Git-инцидент:** два первых коммита (`e9a6bb2`, `b98afb5`) сделаны голым `git commit` и унесли
|
||||
застейдженные файлы оркестратора. Содержимое цело, потеряна атрибуция; историю не переписывали.
|
||||
Норма ратифицирована владельцем как D39.88 — коммит только pathspec-формой с явным списком путей.
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
import comments from '@eslint-community/eslint-plugin-eslint-comments/configs';
|
||||
import js from '@eslint/js';
|
||||
import prettier from 'eslint-config-prettier';
|
||||
import reactHooks from 'eslint-plugin-react-hooks';
|
||||
|
|
@ -61,12 +62,19 @@ export default tseslint.config(
|
|||
// .flat — именно flat-вариант: одноимённый ключ верхнего уровня остался в старом формате.
|
||||
reactHooks.configs.flat['recommended-latest'],
|
||||
reactRefresh.configs.vite,
|
||||
comments.recommended,
|
||||
{
|
||||
languageOptions: {
|
||||
globals: { ...globals.browser, ...globals.node },
|
||||
},
|
||||
rules: {
|
||||
'no-restricted-syntax': ['error', ...tokenGate],
|
||||
// Симметрия с CSS-половиной, где голое отключение запрещено (reportUnscopedDisables).
|
||||
// noInlineConfig не берём: он запрещает и точечные подавления, которые рекомендует
|
||||
// сам React (react-hooks/exhaustive-deps). Отраслевая практика — не бинарный запрет,
|
||||
// а требование называть правило и писать причину.
|
||||
'@eslint-community/eslint-comments/no-unlimited-disable': 'error',
|
||||
'@eslint-community/eslint-comments/require-description': ['error', { ignore: [] }],
|
||||
// Глобальных стилей ровно два файла; всё остальное — CSS Modules, они скоупятся сами.
|
||||
'no-restricted-imports': [
|
||||
'error',
|
||||
|
|
|
|||
31
frontend/package-lock.json
generated
31
frontend/package-lock.json
generated
|
|
@ -16,6 +16,7 @@
|
|||
"react-router": "8.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint-community/eslint-plugin-eslint-comments": "^4.7.2",
|
||||
"@eslint/js": "10.0.1",
|
||||
"@testing-library/react": "16.3.2",
|
||||
"@types/node": "22.20.1",
|
||||
|
|
@ -549,6 +550,36 @@
|
|||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint-community/eslint-plugin-eslint-comments": {
|
||||
"version": "4.7.2",
|
||||
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-plugin-eslint-comments/-/eslint-plugin-eslint-comments-4.7.2.tgz",
|
||||
"integrity": "sha512-LF03qURSwEWm2dz5wtdDCzNk+7Opl0X7q6I3undsaIuNsEiNvRV3BCtqu14Q/6Pzg1tBj44LcxpW2EpSLZStZw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"escape-string-regexp": "^4.0.0",
|
||||
"ignore": "^7.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint-community/eslint-plugin-eslint-comments/node_modules/ignore": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz",
|
||||
"integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint-community/eslint-utils": {
|
||||
"version": "4.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz",
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@
|
|||
"react-router": "8.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint-community/eslint-plugin-eslint-comments": "^4.7.2",
|
||||
"@eslint/js": "10.0.1",
|
||||
"@testing-library/react": "16.3.2",
|
||||
"@types/node": "22.20.1",
|
||||
|
|
|
|||
|
|
@ -38,13 +38,6 @@
|
|||
min-height: 0;
|
||||
}
|
||||
|
||||
.center {
|
||||
display: grid;
|
||||
grid-template-rows: 1.7fr 1fr;
|
||||
gap: var(--gap);
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* Панель — скруглённая карточка без рамок и теней: отделяют только промежуток и заливка. */
|
||||
.panel {
|
||||
display: grid;
|
||||
|
|
@ -77,6 +70,11 @@
|
|||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
/* Подвал с двумя вещами по краям: действие слева, счётчик справа. */
|
||||
.panelFooterSplit {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.tab {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
@ -98,9 +96,9 @@
|
|||
}
|
||||
|
||||
.filter {
|
||||
width: 220px;
|
||||
height: var(--panel-header-height);
|
||||
margin-left: auto;
|
||||
width: 100%;
|
||||
height: var(--row-height);
|
||||
margin-bottom: var(--space-2);
|
||||
border: 1px solid transparent;
|
||||
padding-inline: var(--space-4);
|
||||
border-radius: var(--radius-control);
|
||||
|
|
@ -203,8 +201,8 @@
|
|||
|
||||
.bankRow {
|
||||
display: grid;
|
||||
grid-template-columns: 104px 1fr 88px 88px;
|
||||
gap: var(--space-3);
|
||||
grid-template-columns: auto 1fr;
|
||||
gap: var(--space-4);
|
||||
align-items: center;
|
||||
height: var(--row-height);
|
||||
border-block: var(--row-fill-inset) solid transparent;
|
||||
|
|
@ -213,20 +211,6 @@
|
|||
background-clip: padding-box;
|
||||
}
|
||||
|
||||
.bankHead {
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--font-size-small);
|
||||
}
|
||||
|
||||
.fact {
|
||||
display: grid;
|
||||
grid-template-columns: 92px 1fr;
|
||||
gap: var(--space-3);
|
||||
align-items: center;
|
||||
height: var(--row-height);
|
||||
padding-inline: var(--space-2);
|
||||
}
|
||||
|
||||
.dim {
|
||||
overflow: hidden;
|
||||
color: var(--color-text-secondary);
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import {
|
|||
ChevronDown,
|
||||
Download,
|
||||
FileText,
|
||||
PanelBottom,
|
||||
PanelLeft,
|
||||
PanelRight,
|
||||
Plus,
|
||||
|
|
@ -12,30 +11,29 @@ import {
|
|||
} from 'lucide-react';
|
||||
|
||||
import styles from './Showcase.module.css';
|
||||
import { bankRows, bookFacts, chapters, chapterStates, pairs } from './showcaseData';
|
||||
import { bankRows, chapters, chapterStates, flagReasonLabels, pairs } from './showcaseData';
|
||||
|
||||
// Тонкие линейные монохромные иконки как во Fleet (STACK_DECISIONS §2).
|
||||
const icon = { size: 16, strokeWidth: 1.5 } as const;
|
||||
|
||||
/**
|
||||
* Витрина — не экран продукта, а сверочная страница скриншот-цикла: панели, вкладки,
|
||||
* строка дерева, строка таблицы и пара оригинал/перевод на реальном тексте.
|
||||
* Кладётся рядом с references/fleet.png и сводится глазами (docs/FRONTEND_PLAN.md §4).
|
||||
* Витрина — не экран продукта, а сверочная страница скриншот-цикла. Раскладка при этом
|
||||
* несёт решения владельца 02.08 по структуре (docs/PROGRESS.md, запись «Раскладка»):
|
||||
* навигация слева, документ в центре во всю высоту, справочное о читаемой главе справа.
|
||||
*/
|
||||
export function Showcase() {
|
||||
return (
|
||||
<div className={styles.shell}>
|
||||
<header className={styles.topbar}>
|
||||
{/* Кнопка есть ровно у той панели, которая существует: нижней панели в раскладке нет */}
|
||||
<div className={styles.topbarSide}>
|
||||
<PanelLeft {...icon} />
|
||||
<PanelBottom {...icon} />
|
||||
<PanelRight {...icon} />
|
||||
</div>
|
||||
<div className={styles.topbarTitle}>Путь книгохранилища</div>
|
||||
<div className={styles.topbarTitle}>Гу Чжэньжэнь</div>
|
||||
<div className={styles.topbarSide}>
|
||||
<Search {...icon} />
|
||||
<Download {...icon} />
|
||||
<Settings {...icon} />
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
|
@ -43,19 +41,18 @@ export function Showcase() {
|
|||
<section className={styles.panel}>
|
||||
<div className={styles.panelHeader}>
|
||||
<span className={`${styles.tab} ${styles.tabActive}`}>Книги</span>
|
||||
<span className={styles.tab}>Банк памяти</span>
|
||||
<span className={styles.tab}>Поиск</span>
|
||||
</div>
|
||||
<ul className={styles.panelBody}>
|
||||
<li className={styles.row}>
|
||||
<ChevronDown {...icon} className={styles.chevron} />
|
||||
<BookText {...icon} className={styles.chevron} />
|
||||
<span className={styles.rowTitle}>Путь книгохранилища</span>
|
||||
<span className={styles.rowTitle}>Гу Чжэньжэнь</span>
|
||||
</li>
|
||||
{chapters.map((chapter, index) => (
|
||||
<li
|
||||
key={chapter.title}
|
||||
className={`${styles.row} ${styles.rowNested} ${index === 2 ? styles.rowSelected : ''}`}
|
||||
className={`${styles.row} ${styles.rowNested} ${index === 1 ? styles.rowSelected : ''}`}
|
||||
>
|
||||
<FileText {...icon} className={styles.chevron} />
|
||||
<span className={styles.rowTitle}>{chapter.title}</span>
|
||||
|
|
@ -72,33 +69,38 @@ export function Showcase() {
|
|||
<span className={styles.rowTitle}>Добавить главу</span>
|
||||
</li>
|
||||
</ul>
|
||||
{/* Единственный вход в настройки во всём интерфейсе (промт §3.12) */}
|
||||
<div className={styles.panelFooter}>
|
||||
<Settings {...icon} className={styles.chevron} />
|
||||
<span className={styles.rowTitle}>Настройки</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className={styles.center}>
|
||||
<section className={styles.panel}>
|
||||
<div className={styles.panelHeader}>
|
||||
<span className={`${styles.tab} ${styles.tabDocument} ${styles.tabActive}`}>
|
||||
Глава 1. Перед рассветом
|
||||
Раздел 2. Прозрение пятисот лет
|
||||
</span>
|
||||
<span className={`${styles.tab} ${styles.tabDocument}`}>
|
||||
Раздел 3. Церемония открытия
|
||||
</span>
|
||||
<span className={`${styles.tab} ${styles.tabDocument}`}>Глава 2. Медный ключ</span>
|
||||
</div>
|
||||
<div className={styles.panelBody}>
|
||||
{pairs.map((pair) => (
|
||||
<article
|
||||
key={pair.source}
|
||||
className={`${styles.pair} ${pair.note ? styles.pairNoted : ''}`}
|
||||
className={`${styles.pair} ${pair.flagReason ? styles.pairNoted : ''}`}
|
||||
>
|
||||
<div className={styles.source} lang="zh">
|
||||
{pair.source}
|
||||
</div>
|
||||
<div className={styles.target} lang="ru">
|
||||
{pair.target}
|
||||
{pair.target || <span className={styles.dim}>перевод не получен</span>}
|
||||
</div>
|
||||
{pair.note && <p className={styles.note}>{pair.note}</p>}
|
||||
{/* Показываем продуктовую фразу, а не вердикт движка: detail остаётся внутри */}
|
||||
{pair.flagReason && (
|
||||
<p className={styles.note}>{flagReasonLabels[pair.flagReason]}</p>
|
||||
)}
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
|
|
@ -106,52 +108,34 @@ export function Showcase() {
|
|||
|
||||
<section className={styles.panel}>
|
||||
<div className={styles.panelHeader}>
|
||||
<span className={`${styles.tab} ${styles.tabActive}`}>Банк памяти</span>
|
||||
{/* Единственное цветное пятно панели — акцентная рамка фокуса, как во Fleet.
|
||||
autoFocus здесь ради снимка: иначе акцент на витрине не проверить. */}
|
||||
<input className={styles.filter} placeholder="Фильтр терминов" autoFocus />
|
||||
<span className={styles.tab}>О книге</span>
|
||||
<span className={styles.tab}>Замечания</span>
|
||||
<span className={`${styles.tab} ${styles.tabActive}`}>Банк</span>
|
||||
</div>
|
||||
<div className={styles.panelBody}>
|
||||
<div className={`${styles.bankRow} ${styles.bankHead}`}>
|
||||
<span>Термин</span>
|
||||
<span>Перевод</span>
|
||||
<span>Тип</span>
|
||||
<span>Состояние</span>
|
||||
</div>
|
||||
{bankRows.map((row, index) => (
|
||||
<div
|
||||
key={row.term}
|
||||
className={`${styles.bankRow} ${index === 1 ? styles.rowCurrent : ''}`}
|
||||
>
|
||||
<span lang="zh">{row.term}</span>
|
||||
<span>{row.translation}</span>
|
||||
<span className={styles.dim}>{row.kind}</span>
|
||||
<span className={styles.dim}>{row.state}</span>
|
||||
<input className={styles.filter} placeholder="Фильтр терминов" autoFocus />
|
||||
{bankRows.map((row) => (
|
||||
<div key={row.src} className={styles.bankRow}>
|
||||
<span lang="zh">{row.src}</span>
|
||||
{/* Неподписанный термин приглушён: состояние читается насыщенностью, не цветом */}
|
||||
<span className={row.status === 'approved' ? styles.rowTitle : styles.dim}>
|
||||
{row.dst}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
{/* Связка справочного вида с рабочим: таблица подписи открывается вкладкой в центре,
|
||||
где есть ширина под колонки, клавиатуру и массовые действия (промт §3.5) */}
|
||||
<div className={`${styles.panelFooter} ${styles.panelFooterSplit}`}>
|
||||
<span className={styles.rowTitle}>Подписать банк</span>
|
||||
<span className={styles.dim}>подписано 9 из 11</span>
|
||||
</div>
|
||||
|
||||
<section className={styles.panel}>
|
||||
<div className={styles.panelHeader}>
|
||||
<span className={`${styles.tab} ${styles.tabActive}`}>О книге</span>
|
||||
<span className={styles.tab}>Замечания</span>
|
||||
</div>
|
||||
<dl className={styles.panelBody}>
|
||||
{bookFacts.map((fact) => (
|
||||
<div key={fact.label} className={styles.fact}>
|
||||
<dt className={styles.dim}>{fact.label}</dt>
|
||||
<dd className={styles.rowTitle}>{fact.value}</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<footer className={styles.statusbar}>
|
||||
<span>Путь книгохранилища / Глава 1. Перед рассветом</span>
|
||||
<span className={styles.statusRight}>переведено 12 из 18 · zh → ru</span>
|
||||
<span>Гу Чжэньжэнь / Раздел 2. Прозрение пятисот лет</span>
|
||||
<span className={styles.statusRight}>переведено 118 из 2284 · zh → ru</span>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,14 @@
|
|||
// Материал витрины. Текст реальный — фрагмент backend/example/chapter1-zh.txt с русским
|
||||
// переводом: выдуманный «Lorem ipsum» дал бы неверную плотность колонок, потому что кириллица
|
||||
// длиннее латиницы, а иероглифы короче всего.
|
||||
// Материал витрины. Формы данных сняты с движка, а не выдуманы — иначе моки разойдутся
|
||||
// с настоящим контрактом ещё до того, как появится HTTP-слой.
|
||||
//
|
||||
// ⚠ Источник фактуры лежит ВНЕ репозитория, на диске стенда (`/home/ubuntu/books/gu-zhenren/`):
|
||||
// прогоны, экспорты пар, банки. В git художественного текста почти нет, а оба
|
||||
// `backend/example/sample-*.db` пустые — там только схема. Привязка данных к конкретной
|
||||
// машине — открытый вопрос владельца, см. docs/PROGRESS.md.
|
||||
//
|
||||
// Фрагмент ниже — короткая выдержка из настоящего прогона `rerun2/export-dspro.json`
|
||||
// (蛊真人, zh→ru). Взят объём, которого хватает на проверку плотности колонок, и не больше:
|
||||
// исходник защищён авторским правом, и в репозиторий его целиком не кладут.
|
||||
//
|
||||
// Это ТОЛЬКО данные витрины. Продуктовые фикстуры живут в src/mock/ и заводятся на S3.
|
||||
|
||||
|
|
@ -15,57 +23,112 @@ export const chapterStates: Record<ChapterState, { label: string; colorToken: st
|
|||
};
|
||||
|
||||
export const chapters: { title: string; state: ChapterState }[] = [
|
||||
{ title: 'Глава 1. Перед рассветом', state: 'ready' },
|
||||
{ title: 'Глава 2. Медный ключ', state: 'attention' },
|
||||
{ title: 'Глава 3. Книга без названия', state: 'running' },
|
||||
{ title: 'Глава 4. Звук в глубине', state: 'running' },
|
||||
{ title: 'Глава 5. Разрешение старейшины', state: 'queued' },
|
||||
{ title: 'Глава 6. Пыль и старая бумага', state: 'queued' },
|
||||
{ title: 'Раздел 1. Нет раскаяния', state: 'ready' },
|
||||
{ title: 'Раздел 2. Прозрение пятисот лет', state: 'attention' },
|
||||
{ title: 'Раздел 3. Церемония открытия', state: 'running' },
|
||||
{ title: 'Раздел 4. Класс А', state: 'running' },
|
||||
{ title: 'Раздел 5. Деревня Гуюэ', state: 'queued' },
|
||||
{ title: 'Раздел 6. Первый гу', state: 'queued' },
|
||||
];
|
||||
|
||||
export type BankState = 'подписан' | 'предложен' | 'правится';
|
||||
// Поля — как в таблице `glossary` движка (store/migrate.go:176): src · dst · type · status ·
|
||||
// source. Значения тоже настоящие, из `minirun/guzhenren-minirun.db`.
|
||||
// `status` подписи ровно три: auto | draft | approved — и только approved инжектится
|
||||
// редактору как подтверждённый.
|
||||
export type BankType = 'name' | 'nickname' | 'title' | 'place' | 'org' | 'term';
|
||||
export type BankStatus = 'auto' | 'draft' | 'approved';
|
||||
|
||||
export const bankRows: { term: string; translation: string; kind: string; state: BankState }[] = [
|
||||
{ term: '美樱', translation: 'Мэйин', kind: 'имя', state: 'подписан' },
|
||||
{ term: '拓海', translation: 'Тохай', kind: 'имя', state: 'предложен' },
|
||||
{ term: '藏书阁', translation: 'книгохранилище', kind: 'место', state: 'подписан' },
|
||||
{ term: '长老', translation: 'старейшина', kind: 'титул', state: 'подписан' },
|
||||
{ term: '铜钥匙', translation: 'медный ключ', kind: 'предмет', state: 'правится' },
|
||||
{ term: '师兄', translation: 'старший брат по школе', kind: 'обращение', state: 'предложен' },
|
||||
{ term: '古籍', translation: 'древний свиток', kind: 'предмет', state: 'предложен' },
|
||||
];
|
||||
|
||||
// Единица пары — edit-unit из экспорта движка, а не абзац: местами вся глава окажется
|
||||
// одним блоком, и читалка обязана нормально выглядеть в этом случае.
|
||||
export const pairs: { source: string; target: string; note?: string }[] = [
|
||||
export const bankRows: {
|
||||
src: string;
|
||||
dst: string;
|
||||
type: BankType;
|
||||
status: BankStatus;
|
||||
source: 'seed' | 'ruby' | 'auto' | 'mined';
|
||||
}[] = [
|
||||
{ src: '方源', dst: 'Фан Юань', type: 'name', status: 'approved', source: 'seed' },
|
||||
{ src: '白凝冰', dst: 'Бай Нинбин', type: 'name', status: 'approved', source: 'seed' },
|
||||
{
|
||||
source: '黎明前的藏书阁,被一片寂静笼罩着。',
|
||||
target: 'Перед рассветом книгохранилище тонуло в тишине.',
|
||||
src: '花酒行者',
|
||||
dst: 'Монах Цветочного Вина',
|
||||
type: 'nickname',
|
||||
status: 'approved',
|
||||
source: 'seed',
|
||||
},
|
||||
{ src: '沈嬷嬷', dst: 'матушка Шэнь', type: 'nickname', status: 'draft', source: 'seed' },
|
||||
{ src: '古月山寨', dst: 'деревня Гуюэ', type: 'place', status: 'approved', source: 'seed' },
|
||||
{ src: '蛊', dst: 'гу', type: 'term', status: 'approved', source: 'seed' },
|
||||
{ src: '蛊师', dst: 'гу-мастер', type: 'title', status: 'approved', source: 'seed' },
|
||||
{ src: '开窍', dst: 'открытие апертуры', type: 'term', status: 'approved', source: 'seed' },
|
||||
{
|
||||
src: '春秋蝉',
|
||||
dst: 'Весенне-осенняя цикада',
|
||||
type: 'term',
|
||||
status: 'approved',
|
||||
source: 'seed',
|
||||
},
|
||||
{ src: '学堂', dst: 'школа', type: 'place', status: 'approved', source: 'mined' },
|
||||
{ src: '一气金光虫', dst: 'Золотосветный червь', type: 'term', status: 'draft', source: 'mined' },
|
||||
];
|
||||
|
||||
// Интерфейс не раскрывает конвейер (промт §4.1), а движок отдаёт вердикты своим языком:
|
||||
// `sanitizer_stripped`, «CJK leak in the ru output: 第一节». Поэтому между ними стоит слой
|
||||
// перевода: причина движка → продуктовая фраза. Новый вид замечания правит одну запись здесь,
|
||||
// компонент выноски не трогается. Поле `detail` пользователю не показывается никогда.
|
||||
export const flagReasonLabels: Record<string, string> = {
|
||||
glossary_miss: 'Термин из этого места не подписан в банке памяти',
|
||||
sanitizer_stripped: 'Часть блока не переведена, стоит перечитать',
|
||||
excision_suspect: 'Возможен пропуск: перевод заметно короче оригинала',
|
||||
loop_degenerate: 'Перевод зациклился, блок стоит перевести заново',
|
||||
content_filter: 'Блок не переведён: содержимое отклонено',
|
||||
};
|
||||
|
||||
// Единица пары — edit-unit из `tmctl export --pairs` (pipeline/export.go:25). Замечание
|
||||
// адресует блок целиком: байтовых смещений у проверок движка нет, только счётчики,
|
||||
// поэтому `flagReason` и `detail` — ровно то, что отдаёт движок.
|
||||
export const pairs: {
|
||||
source: string;
|
||||
target: string;
|
||||
flagReason?: string;
|
||||
detail?: string;
|
||||
}[] = [
|
||||
{
|
||||
source: '第二节:逆光阴五百年觉悟',
|
||||
target: 'Раздел 2: Прозрение пятисот лет против течения времени',
|
||||
},
|
||||
{
|
||||
source:
|
||||
'「师兄,我们真的可以进来吗?」美樱压低声音问道。她的手里,紧握着一把古旧的铜钥匙。\n' +
|
||||
'「放心,长老已经准许了,」拓海答道,「趁天亮之前,把那卷古籍找出来。」',
|
||||
'传说中,这个世界存在着一条光阴之河,支撑着这个世界的流转。而利用春秋蝉的力量,就能逆流而上,回到从前。\n' +
|
||||
'对这个传闻,世人众说纷纭。很多人并不相信,有些人则将信将疑。\n' +
|
||||
'几乎没有人真的确信。',
|
||||
target:
|
||||
'— Брат, нам правда можно сюда? — понизив голос, спросила Мэйин. В руке она сжимала старый медный ключ.\n' +
|
||||
'— Не тревожься, старейшина уже дал разрешение, — ответил Тохай. — Успеем до света найти тот свиток.',
|
||||
note: 'Обращение к старшему по школе переведено двумя способами в пределах главы',
|
||||
'В преданиях говорилось, что в этом мире существует река времени, поддерживающая его течение, и с помощью силы Весенне-осенней цикады можно подняться против потока и вернуться в прошлое.\n' +
|
||||
'Об этом слухе люди судили по-разному. Многие не верили вовсе, иные верили наполовину.\n' +
|
||||
'Почти никто не был уверен по-настоящему.',
|
||||
},
|
||||
{
|
||||
source: '但是现在,方源信了,彻底地信了。铁一般的事实摆在他的面前,不容他反驳——他真的重生了!',
|
||||
target:
|
||||
'Но теперь Фан Юань уверовал полностью и бесповоротно. Перед ним лежал железный факт, не допускающий возражений: он действительно переродился!',
|
||||
flagReason: 'glossary_miss',
|
||||
detail: 'В блоке встречается термин без подписи в банке памяти',
|
||||
},
|
||||
{
|
||||
source:
|
||||
'两人在书架之间静静穿行。窗外,东方的天际泛起一丝鱼肚白。尘埃的气味,与旧纸的清香,在空气中浮动。美樱停下脚步,向一本书伸出手去。',
|
||||
target:
|
||||
'Они бесшумно шли между стеллажами. За окном восточный край неба уже отдавал бледной белизной. В воздухе плыл запах пыли и сухой аромат старой бумаги. Мэйин остановилась и потянулась к одной из книг.',
|
||||
'因为每一次使用春秋蝉,都必须付出生命,将整个身躯和所有的修为统统献祭,作为驱动的力量。\n' +
|
||||
'这个代价实在太昂贵了,更让人无法接受的是——往往付出了生命,也不知道结果怎样。',
|
||||
target: '',
|
||||
flagReason: 'content_filter',
|
||||
detail: 'upstream refusal: content policy',
|
||||
},
|
||||
];
|
||||
|
||||
export const bookFacts: { label: string; value: string }[] = [
|
||||
{ label: 'Название', value: 'Путь книгохранилища' },
|
||||
{ label: 'Название', value: 'Гу Чжэньжэнь' },
|
||||
{ label: 'Оригинал', value: 'китайский' },
|
||||
{ label: 'Перевод', value: 'русский' },
|
||||
{ label: 'Жанр', value: 'сянься' },
|
||||
{ label: 'Глав', value: '18' },
|
||||
{ label: 'Объём', value: '412 тыс. знаков' },
|
||||
{ label: 'Разделов', value: '2284' },
|
||||
{ label: 'Объём', value: '7,78 млн знаков' },
|
||||
{ label: 'Добавлена', value: '28.07.2026' },
|
||||
{ label: 'Состояние', value: 'переводится' },
|
||||
];
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue