docs: update a lot of plans, readmes, descriptions

This commit is contained in:
2026-09-27 09:10:26 +05:00
parent 9c6f223b13
commit 82207c82f2
47 changed files with 1029 additions and 4615 deletions
+42 -75
View File
@@ -1,89 +1,56 @@
# Документация проекта
> Статус: **active**. Это индекс документов и карта источников истины для
> разработчиков и агентов. Подробные правила находятся в самих документах, а
> обязательный краткий контракт — в `AGENTS.md`.
> Статус: **active**. Индекс документов. Обязательный краткий контракт — в
> `AGENTS.md`, подробные правила — в самих документах.
## Порядок чтения
1. `AGENTS.md` — команды и обязательные инварианты.
1. `AGENTS.md` — команды, инварианты и карта «что читать под задачу».
2. Этот файл — выбрать актуальный документ по теме.
3. Соответствующий план или checklist темы.
4. Исходники, конфигурация и тесты рядом с изменяемым кодом.
## Активные документы
| Документ | Роль | Статус |
| ---------------------------------- | ---------------------------------------------------- | ------ |
| `AGENTS.md` | Краткий обязательный контракт для агента | active |
| `docs/agent-workflow.md` | Подробный порядок исследования, изменения и проверки | active |
| `docs/testing-strategy.md` | Locator, fixture, i18n и fixme-policy для e2e | active |
| `docs/quality-gates.md` | Команды проверки, warnings и CI | active |
| `docs/backlog.md` | Продуктовые, UX- и инфраструктурные задачи | active |
| `docs/roadmap.md` | Общие направления развития | active |
| `docs/plan-seo.md` | SEO/GEO-план | active |
| `docs/plan-platform.md` | PWA/API/WASM-план | active |
| `docs/checklist-manual-testing.md` | Ручная приёмка production UI | active |
| `docs/tools-map.md` | Живая карта инструментов | active |
| `web/eslint-plugins/README.md` | Детали custom lint rules и token policy | active |
| `docs/archive/README.md` | Правила архивирования и удаления старых документов | active |
| Документ | Когда читать | Статус |
| ---------------------------------- | -------------------------------------------------- | ------ |
| `AGENTS.md` | Всегда: контракт агента | active |
| `docs/architecture.md` | Слои, поток данных, новый инструмент/поле/маршрут | active |
| `docs/quality-gates.md` | Какие проверки запускать, пороги, coverage, CI | active |
| `docs/decisions.md` | Правка самих гейтов; чтение CI после пуша | active |
| `docs/agent-workflow.md` | Порядок работы, нагрузка на машину, протокол ревью | active |
| `docs/testing-strategy.md` | Правка e2e: locator-ы, fixtures, browser matrix | active |
| `docs/backlog.md` | Продуктовые, UX- и инфраструктурные задачи | active |
| `docs/roadmap.md` | Направления развития, фазы 1–10 | active |
| `docs/tools-map.md` | Актуальный список инструментов против реестра | active |
| `docs/tools-audit.md` | Сырые UX/UI-находки по инструментам (вход аудита) | active |
| `docs/plan-fields-audit-fixes.md` | Разбивка аудита на волны правок | draft |
| `docs/plan-seo.md` | SEO/GEO-план | active |
| `docs/plan-platform.md` | API/PWA/шрифты-план | active |
| `docs/checklist-manual-testing.md` | Ручная приёмка production UI | active |
| `web/eslint-plugins/README.md` | Правка lint-правил, токенов и их конфигов | active |
| `docs/archive/README.md` | Архивирование и удаление старых документов | active |
`docs/archive/` содержит временную историческую справку и не является текущим
источником критериев приёмки. Для старых документов действует срок хранения 30
дней; после него их удаляют, а при необходимости достают из git history.
`docs/plan-domain.local.md` исключён из git через `.gitignore` (выбранное имя
домена не должно попасть в историю до покупки). Ссылки на него в `plan-seo.md` и
`backlog.md` при клонировании репозитория не разрешаются — это ожидаемо.
## Где хранить новую информацию
## Правила
- Поведение, которое агент обязан соблюдать сразу, — кратко в `AGENTS.md`.
- Точные команды, параметры скриптов и критерии качества — в
`docs/quality-gates.md` или в README соответствующей подсистемы.
- Архитектура, потоки данных и границы модулей — в отдельном architecture-плане
или документе, не дублируя существующие планы.
- Реализация lint-правил, fixtures и технические детали ESLint — в
`web/eslint-plugins/README.md` и исходниках правил.
- Продуктовые решения и ручная приёмка — в backlog, плане и checklist.
У документа должен быть один источник истины: остальные файлы ссылаются на него,
а не пересказывают. Подробные материалы хранятся в `docs/`, чтобы не засорять
корень репозитория. Не создавать отдельные `docs/commands.md`,
`docs/known-issues.md` или копию backlog: команды принадлежат `quality-gates`,
известные проблемы — backlog и checklist. `README.md` в корне остаётся кратким
обзором продукта, а не вторым руководством агента.
Новый документ создаётся только когда темы нет в существующем файле; иначе
обновляется источник истины. Активный документ имеет статус (`draft`, `active`,
`completed`) и понятную область применения.
Подробные материалы хранятся в `docs/`, а не в корне. Новый файл создаётся
только если темы нет в существующем документе; иначе обновляется источник
истины.
## Текущий baseline
- production registry содержит 122 инструмента;
- `pnpm --dir web check` проходит без ошибок и предупреждений;
- `pnpm --dir web test` — 46 test files / 534 tests, включая тесты token audit,
PostCSS HCT-плагина и правил линтинга;
- `pnpm --dir web test:rules` — 5 test files / 104 tests;
- `pnpm --dir web lint:all` — 0 errors, 0 i18n warnings и 0 unused design tokens
от token audit;
- coverage `src/lib` — statements 91.77%, branches 83.3%, functions 74.31%,
lines 92.53% при порогах 90 / 82 / 72 / 91;
- `pnpm verify` проходит;
- `pnpm --dir web test:e2e` — 276 тестов в четырёх проектах: 276 passed, 0
skipped, 0 failed. Отказ декодера битого PNG зафиксирован per-engine в
`web/e2e/png-fixtures.spec.ts`.
Состав проверок и их семантика описаны в `docs/quality-gates.md`, тестовая
архитектура — в `docs/testing-strategy.md`. Baseline обновляется здесь при
изменении любого из гейтов.
## Правила актуальности
- У активного документа есть статус: `draft`, `active`, `completed` или
`archived`.
- Ссылки на конкретный файл или команду проверяются при изменении источника
истины.
- Архивный документ не возвращается в live-критерии без явной сверки.
- После каждого шага плана агент останавливается, показывает изменённые файлы,
проверки и нерешённые решения, и ждёт ревью.
- У каждой темы ровно один владелец: остальные документы ссылаются на него, а не
пересказывают. Новый документ создаётся, только если темы нет в существующем.
- У документа есть статус (`draft`, `active`, `completed`, `archived`) и
понятная область применения. Ссылки на конкретный файл или команду проверяются
при изменении источника истины.
- Подробные материалы живут в `docs/`, а не в корне. Не создавать отдельные
`docs/commands.md` или `docs/known-issues.md`: команды принадлежат
`quality-gates`, известные проблемы — backlog и checklist. `README.md` в корне
остаётся кратким обзором продукта, а не вторым руководством агента.
- Ссылаясь на инструмент по id, сверять id с реестром (`web/src/lib/registry/`):
исторические названия в старых планах не действуют.
- `docs/backlog.md` содержит только открытые задачи: выполненные удаляются, а
критичные для проекта факты из них переносятся в тематический документ.
- `docs/archive/` — временная историческая справка, не источник критериев
приёмки; срок хранения 30 дней, файлы восстановимы из git history.
- Протокол «остановиться и показать изменения» — `docs/agent-workflow.md`,
раздел 6.
+28 -71
View File
@@ -1,46 +1,31 @@
# Workflow для агентов
> Статус: **active**. Подробная процедура работы с репозиторием; краткие
> обязательные правила остаются в `AGENTS.md`. Этот документ не заменяет source
> of truth конкретной подсистемы.
> Статус: **active**. Порядок исследования, изменения и проверки. Обязательные
> инварианты — в `AGENTS.md`, этот документ не заменяет source of truth
> подсистемы.
## 1. Подготовка к задаче
1. Прочитать `AGENTS.md` и `docs/README.md`.
1. Прочитать `AGENTS.md` и выбрать документ по карте «что читать под задачу».
2. Найти релевантный активный план, backlog или checklist.
3. Проверить `git status`: не перезаписывать чужие незакоммиченные изменения.
4. Найти существующие типы, константы, helpers и тестовые паттерны до создания
нового определения.
5. Определить границу изменения:
- production UI — `web/src/routes/**` и `web/src/lib/components/**`;
- shared — `web/src/lib/core/**` и `web/src/lib/theme.svelte.ts`.
Изменение shared-кода требует проверки production-потребителей. Не добавлять
исключения линтера ради отдельных модулей без записи в baseline.
4. Определить границу изменения: production UI, shared-модуль или инфраструктура
— `AGENTS.md`, раздел «Границы изменений». Изменение shared-кода требует
проверки production-потребителей.
5. Не добавлять исключения линтера ради отдельных модулей без записи в baseline.
## 2. Порядок изменения
- Сначала исправляются тесты или контракт, который фиксирует неверное поведение,
затем production-код — если это не отдельная продуктовая задача.
- Один PR решает одну проблему и не объединяет lint, i18n, UI и архитектурный
рефакторинг без необходимости.
- Изменения держатся небольшими и атомарными, примерно до 500 строк или меньше.
- Не добавлять комментарии, которые пересказывают код. Комментарий объясняет
причину решения или фиксирует неочевидный инвариант.
- Не коммитить изменения автоматически: коммит делает разработчик после ревью.
- Не начинать следующий этап плана в том же шаге: изменение атомарное, ревью
идёт между шагами (раздел 6).
## 3. Проверки
Команды и их область применения перечислены в `AGENTS.md`; точная матрица и
текущая политика warnings/errors поддерживаются в `docs/quality-gates.md`.
| Изменение | Минимальная проверка |
| ------------------------------------------------------- | ------------------------------------------------------ |
| Только Markdown | `pnpm check:docs` |
| TypeScript/Svelte | `pnpm --dir web check`, `pnpm --dir web test` |
| Lint-конфиг или custom rule | `pnpm --dir web test:rules`, `pnpm --dir web lint:all` |
| Маршрут, загрузка, результат, download, тема или локаль | `pnpm --dir web test:e2e` |
| Перед релизом | полный quality gate из плана Q1 |
Матрица «изменение → минимальный набор проверок», состав `pnpm verify`, политика
warnings и чтение CI — в `docs/quality-gates.md`. Здесь только то, что относится
к работе агента.
`lint:all` может завершиться успешно при warnings. Это не означает чистый
baseline: нужно отдельно показать число errors, warnings и token warnings.
@@ -56,75 +41,47 @@ E2E запускается отдельно от быстрых проверок
- Playwright-прогоны запускаются только по явной команде пользователя: один
прогон за раз, без циклов повторов и без фонового запуска.
- Локально `workers: 1` (`web/playwright.config.ts`). Browser matrix из четырёх
проектов проходит медленно, зато не забирает память и не мешает работе
пользователя. Скорость проверки уступает отзывчивости компьютера.
- Локально `workers: 1` (`web/playwright.config.ts`). Browser matrix проходит
медленно, зато не забирает память и не мешает работе пользователя. Скорость
проверки уступает отзывчивости компьютера.
- Нельзя запускать тяжёлый прогон параллельно с другим прогоном или в момент,
когда пользователь работает с приложением.
- Диагностика флаков идёт от меньшего к большему: один spec, один проект,
`--workers=1`, и только затем полный прогон.
- Таймауты не подбираются под конкретную машину. Если проверка падает из-за
нехватки ресурсов, это фиксируется как ограничение окружения, а не лечится
увеличением `expect.timeout`.
увеличением `expect.timeout` (обоснование — `docs/decisions.md`, раздел 7).
- При необходимости повторного прогона сначала спрашивается пользователь.
## 4. Работа с тестами
Политика locator-ов, запрещённые зависимости, fixtures, browser matrix и
`test.fixme` — в `docs/testing-strategy.md`. Здесь только рабочая рутина:
- Поведенческий тест проверяет пользовательский контракт, а не текущую
реализацию.
- Сначала предпочитать `role`, `aria-*`, label, stable id или href.
- `data-testid` ограничен source/result/verdict/empty-state; общие
locator-паттерны собраны в `web/e2e/helpers/page.ts`, policy — в
`docs/testing-strategy.md`.
- `data-testid` использовать только там, где семантического locator нет.
- Спецификация вызывает общие операции из `web/e2e/helpers/page.ts`, а не
повторяет raw-селекторы.
- Не фиксировать абсолютное число инструментов, если проверка не связана с этим
инвариантом.
- Точные переводы проверять в словарях и i18n helpers; в e2e проверять поведение
и отдельный locale smoke.
- `test.fixme` допускается только для зарегистрированной проблемы или явного
тестового долга, с ссылкой на backlog или checklist. После исправления тест и
его issue обновляются вместе.
- Новые fixtures должны быть воспроизводимыми и не требовать ручного создания
больших бинарных файлов.
## 5. Документация
Источник истины выбирается по теме:
- обязательное поведение агента — `AGENTS.md`;
- индекс и карта документов — `docs/README.md`;
- качество и точные команды — `docs/quality-gates.md`;
- тестовая архитектура — `docs/testing-strategy.md`;
- lint implementation — `web/eslint-plugins/README.md`;
- продуктовые решения — backlog и активные планы.
Не дублировать длинные объяснения в нескольких файлах. Если правило требует
пояснения, в `AGENTS.md` оставить короткую понятную формулировку и ссылку на
подробный документ.
### Архивирование завершённых документов
- Если план или исследование больше не является рабочим источником, перенести
файл в `docs/archive/`.
- В начало файла добавить `Статус`, `Архивирован`, `Удалить после` и причину.
- Архив хранить 30 дней для быстрой справки; после срока удалить после проверки
ссылок. Если решение понадобится снова, использовать git history.
- Не переносить активный план в архив только потому, что он пока не начат.
Порядок работы с документами и карта источников истины — `docs/README.md`.
Архивирование — `docs/archive/README.md`.
## 6. Протокол ревью между шагами
После каждого шага плана агент:
1. останавливается и не начинает следующий Q-этап;
1. останавливается и не начинает следующий этап;
2. перечисляет изменённые файлы и принятые решения;
3. указывает выполненные проверки и их фактический результат;
4. отдельно перечисляет warnings, fixme, долги и вопросы;
5. ждёт подтверждения на продолжение.
Коммит и пуш делает разработчик. После пуша агент проверяет результат CI и
сообщает его вместе с локальными проверками, включая случай, когда нужный job не
запускался: статусы смотрятся по отдельным job, потому что общий conclusion
прогона с `continue-on-error` у `e2e` остаётся зелёным. Процедура описана в
`docs/quality-gates.md`.
Следующий шаг начинается только после ревью текущего изменения.
Коммит и пуш делает разработчик. Следующий шаг начинается только после ревью
текущего изменения; после пуша результат CI читается по процедуре из
`docs/decisions.md`, раздел 10.
+172
View File
@@ -0,0 +1,172 @@
# Архитектура
> Статус: **active**. Описание текущего кода: границы модулей, поток данных от
> реестра до скачивания, точки расширения. Канонический источник истины — сам
> код; этот документ объясняет, **куда смотреть и что с чем связано**, и не
> дублирует сигнатуры.
## 1. Слои
```txt
web/src/lib/
registry/ # источник истины: записи ToolEntry
registry-schema.ts # декларативная схема параметров + санитизация дефолтов
catalog.ts # группировка и счётчики для /list-tools
categories.ts # id/названия категорий
tool-icons.ts # id → иконка
core/ # чистый TS: операции над ImageData, без DOM
executor/ # единственная точка исполнения инструментов
i18n/ # словари ru/en, t(), списки для i18n-правил линтера
components/ # UI (fields/, layout/, ui/, schema-компоненты)
zip.ts # сборка мультифайлового результата (1 → many)
theme.svelte.ts # light/dark + localStorage
web/src/routes/
+page.svelte # главная
list-tools/ # каталог
tools/[id]/ # страница инструмента
kit/ # витрина компонентов (production, но исключена из i18n-гейта)
```
Правило зависимостей: `components/**` и `routes/**` знают про `registry/**`,
`executor/**` и `core/**`; `core/**` не знает ни о ком и не зависит от DOM;
`registry/**` описывает данные и знает только `core/**` и `registry-schema.ts`.
Нарушение этого порядка — повод не заводить импорт, а инвертировать зависимость.
## 2. Поток данных
```txt
registry/*.ts ──(TOOLS: ToolEntry[])
│
├─► catalog.ts ──► routes/list-tools (каталог, поиск, фильтры)
│
└─► routes/tools/[id]/+page.svelte
│ getTool(id) → запись реестра
▼
SchemaToolView.svelte оркестратор страницы
│ schema = tool.schema
├─► SchemaFields.svelte поля из schema.fields (FIELDS: kind → контрол)
├─► SchemaSourceTile/SchemaTextSource.svelte вход
└─► SchemaPreview + schema-preview-model.ts вид превью
│ buildSchemaPreviewModel(inputMode, resultKind, …) → source/result/hasResult
▼
executor.execute(ctx) единственная точка исполнения
│ сериализация параметров → worker → worker-handler → tool.run(ctx)
▼
core/* пиксели
│
▼
SchemaResultTile / SchemaTextResult + SchemaActions (download/copy)
│ кодирование по ToolEntry.output (mime/ext/quality)
└─► zip.ts, если result === "files"
```
Ключевые решения:
- **Реестр — единственный источник истины.** Страница, форма и пайплайн строятся
из записи; новый инструмент = новая запись + функция `run`.
- **Исполнение идёт только через `executor.execute`.** Компоненты не зовут
`tool.run` напрямую — иначе нельзя перенести тяжёлое в worker/wasm без правки
вызывающего кода.
- **Форма параметров не знает про инструмент.** Она рендерится из
`registry-schema.ts`, поэтому добавление вида поля не трогает страницы.
- **Модель превью отделена от вёрстки.** `schema-preview-model.ts` — чистая
функция, покрыта unit-тестами; `SchemaPreview.svelte` только рисует её вывод.
## 3. Запись реестра
`ToolEntry` (`web/src/lib/registry/types.ts`):
| Поле | Смысл |
| ------------- | ------------------------------------------------------------ |
| `id` | уникальный ключ; он же URL-anchor и ключ в пайплайне |
| `title` | EN-строка реестра, переводится через `tools.*.title` |
| `description` | EN-строка реестра, переводится через `tools.*.description` |
| `category` | id из `categories.ts` |
| `schema` | `toolSchema({ fields, layout?, meta? })` |
| `input` | `"image" \| "text" \| "none"` — что ждёт инструмент на входе |
| `result` | `"image" \| "text" \| "verdict" \| "files"` — что отдаёт |
| `output` | mime/ext/quality для download (не у всех инструментов) |
| `run(ctx)` | чистая функция: `ctx` → `ToolResult` |
Хелперы `genTool` / `imgTool` / `textGen` из `types.ts` задают `run` для типовых
случаев (приведение типа результата), чтобы запись не повторяла обвязку. `TOOLS`
собирается в `registry/index.ts` из восьми групп; `getTool(id)` — единственный
способ достать запись по id.
## 4. Схема параметров
`registry-schema.ts` — единственное место, где описаны виды полей.
- **Виды поля** (`fieldSpecs`): `number`, `slider`, `color`, `select`, `text`,
`checkbox`, `dimension`, `color-pair`, `colors`, `offset`, `position9`,
`font-style`, `plate`, `gradient`.
- **Фабрика `field.*`** создаёт `Field<T>`; UI-контрол подбирается в
`SchemaFields.svelte` по `spec.kind` через карту `FIELDS`.
- **`schema.layout`** группирует поля (`resolveLayoutGroups`); раскладка
разделов — единственное место, где живёт порядок и группировка.
- **Значения по умолчанию** даёт `defaultSchemaParams`; `applySourceDefaults`
подставляет размеры текущего source; `sanitizeSchemaParams` приводит мусор к
допустимому виду. Правка поведения полей идёт здесь, а не в компоненте.
Новый вид поля требует: спека + фабрика `field.*` → запись в `fieldSpecs` →
контрол в `components/fields/schema/` → ключ в `FIELDS` → дефолт и санитизация →
i18n-подписи → тест в `registry-schema.test.ts`.
## 5. Исполнение
`executor/executor.ts` — фабрика `createExecutor(factories?)` плюс готовый
экспорт `execute`. Контракт асинхронный (`Promise`), параметры сериализуются,
исполнение уходит в `executor.worker.ts`; `worker-handler.ts` разбирает
сообщение и зовёт `run`. Если worker недоступен — прямой вызов.
Отсюда следует: подключение wasm позже означает замену реализации **внутри**
исполнителя, а не правку компонентов.
## 6. i18n
Словари — `web/src/lib/i18n/` (`en.ts`, `ru.ts`, общий `dict.ts` с
`LOCALES`/`BASE_LOCALE`). Пользовательский текст живёт только в словарях и
читается через `t()`; строки реестра (`title`/`description`) — источник для
EN-ключей `tools.*` и словарь-переводчик. Инварианты паритета ключей,
плейсхолдеров и запрет хардкода проверяются линтером, детали —
`web/eslint-plugins/README.md`.
## 7. Точки расширения
| Задача | Где править |
| ------------------------------ | ----------------------------------------------------------------- |
| Новый инструмент | запись в `registry/<group>.ts` + i18n-ключи + тесты (рецепт ниже) |
| Новый вид поля | `registry-schema.ts` + контрол + `FIELDS` (раздел 4) |
| Новый маршрут/страница | `web/src/routes/**` + ссылка в каталоге |
| Новый визуальный элемент | компонент в `components/**` по образцу `ui/` или `layout/` |
| Новый дизайн-токен | `web/src/app.css` (только там) + потребитель сразу |
| Тяжёлая операция → worker/wasm | `executor/**`, без правок компонентов |
| Мультифайловый результат | `result: "files"` + `zip.ts` |
## 8. Рецепт: добавить инструмент
1. **Найти соседей.** Поиск по ключу в `registry/<group>.ts` и по всему
`web/src/` — не заводить дублирующую константу, тип или утилиту.
2. **Выбрать группу** (`geometry`/`alpha`/`color`/`convert`/`filters`/
`generate`/`text`/`analyze`) и положить запись в её экспорт; при новой
категории — сначала `categories.ts`.
3. **Описать схему** через `field.*`, задать `layout`, если поля нужно
сгруппировать.
4. **Написать `run`** поверх `core/*`; пиксельные операции — чистые функции в
`core/`, без DOM.
5. **Проверить обязательные контракты** для нового id: entry попадает в `TOOLS`,
страница `/tools/<id>` prerender-ится, `input`/`result` согласованы с тем,
что делает `run`.
6. **Добавить i18n-ключи во все локали** сразу.
7. **Тесты:** unit — в `registry/*.test.ts` или рядом с `core`-функцией;
e2e-smoke — существующий spec по типу `input`/`result` (правила —
`docs/testing-strategy.md`).
8. **Обновить `docs/tools-map.md`:** новый инструмент обязателен в разделе
«Реализовано», формат строки — в шапке раздела. Проверяет
`web/src/lib/registry/tools-map-doc.test.ts`: отсутствующий в карте id и
несуществующий в реестре id роняют тест. Обоснование и границы автоматизации
— `docs/decisions.md`, раздел 4.
Ожидаемый минимум проверок: `pnpm --dir web check` и `pnpm --dir web test`; для
инструмента, меняющего пользовательский поток, — e2e (см. раздел 3
`docs/quality-gates.md`).
+12 -11
View File
@@ -10,7 +10,10 @@
актуальных правил используются `AGENTS.md`, `docs/README.md` и активные планы.
Если решение снова понадобится, его можно восстановить из git history или
создать новый активный документ.
создать новый активный документ: **архивные файлы удаляются, а не хранятся
вечно.** Удалённые планы восстановимы через
`git log --diff-filter=D -- name-only -- docs/archive/` и
`git show <commit>^:<path>`.
## Обязательные метаданные
@@ -24,19 +27,18 @@
> Использование: только историческая справка
```
Дата `Удалить после` — дата архивирования плюс 30 дней. Старые файлы, созданные
до введения политики, могут не иметь этих дат; они также считаются архивными и
не являются активными требованиями. При следующем касании их можно удалить или
привести к шаблону.
Дата `Удалить после` — дата архивирования плюс 30 дней.
## Порядок архивирования
1. Убедиться, что документ действительно завершён, отменён или заменён более
актуальным источником.
2. Добавить обязательные метаданные.
3. Переместить файл в `docs/archive/`.
4. Проверить ссылки из backlog, планов, checklist и `docs/README.md`.
5. Не менять архивный документ под новые требования.
2. Убрать из него ссылки на несуществующие пути и артефакты прошлых волн.
3. Добавить обязательные метаданные.
4. Переместить файл в `docs/archive/`.
5. Проверить ссылки из backlog, планов, checklist и `docs/README.md`: удалённые
файлы должны потерять и входящие ссылки.
6. Не менять архивный документ под новые требования.
## Удаление
@@ -44,8 +46,7 @@
- проверить, что документ не нужен для текущей работы;
- найти и удалить оставшиеся ссылки на него из активных документов;
- удалить файл из `docs/archive/`;
- при необходимости отметить удаление в истории изменений.
- удалить файл из `docs/archive/`.
До истечения срока архивный файл не превращается обратно в live-документ: если
он снова актуален, создаётся новый документ или возвращается версия из git
-118
View File
@@ -1,118 +0,0 @@
# Demo fidelity: /preview/demo vs refs-html/demo.html
> ⚠️ **УТРАТИЛ АКТУАЛЬНОСТЬ — архивная копия.** Актуальный план фикса верности и
> текущий статус пунктов B/F — в `docs/plan-design-fix.md` (Этап 1). Аудит
> верности теперь автоматизирован (`pnpm audit` → `web/audit/audit-report.md`;
> пиксель-дифф не используется); темы в `refs-html` переключаются (скрипт
> инжектится в `scripts/extract-static.mjs`). Ниже — исходный текст на момент
> написания.
Метод: Playwright, viewport 1440×900, скриншоты обеих страниц + замер
вычисленных стилей ключевых элементов + пиксель-дифф (без чтения картинки
моделью). Пиксель-дифф: **24.9%** (наш скролл 1731px, реф 1579px). Зоны различий
размазаны по всей высоте → править надо глобально, не точечно.
Теперь процесс автоматизирован: `pnpm audit` (Playwright) пишет
`web/audit/audit-report.md` — light/dark токены + поэлементный дифф стилей по
тексту. Пиксель-дифф убран (неудобен для работы). Фиксы можно делать в любой
момент и перепроверять аудитом.
## Баги (объективно, правим в любом случае)
- **B1. Кнопки/инпуты рендерятся шрифтом Arial, а не IBM Plex Sans.**
Доказательство: пробы `Reset pipeline` → `fontFamily: Arial`. Причина: у
`button/input` не задан `font-family: inherit`. Фикс: добавить в reset
`design2.css` `button, input, select, textarea { font-family: inherit; }`.
Влияет на все страницы. (P1)
- **B2. Двойной топбар.** Страница демо рендерит свой `.topbar` (brand / AUTO
PIPELINE / help / theme / lang), который перекрывается глобальным `fixed`
Header из `preview/+layout.svelte` (в нём только theme-toggle). В рефе — один
топбар. Контент демо-топбара (brand, статус, язык) визуально спрятан за
фикс-хедером. Фикс: убрать кастомный `.topbar` из демо и сделать хедер лейаута
реф-стайл топбаром (brand + version + status + help + theme + lang) — улучшит
и C9/C10. (P1)
## Расхождения дизайна (нужно решение)
- **F1. Фон холста.** Реф: плоский серый `#EEF1F4`. Наш: белый +
blueprint-сетка. Решение: совпасть с рефом (убрать сетку на демо) или оставить
сетку как сигнатуру нового дизайна.
- **F2. Бейдж типа шага** (BACKGROUND/TRANSFORM/STYLE). Реф: приглушённый серый
`#6D787F`. Решено C16: `StepCard` рендерит `type` как приглушённый моно-текст
(не синий бейдж); индекс шага (`step-index`) — синий `#1769D2`, как в рефе.
- **F3. Подписи превью-тайлов** (SOURCE / STEP 01 / FINAL OUTPUT). Реф: синий
`#1769D2`, weight 400. Решено C16: `PreviewTile` label — синий `#1769D2`.
- **F4. Раскладка preview-stack.** Реф: одна колонка (5 строк, выше). Наш: 2
колонки (grid). Решение: одна колонка (как реф) или оставить 2 колонки как
выбор нового дизайна.
- **F5. Вес лейблов контролов.** Реф: 400. Наш (`MonoLabel`): 500. Мелко.
- **F6. Мета-подписи** (DIMENSIONS/FORMAT/SIZE): реф fs 9px, наш 10px. Мелко.
- **F7. Копирайт:** `Add tool` у нас `+ Add tool`; кнопка `Download result` —
сверить стиль (в рефе синяя с белым текстом 478×42). Пробы
`Download result`/`Add tool`/`DIRECTION` вернули NOT FOUND — скорее артефакт
замера (вложенный текст/чип размера), проверить визуально.
- **F8. h1 line-height** чуть выше (наш 150px vs реф 115px) — мелкий зазор.
- **F9. Вертикальный ритм:** контент на ~152px выше, зазоры между секциями
отличаются (pipeline-head y 434 vs 400 и т.д.) — подровнять паддинги под реф.
- **F10. Внутренние отступы preview-панели** (Hide intermediate x 907 vs 1218) —
паддинг панели.
## Темы (отложено — F1-F4)
Серый фон `#EEF1F4` — это **светлая** тема. Уточнение по F1: наш токен
`--background` уже равен `#EEF1F4` (совпадает с рефом); видимое отличие — только
blueprint-сетка поверх фона, а не «белый vs серый». То есть F1 = «сетка против
плоского», не цвет фона.
Перед тем как сверять цвета бейджей/тайлов/раскладок (F2-F4), нужно сначала
зафиксировать палитры light/dark как единый источник токенов. Сейчас они есть в
`web/src/lib/styles/design2.css` (`:root` + `[data-theme="dark"]`), но не
сверены с рефом.
**Как проверить темы в refs-html:**
- Открыть `refs-html/demo.html` (и соседние `gradient.html`,
`background-remover.html`) через Playwright/браузер.
- Нажать theme-toggle в топбаре рефа (иконка луны/солнца) — переключает тему
(скорее всего меняет атрибут/класс на `<html>` или `.page` и переопределяет
CSS-переменные).
- Снять скриншоты светлой и тёмной темы, замерить computed-стиль `:root`
переменных рефа (через
`getComputedStyle(document.documentElement).getPropertyValue('--...')`) и
сопоставить с нашими токенами в `design2.css`.
- Рефы, похоже, не доделаны по тёмной теме — зафиксировать, какие именно токены
меняются и где расхождения. На основе этого выровнять наши токены, затем
вернуться к F2 (бейдж типа), F3 (тайлы), F4 (раскладка preview-stack).
Решение по F1-F4 принимается только после сверки токенов светлой/тёмной темы с
рефом.
**Состояние тем (уточнение):** наша dark-тема реализована в `design2.css`
(`[data-theme="dark"]`), но пока подключена только на preview-маршрутах (старые
страницы грузят `app.css`). Этого достаточно для сверки: токены, отличные от
light, соберём по всем `refs-html/*.html` (не только `demo.html`), остальные
токены не меняются. Light-токены уже совпадают с рефом (`--background`,
`--panel`, `--foreground`, `--muted`, `--line`, `--blue` равны).
## Состояние (C16)
Цветовые расхождения против `refs-html/demo.html` устранены: стек шрифтов сверен
с рефом (`"IBM Plex Sans", sans-serif` / `"IBM Plex Mono", monospace`), бренд
топбара — `--foreground`, индекс шага — синий, тип шага и подпись плитки — как в
рефе, `Segmented` — 10px/32px. Аудит: токены light 10 / dark 16, элементы 47.
Остаток — sub-pixel (10px→9px лейблы, letter-spacing) и структурные
(`display`/`rect` от наших flex/grid раскладок против статичного мока);
визуально незначимы, правим точечно при желании. Старые тесты (583) — без
регрессий.
## План действий (предлагаю)
> Актуальный пошаговый план фикса вынесен в `docs/plan-design-fix.md` (Этапы
> 0–1: рефакторинг + верность). Ниже — исходные наброски.
1. Сразу поправить B1 + B2 (чёткие баги, улучшают все страницы).
2. По F1–F4 — решение за тобой (совпадать с рефом дословно ИЛИ оставить как
сигнатуру нового дизайна).
3. F5–F10 — мелочи, подчистить в том же проходе, если решаем «совпадать с
рефом».
4. Переснять скриншоты и перепроверить пиксель-дифф.
-140
View File
@@ -1,140 +0,0 @@
# План: единая дизайн-система
> **СТАТУС: ВЫПОЛНЕН 2026-08-22.**
## 1. Цели и принципы
1. **Один источник правды** — все визуальные константы живут в токенах
`web/src/app.css`; ни один компонент не пишет «магические» цвета/размеры
напрямую.
2. **Никакого дублирования** — повторяющийся визуальный рецепт оформляется один
раз: либо токен, либо глобальный утилитарный класс, либо компонент в `ui/`.
3. **Простые компоненты важнее умных** — если проще сделать отдельные
`SliderField` и `TextField`, делаем два маленьких компонента, а не один
конфигурируемый монстр.
4. **Scoped-стили — только для раскладки** конкретного компонента (сетка,
отступы между блоками). Внешний вид контролов и текстов — из дизайн-системы.
## 2. Аудит: что дублируется прямо сейчас
- **«Приглушённый текст»** (`--text-muted` + размер 0.85–0.95rem) — повторён в
семи местах: `ToolPage` (.description, .hint), главная (.lead, .card-desc),
лейаут (.nav-link, footer), `Preview` (.dims), `ParamForm` (label),
`InfoPanel` (dt).
- **«Заголовок секции капсом»** (uppercase, letter-spacing, muted) — точная
копипаста в `ToolPage` и на главной.
- **Базовые инпуты** (padding, border, radius) — живут только внутри
`ParamForm`; любые новые типы контролов (слайдеры, пипетка цвета) пришлось бы
копировать вручную.
- **Блок «label + отступы»** — дублировался бы в каждом `*Field`; решается
обёрткой `Field.svelte`.
- **«Панель»** (surface + border + radius) — три ручные реализации одного
паттерна: `DropZone`, карточки главной, строки `InfoPanel`.
- **Кнопки** — глобальные классы `.primary`/`.secondary` без компонента;
состояния «занят/недоступен» каждый описывает заново.
- **Ошибка-баннер** — только в `ToolPage`, а понадобится минимум ещё раз в
workspace.
## 3. Токены (расширение `app.css`)
Существующие остаются; добавляются недостающие группы:
```css
:root {
/* типографика */
--text-xs: 0.8rem;
--text-s: 0.85rem;
--text-m: 0.95rem;
--text-l: 1.1rem;
--text-xl: 1.25rem;
--text-xxl: 1.75rem;
--leading-tight: 1.25;
/* тени */
--shadow-card: 0 4px 14px rgb(37 99 235 / 12%);
/* фокус */
--focus-ring: 2px solid var(--accent);
/* контрольные размеры контролов */
--control-height: 2.4rem;
--control-max-width: 16rem;
}
```
Правило именования: семантика, не значение (`--text-muted`, а не `--gray-500`).
## 4. Глобальные утилиты (`app.css`)
Только то, что не требует JS и не является интерактивным:
- `.text-muted` — приглушённый текст (размер наследуется);
- `.text-caption` — мелкий текст `--text-s`;
- `.heading-section` — заголовок секции капсом (вместо двух копипаст);
- `.panel` — поверхность с бордером/радиусом (DropZone, карточки,
InfoPanel-строки);
- `.error-banner` — баннер ошибки;
- `.visually-hidden` — скрытый, но читаемый скринридерами.
Элементные стили из текущего `app.css` (голые `input, select { ... }`) убрать —
внешний вид контролов переезжает в компоненты `ui/`.
## 5. Состав UI-компонентов (`web/src/lib/components/ui/`)
Все — тонкие обёртки над нативными элементами, без состояний кроме своих:
| Компонент | Пропы | Заменяет |
| ---------------------- | -------------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
| `Field.svelte` | `id`, `label`, `hint?`, слот контрола | label + раскладка поля; единая точка для hint'ов и ошибок валидации |
| `Button.svelte` | `variant: 'primary' \| 'secondary'`, `disabled`, `busy`, слот текста | глобальные `.primary`/`.secondary`, локальный busy в `DownloadButton` |
| `TextField.svelte` | `id`, `label`, `type: 'number' \| 'text'`, `min/max/step`, `value = $bindable()` | number/text ветки `ParamForm` |
| `SliderField.svelte` | `id`, `label`, `min/max/step`, `value = $bindable()`, показ значения рядом | ничего — готовая база для будущих слайдеров (яркость, качество, порог) |
| `SelectField.svelte` | `id`, `label`, `options`, `value = $bindable()` | select-ветка `ParamForm` |
| `CheckboxField.svelte` | `id`, `label`, `checked = $bindable()` | чекбокс `ParamForm` |
| `ColorField.svelte` | `id`, `label`, `value = $bindable()` | color-ветка `ParamForm`; точка будущего расширения пипеткой |
| `EmptyState.svelte` | `title`, `hint`, слот действия | «Обработка…», будущие пустые панели исходника/результата |
`Field` — чисто раскладочная обёртка
(`<div class="field"><label for>…</label><slot /></div>`), ничего не знает о
типе контрола. На ней построены `TextField`, `SliderField`, `SelectField`,
`ColorField`. Исключение — `CheckboxField`: у чекбокса label оборачивает контрол
(кликабельная строка), поэтому у него своя раскладка.
## 6. Миграция существующего
1. `ParamForm` — переписывается на `Field` + `*Field`-компоненты; собственная
стилизация контролов и label удаляется.
2. `DownloadButton` и кнопки `ToolPage` — на `Button`.
3. `DropZone`, карточки главной, строки `InfoPanel` — на `.panel` + свои
раскладочные стили.
4. `ToolPage`, главная, лейаут — приглушённые тексты на `.text-*`, заголовки
секций на `.heading-section`, баннер на `.error-banner`.
5. Из `app.css` удалить глобальные классы кнопок и голые стили инпутов после
перехода.
## 7. Порядок работ
| Этап | Содержимое | Проверка |
| ------------ | ----------------------------------------------------- | -------------------------------------------------------------------- |
| A. Фундамент | Токены + утилиты в `app.css` | `pnpm check` |
| B. Примитивы | Восемь компонентов в `components/ui/` | `pnpm check`; каждый используется хотя бы раз после этапа C |
| C. Миграция | Переделка пяти файлов из §6 | Визуально: страницы выглядят как раньше; `test/check/build` зелёные |
| D. Чистка | Удаление устаревших классов и дублей из scoped-стилей | grep: в `components/` нет hex-цветов и `var(--space…)` вне раскладки |
## 8. Критерии готовности
- Ни один компонент не задаёт цвет/радиус/шрифт мимо токенов или классов
системы.
- Все интерактивные контролы на страницах — экземпляры `ui/*`.
- `grep -E '#[0-9a-f]{3,6}' web/src/lib/components` не находит ничего, кроме
токенов в `app.css`.
- Визуальной регрессии нет (ручная сверка всех страниц до/после), тесты и билд
зелёные.
## 9. Что сознательно не делаем
- Никаких CSS-фреймворков и препроцессоров — обычный CSS, как решено на старте.
- Никаких тем/тёмного режима — но токены сразу семантические, чтобы тема
добавилась заменой значений `:root`.
- Никаких сложных составных компонентов (модалки, таблицы) — они появятся, когда
потребуется реальная задача.
-237
View File
@@ -1,237 +0,0 @@
# План начальной реализации: минимальный рабочий MVP
> **СТАТУС: ВЫПОЛНЕН 2026-08-22.**
>
> **Что сделано по плану:**
>
> - Шаг 0 — каркас: SvelteKit + adapter-static, design-токены на CSS, лейаут с
> навигацией по категориям ✔
> - Шаг 1 — ядро: `PixelImage`, декодирование файлов, кодирование PNG/JPEG/WebP,
> скачивание ✔
> - Шаг 2 — операции ядра: color / geometry / alpha / analyze, чистые функции
> без DOM ✔
> - Шаг 3 — реестр из 11 инструментов + санитизация параметров ✔
> - Шаг 4 — универсальные UI-компоненты (DropZone, Preview, ParamForm,
> DownloadButton, InfoPanel, ToolPage) ✔
> - Шаг 5 — маршруты: `/tools/[id]` с prerender всех страниц, главная-каталог по
> категориям ✔
> - Шаги 6–7 — обработка ошибок, состояния, полировка; верификация ✔
>
> **Checkpoint пройден:** все 11 инструментов вручную проверены в браузере
> (загрузка → применение → скачивание), включая найденный и исправленный баг
> resize с сохранением пропорций. Тесты 75/75, svelte-check 0 ошибок,
> статический экспорт собирается.
>
> **Попутные улучшения сверх плана:** корневой скрипт `pnpm dev`;
> `sanitizeParams` против пустых/невалидных значений полей; семантика «вписать в
> размеры» для resize.
## 0. Зафиксированные решения
- **Объём MVP:** только одиночные инструменты «загрузил → настроил → скачал».
Пайплайн-workspace (`/workspace`) вне скоупа этого плана.
- **Набор инструментов первого среза:** ~11 базовых EASY (см. §3). Достаточно,
чтобы проверить насквозь ядро → реестр → универсальную страницу.
- **Стили:** обычный CSS без фреймворка — design-токены на CSS custom
properties, ноль зависимостей.
- **Стек:** SvelteKit + `@sveltejs/adapter-static` + TypeScript, пакетный
менеджер pnpm (уже зафиксирован в корневом `package.json`). Весь код сайта — в
`web/`.
- **Ядро без DOM:** все пиксельные операции — чистые функции над `PixelImage`;
DOM/canvas допускается только в модуле IO и компонентах превью.
---
## 1. Шаг 0 — Каркас проекта
1. Создать SvelteKit-приложение в `web/`: `npx sv create web` (шаблон minimal,
TypeScript, без лишних аддонов).
2. `pnpm install` внутри `web/`.
3. `svelte.config.js`: заменить адаптер на `@sveltejs/adapter-static`.
4. `src/routes/+layout.ts`: `export const prerender = true;` — весь сайт
prerenderится в статику.
5. `src/app.css`: дизайн-токены (палитра, типографика, отступы, радиусы через
CSS custom properties) + минимальный сброс стилей.
6. `src/routes/+layout.svelte`: шапка с названием сайта и навигацией по
категориям инструментов (категории — константа из реестра), футер.
**Проверка:** `pnpm dev` открывает пустой каркас; `pnpm build && pnpm preview`
отдаёт статический экспорт.
---
## 2. Шаг 1 — Ядро: типы и ввод-вывод
Файлы в `web/src/lib/core/`:
- `types.ts`
```ts
export type PixelImage = {
width: number;
height: number;
data: Uint8ClampedArray;
}; // RGBA, как ImageData
```
- `io.ts` — единственное место ядра, где разрешён canvas/DOM:
- `decodeFile(file: File): Promise<PixelImage>` — `createImageBitmap(file)` →
отрисовка на canvas → `getImageData`;
- `encode(img: PixelImage, mime: 'image/png' | 'image/jpeg' | 'image/webp', quality?: number): Promise<Blob>`
— `putImageData` → `canvas.toBlob`;
- `downloadBlob(blob: Blob, filename: string): void`.
**Проверка:** юнит-смоук — декодировать фикстурный PNG, закодировать обратно,
размеры совпадают.
---
## 3. Шаг 2 — Операции ядра (чистые функции)
Файлы в `web/src/lib/core/`, каждая функция: `(img, params) => new PixelImage`,
без мутации входа:
- `color.ts`: `grayscale(img)` (luma), `invert(img)`,
`brightnessContrast(img, brightness −100..100, contrast −100..100)`.
- `geometry.ts`: `flip(img, axis)`, `rotate90(img, turns)`,
`crop(img, x, y, w, h)` — индексная математика; `resize(img, w, h)` —
билинейная интерполяция на TS (не `drawImage`, чтобы ядро осталось DOM-free и
воспроизводимым бит-в-бит).
- `alpha.ts`: `removeColorToAlpha(img, hex, tolerancePercent)` — цветовое
расстояние → альфа 0.
- `analyze.ts`: `imageInfo(img)` → `{ width, height, hasAlpha, colorCount }`.
**Проверка:** vitest на маленьких матрицах пикселей (2×2, 3×3): flip, rotate,
crop, grayscale — ожидаемые значения руками.
---
## 4. Шаг 3 — Реестр инструментов
`web/src/lib/registry.ts` — источник истины для страниц, форм и навигации:
```ts
export type ParamDef =
| {
id: string;
label: string;
type: "number";
min?: number;
max?: number;
step?: number;
default: number;
}
| {
id: string;
label: string;
type: "select";
options: { value: string; label: string }[];
default: string;
}
| { id: string; label: string; type: "checkbox"; default: boolean }
| { id: string; label: string; type: "color"; default: string };
export type ToolEntry = {
id: string; // slug маршрута
title: string;
description: string;
category: CategoryId;
params: ParamDef[];
run: (
img: PixelImage,
params: Record<string, unknown>,
) => Promise<PixelImage> | PixelImage;
};
```
Категории (константа там же): `convert`, `alpha`, `color`, `geometry`,
`analyze`.
### Состав первого среза — 11 инструментов
| # | id | Категория | Параметры |
| --- | -------------------------------- | --------- | ----------------------------------------------------- |
| 1 | `resize-png` | geometry | width, height, checkbox «сохранять пропорции» |
| 2 | `crop-png` | geometry | x, y, width, height |
| 3 | `rotate-png` | geometry | select: 90 / 180 / 270 |
| 4 | `flip-png` | geometry | select: horizontal / vertical |
| 5 | `grayscale-png` | color | — |
| 6 | `invert-colors-png` | color | — |
| 7 | `adjust-brightness-contrast-png` | color | brightness −100..100, contrast −100..100 |
| 8 | `convert-png-to-jpg` | convert | background color (подложка под альфу), quality 0..100 |
| 9 | `convert-png-to-webp` | convert | quality 0..100 |
| 10 | `remove-color-from-png` | alpha | hex color, tolerance % |
| 11 | `png-info` | analyze | — особый случай: не картинка, а текстовая сводка |
Для `png-info` в записи реестра добавляется флаг `resultType: 'info'`; остальные
— `'image'`.
**Проверка:** импорт реестра в тесте — все id уникальны, у каждого `params`
имеют дефолты, `run` определён.
---
## 5. Шаг 4 — Универсальные UI-компоненты
`web/src/lib/components/`:
- `DropZone.svelte` — drag&drop + клик; эмитит `File`; проверка MIME
(PNG/JPEG/WebP/GIF/BMP).
- `Preview.svelte` — рендер `PixelImage` на canvas с fit-масштабированием;
шахматный фон под прозрачность.
- `ParamForm.svelte` — генерирует контролы из `ParamDef[]`, двусторонний
`bind:values`.
- `DownloadButton.svelte` — encode выбранного формата + `downloadBlob`, имя
`<tool-id>.<ext>`.
- `InfoPanel.svelte` — текстовый вывод для `resultType: 'info'`.
- `ToolPage.svelte` — оркестратор страницы инструмента; состояния
`idle → loaded → processing → done | error`; раскладка: слева DropZone, справа
превью + форма параметров + кнопки «Применить» / «Скачать».
**Правило:** новый инструмент = новая запись в реестре (+ функция операции).
Компоненты и маршрут не трогаем.
---
## 6. Шаг 5 — Маршруты
- `/` (`src/routes/+page.svelte`) — сетка карточек инструментов, сгруппированных
по категориям; данные из реестра.
- `/tools/[id]` — универсальная страница:
- `+page.ts`: `entries()` возвращает все `id` из реестра (обязательное условие
prerender при adapter-static);
- `load()` возвращает запись реестра по `id`, неизвестный id → 404;
- `+page.svelte` — тонкая обёртка над `ToolPage`.
**Проверка:** `pnpm build` — в `build/tools/` лежат все 11 страниц; главная
содержит ссылки на каждую.
---
## 7. Шаг 6 — Ошибки и минимальная полировка
- Обработка ошибок: неподдерживаемый формат, ошибка декода, некорректные
параметры crop (выход за границы — клампы или понятная ошибка).
- Состояния: «перетащите файл», спиннер во время обработки, disabled-кнопки до
загрузки.
- Тексты описаний для всех 11 инструментов (title/description уже в реестре).
---
## 8. Шаг 7 — Верификация и checkpoint
Команды:
```bash
cd web && pnpm dev # разработка
cd web && pnpm build # статический экспорт должен проходить без ошибок
cd web && pnpm test # vitest: ядро (геометрия/цвет) + целостность реестра
```
Ручной smoke-тест каждого из 11 инструментов: загрузка PNG → применить → скачать
→ открыть результат.
**Checkpoint MVP:** любой из 11 инструментов доступен по прямому URL с главной
страницы; PNG загружается drag&drop; параметры применяются; результат
скачивается; всё работает офлайн со статического хостинга; ни одной серверной
зависимости.
-213
View File
@@ -1,213 +0,0 @@
# План: аудит верности `preview/*` против `refs-html` (DOM → CSS)
> **СТАТУС: РЕАЛИЗОВАН (2026-09-07) — устарел.** Скрипты
> `web/scripts/audit-dom.mjs`, `audit-css.mjs`, `audit-cdp.mjs`,
> `audit-cdp-responsive.mjs` и npm-скрипты `refs-dom-audit` / `refs-css-audit` /
> `refs-cdp-audit` на месте; старый текстовый `audit.mjs` удалён. Актуальная
> доводка верности и статус пунктов F — в `plan-design-fix.md`.
> Статус: к выполнению. Ветка `feat/redesign`. Источник правды по пикселям:
> `refs-html/*.html` (статический экспорт рефа). Актуальный рабочий аудит сейчас
> — `pnpm refs-audit` (`scripts/audit.mjs`), но он **сломан** (см. «Почему
> текущий аудит не работает») и подлежит замене.
## Почему текущий аудит не работает
`scripts/audit.mjs` сопоставляет элементы **только по тексту** и берёт в
сравнение **только листовые узлы** (у которых нет детей). Контейнеры/карточки/
панели, несущие `border-radius`, отступы и позиционирование, в сравнение
**вообще не попадают** (`byText` кладёт в мапу только элементы с непустым
`text`). Следствия:
- разница радиусов у карточек невидима (карточка — родитель без своего текста);
- позиция кнопки (`Download result`) не ловится — это структурная разница;
- `onlyOurs / onlyRef` — это просто счётчик текстовых строк на одной стороне,
**не метрика верности**.
Плюс он не делает пиксельного сравнения, то есть принципиально не может сказать
«выглядит по-другому». Это текстовый дифф стилей листовых текстов — и всё.
## Решения (подтверждены автором)
1. **Копирайт превью — через i18n `t()` с EN-базой.** Сейчас RU-литералы
хардкодом только в `preview/+page.svelte:26-27` и
`preview/list-tools/+page.svelte:31` (эти страницы не используют i18n, в
отличие от `(old)/` и компонентов `DownloadButton`/`ChainToolBlock`/…).
Заводим их через `t()` — совпадёт с рефом (EN).
2. **Имена классов НЕ сводим к рефу.** Матчинг делаем по **структурному пути**,
а не по классу. Компоненты `kit/` остаются идиоматичными, Stage 0 не
откатываем. Требуется только эквивалентность _структуры_ (теги, вложенность,
порядок сиблингов, наличие узлов, текст).
3. **Пиксельный дифф (Playwright + pixelmatch) пока НЕ делаем.** DOM+CSS-аудита
достаточно; добавим как сеть позже, если стили что-то упустят.
4. **Порядок прогона маршрутов:** `demo` → `list-tools` → затем 2 инструмента
(`tools/linear-gradient-png` ↔ `gradient.html`, `tools/remove-background-png`
↔ `background-remover.html`).
## Принципы
- Никаких правок старого дизайна `(old)/` и старого `ui/`.
- Коммиты атомарные (< ~500 строк), по 1–2 компонента/скрипта; после каждого —
`lint` + `svelte-check` + `build` + соответствующий аудит (diff не должен
расти без причины).
- Коммиты делает автор (агент не коммитит).
- Шрифты уже совпадают: `design2.css:1-6` грузит IBM Plex Sans/Mono через
`@fontsource`, токены `:34-35` — `"IBM Plex Sans"/"IBM Plex Mono"` (как в рефе
`demo.html:67-68`). Пиксельный шум от шрифтов не предвидится.
## Подход: две фазы
Корень проблемы — нет **стабильного ключа сопоставления**. Разбиваем на две
фазы: сначала доводим DOM до структурного равенства с рефом (фаза A), затем на
стабильном DOM сравниваем стили по тому же пути (фаза B). Фаза A сама по себе
ловит именно то, из-за чего возникли претензии (кнопка не в том родителе,
лишние/недостающие обёртки, перепутанный порядок) — и без эстетических решений.
---
## Фаза A — DOM/структурный аудит (`scripts/audit-dom.mjs`)
**Цель:** Svelte рендерит структуру, идентичную `refs-html` (дерево тегов,
вложенность, порядок сиблингов, наличие узлов, текст). Имена классов могут
отличаться.
### Нормализация узла (ключ сравнения)
Для каждого элемента (пропускаем `SCRIPT`/`STYLE`/`NOSCRIPT` и скрытые
поддеревья, игнорируем служебное):
- **тег** (`tagName` в lower-case);
- **классы** — как отсортированный набор (для инфо, не для матчинга);
- **нормализованный текст** — `trim`, `collapse-whitespace`, без SVG-иконок
внутри (текст иконок игнорируем);
- **порядок детей** — позиция среди сиблингов (`nth-child`).
Матчинг — по **структурному пути** от корня сравнения:
`tag:nth-child(i) > tag:nth-child(j) …`. Не по классу и не по тексту.
### Полное соответствие, без IGNORE
Оболочка (наш `TopBar`/`AppShell`/`preview-footer` vs рефовый
`.topbar`/`.footer`/`.language`) тоже сводим к рефу — **IGNORE-список не
заводим**. Сравниваем всё дерево от `<body>` (без пропуска служебного, кроме
Svelte-атрибутов и комментов гидрации), чтобы получить полное структурное
соответствие, а не только контента. Шапка/футер/переключатель языка — часть
верности; позже, после полного фикса дизайна, всё равно планируется рефакторинг
компонентов, где оболочку приведём в порядок и унифицируем. Аудит на этом этапе
просто показывает все расхождения, включая оболочечные.
### Дифф
Для каждого маршрута — набор дельт:
- `added` — у нас, нет в рефе;
- `removed` — в рефе, нет у нас;
- `moved` — тот же текст/тег, другой путь (перепутанный порядок/родитель);
- `textMismatch` — нормализованный текст различается (в т.ч. RU vs EN).
Класс-мисматч — **только информационно**, не блокирует (классы свои по решению
№2). Счётчики: `added / removed / moved / textMismatch`.
### Рендеринг
- **наше:** dev-сервер на `5179` (как сейчас в `audit.mjs`).
- **реф:** `pathToFileURL` к `refs-html/<ref>.html` (`file://`). Реф —
самодостаточный (инлайн `<style>`), рендерится полностью. Шрифты для фазы A не
важны (сравниваем структуру/текст, не растеризацию).
- Переиспользуем из текущего `audit.mjs`: `discoverRoutes()`, `EXCLUDE`
(`/preview`), `DYNAMIC` (`tools/[id]` → `gradient.html` /
`background-remover.html`), `refNameFor()`.
### Шаги (на каждом маршруте)
1. Прогнать `pnpm refs-dom-audit`, получить дельты.
2. Поправить структуру Svelte под реф (добавить/убрать/переставить узлы, вынести
тексты в `t()` с EN-базой).
3. Перезапустить, пока `added/removed/moved/textMismatch` ≃ 0 (кроме `IGNORE`).
---
## Фаза B — CSS-аудит по стабильному DOM (`scripts/audit-css.mjs`)
**Цель:** на совпадающем дереве сравнить вычисленные стили по тому же
структурному пути и получить «элемент по пути X: свойство → наше/реф».
### Сравниваемые свойства (computed)
`borderRadius`, позиция/размер (`getBoundingClientRect`: x/y/w/h), `padding`
(все стороны), `margin`, `color`, `backgroundColor`,
`borderTop/Right/Bottom/Left Width|Color`, `fontFamily`, `fontSize`,
`fontWeight`, `letterSpacing`, `lineHeight`, `gap`, `display`, `boxShadow`.
### Дифф
Для каждого состыкованного по пути элемента — список свойств с дельтой (наше
значение → реф). Элементы, не совпавшие по пути (остаток из фазы A), идут в
`unmatched`. Отчёт: элемент → свойство → наше/реф. Правится одной строкой CSS,
без брутфорса.
### Шаги
1. Прогнать `pnpm refs-css-audit` (только после того, как фаза A по маршруту
дала ≃ 0 структурных расхождений).
2. Поправить стили в `design2.css` / стилях компонентов.
3. Перезапустить, пока delta% по свойствам ≃ 0.
---
## Порядок маршрутов и особенности
1. **`/preview/demo`** (↔ `demo.html`) — уже ближе всего; идеальная «обкатка»
тулинга фаз A и B. Здесь же проверим, что `Download result` стоит в шапке
`.preview-top` (как в рефе), а не внизу панели.
2. **`/preview/list-tools`** (↔ `list-tools.html`) — объёмнее; **зависит от
`docs/plan-list-tools.md`**: реальный аудит имеет смысл только после того,
как страница переписана на `CatalogHeader`/`CatalogToolbar`/`CatalogGroup`
(шаги 1–5 плана list-tools). То есть фаза A/B для list-tools стартует после
landing этих шагов.
3. **`tools/linear-gradient-png`** (↔ `gradient.html`) и
**`tools/remove-background-png`** (↔ `background-remover.html`) —
**последние**. Известные расхождения: наши инструменты могут иметь **другие
наборы параметров / UI**, чем конкретные рефовые тулзы. Ожидаются структурные
и стилевые дельты, которые не сводятся к «подровнять паддинг». Для них —
триаж после прогона:
- реальный разрыв верности → фиксим;
- принципиальное отличие (параметр, которого нет в реф-тулзе; иной контрол) →
либо аккуратно приводим наш UI параметра к рефу, либо **документируем как
допустимое расхождение / заносим в allowlist** конфига аудита (селектор +
причина). «Скипы/фиксы» для инструментов оформляем явно, чтобы аудит не
шумел на осознанных отличиях.
---
## Структура скриптов
- `scripts/audit-dom.mjs` — фаза A (нормализация + структурный дифф).
- `scripts/audit-css.mjs` — фаза B (матчинг по пути + дифф стилей).
- `package.json`: `refs-dom-audit`, `refs-css-audit` (опционально `refs-audit`,
прогоняющий обе фазы).
- Переиспользуем из старого `audit.mjs`: `discoverRoutes`, `EXCLUDE`, `DYNAMIC`,
`refNameFor`, `waitFor` (fast-fail на 4xx/5xx).
- Отчёты: `web/audit/dom-report.{md,json}` и `web/audit/css-report.{md,json}`
(старый `audit-report.*` заменяем).
## Верификация (после каждого коммита)
- `pnpm lint`, `pnpm exec svelte-check --tsconfig ./tsconfig.json`, `pnpm build`
— без ошибок.
- `pnpm refs-dom-audit` (фаза A) / `pnpm refs-css-audit` (фаза B) по затронутому
маршруту: счётчики расхождений не растут без причины.
## Риски / заметки
- **Стабильность матчинга:** нужна аккуратная нормализация (сортировка классов,
игнор svelte-атрибутов/`data-*`, игнор whitespace-текстовых узлов, игнор
внутренностей SVG-иконок). Подстраивается по первым прогонам demo.
- **Оболочка** (TopBar/footer/язык) сейчас отличается по дизайну — сводим к рефу
в рамках полного соответствия (без IGNORE); финальная унификация компонентов —
отдельным рефакторингом позже.
- **Динамические маршруты:** `DYNAMIC` уже есть; оставляем.
- **Hover / адаптив / тёмная тема** — вне объёма статического аудита (initial
state, 1440×900, light). Отмечаем как будущие прогоны.
- **list-tools** зависит от landing `plan-list-tools.md` (шаги 1–5) — не
аудируем вслепую.
-114
View File
@@ -1,114 +0,0 @@
# План C17: перенос preview → корень + (old) → /v1/
> **СТАТУС: ВЫПОЛНЕН (2026-09-11) — устарел.** Перенос завершён:
> preview-страницы на реальных маршрутах, старый дизайн на `/v1/*`,
> `app.css`/`app_v1.css`, isolation-плагин на v1-путях, аудит-скрипты в
> `scripts/archive/`. C18 (правки импортов/редиректов, проверка билда) тоже
> выполнен. Следующий этап — C19–C21 (`plan-redesign.md`, §10).
>
> Оригинальный статус до выполнения: план к выполнению. Предпосылки: tech debt
> закрыт, backlog №14 (старые файлы → lib/old/) выполнен. Домен НЕ блокер (см.
> plan-redesign.md §10, plan-seo.md §6).
## Цель
Новый дизайн становится основным на корневых маршрутах (`/`, `/tools/[id]`,
`/list-tools`). Старый дизайн переезжает на `/v1/*` как архив для справки.
Удаление старого кода (C19) откладывается до полной приёмки нового дизайна.
## Структура маршрутов (до → после)
**До:** `(old)/` даёт URLs `/`, `/demo`, `/list-tools`, `/tools/[id]`.
`preview/` даёт `/preview`, `/preview/list-tools`, `/preview/tools/[id]`,
`/preview/kit`.
**После:** `v1/` даёт `/v1/`, `/v1/demo`, `/v1/list-tools`, `/v1/tools/[id]`.
Корень даёт `/`, `/list-tools`, `/tools/[id]`. Витрина kit — на реальном
маршруте `/kit` (дизайн в стадии тестирования).
## Решения
1. **`v1/` — реальная папка, НЕ route group** — group `(v1)/` не меняет URL, а
нам нужно чтобы старые страницы жили по `/v1/*`.
2. **Корневой layout: shell нового дизайна** — TopBar, Footer, тема, хлебные
крошки, импорт `app.css`, `@layer app, design;`.
3. **`v1/+layout.svelte` — только импорт `app_v1.css`** — без
header/footer/shell.
4. **Тема:** использовать `theme.svelte.ts` (не дублировать локальный `$state`).
5. **`@layer app, design;`** остаётся — design выигрывает у app везде.
6. **`noindex` остаётся** на `tools/[id]/+page.svelte` до покупки домена.
7. **CSS-файлы переименовываются:** `preview.css` → `app.css`, `old.css` →
`app_v1.css`. Все ссылки обновляются.
8. **`kit/` витрина → `/kit`** — переносится на реальный маршрут вместе с
остальными preview-страницами; отдельный `preview/+layout` удаляется (`/kit`
наследует корневой shell).
## Что делать (шаги)
### 1. Rename `(old)/` → `v1/`
Переименовать папку. Обновить `v1/+layout.svelte`: убрать «Preview v2» ссылку,
обновить пути навигации, поправить импорт CSS.
### 2. Новый корневой `+layout.svelte` + rename CSS
Перезаписать текущий нейтральный layout содержимым из `preview/+layout.svelte`
(с правками: тема через `theme.svelte.ts`, путь к CSS). Переименовать
`src/preview.css` → `src/app.css`, `src/old.css` → `src/app_v1.css`. Обновить
все ссылки на эти файлы в проекте ( layout, лайнтеры, eslint-плагины, скрипты,
тесты).
### 3. Move preview pages → корень
Переместить страницы из `preview/` в корень `routes/`: `+page` → `/`,
`list-tools` → `/`, `tools/[id]` → `/`, витрину `kit` → `/kit`. Удалить
`preview/+layout.svelte` (корневой shell заменяет его).
### 4. Update hardcoded `/preview/` URLs
В компонентах с захардкоженными путями `/preview/` заменить на корневые.
### 5. Flatten `kit/` → `components/`
Перенести компоненты из `lib/components/kit/` в `lib/components/`, обновить все
импорты, удалить пустую `kit/`.
### 6. Flatten `preview/` → `lib/`
Перенести код из `lib/previw/` в `lib/`, обновить все импорты, удалить пустую
`preview/`.
### 7. Перенести `old/` → `v1/`
- `$lib/old/` — переименовать в `lib/v1/`, обновить все импорты
### 8. E2E тесты + playwright config
Заменить `/preview/...` на `/...` во всех e2e файлах и конфиге playwright.
### 9. ESLint config + AGENTS.md
Обновить glob-паттерны: `routes/preview/**` → `routes/**`, `components/kit/**` →
`components/**`. Обновить `AGENTS.md`.
### 10. Isolation plugin
Обновить `DEFAULT_OLD`: `routes/(old)/**` → `routes/v1/**`. Убрать
`routes/preview/**` из `DEFAULT_NEW`.
### 11. Archive audit scripts
Перенести скрипты аудита в `scripts/archive/`, убрать вызовы из манифестов.
## Что НЕ меняется
- `routes/+layout.ts` — prerender = true остаётся
- Слаги инструментов (`*-png`) — без изменений
- `/kit` — витрина дизайна остаётся (в стадии тестирования)
## Риски
- **v1/ без shell** — старый дизайн без header/footer.
- **`@layer` взаимодействие** — проверить что design-токены перекрывают
app-токены на обоих маршрутах.
- **Isolation: shared-классификация** — маршруты нового дизайна shared до C19.
-593
View File
@@ -1,593 +0,0 @@
# План: составные типы параметров и полная типизация pipeline
> **СТАТУС: ЗАКРЫТ (2026-09-07).** Типизация pipeline, составные типы
> (dimension/color-pair/offset/position9/font-style/plate/gradient),
> `schema.layout`, изоляция веток и вся миграция инструментов (121/125,
> остальные 4 — отложены решениями) выполнены. Ключевые архитектурные решения
> (параллельная сборка, правило копий, изоляция old↔preview) закреплены в
> `AGENTS.md`. **Незакрытое из этого плана:** шаг 37 — физический переезд старых
> файлов в папки `old/` (перенесён в `backlog.md`), и удаление старого кода — по
> `plan-redesign.md` (C17–C21). Отложенные инструменты — `watermark-image-png`
> (overlay), `png-info`, `reduce-to-size` — в `backlog.md`. Ниже — полное
> состояние на момент закрытия.
> Статус: **в реализации.** Фаза 0 (фундамент) ✔, Фаза 1 (рабочий инструмент в
> preview) ✔, Фаза 2 (простые инструменты без составных типов) — переведены все
> одиночные инструменты (18 шт). Фаза 3 — **составной тип `dimension` полностью
> переведён** (12 инструментов: create-empty, single-color, random-noise,
> linear-gradient, color-spectrum, random-colors, draw-grid, placeholder,
> fit-on-background, change-canvas-size, resize, crop); **составной тип
> `color-pair` полностью переведён** (4 инструмента: blend-two, step-colors,
> linear-gradient, two-colors); **составной тип `offset` полностью переведён**
> (4 инструмента: circle-mask, square-mask, star-mask, wavy-mask); **составной
> тип `position9` — переведены add-text-png и date-stamp-png** (водяной
> знак-картинка — отдельный шаг: требует overlay-механику в новом превью);
> **составной тип `font-style` полностью переведён** (text-to-png, add-text,
> date-stamp); **составной тип `plate` полностью переведён** (add-text,
> date-stamp); **составной тип `gradient` полностью переведён**
> (linear-gradient): вместо горизонтального/ вертикального select — **угол
> 0..360°** (новый виджет `AngleControl`: slider + кнопки-пресеты
> 0°/90°/180°/270°, переиспользуемый компонент); preview научен применять
> **генераторы** (`executeGenerate`, кнопка Generate) и рендерить все kinds
> схемы (slider/number/color/select/checkbox/dimension/
> color-pair/offset/position9/font-style/plate/gradient). **Пер-инструмент
> раскладка `schema.layout` реализована** (шаг 32): тип `ToolSchemaLayout`
> {groups: [{title?, cols?, fields}]}, рендер групп в `SchemaFields.svelte`
> (заголовок группы + сетка колонок, неупомянутые поля — в общей группе), пилот
> — `add-text` (группы Text/Placement/Plate). **UI-макеты расставлены по
> переведённым инструментам** (шаг 33): geometry (fit-on-background,
> change-canvas-size, resize, crop), маски alpha (Shape/Position), generate
> (create-empty, linear-gradient, color-spectrum, random-colors, draw-grid,
> step-colors, placeholder, text-to-png), text (add-text, date-stamp), filters
> (randomize-pixels, add-noise) — везде, где канвас отделён от параметров
> эффекта либо фигура от позиции. Следующее закрыто в
> `archive/plan-migrate-remaining.md` (2026-09-07): перевод оставшихся
> инструментов выполнен (всего 121/125 в preview; 4 хвоста —
> `watermark-image-png`, `png-info`, `reduce-to-size-png`, `compress-png` —
> отложены решениями в `backlog.md`). Незакрытые продолжения: переезд старых
> компонентов в папки `old/` (шаг 37 ниже), закрытие долгов и сбор фидбека — см.
> также `plan-design-fix.md`.
>
> Ключевые файлы нового registry: `web/src/lib/registry-new/{types,index,*}.ts`
> (по файлу на категорию: geometry/alpha/convert/analyze/filters/color/generate)
>
> - `web/src/lib/preview/categories.ts`, `web/src/lib/registry-schema.ts` (kind
> `dimension`), `kit/fields/DimensionField.svelte`,
> `preview/executor/index.ts` (`executeGenerate`), `SchemaFields.svelte`
> (полный рендер kinds). Старый `web/src/lib/registry.ts` разбит по категориям
> в `web/src/lib/registry/` (см. `registry.ts` — тонкий баррель).
## Ключевая стратегия: параллельная сборка, старый UI не трогаем
Старый UI (группа `(old)/`, `ParamForm.svelte` + старый pipeline) **продолжает
работать как сейчас, без рефакторинга**. Новый `ToolSchema<P>` строится рядом и
служит источником для **нового UI**. Инструменты переписываются под новый
registry по мере нужды — для нового UI.
Это та же логика, что в `plan-redesign.md` применена к дизайну: параллельная
сборка, изоляция от старого, затем новый становится основным и старый удаляется
вместе со старым дизайном.
**Правило копий (важно!):** если для нового UI/registry нужно внести изменения в
файл, который **прямо или косвенно** уже используется `(old)` веткой, — этот
файл **НЕ трогаем**. Вместо этого делаем **копию** в новом месте (например, в
`lib/preview/`) и правим копию. Это приводит к дублированию, но **гарантированно
не задевает старую ветку сайта**. Пример: категории — новый
`lib/preview/categories.ts` (object as const) копирует и заменяет собой
`../categories` для нового кода, старый `categories.ts` обслуживает `(old)` и
остаётся без изменений. После перехода (Фаза 5) копия становится основной, а
исходник продолжает обслуживать `(old)` UI (он не удаляется — см. шаг 35).
**Разделение схем: две независимые схемы.**
| Схема | Источник | Использование |
| --------------------- | ------------------------------------------------- | ---------------------------------------------------------------- |
| Старый `ParamDef[]` | В `ToolEntry.params` — **остаётся без изменений** | Старый UI, старый pipeline (`ParamForm.svelte`, executor, chain) |
| Новый `ToolSchema<P>` | Новое поле `ToolEntry.schema` — добавляется рядом | Новый UI (kit/`ParamControl`, новый pipeline) |
- Старая и новая схемы **не выводятся друг из друга** — это два независимых
описания инструмента.
- Пока новый UI не готов — `schema` просто отсутствует у большинства
инструментов, старый UI ничего не замечает.
- Удаление старого UI (после перехода) попутно удаляет и старый `ParamDef[]`
### Разделение registry по UI (новое решение, Фаза 2+)
Помимо двух схем, registry тоже разделён по UI (по факту миграции):
- **Старый registry** (`web/src/lib/registry.ts` + `registry/` +
`registry-helpers.ts`) — работает на старом UI, использует `ParamDef[]`. **Не
трогаем**; остаётся обслуживать `(old)/` маршруты (см. шаг 35).
- **Новый registry** (`web/src/lib/registry-new/`) — строится **с нуля «как
надо»**: `ToolEntry<P>` с обязательным `schema`, типизированный `run`, **без**
`ParamDef[]` и **без** связи со старым. Импортирует core-функции
(`expandCanvas`, `strokeImage`, …) напрямую. Наполняется **по-инструментно**
по мере миграции (не разовым переводом всех 130). Preview показывает только
переведённые инструменты.
Preview (`catalog.ts`, маршруты `preview/**`, `SchemaToolView`, `SchemaFields`)
импортируют из `$lib/registry-new`. После удаления старого UI `registry-new/`
переименуется в `registry`.
**Executor/worker для нового UI (осознанное дублирование):** общий
`executor.worker.ts` резолвит инструменты по `id` в **старом** registry — для
новых инструментов их там нет. Поэтому `executor.ts` + `executor.worker.ts`
**скопированы** в `web/src/lib/preview/` и переписаны под `registry-new` +
`sanitizeSchemaParams`. Старый executor/worker под старый UI остаются
нетронутыми; `SchemaToolView` использует `$lib/preview/executor`. Это
дублирование необходимо до ухода старого UI (после — preview-executor заменяет
общий).
## Проблема (текущая)
- `ParamDef` — плоский union из 6 примитивов
(`number | slider | select | checkbox | color | text`). Параметры инструмента
— отдельные записи в плоском массиве.
- `run()`/`generate()` получают `Record<string, unknown>`; значения достаются
через `num(p, "fromColor")` — runtime string lookup **без type safety**, легко
напутать id.
- Поля инструмента **слабо связаны** (например «ширина» и «цвет» рамки — два
независимых поля без общего контракта) → общий генерализованный рендер, нет
возможности сделать идеальный пер-инструмент UI.
- Preview-система (`tool-views.ts`) **дублирует** registry (FieldDef vs
ParamDef) — одни и те же инструменты описаны дважды.
## Решения (принятые в обсуждении)
1. **Полная типизация.** `interface Params` (runtime-тип) объявляется явно в
каждом инструменте. `run`/`generate` типизируются прямо на него, а не на
`Record<string, unknown>`.
2. **Отдельная типизированная схема для UI/дефолтов/валидации.** Два понятия на
инструмент: runtime-тип + схема. Схема проверяется компилятором на
соответствие `Params` (чтобы не рассинхронизировались).
3. **Общий рендер,** но с пер-инструмент layout: общие компоненты рисуют поля по
схеме; схема инструмента задаёт layout и группировку. Без отдельного
Svelte-компонента на каждый инструмент.
4. **Составные типы** (`gradient`, `color-pair`, `dimension`, `offset`,
`position9`, `font-style`, `plate`) остаются в плане — это способ выразить в
схеме связанную группу полей и переиспользовать её на нескольких инструментах
и в UI.
5. Рано или поздно `tool-views.ts` поглощается registry (preview/lede/layout
переезжают в meta инструмента) — но это отдельный шаг, см. ниже.
6. **Старый UI не рефакторим.** Две независимые схемы: старый `ParamDef[]`
остаётся источником для старого UI без изменений; новый `ToolSchema<P>`
строится рядом для нового UI (см. «Ключевая стратегия» выше).
## Целевая архитектура (на примере «добавить рамку»)
Цель: «добавить рамку: ширина и цвет» — **один интерфейс**, а не два слабо
связанных поля.
```ts
// 1. Runtime-тип — что получает run/generate
interface AddBorderParams {
thickness: number; // рамка: ширина
color: string; // рамка: цвет
}
// 2. Типизированная UI-схема, проверяемая компилятором против Params
// Ключи схемы обязаны совпадать с полями Params; default совместим с типом.
const addBorderSchema = toolSchema<AddBorderParams>({
thickness: field.slider({ min: 1, max: 500, default: 5 }),
color: field.color({ default: "#000000" }),
layout: { group: "border", cols: 2 }, // пер-инструмент раскладка
});
// 3. Инструмент в registry
const addBorder: ToolEntry<AddBorderParams> = {
id: "add-border-png",
title: "Add border to PNG",
category: "geometry",
schema: addBorderSchema,
run: (img, p) => expandCanvas(img, 4x..., p.color), // p.thickness — type safe
};
```
### Что даёт
- `p.thickness`, `p.color` — IDE autocomplete + compile-time ошибка при ошибке.
- `run()` нельзя передать лишний/чужой ключ.
- Схема — единый источник для дефолтов, валидации и рендера.
- Схема одного инструмента может задавать layout, которого нет в общем рендере.
## Типовая система (набросок)
```ts
// ─── Поле схемы ───
interface NumberField { kind: "number"; default: number; min?: number; max?: number; step?: number }
interface SliderField { kind: "slider"; default: number; min: number; max: number; step?: number }
interface ColorField { kind: "color"; default: string }
interface SelectField { kind: "select"; default: string; options: { value: string; label: string }[] }
interface TextField { kind: "text"; default: string; placeholder?: string }
interface CheckboxField { kind: "checkbox"; default: boolean }
type FieldDef =
| NumberField | SliderField | ColorField | SelectField | TextField | CheckboxField;
// ─── «поле» описывает тип значения + схему вместе, чтобы не рассинхронизировать ───
interface Field<T> {
schema: FieldDef;
// T — ожидаемый runtime-тип этого поля (number|string|boolean)
}
// ─── Builder: field.slider({...}) возвращает Field<number> с типизированной схемой ───
const field = {
slider: (s: SliderField): Field<number> => ({ schema: s }),
number: (s: NumberField): Field<number> => ({ schema: s }),
color: (s: ColorField): Field<string> => ({ schema: s }),
select: <V extends string>(s: SelectField & { options: {value: V}[] }): Field<V> => ({ schema: s }),
text: (s: TextField): Field<string> => ({ schema: s }),
checkbox: (s: CheckboxField): Field<boolean> => ({ schema: s }),
};
// ─── toolSchema<P>: проверяет, что ключи схемы === полям P и типы сходятся ───
type FieldsOf<T> = { [K in keyof T]: Field<T[K]> } & {
layout?: { group?: string; cols?: number };
};
function toolSchema<P>(fields: FieldsOf<P>): ToolSchema<P> { ... }
// TS выдаст ошибку, если schema содержит ключ, отсутствующий в Params, или
// если type поля не совпадает с типом Params[K].
```
Ключевая идея связки: **builder `field.x<T>()` связывает runtime-тип поля с его
схемой на этапе компиляции**, а `toolSchema<P>` заставляет ключи схемы совпадать
с ключами `P`. Это и есть защита от рассинхрона.
## Составные типы (для переиспользования и группировки)
| Тип | Под-поля | Инструментов |
| ------------ | -------------------------- | ------------ |
| `dimension` | width + height | 12 |
| `color-pair` | from + to | 4 |
| `offset` | x + y | 4 |
| `gradient` | from + to + dir (+ type?) | 1 |
| `position9` | position (3×3 grid) | 3 |
| `font-style` | font + size + bold + color | 3-4 |
| `plate` | enabled + color + opacity | 2-3 |
Составной тип = группы полей, которые в интерфейсе `Params` могут
разворачиваться во вложенный объект или плоские ключи — решается на этапе
реализации. Пример для `gradient`:
```ts
interface GradientParams {
from: string;
to: string;
dir: string;
}
// схема: field.color(from), field.color(to), field.select(dir)
// плюс annotation в схеме: { group: "gradient" } → UI рисует как цветовой переход
```
## Инвентарь: какие инструменты мигрировать
### dimension (12)
resize-png*, crop-png*, fit-on-background-png, create-empty-png,
single-color-png, random-noise-png, linear-gradient-png, color-spectrum-png,
random-colors-png, draw-grid-png, placeholder-png, change-canvas-size-png
(\* у resize/crop — не просто dimension, есть специфика: keepAspect, x/y и т.д.)
### color-pair (4)
blend-two-png, step-colors-png, linear-gradient-png, two-colors-png
### offset (4)
circle-mask-png, square-mask-png, star-mask-png, wavy-mask-png
### position9 (3)
add-text-png, date-stamp-png, watermark-image-png
### font-style (3-4)
text-to-png, add-text-png, date-stamp-png
### plate (2-3)
add-text-png, date-stamp-png
## Пошаговый план перевода инструментов
> Каждый шаг — **атомарное, маленькое изменение** (в идеале 1 инструмент = 1
> diff, укладывается в правило «коммиты < ~500 строк»). После каждого шага —
> ревью; параллельно можно делать UI нового инструмента.
>
> **Принцип малых шагов:** перевод инструмента не меняет поведение старой схемы
> (`params` остаётся), не ломает старый UI. Инструмент получает ДОПОЛНИТЕЛЬНО
> новое поле `schema` + `interface Params`, старое `run`/`params` не трогаем.
> Это гарантирует, что старый UI продолжает работать без регресса.
>
> **Новый подход (решение пользователя):** сначала делаем **один полноценный
> рабочий инструмент в preview** — не эталон «на бумаге», а реально юзабельный,
> чтобы руками оценить, насколько это рабочее решение и какие возникнут
> сложности. Оцениваем, при необходимости правим инфраструктуру, и только потом
> переводим остальные инструменты тем же проверенным паттерном.
### Фаза 0 — фундамент ✔ (сделано)
Typed field builders + `Field<T>` + `toolSchema<P>()` + `ToolSchema<P>` — в
`web/src/lib/registry-schema.ts`. `ToolEntry<P>` generic + опциональное поле
`schema`. Новые `defaultSchemaParams`/`sanitizeSchemaParams`. Старый
`ParamDef[]`/pipeline не тронуты. Проверено: compile-time-защита
(лишний/неверный/отсутствующий ключ ловит TS), тесты, svelte-check без новых
ошибок.
### Фаза 1 — ОДИН полноценный рабочий инструмент в preview (срез) ✔ (сделано)
Инструмент переехал в `registry-new` и рендерится через `SchemaToolView`
(`SchemaFields`/`SchemaPreview`); применение реально работает end-to-end до
картинки. Паттерн подтверждён. `find-contour-png` — следующий в этой фазе-2.
5. **Инструмент «добавить рамку» (`add-border-png`)** — сделать полностью
рабочим в preview:
- `interface AddBorderParams { thickness: number; color: string }`
- `addBorderSchema = toolSchema<AddBorderParams>(...)` + `schema: ...` в
entry (старый `params`/`run` остаются — старый UI не регрессит).
- **Новый рендер в preview** (`kit/ToolView` → `ParamControl` или новый
компонент схемы): читает `ToolSchema<P>`, рисует поля рамки, применение
реально работает (end-to-end до картинки).
- Дефолты/валидация/значения — из схемы (`defaultSchemaParams`,
`sanitizeSchemaParams`).
- **Ревью-оценка:** удобно ли это, что сломалось/усложнилось, что поправить в
инфраструктуре до масштабирования. Принимается макет/решение.
Результат: один реально рабочий инструмент на новой системе = доказываем
паттерн, прежде чем переводить остальных.
### Фаза 2 — простые инструменты без составных типов (1 инструмент = 1 шаг)
Одиночные/небольшие инструменты, где `interface Params` + `schema` не требуют
составных типов. Каждый — отдельный маленький diff (~15-30 строк), тем самым
проверенным в Фазе 1 паттерном. **Переезжают в `registry-new`.**
Переведено: `add-border-png` (Фаза 1), `add-stroke-png`, `find-contour-png`,
плюс весь блок простых инструментов ниже (пункты 8-22). Следующее — Фаза 3.
6. ~~add-stroke-png (color + slider)~~ → переведён в `registry-new` ✔
7. ~~find-contour-png (color + slider)~~ → переведён в `registry-new` ✔
8. ~~convert-png-to-jpg (color + slider)~~ → переведён в `registry-new` ✔
9. ~~convert-png-to-webp (slider)~~ → переведён в `registry-new` ✔
10. ~~remove-color-from-png (color + slider)~~ → переведён в `registry-new` ✔
11. ~~extract-color-from-png (color + slider)~~ → переведён в `registry-new` ✔
12. ~~add-noise-png (slider + select + number)~~ → переведён в `registry-new` ✔
13. ~~randomize-pixels-png (slider + number)~~ → переведён в `registry-new` ✔
14. ~~pixelate-png (slider)~~ → переведён в `registry-new` ✔
15. ~~vignette-png (slider)~~ → переведён в `registry-new` ✔
16. ~~gamma-png (slider)~~ → переведён в `registry-new` ✔
17. ~~temperature-png (slider)~~ → переведён в `registry-new` ✔
18. ~~tint-png (color + slider)~~ → переведён в `registry-new` ✔
19. ~~quantize-png (slider)~~ → переведён в `registry-new` ✔
20. ~~custom-palette-png (text)~~ → переведён в `registry-new` ✔
21. ~~dithering-png (slider + select)~~ → переведён в `registry-new` ✔
22. ~~jpeg-artifacts-png (slider)~~ → переведён в `registry-new` ✔
> **Замечание (конвертеры):** старые `convert-png-to-jpg`/`convert-png-to-webp`
> несли `output`-метаданные (`mime`/`ext`/`qualityParamId`) для выбора формата
> вывода. В новом `ToolEntry<P>` поля `output` пока нет, поэтому при переводе
> эти метаданные не перенесены — preview пока отдаёт результат как PNG. Механика
> выбора формата/качества в новом UI — отдельный шаг (не блокирует Фазу 2).
### Фаза 3 — инструменты с составными типами (по типу, затем по инструментам)
24. `dimension` — **переведены все 12 инструментов** ✔ (`create-empty-png`,
`single-color-png`, `random-noise-png`, `linear-gradient-png`,
`color-spectrum-png`, `random-colors-png`, `draw-grid-png`,
`placeholder-png` — генераторы в `registry-new/generate.ts`;
`fit-on-background-png`, `change-canvas-size-png`, `resize-png`, `crop-png`
— в `registry-new/geometry.ts`). Составной тип во всех видах: вложенный
объект `size: { width, height }` в Params + `field.dimension`, виджет
`kit/fields/DimensionField.svelte`. Preview: генераторы применяются через
`executeGenerate` (кнопка Generate), `SchemaFields` рендерит все kinds схемы
(slider/number/color/select/ checkbox/dimension). Тесты: 592 passed.
25. `color-pair` — **все 4 инструмента переведены** ✔ (`blend-two-png`,
`step-colors-png`, `linear-gradient-png` — генераторы в
`registry-new/generate.ts`, `two-colors-png` — run в
`registry-new/color.ts`). Составной тип во всех видах: вложенный объект
`pair: { from, to }` + `field.colorPair`, виджет
`kit/fields/schema/ColorPairControl.svelte`, kind `color-pair` в схеме
(default/sanitize). Тесты: +4 (596 passed).
26. `offset` — **все 4 инструмента переведены** ✔ (`circle-mask-png`,
`square-mask-png`, `star-mask-png`, `wavy-mask-png` — run в
`registry-new/alpha.ts`). Составной тип во всех видах: вложенный объект
`offset: { x, y }` + `field.offset`, виджет
`kit/fields/schema/OffsetControl.svelte`, kind `offset` в схеме
(default/sanitize). Тесты: +4 (600 passed).
27. `position9` — **переведены `add-text-png` и `date-stamp-png`** ✔ (run в
`registry-new/text.ts`, оба `domOnly`). Составной тип во всех видах:
значение — строка `Position9`, kind `position9` в схеме (default/sanitize по
`POSITION9_VALUES`), виджет 3×3 `kit/fields/schema/PositionControl.svelte`.
В `ToolEntry` добавлен флаг `domOnly` (превью-executor держит такие
инструменты вне worker). `watermark-image-png` — отдельный под-шаг: ему
нужен overlay-source (`getOverlay`/store), которого в новом превью пока нет.
Тесты: +2 (602 passed). `font-style` и `plate` на этих инструментах сводятся
в шаги 28-29.
28. `font-style` — **все 3 инструмента переведены** ✔ (`text-to-png` — генератор
в `registry-new/generate.ts` (domOnly), `add-text-png`/ `date-stamp-png` —
рефакторинг в `registry-new/text.ts`). Составной тип во всех видах:
вложенный объект `style: { font, size, bold, color }` + `field.fontStyle`,
виджет `kit/fields/schema/FontStyleControl.svelte`, kind `font-style` в
схеме (default/sanitize, clamp размера к min/max). Тесты: +1 (603 passed).
29. `plate` — **все 2 инструмента переведены** ✔ (`add-text-png`,
`date-stamp-png` — рефакторинг в `registry-new/text.ts`). Составной тип во
всех видах: вложенный объект `plate: { enabled, color, opacity }` +
`field.plate`, виджет `kit/fields/schema/PlateControl.svelte` (чекбокс +
цвет + слайдер непрозрачности, деактивируется при выключенной плашке), kind
`plate` в схеме (default/sanitize, clamp opacity к 0..100). Покрытие тестов
расширено (дефолты/sanitize plate в существующих тестах).
30. `gradient` — **переведён** ✔ (linear-gradient-png). Направление градиента —
не горизонталь/вертикаль, а **произвольный угол**: новый переиспользуемый
виджет `kit/fields/schema/AngleControl.svelte` (слайдер 0..360° + кнопки
стандартных углов 0°/90°/180°/270° с активным состоянием). Составной тип
`gradient: { from, to, angle }` во всех видах (`field.gradient`, kind
`gradient` в схеме — default/sanitize, clamp угла к 0..360, валидация
цветов; виджет `kit/fields/schema/GradientControl.svelte` — пара цветов +
AngleControl). Рендер по углу — локальный `angleGradient` в
`registry-new/generate.ts` (core-`gradientImage` не трогали: он умеет только
horizontal/vertical и используется старым UI). Семантика: 0° слева направо,
90° сверху вниз, рост угла по часовой (ось Y вниз); нормализация по полному
диапазону проекции на ось (180°/270° разворачивают градиент). Тесты:
дефолты, направление по углу (0°/90°/180°), sanitize/кламп.
### Фаза 4 — масштаб UI на остальные инструменты
31. По мере перевода инструментов в Фазе 2-3 — рендер схемы (из Фазы 1)
покрывает их автоматически; составные виджеты (dimension, color-pair,
offset, position9, font-style, plate, gradient) — по одному, каждый с ревью.
32. Пер-инструмент layout (`schema.layout`) — **реализован** ✔.
`ToolSchemaLayout.groups: { title?, cols?, fields[] }` в registry-schema;
`SchemaFields.svelte` рендерит группы (подпись + сетка `1..n` колонок,
неупомянутые поля — в общей группе в конце, без заголовка). Пилот —
`add-text-png` (Text: text+style; Placement: position+margin; Plate). Тесты:
schema-layout в registry-schema.test.ts; полные запуски чисты.
33. Для каждого переведённого инструмента — UI-макет **расставлен** ✔.
Layout-группы заданы там, где группировка содержательна (канвас отдельно от
параметров эффекта, фигура отдельно от позиции; составные виджеты — внутри
групп целиком):
- geometry: fit-on-background (Canvas/Background), change-canvas-size
(Canvas/Anchor), resize (Canvas/Scaling), crop (Offset/Crop area);
- alpha-маски: circle/square/star/wavy (Shape — с 2 колонками где плотно,
Position — offset);
- generate: create-empty (Canvas/Fill), linear-gradient (Canvas/Colors),
color-spectrum (Canvas/Spectrum), random-colors (Canvas/Random), draw-grid
(Canvas/Grid), step-colors (Colors/Output), placeholder
(Canvas/Colors/Text), text-to-png (Text/Background/Padding);
- text: add-text, date-stamp (Text/Placement/Plate);
- filters: randomize-pixels (Blocks), add-noise (Noise/Seed). Инструменты с
1–3 простыми полями остались без layout (одна общая группа).
### Фаза 5 — изоляция старого UI
34. Поглощение `tool-views.ts` (preview/lede/layout → meta инструмента) ✔.
Отдельного `tool-views.ts` в репо нет: preview сразу строился на
`registry-new`. Meta инструмента живёт в `ToolEntry` (`title`,
`description`, `category`, `run`/`generate`), layout — в `schema.layout`
(шаг 32), рендер — `SchemaToolView`/`SchemaFields`/`SchemaPreview`.
Дублирующей системы нет.
35. Старый UI **не удаляется**, а выносится в `(old)/`-маршруты и остаётся там
временно (посмотреть, как работает, сравнить с новым; старые тесты
продолжают проходить):
- маршруты `(old)/{+page,demo,list-tools,tools/[id]}` — старый дизайн, тянет
`old.css` (не `design2.css`), старый header/footer;
- старый `registry.ts`/`registry/` + `ParamDef[]` обслуживают только
`(old)/`-инструменты — не удаляются, не рефакторятся;
- удаление происходит позже, отдельным решением (когда новый UI покроет все
инструменты и ревью завершено).
36. **Линтер-изоляция веток** (гарантия, что old и preview не смешиваются) ✔.
Кастомный ESLint-плагин `web/eslint-plugins/isolation/no-mixed-imports`
Резолвит каждый импорт (и `$lib/...`, и относительные `./`/`../`) до
реального файла, классифицирует источник и цель по фактическому пути и
ругается на old→new и new→old. Конфигурация (`old`/`new` glob-паттерны,
`root`, `alias`) вынесена в настройки правила — единая точка правды:
- старое: `routes/(old)/**`, `lib/registry.ts`, `lib/registry/**`,
`lib/registry-helpers.ts`, `lib/categories.ts`, `lib/tools/**`,
`lib/components/**` (кроме `kit/`);
- новое: `routes/preview/**`, `lib/registry-new/**`, `lib/preview/**`,
`lib/registry-schema.ts`, `lib/registry-schema.test.ts`,
`lib/components/kit/**`;
- общее (разрешено обоим): всё прочее — `core/`, `i18n/`, `theme`,
`assets/`, корневой `lib` (`index.ts`, тесты). Достигнутая полная изоляция
(одиночные пересечения устранены):
- старые пилоты `registry/geometry.ts` (`add-border`) и `registry/alpha.ts`
(`add-stroke`) получали `schema` из нового `registry-schema` — убрано; оба
инструмента работают в старом UI через `params: ParamDef[]`, в preview —
через свои schema-версии в `registry-new/`;
- `registry.ts` больше не импортирует `ToolSchema` из `registry-schema`;
- preview `list-tools` тянул `TOOL_ICONS` из старого `lib/tools/tool-icons`
→ создана копия `lib/preview/tool-icons.ts` (правило копий).
37. Перенос всех старых компонентов/библиотек в папки `old/` (**следующее**):
переезд не трогает плагин — достаточно дописать один glob-паттерн в
настройку правила (например `lib/old/**`, `lib/components/old/**`), а сама
проверка работает по фактическим путям автоматически.
### Как ревьюить каждый шаг
- Diff **только один инструмент** (или фундамент без инструментов).
- Старый `params`/`run`/`defaultParams`/`sanitizeParams` — **не изменены**
(кроме добаление `schema`, если это пилот).
- Старый UI и старые тесты продолжают работать — запустить `pnpm --dir web test`
и `pnpm --dir web exec svelte-check`.
- Порядок полей в `interface Params` и в `schema` совпадает; дефолты в сeme
равны старым `default` из `params`.
- Пилот из Фазы 1 проверяется **руками в preview**: применяется рамка,
дефолты/валидация/смена значений работают end-to-end.
## Поток данных (runtime)
### Старый pipeline (не трогаем)
Всё как сейчас: старое `defaultParams`/`sanitizeParams`/executor/chain работают
на `ParamDef[]` и `Record<string, unknown>`. Никаких изменений.
### Новый pipeline (строим рядом)
- **Хранилище значений** — типизировано на схему; у нового UI значения полей
собираются в `Params` по `ToolSchema<P>`.
- **defaultParams / sanitizeParams (новые)** — выводятся из `ToolSchema<P>`,
дефолты лежат в схеме. Старые функции не трогаем, добавляем новые рядом.
- **Executor/worker (новые)** — получает `Record<string, unknown>`, собирает
`Params` по схеме, вызывает `run`. Остаётся serializable.
- **Pipeline/chain (новый)** — шаги хранят значения, типизируются по схеме.
## Изменения в UI-компонентах
- **Старый UI** (`ParamForm.svelte`, группа `(old)/`) — **не трогаем.** Работает
на `ParamDef[]` как раньше.
- **Новый UI** (kit/`SchemaToolView` + `SchemaFields` + `SchemaPreview`, читает
`ToolSchema<P>`) — сейчас рендерит поля по схеме (number/slider/color).
- `tool-views.ts` отсутствует — preview/lede/layout уже живут в meta инструмента
(`ToolEntry` + `schema.layout`), дублирования нет.
## Оценка трудозатрат
| Часть | Сложность | Время |
| ----------------------------------------------------- | ------------------------ | ---------------------- |
| Typed field builders + toolSchema\<P\> | Средняя | ~2ч |
| Новые defaultParams/sanitizeParams под схему | Средняя | ~2-3ч |
| ToolEntry\<P\> generic + поле schema (рядом с params) | Низкая-Средняя | ~1-2ч |
| Новый executor/worker/pipeline под схему | Средняя | ~2-3ч |
| Миграция инструментов на interface Params + схемы | Механическая, но крупная | ~8-12ч |
| Новый рендер (kit/ParamControl) + составные виджеты | Средняя | ~4-5ч |
| Поглощение tool-views.ts | Средняя | ~1-2ч |
| Вынос старого UI в (old)/ + адаптация маршрутов | Средняя-Низкая | ~2-3ч |
| **Итого** | | **~22-32ч** (поэтапно) |
> Оценка выросла по сравнению с ранней версией плана, потому что принят путь
> «явный interface Params + схема + общий рендер с пер-инструмент layout» — это
> полный рефакторинг pipeline, а не только добавление составных типов.
>
> Старый UI/`ParamDef[]`/старый pipeline в смету **не входят** — они не
> рефакторятся, а продолжают работать на `(old)/`-маршрутах до перехода (затем
> убираются отдельным решением — см. шаг 35).
## Порядок реализации (кратко)
Детальный пошаговый план с атомарными изменениями и порядком ревью — в разделе
«Пошаговый план перевода инструментов» выше. Здесь краткая сводка фаз:
1. **Фаза 0** — фундамент: builders + `toolSchema<P>` + `ToolEntry<P>.schema` +
новые default/sanitize. Ни одного инструмента не изменено. ✔
2. **Фаза 1** — **один полноценный рабочий инструмент в preview** («добавить
рамку»): schema + рендер + end-to-end применение. Ручная оценка
работоспособности решения/сложностей.
3. **Фаза 2** — простые/одиночные инструменты (проверенный паттерн Фазы 1), 1
инструмент = 1 диф.
4. **Фаза 3** — инструменты с составными типами (dimension → color-pair → offset
→ position9 → font-style → plate → gradient), пилот каждого типа отдельно.
5. **Фаза 4** — масштаб UI на остальные: составные виджеты, пер-инструмент
layout (для каждого инструмента — UI-макет).
6. **Фаза 5** — изоляция: старый UI на `(old)/`-маршруты (не удаляется, остаётся
для ревью); preview на `registry-new` полностью; линтер-изоляция веток
(плагин `isolation`), затем перенос старых компонентов/библиотек в папки
`old/`.
## Зависимости
- **UI сперва доделать** — текущая активная работа; типизация params ожидает
готовности нового дизайна.
- Ветка redesign (`plan-redesign.md`) — параллельная, учесть при миграции
kit/`ParamControl.svelte` и `overlay-store`.
- Старый pipeline остаётся рабочим и шippable на всём протяжении перехода
(аналогично стратегии `plan-redesign.md`).
-139
View File
@@ -1,139 +0,0 @@
# План: доводка дизайна до верности (фикс по refs)
> **СТАТУС: ЗАКРЫТ (2026-09-07).** Актуальный дизайн (`preview/*`) ушёл вперёд
> от референсов `refs-html/` — сверка с ними больше не информативна (новые
> компоненты `SchemaToolView`/`SchemaPreview`/каталог не имеют рефовой копии).
> Аудит против рефа прекращается; качество дизайна дальше гоняется ревью руками
> и специализированными проверками (`lint:all` —
> stylelint/design-tokens/токен-аудит). Порядок удаления старого и перехода — по
> `plan-redesign.md` (C17–C21).
> Статус: к выполнению. Ветка `feat/redesign`. Источники:
> `docs/archive/demo-fidelity.md` (архивная копия старого отчёта по верности,
> неактуальна), `docs/plan-redesign.md` (общая стратегия, фазы 1–4 выполнены до
> C16). Актуальный аудит верности — `pnpm refs-cdp-audit` /
> `pnpm refs-css-audit` / `pnpm refs-dom-audit` → `web/audit/audit-report.md`
> (текущий список скриптов — в `web/package.json`, см. `refs-*audit`).
## Контекст
- Новый дизайн собран параллельно под `preview/*` (см. `plan-redesign.md`),
старый сайт `(old)/` не трогаем до C17.
- `refs/` и `refs-html/` приведены к консистентному виду — текущая «правда по
пикселям». Механизм синхронизации ref→код не создаётся (см. §9 plan-redesign).
- Аудит верности автоматизирован: `pnpm refs-cdp-audit` (1440×900,
`scripts/audit-cdp.mjs`) пишет `web/audit/audit-report.md` — light/dark
токены + поэлементный дифф стилей; пиксель-дифф не используется. Палитры
токенов уже сверены с рефом (light совпадает, dark собран). Темы в `refs-html`
переключаются — скрипт инжектится в `scripts/extract-static.mjs`.
- Исходный отчёт `demo-fidelity.md` перенесён в `docs/archive/` как утративший
актуальность; его данные актуализированы ниже (Этап 1).
- Зафиксированные решения (подтверждены автором):
- **F1 — плоский фон холста**, без blueprint-сетки (как в рефе).
- **F4 — preview-stack в одну колонку** (как в рефе).
## Принципы
- Никаких правок старого дизайна `(old)/` и старого `ui/`.
- Компоненты пишем в `kit/` (переименование в `ui/` — на C19 по plan-redesign).
- Коммиты атомарные (< ~500 строк), по 1–2 компонента; после каждого —
`svelte-check` + `lint` + `build` + `refs-cdp-audit` (diff% не должен расти
без причины).
- Коммиты делает автор (агент не коммитит).
## Этап 0 — Рефакторинг (SRP / дизайн-обёртки), без изменения визуала
Гигантские страницы (`demo` ~513, `tools/[id]` ~451 строк) дробим на компоненты
единой ответственности; убираем дублирующиеся inline-стили.
Новые обёртки в `src/lib/components/kit/`:
- `FieldGrid` — сетка полей (заменяет `.controls` / `.controls.compact`).
- `ToggleRow` — лейбл + `Toggle` (дублировался в `demo` и `tools/[id]`).
- `PreviewStack` — контейнер превью-плиток, **одна колонка** (реализует F4 в
одном месте).
- `SectionLabel` — label + strong + actions (вместо `.pipeline-head` /
`.preview-top`).
- `FileChip` — пилюля файла.
- `PipelineFooter` — reset + auto-note.
- `WorkspaceLayout` — двухпанельная сетка (settings | preview) со sticky +
брейкпоинт 800px.
Декомпозиция страниц:
- `tools/[id]`: вынести `ParamControl` (диспетчер полей по `ParamDef`),
`SettingsPanel` (левая панель), `PreviewPanel` (правая панель) — колоцированно
в `routes/preview/tools/[id]/`. Страница сжимается до состояния + глёв.
- `demo`: переписать на те же обёртки + `WorkspaceHeader` (eyebrow + h1 + lede +
file-chip).
Побочный баг, устраняемый тут же: в `demo` поля сейчас обёрнуты в
`<div class="control-block"><MonoLabel>…</MonoLabel><SliderField label>` —
**двойной лейбл**, т.к. `ColorField`/`SliderField`/`SelectField`/`NumberField`
уже содержат `Field` (MonoLabel) изнутри. При рефакторе убираем внешний
`MonoLabel`, оставляем само поле.
Критерий Этапа 0: визуал идентичен (audit diff% без регрессии), старые тесты
зелёные.
## Этап 1 — Верность (по refs-html; статус на момент плана)
Аудит: `pnpm refs-cdp-audit` (1440×900) пишет `web/audit/audit-report.md` —
light/dark токены + поэлементный дифф стилей. Пиксель-дифф не используется.
Палитры токенов уже сверены с рефом: light совпадает (`--background`, `--panel`,
`--foreground`, `--muted`, `--line`, `--blue`), dark собран (16 токенов). Темы в
`refs-html` переключаются (скрипт инжектится в `extract-static.mjs`).
Открытые / решённые пункты (исходник — `docs/archive/demo-fidelity.md`):
- **B1** (открыт, P1): `button, input, select, textarea` рендерятся Arial —
добавить в reset `design2.css` `font-family: inherit`. Влияет на все страницы.
- **B2** (решён — проверить): двойного топбара нет. `preview/+layout.svelte`
рендерит ровно один `TopBar`; `demo` свой `.topbar` не рендерит. Подтвердить
аудитом/визуально при Этапе 1.
- **F1** (решено — плоский фон): наш `--background` уже `#EEF1F4` (как реф);
видимое отличие было в blueprint-сетке поверх фона. Фикс: убрать
`BlueprintGrid` из `AppShell`.
- **F2** (сделано C16): тип шага (`StepCard`) — приглушённый mono-текст, не
синий бейдж; индекс шага — синий `#1769D2`.
- **F3** (сделано C16): подпись превью-тайла (`PreviewTile`) — синяя `#1769D2`.
- **F4** (решено — одна колонка): реализуется в `PreviewStack` (Этап 0), колонка
= `1fr`.
- **F5** (открыт): вес `MonoLabel` 500 → 400 (как в рефе).
- **F6** (открыт): мета-подписи (MetaRow/MetaList, DIMENSIONS/FORMAT/SIZE) fs
10px → 9px.
- **F7** (низкий): копирайт/стиль — у нас `+ Add tool` (реф `Add tool`); кнопка
`Download result` — сверить с рефом (синяя, ~478×42). Проверить визуально
(замеры NOT FOUND — артефакт вложенного чипа размера).
- **F8** (низкий): `h1` line-height чуть выше рефа (150px vs 115px).
- **F9** (открыт): вертикальный ритм — подровнять паддинги секций под реф
(pipeline-head и пр.).
- **F10** (открыт): внутренние отступы preview-панели (паддинг панели).
После Этапа 1 — повторный `refs-cdp-audit`, зафиксировать остаточный diff
(sub-pixel / структурный) и обновить статус пунктов выше прямо в этом файле.
## Этап 2 — plan-redesign.md (C17–C21), ОТЛОЖЕНО
Выполняется **только после** того, как новый дизайн доведён до приемлемого
уровня верности (Этапы 0–1). Сами пункты — в `plan-redesign.md` §10 Шаг 5–6:
- **C17**: `preview/*` → реальные маршруты, удалить `(old)/`, корневой
`+layout.svelte` ← `design2.css`, поправить `newCode` в `eslint.config.js`
(`src/routes/preview/**` → `src/routes/**`).
- **C18**: правки импортов/редиректов, проверка билда.
- **C19**: удалить старое `ui/` + `app.css`, `kit/` → `ui/` (или оставить),
раскрыть ESLint `recommended` на весь код.
- **C20**: брейкпоинты 1200/1100/800/480; build без предупреждений; `grep` по
старым токенам/классам пуст.
- **C21**: опц. удалить `refs/` и `refs-html/` из репо.
## Порядок коммитов (Этап 0)
1. `kit/`: `FieldGrid`, `ToggleRow`, `PreviewStack`, `SectionLabel`, `FileChip`,
`PipelineFooter`, `WorkspaceLayout` — по 1–2 на коммит.
2. `tools/[id]`: `ParamControl` → `SettingsPanel` + `PreviewPanel` → тонкая
страница.
3. `demo`: переписать на обёртки.
После каждого коммита: `svelte-check` + `lint` + `build` + `refs-cdp-audit`.
-99
View File
@@ -1,99 +0,0 @@
# План: вторая волна EASY-инструментов
> **СТАТУС: ВЫПОЛНЕН 2026-08-23.**
## 1. Принципы отбора
- Максимум переиспользования: готовые контракты записей (`run`, `preview`,
формат вывода, info-режим) и существующие операции ядра.
- Новые механизмы ядра — минимальные, чистые, покрытые тестами до подключения
инструментов.
- Canvas/DOM-зависимые утилиты (SVG-вход, надписи и водяные знаки, эффекты через
фильтры контекста) — вне этого пакета, отдельная волна.
- Целевой объём волны: около 37 новых инструментов, каталог вырастает с 11 до
~48.
## 2. Новые механизмы
- **Генераторы без входного файла**: у записи появляется признак «источник не
нужен»; страница такого инструмента не показывает левую панель и зону
перетаскивания — только параметры и результат.
- **Текстовый ввод и вывод**: часть утилит принимает вставленный текст вместо
файла и выдаёт текстовый результат (панель с полем только для чтения, кнопки
«копировать» и «скачать как текст»). Распространяется на base64, data-uri,
hex.
- **BMP-энкодер в ядре**: 24-битный, чистая функция из пикселей в байты, с
тестами на маленьких матрицах (заголовок, порядок каналов BGR, паддинг
строки).
## 3. Состав пакета
**Конвертация — 6** `jpg-to-png`, `webp-to-png`, `gif-to-png` (первый кадр),
`bmp-to-png`, `ico-to-png` — всё через уже умеющий декодировать ввод, сохранение
в PNG; плюс `png-to-bmp` на новом энкодере.
**Текстовые представления — 6** `png-to-base64`, `base64-to-png`,
`png-to-data-uri`, `data-uri-to-png`, `png-to-hex`, `hex-to-png`.
**Цвет — 9** `change-opacity` (множитель прозрачности), `sepia`, `change-hue`
(сдвиг тона), `extract-channel` (выбор R/G/B в оттенках серого), `swap-channels`
(перестановка парой), `threshold-black-white` (порог яркости), `posterize`
(число уровней), `two-colors` (два цвета + порог), `invert-alpha`.
**Прозрачность и холст — 7** `remove-alpha-channel` (подложка белым),
`set-alpha-channel` (константная прозрачность), `extract-alpha-mask` (альфа в
чёрно-белую маску), `add-padding` (отступ + цвет/прозрачность), `add-border`
(толщина + цвет), `round-corners` (радиус), `fit-on-background` (вписать на
полотно цвета с полями).
**Композиция — 2** `tile` (повтор по сетке), `center-by-alpha` (центрирование по
непрозрачному содержимому).
**Генераторы — 4** `create-empty` (размер + цвет или прозрачность),
`single-color`, `random-noise` (с фиксированным зерном для воспроизводимости),
`linear-gradient` (два цвета, направление).
**Анализ — 3** `is-grayscale`, `is-transparent`, `orientation`
(портрет/ландшафт/квадрат) — текстовые сводки в info-режиме.
## 4. Новые операции ядра (все — чистые функции с тестами)
- Холст: расширение полотна (общая база для padding/border/fit), тайлинг, центр
по bounding-box альфы, скругление углов маской радиуса.
- Цвет: сепия, сдвиг тона через HSL, множитель альфы, извлечение/перестановка
каналов, пороги (яркость и двухцветный), постеризация уровней, инверсия альфы.
- Генерация: заливка цветом, шум с зерном, линейный градиент.
- Форматы: BMP-энкодер; кодеки текстовых представлений (base64/data-uri/hex в
обе стороны).
- Анализ: проверка монохромности, полной непрозрачности, ориентации.
## 5. Этапы и проверки
- **A. Механизмы** — генераторный режим страницы, текстовый ввод/вывод,
BMP-энкодер. Проверка: `test/check/build`.
- **B. Конвертация и текстовые представления** — 12 инструментов. Проверка:
ручной прогон каждой конвертации туда-обратно.
- **C. Цвет** — 9 инструментов. Проверка: юнит-тесты операций + визуально.
- **D. Прозрачность, холст, композиция** — 9 инструментов. Проверка:
юнит-тесты + визуально.
- **E. Генераторы и анализ** — 7 инструментов. Проверка: детерминизм шума (одно
зерно — одно изображение), своды анализа.
- **F. Итог** — полный прогон, смоук всех новых инструментов «настроить →
скачать», регресс старых одиннадцати.
## 6. Критерии готовности
- Каталог содержит ~48 рабочих инструментов, каждый доступен страницей с формы
из реестра.
- Все новые операции ядра покрыты тестами; обратимые конвертации проверены
круговым прогоном (например PNG → base64 → PNG даёт те же пиксели).
- Генераторы работают без загрузки файла; текстовые представления копируются и
скачиваются.
- `test/check/build` зелёные.
## 7. Что сознательно не делаем
- SVG-вход, рисование текста и водяных знаков, эффекты через фильтры контекста,
GIF/APNG-энкодинг, пакетная обработка — последующие волны.
- Семейства logo/icon/signature/stamp как отдельные страницы — они будут
конфигурациями тех же операций после этой волны.
-87
View File
@@ -1,87 +0,0 @@
# План: третья волна EASY — трансформации, свет, SVG
> **СТАТУС: ВЫПОЛНЕН 24.08.2026.**
## 1. Что получается
Одиннадцать новых инструментов: свободные трансформации формы (наклон, поворот
на произвольный угол, зум, сдвиг), светокоррекция (гамма, автоконтраст,
температура, тонирование), виньетка, эффект «артефактов сжатия» и вход SVG.
Каталог вырастает с 58 до ~69.
Текстовые инструменты и водяные знаки в волну не входят — им нужен рендер текста
на канвасе, это отдельная четвёртая волна.
## 2. Новое ядро
- **Аффинные трансформации** (`core/affine.ts`): единая функция обратного
отображения — для каждого пикселя результата вычисляется исходная координата
матрицей и семплируется билинейно (общий семплер, вынесенный из `resize`).
Поверх неё: наклон по осям, поворот на произвольный угол (с расширением холста
под новые габариты), зум к центру, сдвиг.
- **Свет и цвет** (`core/color.ts` дополнения): гамма-коррекция через LUT,
автоконтраст (растяжка по гистограмме яркости с отсечкой хвостов), температура
(множители тёплых/холодных каналов), тонирование (умножение RGB на цвет с
сохранением яркостного баланса).
- **Виньетка** (`core/effects.ts`): радиальное затемнение от центра, сила
слайдером; чистая математика без canvas.
- **IO-расширения** (`core/io.ts`): декодирование SVG из текста/файла через
blob-URL и `Image`; раундтрип «закодировать JPEG с качеством Q → декодировать
обратно» для эффекта артефактов сжатия.
## 3. Инструменты — 11
- **Геометрия:** `skew-png` (углы X/Y), `rotate-free-png` (любой угол, холст
расширяется), `zoom-png` (масштаб ≥100 % к центру), `shift-png` (сдвиг со
смещением содержимого).
- **Фильтры:** `vignette-png` (сила затемнения), `jpeg-artifacts-png` (имитация
пережатия).
- **Цвет:** `gamma-png` (0.1–3.0), `auto-contrast-png` (без параметров),
`temperature-png` (−100 тёплый…+100 холодный или наоборот — подпись честная),
`tint-png` (цвет + сила).
- **Конвертация:** `svg-to-png` (текст SVG слева, как base64-режим).
## 4. Этапы и проверки
- **A. Ядро** — семплер, аффинный модуль, свет/цвет, эффекты + точные тесты:
поворот квадрата на 90° совпадает с существующим `rotate90`, зум ×2 удваивает
размеры и центр остаётся центром, гамма 1 — идентичность, автоконтраст
растягивает известный диапазон, виньетка симметрична и не трогает центр при
нулевой силе. Проверка: `pnpm test/check`.
- **B. IO** — SVG-декод и JPEG-раундтрип. Проверка: сборка; живой прогон на
этапе D.
- **C. Реестр** — одиннадцать записей, иконки, тесты целостности. Проверка:
`test/build`, страницы в экспорте.
- **D. Итог** — полный прогон, смоук §7.
## 5. Смоук-чеклист
1. Наклон X/Y по отдельности и вместе — форма искажается предсказуемо, без
чёрных полос при малых углах.
2. Поворот на 30° — холст вырос, углы прозрачны, содержимое повернуто.
3. Зум 200 % — содержимое увеличено к центру, холст прежнего размера, края
обрезаны.
4. Сдвиг уводит содержимое за край, противоположный край заполняется фоном.
5. Гамма 0.5 темнее, 2.0 светлее, 1.0 — без изменений.
6. Автоконтраст на тусклом фото вытягивает диапазон.
7. Температура ±50 заметно теплеет/холодеет.
8. Тонирование красным даёт равномерный оттенок, белые области окрашиваются
сильнее всего.
9. Виньетка затемняет только края, центр не трогает.
10. Артефакты: качество 10 даёт видимые квадраты JPEG.
11. SVG вставленный слева декодируется в результат справа; битый SVG — баннер.
12. Все новые инструменты работают в цепочке и скачиваются из своих секций.
## 6. Критерии готовности
- Одиннадцать инструментов работают и покрыты юнит-тестами ядра (кроме
IO-дом-части).
- Аффинные операции опираются на общий билинейный семплер; дублирования
сэмплинга нет.
- Каталог ~69 инструментов; `test/check/build` зелёные, смоук §5 пройден.
## 7. Что сознательно не делаем
- Рендер текста, водяные знаки, штампы — четвёртая волна (DOM-текст).
- Субпиксельная точность аффинных трансформаций сверх билинейного семплинга.
- Пакетное применение и пресеты.
-97
View File
@@ -1,97 +0,0 @@
# План: закрытие EASY/MEDIUM-пробелов из сравнения с onlinepngtools
> **СТАТУС: ВЫПОЛНЕН (2026-09-07) — устарел.** Все волны W1–W9 закрыты. Идеи из
> раздела «Вне очереди» (region-инструменты, мультифайловый вывод, анимационные,
> HARD-серия) перенесены в `backlog.md`.
> Статус: черновик на ревью
## Принципы
- Волны собираются вокруг общего математического ядра: одна волна = один
движок + N инструментов на нём.
- Каждая волна оформляется отдельным планом при старте; здесь — очередь и
состав.
- Порядок: сначала кластеры, дающие много инструментов малой кровью, затем
одиночные эффекты, MEDIUM-пакеты в конце.
- Полный список параметров каждого инструмента живёт в карте инструментов
(tools-map.md); здесь только состав и ядро.
## Очередь волн
### W1. Палитры — ВЫПОЛНЕНА (11 инструментов)
Ядро: RGB↔HSL + гармонии цветового круга. Вывод: свотч-полотно (генератор).
Состав: color-wheel, complementary, monochromatic, analogous, triadic, tetradic,
similar-shades, sort-colors. Плюс утилиты смешения тем же ядром: mix-colors,
average-color, blend-two, step-between (+4, итого 12).
### W2. Каналы и пространства — ВЫПОЛНЕНА (6 инструментов)
Ядро: матрицы преобразования RGB→(HSL/HSV/HSI/CMYK/YCbCr/LAB) + визуализация
выбранного компонента серым или окрашенно. Общий select «компонент» + select
«режим отображения».
### W3. Маски по свойствам пикселей — ВЫПОЛНЕНА (7 инструментов)
Ядро: предикат над пикселем → бинарная маска (с инверсией и подсветкой цветом).
Состав: show-transparent, show-grayscale, show-color, light-mask, dark-mask,
unique-color-mask, extract-by-color.
### W4. Фигурные маски — ВЫПОЛНЕНА (4 инструмента)
Ядро: SDF фигуры (круг/квадрат/звезда/волна) → альфа-маска с fit-режимами.
Состав: circle-mask, square-mask, star-mask, wavy-mask.
### W5. Геометрия-добивки — ВЫПОЛНЕНА (5 инструментов)
Ядро: bbox по альфе (trim) переиспользуется тремя инструментами источника.
Состав: trim-empty-space (закрывает remove border/space), change-canvas-size,
change-aspect-ratio, landscape↔portrait, symmetric-copy.
### W6. Эффекты лёгкие — ВЫПОЛНЕНА (6 инструментов; color-blocks покрыт pixelate)
Состав: pixelate, color-blocks, randomize-pixels (seed), add-noise,
feather-edges, clean-edges, silhouette. Shadow/glow — сюда же, если потянет
этап: оба = размытая альфа + смещение + цвет (ядро blur уже есть).
### W7. Конвертеры и генераторы — ВЫПОЛНЕНА (11 инструментов)
PNG↔bytes, PNG↔rgb-values (текстовые результаты), verify-is-png (анализ
сигнатуры), text-to-png (domText без входной картинки), emoji-to-png,
placeholder-png, color-spectrum, colorful-random (seed), draw-grid.
multi-color-gradient — если успеем новый тип параметра «список цветов», иначе
перенос.
### W8. Цветовые MEDIUM — ВЫПОЛНЕНА (4 инструмента; median-cut + Floyd–Steinberg/Bayer, custom-palette через text-параметр)
Ядро: квантование (median-cut или k-means). Состав: quantize (k),
decrease-color-count (=quantize с пресетами), custom-palette (маппинг на список
цветов — нужен тип параметра «список»), dithering (Floyd–Steinberg/Bayer поверх
квантования).
### W9. Сжатие — ВЫПОЛНЕНА (3 инструмента; optimize/change-quality осознанно не выделены — покрыты пресетами compress и W8)
Без wasm честное управление размером PNG ограничено: реальный рычаг —
квантование (W8) + обрезка метаданных + итеративный подбор под целевой KB.
Состав: compress (пресеты усилий), reduce-to-size (целевой KB, бинарный поиск по
k), optimize (re-encode), change-quality/low-quality — переосмыслить как пресеты
W8; jpeg-artifacts уже покрывает «испортить».
## Вне очереди
- Region-инструменты
(censor/erase/pixelate-area/blur-area/sharpen-area/reverse-area) — ждут UI
выделения области на превью; отдельное решение.
- Мультифайловый вывод (split-parts, gif-frames, separate-colors) — ждут
механизм «результат = набор файлов».
- Анимационные (slow-reveal/fade/scrolling) — выход не PNG; отдельное решение о
формате.
- HARD: glitch, barcode, signature-extract, handwritten-digital.
- Нишевые серии logo/icon/stamp/signature — не копируем.
## Оценка масштаба
W1–W7 дают **~47 новых инструментов**, почти все EASY. W8 добавляет ядро
квантования (открывает custom-palette/dithering и улучшает W9). Суммарно каталог
достигает ~120+ при текущих 71.
-83
View File
@@ -1,83 +0,0 @@
# План: герой-поиск v2 — иконки, популярность, карточки
> **СТАТУС: ВЫПОЛНЕН 2026-08-23.**
## 1. Что получается
Выдача поиска на главной превращается из списка строк в сетку карточек: слева
иконка инструмента, справа название и описание. Инструменты получают два новых
поля реестра — `popularity` (приоритет в выдаче) и `icon` (идентификатор
иконки). Иконки переиспользуются в каталоге `/list-tools` и в легендах шагов
цепочки. Поиск работает по-прежнему: нечёткий скоринг, клавиатура ↑↓ Enter Esc.
## 2. Данные реестра
- **`popularity?: number`** (0–100, по умолчанию 50) — приоритет внутри равного
скоринга запроса. Значения проставляются вручную осознанно: базовые операции
(обрезка, размер, удаление фона, конвертации JPG/PNG) — 80–95; цветовые и
геометрия — 40–70; узкие и генераторы — 10–30.
- **`icon?: string`** — имя иконки из набора Lucide. Явный словарь-маппинг для
всех инструментов в одном месте; отсутствие записи даёт нейтральную заглушку
(Wrench).
- Тест целостности расширяется: популярность в диапазоне, иконка — непустая
строка из разрешённого набора.
## 3. Иконки
Зависимость `lucide-svelte` (MIT) — один пакет, иконки рендерятся компонентами,
дерево не раздувается за счёт выборочного импорта.
Маппинг ведётся одним объектом `tool-icons.ts` рядом с реестром: 51 запись вида
`'crop-png': Crop`. Подбор по смыслу: обрезка — Crop, размер — Scaling, поворот
— RotateCw, отражение — FlipHorizontal2, оттенок — Palette, размытие — Droplets,
резкость — Focus, удаление фона — Scissors, анализ — Info и так далее; для
семейств конвертаций — FileInput/FileOutput варианты.
Иконка появляется в трёх местах: карточка выдачи, карточка каталога
`/list-tools`, легенда шага цепочки («Шаг N: [icon] Название»).
## 4. Карточки выдачи
- Сетка: одна колонка на узком экране, две — начиная со среднего героя.
- Композиция карточки: квадрат иконки слева, справа название (полужирное) и
однострочное описание с обрезкой.
- Состояния hover и активной с клавиатуры — тот же акцент подсветки, что сейчас.
- Сортировка выдачи: скоринг запроса ↓, затем популярность ↓, затем название.
## 5. Этапы и проверки
- **A. Реестр** — поля `popularity` и `icon`, словарь иконок на все 51,
расширение тестов целостности. Проверка: `pnpm test/check`.
- **B. Карточки поиска** — сетка выдачи с иконками и учётом популярности,
клавиатура без изменений. Проверка: визуально, `check/build`.
- **C. Распространение** — иконки в каталоге `/list-tools` и легендах звеньев.
Проверка: визуально, `build`.
- **D. Итог** — полный прогон и смоук §8.
## 6. Смоук-чеклист
1. Пустой запрос на герое показывает популярные первыми после первого символа.
2. Запрос «обрез» поднимает карточку обрезки выше прочих совпадений.
3. У каждой карточки есть иконка; у большинства — разные, соответствующие
смыслу.
4. Наведение и стрелки подсвечивают карточку; Enter открывает инструмент.
5. Открытый инструмент работает как раньше: параметры в колонках, результат,
скачивание.
6. Цепочка: легенда каждого шага показывает иконку выбранного инструмента.
7. `/list-tools`: карточки категорий с иконками, ссылки работают.
8. Регресс: старые сценарии (загрузка файла, цепочка, скачивание) не изменились.
## 7. Критерии готовности
- Все инструменты имеют иконку и ненулевую популярность; тесты это гарантируют.
- Выдача поиска — сетка карточек с корректной сортировкой и живой клавиатурой.
- Иконки присутствуют в поиске, каталоге и легендах шагов.
- `test/check/build` зелёные, смоук §6 пройден.
## 8. Что сознательно не делаем
- Автоматический расчёт популярности из статистики использования — значения
ручные.
- Собственный набор SVG-иконок — берём готовый открытый.
- Переводы названий и поиск на других языках — отдельная i18n-волна.
- Изображения-превью в карточках поиска — только иконка и текст.
-111
View File
@@ -1,111 +0,0 @@
# План: i18n — русский и английский
> **СТАТУС: ВЫПОЛНЕН 25.08.2026.** Базовая локаль — английский (реестр), русский
> — перевод в словаре; строки инструментов в реестре переведены на английский в
> рамках этапа C.
## 1. Что получается
Сайт работает на двух языках: русский (базовый) и английский. Переключатель в
шапке, выбор сохраняется в localStorage, тег `html lang` обновляется на клиенте.
Переведено всё: каркас интерфейса, карточки инструментов, цепочки, поиск, тексты
ошибок. Поиск находит инструменты по запросу на любом из двух языков независимо
от активной локали.
Каталог и поведение не меняются: ни новых инструментов, ни изменений логики
существующих.
## 2. Решения
- Без сегментов языка в URL. Статика пререндерится на русском; переключение на
клиенте, выбор живёт в localStorage. hreflang/canonical и вторая копия страниц
для поисковиков — не в этой волне.
- Ядро не знает о локалях. Ошибки в core выбрасываются стабильными кодами
(например ERR_BAD_HEX); перевод кода в человекочитаемый текст происходит в
слое исполнителя/UI.
- Английский — базовая локаль и фолбэк: строки инструментов в реестре записаны
по-английски. Переводы живут в секции tools словарей по ключу id инструмента
(title, description, подписи параметров, подписи опций select,
тексты-результаты анализаторов); сейчас перевод — русский в ru.ts. Доступ из
компонентов только через хелперы
toolTitle/toolDescription/paramLabel/optionLabel. Так каждая строка имеет
единственный источник: en — реестр, переводы — словари.
## 3. Структура i18n-модуля
- web/src/lib/i18n/locale.svelte.ts — состояние активной локали (runes), персист
в localStorage (ключ locale, guard на SSR), обновление
document.documentElement.lang.
- web/src/lib/i18n/t.ts — функция перевода с фолбэком на базовый язык и
интерполяцией переменных вида {n}.
- web/src/lib/i18n/ru.ts и en.ts — словари одного типа Dict; тип экспортируется
из ru.ts, en.ts обязан ему соответствовать — пропущенные ключи ловятся на
компиляции.
- Секции словаря: header, home, catalog, toolPage, chain, sourceCard,
resultCard, paramsCard, textInput, textResult, download, infoPanel, dropZone,
search, ui (aria кнопок слайдера и пипетки), categories, errors, tools.
## 4. Этапы и проверки
Каждый этап заканчивается зелёными test/check/build и ревью.
- A. Инфраструктура — модуль состояния, функция перевода с фолбэком и
интерполяцией, словари-скелеты со всеми секциями (заполнены header и
categories как пилот), переключатель RU/EN в шапке, реактивный lang. Тесты:
фолбэк на ru при отсутствии ключа в en, интерполяция, персист локали.
- B. Каркас интерфейса — перенос всех хардкод-строк компонентов и маршрутов в
словари: layout (шапка, футер), главная (герой, «вернуть последний», title),
каталог, карточки источника/результата/параметров, текстовый вход и результат,
«Показать маску», DropZone/DropOverlay, кнопка скачивания (busyText),
InfoPanel плюс форматирование чисел через тег локали вместо жёсткого ru-RU,
ToolPage (легенды, «Шаг n», ошибки шага), ChainToolBlock, placeholder и пустой
результат поиска, страницы инструмента (title, 404). Обёрточные сообщения
исполнителя («Ошибка исполнения в воркере») — тоже здесь.
- C. Контент реестра — переводы 69 инструментов
(title/description/labels/options) в tools-секции словаря en; компоненты
получают строки через хелперы toolTitle/toolDescription/paramLabel/optionLabel
с фолбэком на реестр. Тест целостности: у каждого инструмента есть непустые
перевод title/description, полный набор param-ключей и подписи всех опций
select; лишние ключи словарей отсутствуют.
- D. Поиск по любому языку — нормализация запроса и полей: нижний регистр, ё→е,
снятие диакритики через NFD; скоринг против активной и базовой локали сразу,
лучший балл побеждает; сортировка совпадений через Intl.Collator активной
локали. Тесты нормализации: ё/е, диакритика, английский запрос при русской
локали и наоборот.
- E. Ошибки на кодах — около 40 мест throw в core, registry, pipeline заменяются
на коды; исполнитель переводит код в текст активной локали, неизвестные
сообщения показываются как есть. Пять тестов, матчащих русские подстроки
ошибок (executor, pipeline, registry, io, alpha), переходят на коды — это
упрощает ассерты. Русские describe/it в тестах не трогаем.
- F. Итог — полный прогон, смоук чеклист §6, архивация плана.
## 5. Смоук-чеклист
1. Переключение RU/EN в шапке мгновенно меняет язык без перезагрузки; после
перезагрузки выбор сохранён.
2. Тег html lang соответствует активной локали.
3. Главная, каталог, страница инструмента, цепочка из трёх звеньев — везде
переведено, без смеси языков.
4. Форма параметров: подписи полей и опции select переведены у произвольных пяти
инструментов из разных категорий.
5. Битый вход (некорректный HEX в тонировании, нулевой размер) показывает ошибку
на активном языке.
6. Поиск: «повер» находит повороты при русской локали; «rotate» находит их же;
при английской локали наоборот. Диакритика и ё не мешают совпадению.
7. Числа в панели информации отформатированы по локали.
8. Скачивание, пипетка, маски, слайдеры работают одинаково на обеих локалях —
функциональность не задета.
## 6. Критерии готовности
- Оба словаря типобезопасны и полны; тест целостности секции tools зелёный.
- Ни одного пользовательского русского строки вне словарей (кроме тестовых
describe/it).
- test/check/build зелёные, смоук пройден.
## 7. Что сознательно не делаем
- Сегменты языка в URL, prerender обеих локалей, hreflang/canonical.
- Транслитерация поисковых запросов ru-en.
- Третьи и дальнейшие языки (структура позволяет, добавление — отдельная волна).
- Перевод описаний коммитов, README и документации docs/.
-83
View File
@@ -1,83 +0,0 @@
# План: параметры между исходником и результатом
> **СТАТУС: УСТАРЕЛ (2026-09-07).** Раскладка описывалась для старого
> `ToolPage`; перекрыта редизайном (`plan-redesign.md`) — страницы инструментов
> теперь `SchemaToolView`/`SchemaPreview` в новом дизайне. Сама идея «инлайн:
> исходник → параметры → результат» живёт в `backlog.md` (№2).
> Статус: план к выполнению.
## 1. Что получается
Альтернативная раскладка этапа инструмента: три колонки «Исходник | Параметры |
Результат» на широких экранах, вертикальный стек на узких. Цель — отдать вариант
дизайнеру как отправную точку. Откат к текущей раскладке — замена одной строки в
`ToolPage`.
## 2. Решения
- Два взаимозаменяемых компонента этапа с одинаковым набором пропсов; `ToolPage`
рендерит один тег. Попробовать новое — поменять компонент в этом теге; вернуть
старое — поменять обратно. Никаких флагов и конфигов.
- Общая типизация пропсов в одном файле рядом с компонентами; сами карточки
(`SourceCard`, `ResultCard`, `TextInputCard`, `ParamsCard`) переиспользуются
как есть — вся логика исполнения, маски, пипетки и автозапуска остаётся в
`ToolPage` и приходит сверху.
- Chain-звенья используют ту же панельную систему «Вход | Параметры | Результат»
— общие классы `.pane` / `.pane-legend` / `.pane-params` вынесены в app.css,
вертикальные поля в узкой колонке заданы один раз глобально.
- Один порог отзывчивости: три колонки от ~75rem, ниже — стек «исходник →
параметры → результат». Промежуточные перестроения не придумываем до фидбека
дизайнера.
## 3. Компоненты
```txt
src/lib/components/stage/
stage-props.ts # интерфейс StageProps + сборщик дефолтов
ToolStageClassic.svelte # текущая разметка .tool-stage, перенесена 1:1 из ToolPage
ToolStageInline.svelte # новая сетка [source | params | result]
```
- `ToolStageClassic` — вырезка существующего шаблона со стилями; после
извлечения страница должна быть пиксель-в-пиксель прежней.
- `ToolStageInline` — grid-колонки
`minmax(0, 1fr) | фикс-ширина параметров | minmax(0, 1fr)`; легенда
«Параметры» над средней колонкой в том же стиле edge-legend.
- Модификаторы состояний через классы на корне компонента: `no-params`
(инструменты без параметров → две колонки), `no-source` (генераторы →
параметры и результат), текстовый вход до декодирования занимает левую ячейку
как сейчас.
## 4. Этапы и проверки
Каждый этап заканчивается зелёными test/check/build и ревью.
- **A. Извлечение классики** — перенос разметки и стилей этапа из `ToolPage` в
`ToolStageClassic` без изменений поведения; пропсы собираются в одном объекте.
Критерий: страница неотличима, diff по логике `ToolPage` нулевой.
- **B. Инлайн-раскладка** — `ToolStageInline`, переключение в `ToolPage` на
новый компонент, прогон смоука §5. Ревью в браузере; решение дизайнера —
потом, код уже позволяет жить в обоих вариантах.
## 5. Смоук
1. Замена компонента этапа — одна строка; обе версии собираются без
предупреждений.
2. Инструмент без параметров (например Автоконтраст): две колонки, пустой
середины нет.
3. Генератор без исходника: параметры и результат в две колонки.
4. Текстовый инструмент до декодирования: вход слева, параметры в центре, пустое
состояние результата справа.
5. Маска, пипетка, слайдеры в средней колонке; изменение параметра перезапускает
инструмент как раньше.
6. Узкий экран: стек без горизонтального скролла, порядок исходник → параметры →
результат.
7. Легенды присутствуют у всех трёх зон в обеих раскладках.
## 6. Что сознательно не делаем
- Редизайн визуального языка (цвета, типографика, формы) — материал для
дизайнера, не для этого плана.
- Новая логика: исполнение, автосохранение, пресеты — раскладка ничего не знает
про исполнение.
-103
View File
@@ -1,103 +0,0 @@
# Plan: `/preview/list-tools` → design2 (catalog)
> **СТАТУС: ВЫПОЛНЕН (2026-09-07) — устарел.** Каталог `/preview/list-tools`
> переписан на `CatalogHeader` / `CatalogToolbar` / `CatalogGroup` / `ToolCard`
> (kit), поиск и фильтры функциональны. Доводка верности против рефа остаётся в
> рамках `plan-design-fix.md`.
Ветка `feat/redesign`. Цель — привести каталог инструментов к рефу
`refs-html/list-tools.html`. Сейчас страница вообще не на design2: по
мульти-аудиту `onlyOurs=257, onlyRef=54` (демо-страница — `18/11`).
## Текущее состояние
- `src/routes/preview/list-tools/+page.svelte` использует собственную вёрстку
(`.catalog` / `.cat-head` / `.cat-grid`) + `Panel` / `PanelHeading` /
`ToolCard` (kit). Нет поиска, фильтров, счётчика, навигация по категориям
сделана через `Panel`-блоки, а не через `.catalog-groups`.
- Реф (`list-tools.html`) строит:
- `.catalog-page` → `.catalog-head` (`.eyebrow`, `h1` «Tool catalog», `p`
lede, `.catalog-total` «NN TOOLS AVAILABLE» с большим числом),
- `.catalog-toolbar` (`.catalog-search` + `.catalog-filters` с кнопками),
- `.catalog-groups` (grid `repeat(2,minmax(0,1fr))`, gap `56px 28px`) →
`.catalog-group` (`.group-title` «CAT · NN TOOLS») → `.tool-cards` →
`.tool-card`.
- `.tool-card` (реф) — `display:grid`, колонки `42px minmax(0,1fr) 24px 18px`,
`min-height:106px`, `gap:14px`, `padding:16px`: `.tool-icon` (42×42 синий
размытый квадрат), `.tool-copy` (`strong` 13px mono + `span` 12px mono muted),
`.tool-index` (10px mono, сверху), `.tool-arrow` (появляется при hover).
- Токены `design2.css` уже совпадают с рефом (`--background #eef1f4`,
`--panel #f8fafb`, `--foreground #17212b`, `--muted #6d7883`,
`--line #cbd3da`, `--blue #1769d2`, `--radius 4px`) — править токены не нужно.
## Шаги
1. **Restyle `kit/ToolCard.svelte`** под реф (меняется «на месте» — используется
ещё в `preview/+page` и `preview/kit`, стиль унифицируется, ок):
- grid-колонки `42px minmax(0,1fr) 24px 18px`, `align-items:center`,
`gap:14px`, `min-height:106px`, `padding:16px`, `border:1px solid line`,
`background:panel`, `border-radius:radius`.
- `.tool-icon`: 42×42 `place-items:center`,
`background:color-mix(in srgb, var(--blue) 12%, transparent)`,
`color:var(--blue)` (реф использует `--panel-strong`; вместо нового токена
берём этот `color-mix`).
- `.tool-copy strong`: `600 13px var(--font-mono)`; `span`: `12px/1.5 mono`
`muted`.
- `.tool-index`: `10px mono muted`, `align-self:start`.
- `.tool-arrow`: `color:var(--blue)`, `opacity:0` → `1` +
`translate(2px,-2px)` на `.tool-card:hover`.
- DOM-порядок как в рефе: `icon, copy, index, arrow`.
- `href` через `resolve` из `$app/paths` (убрать `svelte-ignore`).
2. **`kit/CatalogHeader.svelte`**: `.eyebrow` «EASY-PNG-TOOLS / CATALOG», `h1`
«Tool catalog», lede; `.catalog-total` с динамическим `TOOLS.length` (реф
показывает `32`).
3. **`kit/CatalogToolbar.svelte`**: `.catalog-search` (иконка + `<input>`) и
`.catalog-filters` (кнопки по категориям). Состояние `query` / `category`
поднимается наружу через `bind:` (или callback-пропы), чтобы страница
фильтровала список. Фильтры функциональные (клиентский фильтр по названию +
категории), не декоративные.
4. **`kit/CatalogGroup.svelte`**: `.group-title` (uppercase label категории +
«NN TOOLS») + слот со списком карточек; сетка `.tool-cards` — `gap:10px`
(реф), на мобиле (`@media max-width:800px`) всё схлопывается в 1 колонку.
5. **Переписать `preview/list-tools/+page.svelte`** как тонкую композицию:
- обёртка `.catalog-page` (`padding:60px clamp(24px,4vw,72px) 72px`);
- `<CatalogHeader />`;
- `<CatalogToolbar bind:query bind:category />`;
- `.catalog-groups` из отфильтрованных групп (по `CATEGORIES`, пересчитав
`tools` с учётом `query`/`category`);
- внутри `<CatalogGroup>` — `<ToolCard>` на каждый tool
(`href= "/preview/tools/{id}"`, `icon` из `tool-icons`, `index` по порядку,
`title`/`description` из registry);
- убрать `Panel`/`PanelHeading` из этого маршрута.
6. **Категории/локализация**: labels привести к рефу (реф: ALL / CONVERT /
TRANSPARENCY / COLOR / GEOMETRY / FILTERS / ANALYZE). Наши `CATEGORIES`
(`convert/alpha/color/geometry/filters/text/analyze/generate`) — сопоставить
(`alpha`→TRANSPARENCY и т.п.); точное сопоставление уточнить по рефу и
`i18n/ru.ts`. Фильтры = ALL + те же label'ы. Тексты заголовков/lede — по рефу
(англ. «Tool catalog» / «Focused utilities for working with PNG…»), либо
оставить RU-вариант, если это сознательное отклонение (зафиксировать).
7. **Адаптив**: перенести правила рефа (`@media max-width:800px`) —
`.catalog-groups` → 1fr, `.tool-card` → `38px minmax(0,1fr) 18px`, скрыть
`.tool-index`/показать `.tool-arrow`, `.catalog-head` в колонку и т.д.
## Верификация
- `pnpm lint`, `pnpm exec svelte-check --tsconfig ./tsconfig.json`, `pnpm build`
— без ошибок.
- `pnpm refs-audit` (теперь мульти-маршрутный) → глянуть секцию
`/preview/list-tools`: `onlyOurs`/`onlyRef` должны упасть с `257/54` примерно
до уровня demo (`18/11`). Остаточный diff (если будет) зафиксировать в этом
файле.
## Примечания
- `ToolCard` правится на месте — влияет на `preview/+page` и `preview/kit` (там
тоже карточки каталога, унификация уместна).
- Старый дизайн (`routes/(old)/**`, `components/search/**`) не трогать.
- Коммиты делает автор (агент не коммитит).
-97
View File
@@ -1,97 +0,0 @@
# План: MEDIUM-волна №1 — удаление фона, размытие, резкость + Web Worker
> **СТАТУС: ВЫПОЛНЕН 2026-08-23.**
## 1. Зачем
- Первые операции настоящего алгоритмического уровня: свёртки и заливка связной
области.
- Витринная фича каталога — удаление однотонного фона без нейросетей: порог по
цвету + заливка от краёв изображения + сглаживание границы маски.
- Эти операции реально блокируют UI-поток на больших картинках — повод включить
давно подготовленный путь: исполнитель `executeStep` переезжает в Web Worker,
страницы не меняются вовсе.
## 2. Инструменты — 3
- **Удалить фон PNG** (`remove-background-png`, категория «Прозрачность») —
параметры: цвет фона, допуск похожести %, режим «только внешние области»
(заливка от краёв вместо глобального совпадения), сглаживание границы (проходы
мажоритарного фильтра маски). Даёт превью маски через существующий контракт
`preview`.
- **Размытие PNG** (`blur-png`, новая категория «Фильтры») — гауссово
приближение тремя проходами разделяемого бокса; радиус слайдером 1–32 px.
- **Резкость PNG** (`sharpen-png`, «Фильтры») — свёртка ядром резкости с
регулируемой силой (смесь исходного и обработанного), сила слайдером 0–100 %.
## 3. Новое ядро (чистые функции, детерминированные, покрытые тестами)
- `core/convolution.ts`
- универсальная свёртка с клампом краёв;
- разделяемый бокс-блюр и сборка гауссова приближения из трёх проходов;
- ядро резкости и смешивание с оригиналом по силе.
- `core/background.ts`
- построение маски удаления по цветовому расстоянию (та же метрика, что у
удаления цвета);
- заливка связной области от всех граничных пикселей (очередь, без рекурсии);
- мажоритарное сглаживание маски заданным числом проходов;
- сборка результата: альфа 0 у удалённых пикселей; плюс функция маски для
превью.
## 4. Web Worker
- Точка исполнения одна — `tools/executor.ts`; меняется только её внутренность.
- Воркер-файл импортирует реестр и исполняет операцию по `toolId`; картинка
приходит копией (структурный клон), результат возвращается передаваемым
буфером.
- Ленивая инициализация воркера при первом вызове; если воркер недоступен
(старый браузер/тестовая среда) — автоматический откат на прямое исполнение в
главном потоке. Обе ветви обязаны давать побайтово одинаковый результат.
- Генераторы и текстовые представления остаются в главном потоке — они
мгновенные.
- Пипетка и лупа работают с уже готовыми картинками и не зависят от переноса.
## 5. Этапы и проверки
- **A. Ядро** — свёртки и фон с тестами на малых матрицах (точные ожидания
руками). Проверка: `pnpm test/check`.
- **B. Реестр** — категория «Фильтры», три записи, превью маски. Проверка: тесты
целостности, визуально.
- **C. Web Worker** — перенос исполнителя, отккат-ветвь. Проверка: результаты
до/после переключения идентичны байт-в-байт на фиксированной картинке и
параметрах; `test/build` зелёные.
- **D. Итог** — полный прогон и смоук §8.
## 6. Смоук-чеклист
1. Размытие: радиус 1 и 32 — плавно усиливается, прозрачные края не темнеют.
2. Резкость: сила 0 не меняет картинку; сила 100 заметно подчёркивает края без
цветового мусора.
3. Удаление фона на логотипе с однотонным фоном: фон исчез, содержимое цело.
4. Режим «только внешние области»: дырка внутри объекта того же цвета остаётся
непрозрачной.
5. Сглаживание 0/2/4 прохода — граница от жёсткой к мягкой, результат
воспроизводим.
6. Превью маски показывает белым удаляемую область; скачивание всегда отдаёт
результат.
7. Во время размытия большой картинки интерфейс остаётся отзывчивым (чип
«Пересчёт…», скролл живой).
8. Цепочка: размытие → резкость → удаление фона работает; каждый блок скачивает
свой промежуток.
9. Регресс старых инструментов и главной-поиска без изменений поведения.
## 7. Критерии готовности
- Три новых инструмента проходят смоук §6; операции ядра покрыты точными
юнит-тестами.
- Исполнение изображений идёт через воркер там, где он доступен, с
гарантированно одинаковым результатом обеих ветвей.
- `test/check/build` зелёные.
## 8. Что сознательно не делаем
- Многопроходное гауссово ядро «по-настоящему» (O(r²)) — трёхпроходного
боксового приближения достаточно визуально и оно быстрее.
- Хромакей по нескольким цветам, ИИ-сегментация — следующие волны.
- Отмена выполняющегося прогона воркера — устаревшие результаты отбрасываются
токеном, сама работа завершается фоном.
-95
View File
@@ -1,95 +0,0 @@
# План: MEDIUM-волна №2 — морфология прозрачности
> **СТАТУС: ВЫПОЛНЕН 2026-08-23.**
## 1. Зачем
Одна пара функций — расширение и сужение непрозрачных областей — открывает целое
семейство пользовательских операций: обводка логотипов и иконок, контуры,
утолщение и утончение подписей, удаление мелкого мусора и закрытие дырок. Всё
это частые запросы в наборах PNG-утилит, а ядро получается компактным и легко
покрывается тестами.
## 2. Инструменты — 7 (все в категории «Прозрачность»)
- **Обвести PNG** (`add-stroke-png`) — кольцо выбранного цвета вокруг
непрозрачного содержимого; параметры: цвет, толщина 1–10 px.
- **Найти контур PNG** (`find-contour-png`) — линия по краю непрозрачных
областей; параметр: толщина линии.
- **Утолщить PNG** (`make-thicker-png`) — расширение содержимого на N пикселей.
- **Утончить PNG** (`make-thinner-png`) — сужение содержимого на N пикселей.
- **Жёсткие края PNG** (`harden-alpha-png`) — бинаризация альфы по порогу:
полупрозрачность уходит, остаётся два состояния.
- **Убрать мусор PNG** (`despeckle-alpha-png`) — открытие: удаляет одиночные
полупрозрачные пиксели и мелкие крапинки.
- **Закрыть дыры PNG** (`close-holes-png`) — закрытие: заполняет одиночные
прозрачные точки внутри объекта.
Все операции применяются к альфа-каналу; RGB пикселей сохраняется.
## 3. Ядро `core/morphology.ts`
Чистые функции над бинарной маской и над альфа-каналом:
- `buildAlphaMask(img)` — 1 там, где альфа > 0.
- `dilateMask(mask, w, h, radius, shape)` — расширение; `shape`: квадрат или
диск (для обводок диск даёт ровную круглую линию).
- `erodeMask(...)` — зеркальная операция.
- `dilateImage / erodeImage(img, radiusPx)` — применяют маскирование к альфе
изображения, RGB сохраняется.
- Сборщики:
- обводка: `dilate(mask) − mask` → новое кольцо заливается цветом;
- контур: `mask − erode(mask)` → линия;
- открытие: `erode → dilate`; закрытие: `dilate → erode`.
- `hardenAlpha(img, thresholdPercent)` — отдельно от морфологии, но той же
волной.
Структурирующий элемент при радиусе r — диск радиуса r (расстояние до центра ≤
r); квадрат не используем, чтобы углы обводок не были угловатыми.
## 4. Web Worker
Ничего не требуется: новые операции попадают в реестр, исполнитель автоматически
исполняет их в воркере. Тесты ядра идут мимо воркера (чистые функции).
## 5. Этапы и проверки
- **A. Ядро** — `morphology.ts` + точные тесты на матрицах 5×5: расширение
диска, сужение, открытие убирает одиночный пиксель, закрытие закрывает
одиночную дыру, кольцо обводки нужного цвета и толщины. Проверка:
`pnpm test/check`.
- **B. Реестр** — семь записей, тест целостности обновляется (51 → 58).
Проверка: `test/build`, визуально.
- **C. Итог** — полный прогон, смоук §7.
## 6. Критерии готовности
- Семь новых инструментов работают на прозрачных PNG: обводка, контур,
толще/тоньше, жёсткие края, убрать мусор, закрыть дыры.
- Все операции детерминированы и покрыты юнит-тестами с ручными ожиданиями.
- Исполнение автоматически идёт в воркере.
- `test/check/build` зелёные, смоук §7 пройден.
## 7. Смоук-чеклист
1. Обводка толщиной 3 вокруг круглого стикера — ровное кольцо выбранного цвета,
без угловатости.
2. Толщина 1 — тонкая аккуратная линия; 10 — толстая, форма содержимого не
искажается сверх роста.
3. Контур показывает только край содержимого линией.
4. Толще/тоньше на тонкой надписи — штрихи растут/сжимаются без разрывов при
умеренных значениях.
5. Жёсткие края: полупрозрачность исчезает, силуэт сохраняется.
6. Убрать мусор убирает одиночные точки-крапинки, не трогая крупные области.
7. Закрыть дыры заполняет одиночные прозрачные точки внутри логотипа.
8. Цепочка: обводка после удаления фона работает; каждое звено скачивает свой
промежуток.
9. Регресс остальных инструментов без изменений.
## 8. Что сознательно не делаем
- Морфология по яркости для непрозрачных сканов — волна фокусируется на альфе
прозрачных PNG.
- Выбор структурирующего элемента пользователем — всегда диск.
- Feather/свечение краёв — отдельная будущая задача поверх размытия.
- Произвольные SVG-маски и градиентные обводки.
-181
View File
@@ -1,181 +0,0 @@
# План: дожать перевод инструментов в preview и переезд ветки `old/`
> **СТАТУС: ЗАКРЫТ (2026-09-07).** Перевод каталога завершён — 121 из 125
> инструментов в `registry-new`; 4 хвоста отложены решениями и перенесены в
> `backlog.md` (`watermark-image-png` → overlay-райз, `png-info` → exif-райз,
> `reduce-to-size-png` → download-райз, `compress-png` → закрыт дубликатом).
> Дальнейшие «хвосты плана» — переезд старых компонентов в папки `old/`,
> закрытие долгов и сбор фидбека — ~уже учитываются в `plan-composite-params.md`
> (шаги 36–37) и `plan-design-fix.md`, поэтому план закрыт, а не продолжен.
> Архивная копия, ниже — состояние на момент закрытия.
> Статус: **переведено 121 из 125; остальные 4 — отложены/закрыты решениями.**
> Текущее состояние: **121 инструмент переведён в `registry-new`**, старый UI
> уже живёт на `(old)/`-маршрутах, линтер-изоляция old↔preview на месте (плагин
> `isolation/no-mixed-imports`). Здесь — что сделать, чтобы:
>
> 1. в preview был **весь** каталог инструментов (плюс закрыты известные долги);
> 2. физически перенести старую ветку в папки `old/` (шаг 37 основного плана);
> 3. после переезда провести детальную проверку в реальных условиях и собрать
> отзывы пользователей (правки поверх — отдельными задачами).
>
> Порядок: перевод остатка → починить известные долги → переезд в `old/` →
> пользовательская проверка/фидбек → (позже, отдельным решением) удаление
> старого кода.
## Текущая картина (факты)
- `registry.ts` + `registry/` — **125 инструментов**; `registry-new/` — **121**.
Не переведены только: `png-info` (отложен, хвост-фича с exif), `compress-png`
и `reduce-to-size-png` (закрыто решениями, см. ниже), `watermark-image-png`
(overlay — единственный реальный остаток).
- Preview (`/preview`) показывает переведённые; старый UI работает на
`/`-маршрутах (группа `(old)/`): `+page`, `demo`, `list-tools`, `tools/[id]`.
Переведено всё кроме четырёх хвостов: color 18, filters 3, geometry 13, alpha
13 (+watermark-image в остатке), generate 10 (mix/sort), analyze 12 (6 масок +
verify + 5 вердиктов), convert 14 (jpg/webp/bmp + 5 png→texт + 6 text→png),
text `watermark-tile-png`.
- Плагин `isolation/no-mixed-imports` (`web/eslint-plugins/isolation/`)
полностью разделяет ветки: старый `lib/registry.ts|/registry/**`,
`lib/registry-helpers.ts`, `lib/categories.ts`, `lib/tools/**`,
`lib/components/**`(кроме `kit/`) vs новый `routes/preview/**`,
`lib/registry-new/**`, `lib/preview/**`, `lib/registry-schema.ts`,
`lib/components/kit/**`. Общее: `core/`, `i18n/`, `theme`, `assets/`, корневой
`lib`.
- Правило копий (из основного плана): файл, используемый `(old)`, не трогаем —
делаем копию в `lib/preview/` (пример: `lib/preview/tool-icons.ts`).
- Известные долги (предсуществующие, не создавались нами): ошибка
`ToolCard.svelte:19:36` (svelte-check), фейл `palette.test.ts` (ENOENT
`src/app.css` — из-за него `pnpm test` красный при 607 passed), ~335
design-tokens ошибок `lint:all` в старых kit-компонентах, a11y-warning в
`ColorField.svelte`, пустой ruleset в `Toggle.svelte`, неиспользуемые токены в
`preview.css`.
## Скоуп перевода: переведено всё переводимое (отложено 4 хвоста)
Каталог дожат до предела текущих решений: переведено 121 из 125. Оставшиеся 4
инструмента **отложены осознанно** (не делаем сейчас):
- **`watermark-image-png` (alpha)** — **отложен**. Нужна overlay-механика
(второе изображение-знак через `getOverlay`/store + параметры
scale/opacity/position/margin/aspect); решается отдельно, вместе с вопросом,
как второй источник изображения вписывается в schema-driven preview.
- **`png-info`** — **отложен осознанно**: будет серьёзно дорабатываться
отдельным райзом (exif-теги, редактирование, структурированный вывод), для
него нужен свой отдельный случай в UI, не «ещё один text/verdict».
- **`compress-png`** — **закрыт как дубликат** `decrease-color-count-png`:
пресеты сжатия 192/96/44 добавлены прямо в схему `decrease-color-count`.
- **`reduce-to-size-png`** — **отложен** до достройки кнопки Download (backlog
№7, «экспорт с лимитом размера»).
### Переведено в этом проходе (text-механика)
- **Типы:** `ToolEntry.input: "file" | "text" | "none"`,
`ToolEntry.result: "image" | "text" | "verdict"`, `OutputFormat` (mime/ext/
qualityParamId) для download.
- **Executor:** `executeFromText` / `executeToText` / `executeTextToText`
(прямые, без worker), экспорт в `preview/executor/index.ts`.
- **UI:** `SchemaTextSource` (textarea + Render), `SchemaTextResult` (кол-блок с
Copy/Download .txt или бейдж-вердикт по префиксу Yes/No), интеграция в
`SchemaPreview`/`SchemaToolView` (режимы file/text/generate, рендер
image/text/verdict, скрытие image-download для text-результата).
- **Инструменты:** convert 5×png→text (base64/data-uri/hex/bytes/rgb-values),
6×text→png (base64/data-uri/hex/bytes/rgb-values/svg, +width), analyze
verify-is-png (text→text) и 5 вердиктов (grayscale/file-size/transparent/
orientation). Доработка: `base64ToBytes` сужен до `Uint8Array<ArrayBuffer>`
(совместимость с `decodeBytes`).
- **Гейты:** eslint 0, prettier чист, svelte-check — только предсуществующий
ToolCard, тесты — только предсуществующий фейл `palette.test.ts`.
## Известные долги перед переездом (завести tasks и закрыть)
По AGENTS.md техдолг «чинится только по заведённым tasks, не игнорируется»:
`lint:all` (~335 design-tokens в kit), неиспользуемые токены preview.css, a11y
`ColorField`, пустой ruleset `Toggle`. Плюс два, мешающих зелёной проверке
до/после переезда: svelte-check error `ToolCard.svelte:19:36` и фейл
`palette.test.ts` (ENOENT `src/app.css`). После их закрытия целевое состояние
проверок перед переездом: `pnpm --dir web exec svelte-check` → 0 errors,
`pnpm --dir web test` → зелёный, `pnpm --dir web lint:all` → только задокумент.
ированный остаток (или 0).
## Шаги перевода остатка (порядок)
1. **Простые пачки** — **выполнено**: color (18) → filters (3) → geometry (13) →
alpha (13/14, кроме watermark-image) → generate (10, включая mix/sort через
kind `colors`) → analyze-маски (6) → text `watermark-tile-png`.
2. **Text-механика** — **выполнено**: типы `input`/`result` + output-формат,
executor `executeFromText`/`toText`/`textToText`, UI
`SchemaTextSource`/`SchemaTextResult` + интеграция в preview, перевод
конвертеров convert (5 png→text + 6 text→png) и analyze (verify + 5
вердиктов). `png-info` отложен отдельным райзом (exif).
3. **Шаг про overlay не делается** — `watermark-image-png` **отложен решением**
(см. «Скоуп перевода» выше), отдельным райзом вместе с вопросом второго
источника изображения в schema-driven preview.
4. После категории — `schema.layout` для сгруппированных инструментов (шаг 32-33
паттерн) и ручная проверка в `/preview`.
## Переезд в `old/` (шаг 37 основного плана)
> Скоп-сдвиг: «Scoped-пути двигаются вместе с папками» — обновляется
> `eslint.config.js` (isolation-паттерны и design-tokens glob), не код правила.
1. **Что переезжает:** `lib/registry.ts`, `lib/registry/**`,
`lib/registry-helpers.ts`, `lib/categories.ts`, `lib/tools/**`,
`lib/components/**` (кроме `kit/`) → папки `old/` (например
`lib/old/registry …`, `lib/old/tools`, `lib/old/components`) — то есть всё
старое в одном поддереве. `routes/(old)/` остаётся как есть (уже отделено).
2. **Обновить конфиг плагина** `isolation/no-mixed-imports`: старые glob-ы
`lib/registry/**…` заменить на `lib/old/**` (+ `lib/old/components/**` и
т.п.), «новое» и «общее» не меняются. Проверить, что правило по-прежнему
разделяет (тест фолс-позитивов: временный импорт old↔new ловится).
3. **Импорты внутри старого кода** — относительные пути остаются рабочими (папки
двигаются целиком); поправить только то, что ссылалось на `$lib/...` в другом
стиле. Старые тесты (`registry.test.ts` и др.) переезжают вместе с
исходниками и продолжают проходить.
4. **Гейты переезда:** `svelte-check` 0 errors, `pnpm --dir web test` зелёный,
`prettier --check .` чистый, `lint:all` = задокументированный остаток, обе
ветки открываются (старая `/`, новая `/preview`).
## Проверка и сбор отзывов после переезда
1. **Регресс-чеклист по каталогу** — пройти все инструменты в `/preview`:
дефолты совпадают со старыми, валидация/клампы поведения не меняют, результат
эквивалентен старому UI (сверить на одинаковых входах).
2. **Тяжёлые кейсы руками:** генераторы, маски, font-style/plate/gradient на
всем наборе, text-source, output-формат. (`watermark-image` — позже, после
overlay-райза.)
3. **Сбор фидбека** — отдельные задачи на каждый найденный фикс; чеклист
зафиксировать в этом документе по мере находок (или ссылкой на tasks).
4. Сверка каталога с `tools-map.md` (покрытие/дедупликация
`convert-png-to-jpg`/`webp`), при необходимости — обновить cards/мета.
5. **Решение об удалении старого** — после завершения ревью и фидбека, отдельным
шагом: тогда чистятся `lib/old/**`, `routes/(old)/**`, `old.css`, `exports`,
изоляционный плагин (glob-ы больше не нужны) и открываются «правило
копий»-дубли.
## Как ревьюить каждый шаг (гайды)
- Диф на **один инструмент** (или фундамент без инструментов) < ~500 строк.
- Старый `params`/`run`/`defaultParams`/`sanitizeParams` не изменены; в
`registry-new` копия логики + `schema` (дефолты равны старым).
- Старый UI и старые тесты продолжают работать: `pnpm --dir web test`,
`pnpm --dir web exec svelte-check`.
- Новый инструмент проверяется **руками в `/preview`** end-to-end (применение +
сброс дефолтов + смена значений).
- После переезда каждый move-шаг гоняет 4 гейта (см. выше).
## Оценка трудозатрат
| Часть | Сложность | Оценка |
| ---------------------------------------------------------------------- | -------------- | ------------------------- |
| Простые инструменты (color/filters/geometry/alpha/generate/маски/text) | Низкая-Средняя | **выполнено** (62 шт) |
| Text-механика + конвертеры/вердикты | Средняя | **выполнено** (16 шт) |
| watermark-image / png-info / reduce-to-size (отложены решениями) | Высокая | отложено, отдельные райзы |
| Долги (ToolCard, palette.test.ts, lint:all остаток) | Средняя | ~2-4ч |
| Переезд в `old/` + конфиг плагина + гейты | Средняя-Низкая | ~2-4ч |
| Проверка/фидбек + правки | Зависит | ~3-6ч |
| **Итого** | | **~9-18ч** по фазам |
> Оценки ориентировочные; особые случаи — с ревью решения, объём финальных
> правок по фидбеку не предсказуем.
-131
View File
@@ -1,131 +0,0 @@
# План: единый `run(ctx)` в registry-new (унификация 5 режимов исполнения)
> **СТАТУС: ВЫПОЛНЕН (2026-09-10) — устарел.** Убивал пару багов-классов
> «инструмент с generate показывает загрузку и не выдаёт результат»
> (linear-gradient-png и все генераторы): причина — `input` не задан, диспетч по
> факту наличия методов.
## Исходная проблема
Два бага у `linear-gradient-png` (и у всех генераторов):
1. Показывается загрузка изображения — `SchemaToolView.svelte` считает
`inputMode = tool.input ?? "file"`, а `tool.input` у генераторов не задан.
2. Результата нет — `SchemaActions` рендерит кнопку «Generate» только при
`inputMode === "none"`, а он всегда `"file"`. Ui-контур «догадывается» о
генераторе по эвристике `Boolean(tool.generate && !tool.run)`.
Корневая причина архитектурная: у инструмента 5 режимов исполнения (`run` 79
шт., `generate` 21, `runFromText` 6, `toText` 9, `textToText` 1), и диспетч
«какой метод вызывать» размазан между UI и executor. Способ исполнения
(worker/поток, было `domOnly`) просочился в метод-диспетч. Договорились:
**свести всё к одному методу `run(ctx)`**, а требование входа (`input`) — к
обязательному полю. `domOnly` остаётся только executor-хинтом (уйдёт при
переезде на WASM-ядро).
## Целевой контракт
```ts
export type InputMode = "image" | "text" | "none"; // было "file"
export type ResultKind = "image" | "text" | "verdict";
export interface ToolContext<P> {
params: P;
source?: PixelImage; // присутствует ⇔ input === "image"
text?: string; // присутствует ⇔ input === "text"
}
export type ToolResult = PixelImage | string;
export type ToolEntry<P = Record<string, unknown>> = {
id: string;
title: string;
description: string;
category: CategoryId;
schema: ToolSchema<P>;
input: InputMode; // ОБЯЗАТЕЛЬНОЕ, дефолта нет
result?: ResultKind; // default "image"
run(ctx: ToolContext<P>): Promise<ToolResult> | ToolResult;
output?: OutputFormat;
icon?: string;
domOnly?: boolean; // executor-хинт, уйдёт с WASM
};
```
Удаляем: `generate`, `runFromText`, `toText`, `textToText`. Хелперы контракта
(бросают `ToolError`): `requireSource(ctx)`, `requireText(ctx)`. Обёртки для
миграции тел 1:1: `imgTool((img, p) => …)`, `genTool((p) => …)`,
`textGen((t, p) => …)`.
```ts
const imgTool =
<P>(fn: (img: PixelImage, p: P) => ToolResult) =>
(ctx: ToolContext<P>) =>
fn(requireSource(ctx), ctx.params);
const genTool =
<P>(fn: (p: P) => ToolResult) =>
(ctx: ToolContext<P>) =>
fn(ctx.params);
const textGen =
<P>(fn: (t: string, p: P) => ToolResult) =>
(ctx: ToolContext<P>) =>
fn(requireText(ctx), ctx.params);
```
## Executor (единый)
```ts
export async function execute(
tool: ToolEntry,
ctx: { params: Record<string, unknown>; source?: PixelImage; text?: string },
): Promise<ToolResult> {
const params = sanitizeSchemaParams(tool.schema, ctx.params);
if (tool.input === "image" && !ctx.source)
throw new ToolError("errors.sourceRequired");
if (tool.input === "text" && !ctx.text?.trim())
throw new ToolError("errors.textRequired");
return route(tool, { params, source: ctx.source, text: ctx.text });
}
```
`route()` — единственное место выбора места исполнения: `domOnly` или нет Worker
→ прямой вызов `tool.run(ctx)`, иначе worker с fallback (логика
`ensureWorker/runInWorker/disableWorker` остаётся). Worker-протокол —
`{ id, toolId, params, source?, text? }`.
## Шаги (ревью после каждого)
Все 11 шагов выполнены; гейт пройден: `svelte-check` → 0 errors,
`pnpm --dir web lint` → чисто, `pnpm --dir web test` → 618/618 зелёный. E2E не
делали.
| # | Шаг | Файлы | Что |
| --- | --------------- | ------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------- |
| 1 | Типы | `registry-new/types.ts` | новый `ToolEntry`/`ToolContext`/`ToolResult`/`InputMode "image"`, хелперы `requireSource`/`requireText` + `imgTool`/`genTool`/`textGen` |
| 2 | Executor+Worker | `preview/executor/executor.ts`, `executor.worker.ts`, `index.ts` | единый `execute`, `route` по `domOnly`, протокол `source?`/`text?` |
| 3 | UI kit | `kit/SchemaToolView.svelte`, `SchemaPreview.svelte`, `SchemaActions.svelte`, `SchemaSourceTile.svelte` | одна `run()`, без `isGenerator`-эвристики, `inputMode` → `"image"` |
| 4 | Генераторы | `registry-new/generate.ts` | 21 `generate(p)` → `run(ctx)` + `input: "none"` (чинит градиент и др.) |
| 5 | Текст | `registry-new/convert.ts`, `analyze.ts` | `runFromText`/`toText`/`textToText` → `run`; `input: "image"` на run-инструментах |
| 6 | Текст | `registry-new/text.ts` | domOnly run-инструменты → `run(ctx)` + `input: "image"` |
| 7 | Геометрия | `registry-new/geometry.ts` | run → `run(ctx)` + `input: "image"` |
| 8 | Альфа/цвет | `registry-new/alpha.ts`, `color.ts` | run → `run(ctx)` + `input: "image"` |
| 9 | Фильтры | `registry-new/filters.ts` | run → `run(ctx)` + `input: "image"` |
| 10 | Тесты | `registry-new/registry-new.test.ts` | `generate!`/`run!` → `run(ctx)`; guard-тест: `input` всегда задан, image-инструменты работают с источником, `none` — без |
| 11 | Гейт | — | `svelte-check` → 0 errors, `pnpm --dir web lint` → чисто, `pnpm --dir web test` → 618/618 |
Примечание шага 8: поля `preview:` (маска у remove-color/remove-background)
удалены — новый executor зовёт `run()` напрямую; идея вернуть отображение маски
— в `docs/backlog.md` №17. Prettier-варнинги `prettier --check` по
SchemaToolView.svelte и executor-файлам — предсуществующий долг.
По миграции: `input: "image"` добавлен всем run-инструментам (~80 шт.),
`input: "text"` стоит у 7 text-инструментов, `input: "none"` — у 21 генератора.
## Известные последствия
- **Красная сборка с шага 1 до конца шага 10** — осознанное решение, ревью идёт
по diff'у шагов.
- Старый контур (`registry/**` + `lib/tools/executor.ts` + `ToolPage.svelte`,
`sourceMode`) НЕ трогаем — он на удаление (C19).
- `input: "file"` переименован в `input: "image"` — семантика «нужно именно
изображение», а не «файл» (между инструментами может передаваться не файлом).
-104
View File
@@ -1,104 +0,0 @@
# План: range-слайдеры и пипетка цвета
> **СТАТУС: ВЫПОЛНЕН 2026-08-23.**
## 1. Целевое поведение
1. «Процентные» параметры (яркость, контраст, качество JPEG/WebP, порог
похожести) — слайдеры с числовым значением рядом; точные координаты и размеры
(resize/crop) остаются полями ввода.
2. У цветовых параметров появляется кнопка-переключатель «Пипетка»: активна →
клик по превью исходника снимает цвет пикселя и подставляет его в поле цвета;
после выбора режим гаснет.
3. В режиме пипетки возле курсора видна лупа: увеличенный блок пикселей вокруг
точки с перекрестием и подписью hex текущего пикселя; клик выбирает центр
перекрестия.
4. Активная пипетка видна на превью перекрестием курсора; повторный клик по
кнопке отменяет режим без выбора.
5. Всё работает поверх текущего процесса: авто-применение с задержкой, токен
запусков, маски — без изменений.
## 2. Слайдеры
- В реестре новый тип параметра `'slider'` с обязательными `min`/`max`;
валидируется как число (дефолт в диапазоне, минимум меньше максимума).
- Форма маппит его на готовый `SliderField` из `ui/`; санитизация параметров
обрабатывает slider так же, как number.
- Переводятся: яркость и контраст, качество JPEG, качество WebP, порог
похожести. Остальные инструменты — без изменений.
## 3. Пипетка и лупа
- В ядро добавляется чистая функция `rgbToHex(r, g, b)` → строка вида `#rrggbb`,
с юнит-тестами; чтение пикселя — из уже отрисованного canvas превью исходника
через `getImageData(x, y, 1, 1)`.
- `Preview.svelte` получает опциональный режим пипетки: пропсы «активна» и
колбэк «цвет выбран»; курсор перекрестие, клик вычисляет координату пикселя с
учётом разницы CSS-размера canvas и его внутренней ширины.
- **Лупа** — компонент `PipetteLoupe.svelte`: канвас около 110×110 px у курсора
(позиция fixed, `pointer-events: none`); рисует блок 15×15 пикселей из
исходника с увеличением ×8 без сглаживания, перекрестие в центре, подпись hex
текущего пикселя. Координаты блока клампятся у краёв изображения; лупа скрыта
вне превью и вне режима пипетки. Тач-устройства: прямой тап выбирает пиксель
без лупы.
- Проводка: кнопка у каждого цветового поля в карточке параметров → состояние
«активное цветовое поле» живёт в оркестраторе → выбор цвета записывает hex в
это поле и выключает режим.
- Пипетка работает только по левой панели (исходнику): удаляемый цвет и подложка
берутся с оригинала.
## 4. Этапы и проверки
- **A. Слайдеры** — тип в реестре, перевод четырёх инструментов, тесты реестра и
санитизации, тест `rgbToHex`. Проверка: `pnpm test/check`, визуально слайдеры.
- **B. Пипетка** — режим в Preview, проводка через карточки до полей цвета.
Проверка: смоук §6 пп. 5–7.
- **B2. Лупа** — компонент лупы поверх режима пипетки. Проверка: смоук §6 пп.
8–10.
- **C. Итог** — полный прогон `test/check/build`.
## 5. Изменения по файлам
- `registry.ts` — тип `'slider'` в `ParamDef`, перевод четырёх записей, ветка в
`sanitizeParams`.
- `ParamForm.svelte` — ветка slider → `SliderField`.
- `core/color.ts` — `rgbToHex`.
- `Preview.svelte` — режим пипетки (курсор, клик, колбэк).
- `PipetteLoupe.svelte` — лупа у курсора в режиме пипетки.
- `SourceCard.svelte` — проброс режима и колбэка наверх.
- `ParamsCard.svelte` + `ColorField.svelte` — кнопка «Пипетка» у цветового поля.
- `ToolPage.svelte` — состояние активного поля, обработчик выбора цвета.
## 6. Смоук-чеклист
1. Яркость/контраст — слайдеры со значением справа, результат обновляется сам
при перетаскивании.
2. Качество JPEG/WebP — слайдер от 1 до 100.
3. Порог похожести — слайдер от 0 до 100.
4. Resize и crop остались числовыми полями.
5. Удаление цвета: «Пипетка» → клик по красной области исходника → поле цвета
получил hex этого пикселя, режим погас, результат пересчитался.
6. Повторный клик по кнопке или выбор без подтверждения — режим снят, цвет не
изменился.
7. Подложка JPEG: пипетка проставляет цвет подложки тем же способом.
8. В режиме пипетки лупа следует за курсором, перекрестие показывает пиксель под
ним, подпись hex совпадает с выбранным при клике.
9. У края картинки блок лупи корректно клампится — за границей тянется
чёрный/прозрачный фон, выбор по-прежнему точен.
10. Курсор ушёл с превью или режим выключен — лупа скрыта; клики по кнопкам под
превью не перехватываются лупой.
11. Слайдеры и пипетка дружат с быстрыми правками: одна финальная версия
результата, чип «Пересчёт…» на месте.
## 7. Критерии готовности
- Все процентные параметры — слайдеры; числовые поля остались только там, где
нужен точный ввод.
- Цвет любого color-параметра можно взять кликом с исходника.
- `test/check/build` зелёные, чеклист §6 пройден.
## 8. Что сознательно не делаем
- Пипетка и лупа на превью результата и маски — только исходник.
- Кастомная стилизация слайдеров за пределами дизайн-системы.
- Лупа для тач-устройств — там прямой тап без лупы.
-82
View File
@@ -1,82 +0,0 @@
# План: инструмент «Разрезать PNG на части» (split-into-parts-png)
> Статус: **выполнено и в архиве** (2026-09). Инструмент и механизм «результат =
> набор файлов» (1 → many) реализованы; первый потребитель —
> `split-into-parts-png`. Общий процесс таких инструментов — задача 18 в
> `docs/backlog.md`.
## Цель
Пользователь загружает одну картинку, выбирает число столбцов и строк, картинка
разрезается на равномерные части. Результат — набор PNG-файлов, скачивается
ZIP-архивом.
## Решения
- **Неравномерное деление:** канвас дополняется прозрачным до кратного размера
(`pieceW = ceil(w / cols)`, `newW = pieceW * cols`), все части строго равные,
картинка покрывается целиком.
- **Дефолт схемы:** 2 × 2.
- **Zip-библиотека:** `fflate` (zero deps, малый размер), скачивание из UI-слоя
(`encode()` требует DOM).
- **Результат инструмента:** `result: "files"` — терминальный тип, в пайплайн
(следующий шаг) не передаётся.
## Шаги
1. **Зависимость:** `fflate` в `web/package.json`.
2. **`registry/types.ts`:** `RESULT_KINDS.files`, тип `ToolImageFile`,
`FileResult`, расширение `ToolResult = PixelImage | string | FileResult`.
3. **`core/geometry.ts`:** `splitToParts(img, columns, rows)` — padding + сетка
через `crop`.
4. **`web/src/lib/zip.ts` (новый):** `downloadZip(files, zipName)` через
`zipSync`.
5. **`core/errors.ts`:** ключ `errors.tooManyParts` (страховочный лимит
cols·rows ≤ 1000; при максимуме 6×6 недостижим).
6. **`registry/geometry.ts`:** схема (`columns`, `rows`, дефолт 2×2, min 1, max
6 — максимум 36 частей) + entry `split-into-parts-png` в `geometryEntries`.
7. **Executor:** сериализация/десериализация `FileResult` в `executor.worker.ts`
и тип ветки в `executor.ts`.
8. **UI:** `SchemaToolView.svelte` (`fileResult`, Download → zip),
`SchemaPreview.svelte` (canDownload для files), `SchemaResultTile.svelte`
(сетка-превью частей, мета «N parts / ZIP»).
9. **i18n:** переводы в `ru.ts`/`en.ts` **не добавляем** — словари `tools`
привязаны к v1-реестру (тест «нет лишних ключей»), а preview берёт заголовки
из registry (EN). Перевод придёт вместе с беклог-задачей «i18n в preview».
Поиск по id/заголовку работает из registry.
10. **Тесты:** юнит `splitToParts`, registry (result: "files"), i18n coverage.
11. **Docs:** `docs/backlog.md` (задача «процесс 1 → many и many → 1», отметить
пункт 11 «Мультифайловый вывод»), `docs/tools-map.md` (перенос из идей).
## Файлы
| Файл | Изменение |
| ------------------------------------------------ | ----------------------------------------------------------------- |
| `web/package.json` | `+ fflate` (dependencies) |
| `web/src/lib/registry/types.ts` | `RESULT_KINDS.files`, `ToolImageFile`, `FileResult`, `ToolResult` |
| `web/src/lib/core/geometry.ts` | `splitToParts()` |
| `web/src/lib/zip.ts` | **новый** — `downloadZip()` |
| `web/src/lib/core/errors.ts` | `errors.tooManyParts` |
| `web/src/lib/registry/geometry.ts` | schema + `splitPartsTool` |
| `web/src/lib/executor/executor.worker.ts` | ветка `FileResult` |
| `web/src/lib/executor/executor.ts` | тип ветки `FileResult` |
| `web/src/lib/components/SchemaToolView.svelte` | `fileResult`, download → zip |
| `web/src/lib/components/SchemaPreview.svelte` | props `fileResult`, `canDownload` |
| `web/src/lib/components/SchemaResultTile.svelte` | сетка частей, «N parts» |
| `web/src/lib/i18n/ru.ts`, `en.ts` | записи инструмента |
## Нейминг файлов в ZIP
`part-<row>-<col>.png` (row, col нумеруются с 1, при 2×2 — `part-1-1.png`).
## Верификация
```bash
pnpm --dir web exec svelte-check --tsconfig ./tsconfig.json
pnpm --dir web test
pnpm --dir web lint
```
Ручная проверка: PNG не кратного размера (например 101×77), 3×2 → 6 частей
одинакового размера, скачивается ZIP; на кратном размере (100×80, 3×2) padding
= 0.
-95
View File
@@ -1,95 +0,0 @@
# План: закрытие техдолга перед переездом `old/` (C17)
> **Статус: ЗАКРЫТ.** Все гейты пройдены, C17 разблокирован.
## Состав техдолга
- **svelte-check error** — `ToolCard.svelte:19:36`: `resolve(normalizedHref)`
принимает только типизированный роут, а `normalizedHref: string`. Красный
svelte-check, висит на каждом прогоне.
- **фейл теста** — `src/lib/palette.test.ts`: `ENOENT src/app.css` (файл
разъехался на `old.css` + `design2.css` при редизайне). Тест проверял
контрасты палитры по токенам старого `app.css`
(`text/surface/bg/link/ accent/danger/border`). При гейтах `pnpm test`
красный, хотя 607 тестов проходят.
- **design-tokens в `kit/**`** — ~335 ошибок `lint:all` от локального плагина
`eslint-plugins/design-tokens` (захардкоженные цвета/размеры/длительности/
z-index/`color-mix()` в `<style>`-блоках svelte), плюс stylelint по CSS.
- **a11y-предупреждение** — `ColorField.svelte`.
- **пустой ruleset** — `Toggle.svelte`.
## Этапы
### Этап 1 — «зелёные» гейты (мелкие, по ~10–40 строк каждый)
1. **ToolCard** — убрать ошибку типов: либо валидировать `href` против набора
известных маршрутов, либо явно протащить типизированный роут из каталога.
Проверка: svelte-check 0 errors.
2. **palette.test.ts** — переписать чтение на новый словарь: тест контрастов
гоняется по токенам `preview.css` (`:root` + `[data-theme="dark"]`, цвета
`--color-*`/`--brand-*`). Отдельно: взять из нового дизайна, какие пары
реально проверить (текст/фон, muted, акцент/бренд, бордер). Если часть
проверок потеряла смысл в новом дизайне — сократить, не растягивать.
3. **a11y `ColorField` + пустой ruleset `Toggle`** — пофиксить прямо в этих
компонентах.
Гейт этапа: `svelte-check` 0 errors, `pnpm test` зелёный.
### Этап 2 — миграция `kit/**` на design-токены (main объём)
Цель — `lint:all` без ошибок design-tokens (все примитивы/`color-mix()` уходят в
токены `preview.css`).
**Вариант А (основной): авто-фикс одноразовым скриптом.** Писать скрипт ТОЛЬКО
если он гарантированно ничего не испортит. Гарантии:
- Скрипт — одноразовый (`web/scripts/fix-design-tokens.mjs`), после прогона
удаляется либо помечается неактуальным.
- Строгая карта замен: примитив → токен **1:1, только если значение точно
совпадает с уже существующим токеном словаря** `preview.css` (цвет →
`--color-*`/`--brand-*`, радиус → `--radius-*`, размер →
`--space-*`/`--size-*`, длительность → `--duration-*`, z-index → `--z-*`).
Никаких новых токенов скрипт не генерирует.
- Замены — только примитивы, не содержащие `var()`/`calc()`/`color-mix()`;
исключения по правилам плагина (0, 0px, 1px, проценты, unitless) не трогаются.
`color-mix()` → выносится в `preview.css` как производный `--color-*` токен
(может требовать ручного решения имени).
- Скрипт обрабатывает только `src/lib/components/kit/**` и `src/preview.css`
(scoped-пути плагина) и только `<style>`-блоки/`.css` — логику/разметку не
меняет, только строки стилей.
- После прогона: обязательный дифф-ревью (все изменения — замена значений на
`var(--x)`, ничего удалено/переставлено), затем полный `lint:all` +
`svelte-check` + `pnpm test`. Если хоть что-то вне «замена примитива на токен»
— скрипт считается проваленным, откатываем и переходим на вариант Б.
**Вариант Б (страховка): руками, пакетами по компонентам.** Если скрипт не
удался — чистим/заводим токены вручную, пакетами по компонентам (1–2 на задачу,
дифф < ~500 строк). Отсутствующие токены (например
`--space-2xl`/`--space-panel`, уже дважды обходились в SchemaTextSource)
добавляем в словарь `preview.css` осознанно.
Гейт этапа: `pnpm --dir web lint:all` — только допустимый остаток
(неиспользуемые токены — варнинг), плюс регресс-прогон тестов и svelte-check.
### Этап 3 — чистота словаря `preview.css`
- Разобрать варнинги неиспользуемых токенов (check-tokens): либо использовать,
либо удалить из словаря.
- После этапа 2 — повторный аудит токенов
(`pnpm --dir web exec node scripts/check-tokens.mjs`): parity light/dark,
hct-only, без мёртвых токенов.
### Этап 4 — финальные гейты (гейт переезда `old/`)
1. `pnpm --dir web exec svelte-check --tsconfig ./tsconfig.json` → 0 errors
2. `pnpm --dir web test` → зелёный
3. `pnpm --dir web exec prettier --check .` → чистый
4. `pnpm --dir web lint:all` → 0 (или только задокументированный остаток)
5. Обе ветки открываются: старый UI `/`, новый `/preview`
## Порядок и коммиты
- Коммиты атомарные, < ~500 строк (AGENTS). По завершении каждой мини-задачи —
ревью (агент не коммитит).
- Техдолг чинится по заведённым задачам, правила линтеров не отключаются и не
игнорируются.
-194
View File
@@ -1,194 +0,0 @@
# План: тестирование preview перед промоушеном в главную
> Статус: **реализация завершена**. Playwright-набор в `web/e2e/` (108 тестов:
> 104 прошли, 4 `test.fixme` — известные баги №1–4 ниже), ручной чек-лист —
> `docs/checklist-manual-testing.md`. План ветки preview
> (`web/src/routes/preview/**`, регистр `lib/registry-new/`, 121 инструмент)
> перед шагом C17 из `docs/plan-redesign.md` (переезд preview → реальные
> маршруты).
>
> Охват: **документация плана** — реализация тестов (Playwright-раннер, сами
> тесты) делается отдельными задачами после ревью этого документа.
>
> Приоритет: **отсутствие падений/ошибок** > пиксельная корректность. Дизайн
> доделываем по ходу, визуальная сверка — ручная и вторичная.
>
> Команды для проверки в процессе:
>
> - `pnpm --dir web test` — существующий Vitest (42 файла, 121 инструмент
> частично покрыт юнит-тестами).
> - `pnpm --dir web test:e2e` — Playwright (`web/e2e/`), сборка +
> `scripts/serve-static.mjs` на 4173.
> - `pnpm --dir web exec svelte-check --tsconfig ./tsconfig.json` — проверка
> типов.
> - `pnpm --dir web lint:all` — ESLint + stylelint + токен-аудит.
> - `pnpm --dir web build` — статическая сборка (adapter-static).
## 0. Что тестируем
Preview-ветка — клиентское SPA на SvelteKit (adapter-static, всё в браузере,
никакого API). Структура:
| Маршрут | Что это |
| ------------------------------------- | -------------------------------------------------------------------------------------------- |
| `/preview` | Workspace: поиск + сетка `ToolCard` (топ-24), «Open last project» (stub), футер |
| `/preview/kit` | Витрина всех kit-компонентов (light/dark) |
| `/preview/list-tools` | Каталог: 8 групп, 121 инструмент, фильтр по категории, поиск |
| `/preview/tools/[id]` | Страница инструмента: `SchemaToolView` (настройки + live preview), prerendered на все 121 id |
| `/preview/tools/xxx` (несуществующий) | 404 → `EmptyState` «Tool not found» |
Инструменты: **121**, категории CONVERT / TRANSPARENCY (alpha) / COLOR /
GEOMETRY / FILTERS / TEXT / ANALYZE / GENERATE. Режимы инструмента:
- **file** (transform): upload → `run` → картинка → download;
- **text** (convert-source): ввод текста → `runFromText` → картинка, либо
`textToText` → текстовый результат;
- **verdict** (`result: "verdict"`, file): `toText` → текстовый вердикт;
- **text-output** (`result: "text"`, file): `toText` → текстовый результат;
- **generate**: без входа, `executeGenerate` → картинка.
Исполнение: `preview/executor` — worker с fallback на main-thread (domOnly / нет
Worker / ошибка worker). Auto-run с debounce 200 мс.
## 1. Функциональные блоки для проверки
### A. Инфраструктура и навигация
1. Все 4 маршрута + 404 открываются без console errors, uncaught exceptions и
failed requests.
2. Поиск на `/preview` — фильтрация по названию/описанию, пустой результат →
аккуратное empty-state.
3. Каталог `/preview/list-tools` — 8 групп, счётчик 121, фильтр категории,
иконки не падают.
4. Тема light/dark и язык RU/EN переключаются на превью-страницах без регресса
рендера.
5. 404 на несуществующий id инструмента → EmptyState, без краша.
6. Prerender: `pnpm --dir web build` проходит; все 121 страницы
`/preview/tools/[id]` генерируются (проверка `entries()`/`getTool`).
### B. Конвейер инструмента (file-инструменты)
7. Upload → авто-run → результатный тайл появился, поле ошибки пустое.
8. Download — событие download, имя `<toolId>.<ext>`, MIME соответствует
`output`.
9. Reset — возвращает дефолты, сбрасывает результат и ошибку.
10. Смена параметра → авто-перезапуск (debounce 200 мс) без «залипания»
running-стейта.
11. Worker-исполнение и fallback-исполнение оба дают результат.
### C. Текстовые инструменты и вердикты
12. Text-source: ввод → текстовый результат или картинка; copy/download .txt.
13. Вердикты (analyze): корректный вердикт на PNG с альфой, на PNG без альфы, на
не-PNG входе → без падений.
14. Пустой text-source → run пропускается без ошибки.
### D. Генераторы
15. Каждый генератор по дефолтам создаёт картинку; смена размеров/цвета
перегенерирует; канва ожидаемого размера.
## 2. Репрезентативная выборка по категориям (для «нет падений»)
Полный прогон всех 121 — опционально (C/all); минимальный чек — по группам:
| Категория | Кол-во | Охват для «нет падений» |
| ------------ | ------ | --------------------------------------------------------------------------------- |
| CONVERT | 14 | convert-png-to-jpg, convert-png-to-webp, png-to-base64, base64-to-png, svg-to-png |
| TRANSPARENCY | 20 | remove-background, round-corners, add-stroke, remove-color-from, circle-mask |
| COLOR | 25 | grayscale, invert, black-and-white, quantize, dithering, png-to-hsl |
| GEOMETRY | 18 | resize, crop, rotate, flip, add-border, tile |
| FILTERS | 8 | blur, sharpen, pixelate, add-noise |
| TEXT | 3 | add-text, date-stamp, watermark-tile |
| ANALYZE | 12 | extract-color-from, show-transparent-pixels, verify-is-png, png-is-transparent |
| GENERATE | 21 | create-empty, linear-gradient, two-colors, placeholder, text-to-png |
## 3. Playwright (авто) против ручного — разбиение
### ✅ Playwright — детерминированные сквозные потоки и «нет падений»
- Все маршруты + 404: открытие, отсутствие console errors / uncaught exceptions
/ failed requests (слушатели `page.on('console'/'pageerror')`).
- Поиск и каталог: фильтрация, счётчики, переходы.
- Upload (`setInputFiles` с фикстурой PNG) → авто-результат → Download (ожидание
`download` события, проверка имени/расширения/suggestedFilename).
- Reset / смена параметра → перегенерация, running не «залипает».
- Тема light/dark и язык RU/EN.
- Текстовые инструменты: ввод → текстовый результат → копия/скачивание.
- Вердикты на подготовленных фикстурах (PNG с альфой, PNG без альфы,
JPG/некартинка).
- Генераторы: дефолты → результат появился, размер канвы ожидаемый.
- 404 и empty-state.
- Worker и fallback: сравнить оба пути рендера (инжект отключения Worker).
### 🔄 Playwright + ручная сверка
- Пиксельная корректность результата (сравнение `<canvas>` через
`toDataURL`/тест-per-pixel) — по приоритету вторично; эталоны задаются
вручную, делать отдельной задачей.
- Реальные пользовательские картинки (не фикстуры) — вручную.
### ✋ Ручное тестирование
- **Краевые PNG:** большие (мегапиксели), 1×1, анимация, палитровые,
чекерборд/полупрозрачность, повреждённые файлы → нет зависаний, понятные
сообщения об ошибке.
- **Края параметров:** экстремальные значения, NaN/пустые поля, инвалидные цвета
в плашках/градиентах → корректная санитизация (silent repair).
- **Производительность/отзывчивость:** задержка авто-реранов на слайдерах, нет
«залипания» UI, скролл при длинных настройках.
- **Визуальный слой дизайна** (доделываем по ходу): сетки, выравнивание, отступы
на брейкпоинтах mobile/tablet/desktop (640/800/1100).
- **Буфер обмена** (copy) в реальном браузере.
- **Drag-n-drop** файла на dropzone (UX-ощущения).
- **Разные браузеры:** Firefox/Safari-нюансы (webkit в Playwright опционален).
- **Субъективная корректность:** dithering, two-colors, watermark, текст
не-ASCII/кириллица в add-text/date-stamp/watermark-tile.
## 4. Критерии приёмки (доступ в главную)
1. Все 4 маршрута + 404 открываются без console errors и исключений.
2. Репрезентативный прогон по 8 категориям: ни один инструмент не падает и не
показывает поле ошибки.
3. Upload → результат → download завершается для file-инструментов;
текст/вердикты/генераторы дают корректный output-тип.
4. `pnpm --dir web test`, `pnpm --dir web lint:all`, `pnpm --dir web build`
зелёные.
5. Тема и язык переключаются без регрессов.
## 5. Оценка покрытия
- **~60–70%** «нет падений/ошибок» закрывается Playwright детерминированно
(маршруты, сквозные потоки, отсутствие ошибок).
- **~30–40%** — руками: краевые PNG/параметры, реальные пользовательские файлы,
буфер обмена, кросс-браузеры, визуальный/производительный слой, субъективная
корректность.
## 6. Шаги реализации (после ревью)
1. ✅ Playwright-раннер: `web/playwright.config.ts` (webServer на
`pnpm build + `scripts/serve-static.mjs` `--port
4173`), папка `web/e2e/`, npm-скрипт `test:e2e`.
2. ✅ Фикстуры PNG (генерируются в рантайме, `web/e2e/helpers/fixtures.ts`): с
альфой, без альфы, 1×1, большой, повреждённый, ландшафт.
3. ✅ E2E по блокам A–D и секции 3 (авто-часть): `navigation.spec.ts`,
`catalog.spec.ts`, `pipeline.spec.ts`, `text-and-verdicts.spec.ts`,
`tools-smoke.spec.ts` (матрица ~71 инструмента), `generators.spec.ts`,
`known-issues.spec.ts` (fixme).
4. ✅ Ручной чек-лист из секции 3 (ручная часть) —
`docs/checklist-manual-testing.md` (разделы A–G, включая известные баги).
5. ✅ Найденные баги (зафиксированы как `test.fixme` + чек-лист §G):
1. генераторы (21/121) не имеют кнопки «Generate» и полей схемы — результат
через UI недостижим;
2. `resize-png` с дефолтом 0×0 → `errors.resizeSize` при любом входе;
3. `crop-png` с дефолтом 0×0 → `errors.cropSize` при любом входе;
4. ключи ошибок показываются сырыми (`errors.*`) вместо переведённого текста
из `en.ts`/`ru.ts`. Исправления — отдельными атомарными задачами по
конвенции AGENTS.md (после правок: `lint:all`, `test:e2e`).
## 7. Итоговое состояние на момент сдачи
- `pnpm --dir web test:e2e` — **104 passed, 4 skipped (fixme), 0 failed**.
- `pnpm --dir web lint:all` и `svelte-check` — зелёные.
- Визуальный слой, краевые PNG/параметры, кросс-браузеры — остаются на ручной
прогон по `docs/checklist-manual-testing.md`.
-81
View File
@@ -1,81 +0,0 @@
# План: волна 4 — текст, штампы, водяные знаки
> **СТАТУС: ВЫПОЛНЕН 25.08.2026.**
## 1. Что получается
Четыре инструмента новой категории «Текст»: надпись на изображении, дата-штамп,
защитная плитка (повторяющийся полупрозрачный текст под углом), водяной знак
картинкой. Каталог вырастает с 69 до 73. Это первые операции, рисующие через
canvas 2D — класс perceptual по роадмапу: точные пиксельные тесты на них не
пишутся, юнит-тестами покрывается чистая математика раскладки.
## 2. Решения
- Вся геометрия — чистые функции без DOM: якорь из 9-позиционной сетки →
координаты, перенос текста по ширине с инъекцией измерителя, раскладка плитки
с углом и шагом. Canvas-вызовы — тонкий слой рядом (`core/textdraw.ts`), в
юнит-тесты не попадает.
- Один движок рендера текста на все три текстовых инструмента: шрифт из
системного набора (sans/serif/mono), размер, цвет, жирность, прозрачность,
поворот. Различия инструментов — только в пресетах параметров и количестве
слоёв (одна надпись vs плитка).
- Дата-штамп — надпись с автотекстом: строка формата (`YYYY-MM-DD hh:mm`)
разворачивается в текущую дату в момент запуска; свой мини-форматтер без
зависимостей.
- Водяной знак картинкой — первый двухисточниковый инструмент: вторая картинка
грузится отдельной зоной в этапе, живёт только в состоянии страницы и
сознательно не сохраняется ни в workspace-pipeline, ни в экспорте JSON; после
восстановления цепочки знак нужно выбрать заново. Ограничение фиксируем в
описании инструмента.
- Новая категория `text` встаёт после «Фильтров»; названия в обоих словарях,
порядок в CATEGORIES один.
## 3. Инструменты
- `add-text-png` — параметры: текст (строка), размер (слайдер 8–200), цвет,
шрифт (select), жирность (чекбокс), позиция (select 3×3: углы/стороны/центр),
отступ (слайдер). Опциональная подложка: цвет + прозрачность плашки за
текстом.
- `date-stamp-png` — тот же набор минус текст, плюс формат даты; дефолт — нижний
правый угол.
- `watermark-tile-png` — текст, размер, цвет, прозрачность (0–100), угол
(-90…90), шаг по X/Y; плитка заполняет весь холст.
- `watermark-image-png` — второй источник (загрузка PNG с альфой), масштаб знака
(% от ширины холста), прозрачность, позиция 3×3, отступ.
## 4. Этапы и проверки
Каждый этап заканчивается зелёными test/check/build и ревью.
- **A. Фундамент** — категория `text` (categories + словари + порядок),
`core/textdraw.ts`: anchor9, wrap, tileLayout с юнит-тестами на математике
(координаты углов/центра, переносы слов, плотность плитки), тонкий
canvas-модуль `domText.ts` (без тестов). Реестр пока пуст в категории.
- **B. Надпись и дата** — `add-text-png`, `date-stamp-png` на общем движке;
иконки, переводы, записи реестра. Смоук: обе локали, тёмная тема (плашка
читаема), длинный текст переносится и не вылезает за край.
- **C. Плитка** — `watermark-tile-png`; смоук: отрицательные и нулевой угол,
прозрачность 0/50/100, мелкий шаг не подвешивает страницу на больших холстах
(ограничение количества плиток капом).
- **D. Знак картинкой** — двухисточниковый UX: вторая drop-зона в этапе
(компонент рядом с SourceCard, виден только у этого инструмента),
масштаб/позиция/отступ; ограничение про сохранение — в описании и подсказке.
Этап режется без ущерба остальным, если решим отложить.
- **E. Итог** — полный прогон, смоук §6, архивация плана.
## 5. Критерии готовности
- 73 инструмента; вся математика раскладки покрыта юнит-тестами; canvas-слой
изолирован в одном модуле.
- test/check/build зелёные; новые инструменты работают в цепочке как обычные
шаги (кроме источника знака — см. ограничение).
## 6. Что сознательно не делаем
- Выбор произвольных пользовательских шрифтов (файлами) и веб-шрифтов — только
системный набор.
- Кернинг/линии/многострочное редактирование с абзацами — один абзац с
автопереносом.
- Сохранение картинки-знака в пайплайне и её передача между сессиями.
- Текст по контуру, градиентные заливки текста.
-61
View File
@@ -1,61 +0,0 @@
# План: светлая и тёмная тема
> **СТАТУС: ВЫПОЛНЕН 25.08.2026.**
## 1. Что получается
Переключатель light/dark в шапке рядом с языковым. Выбор в localStorage
(`theme`), первый визит — по `prefers-color-scheme`. Тёмная палитра —
переопределение токенов селектором `[data-theme='dark']`; компоненты не
правятся.
## 2. Решения
- Два состояния, без «авто»: ключ хранит только явный выбор, отсутствие ключа =
системная тема.
- Анти-вспышка: инлайн-скрипт в `app.html` до гидрации ставит `data-theme` из
localStorage или `matchMedia`; пререндеренная разметка остаётся светлой.
- `color-scheme: dark` на корне — нативные контролы (input[type=color], range,
select, скроллбары) темнеют без правок.
- Модуль `theme.svelte.ts` по образцу locale: состояние, персист, применение
атрибута. Стартовую установку делает скрипт из `app.html`, модуль
синхронизирует runes-состояние после гидрации.
## 3. Палитра
- Переопределяем: `--bg`, `--surface`, `--border`, `--text`, `--text-muted`.
- Акцент `#2563eb` не меняется; `--accent-hover` — светлее для тёмного фона.
- `--danger` осветлить для читаемости.
- `--check-a`/`--check-b` шахматки — приглушённые серые: паттерн прозрачности
остаётся узнаваемым, но не слепит.
- `--shadow-card` — почти чёрная с низкой альфой вместо синей.
- Все `color-mix(… var(--surface))` пересчитаются сами — проверить визуально
легенды, recalc-бейдж, drag-оверлеи.
## 4. Этапы и проверки
Каждый этап заканчивается зелёными test/check/build и ревью.
- **A. Инфраструктура** — `theme.svelte.ts`, скрипт в `app.html`, переключатель
в шапке, черновая тёмная палитра. Тесты: персист выбора, игнор мусора в
localStorage, применение атрибута (стаб document).
- **B. Палитра** — выверка значений: герой, каталог, страница инструмента,
цепочка, баннер ошибки, маска, текстовые инструменты; фокус-ринги и hover; обе
локали.
## 5. Смоук
1. Переключение мгновенное, без перезагрузки; выбор переживает её же.
2. При системной тёмной теме первый визит открывается сразу тёмным.
3. Нативные контролы выглядят тёмными.
4. Шахматка различима, легенды и бейджи читаемы.
5. Баннер ошибки и пипетка заметны на тёмных панелях.
## 6. Критерий готовности
Grep: ни одного цвета вне токенов в компонентах и маршрутах.
## 7. Не делаем
Три позиции в переключателе; пользовательские цветовые схемы; автосмену по
времени суток; отдельную тему для превью изображений.
-98
View File
@@ -1,98 +0,0 @@
# План: декомпозиция страницы инструмента, реактивные параметры, предпросмотр маски
> **СТАТУС: ВЫПОЛНЕН 2026-08-23.**
## 1. Зачем
- Страница инструмента (`ToolPage.svelte`) разрослась: состояние процесса,
загрузка файлов, вставка из буфера и разметка трёх зон живут в одном файле.
- Назревают два требования к процессу: параметры применяются автоматически без
кнопки; у части утилит на экране показывается одно — визуализация
фильтра/маски, а скачивается (и в будущем передаётся следующему инструменту)
другое — результат.
## 2. Контракт «результат и маска» в реестре
- `run(img, params)` — как сейчас: возвращает итоговое изображение. Его
скачивают и будут передавать дальше по цепочке.
- Запись реестра получает опциональное поле `preview?(img, params)` — возвращает
картинку для показа на экране (маску/фильтр).
- Если у записи есть `preview`, в карточке результата появляется переключатель
«Показать маску». Включён — на превью маска, выключен (по умолчанию) —
результат. Скачивание всегда отдаёт результат `run`, независимо от положения
переключателя.
- Первая реализация — удаление цвета: в ядро добавляется чистая функция
`colorMask(img, hex, tolerance)` — чёрно-белая маска, где удаляемые пиксели
белые; с юнит-тестами; `preview` записи вызывает её.
## 3. Декомпозиция страницы
Новая папка `components/tool/`; карточки — чисто отображательные, состояние
живёт только в оркестраторе:
- **`SourceCard.svelte`** — левая половина сцены: пусто → зона перетаскивания;
загружено → превью исходника под оверлеем замены с кнопкой «Заменить
изображение».
- **`ResultCard.svelte`** — правая половина: пустые состояния, превью результата
или сводка, чип «Пересчёт…», кнопка скачивания, при наличии `preview` —
переключатель маски.
- **`ParamsCard.svelte`** — форма параметров и подпись «Изменения применяются
автоматически»; для инструментов без параметров не выводится.
- **`ToolPage.svelte`** остаётся оркестратором: состояние (исходник, результат,
сводка, статус, ошибка, значения), декодирование файлов, вставка из буфера,
сброс, токен запусков и эффект с задержкой. Ожидаемо худеет примерно вдвое.
Карточки переиспользуются будущим многошаговым режимом.
## 4. Реактивные параметры (в этом же проходе)
- Кнопки «Применить» больше нет ни у одного инструмента.
- Изменение параметра запускает вычисление через 300 мс после последнего
изменения; каждое новое изменение перезапускает таймер.
- Токен запусков: если во время вычисления параметры поменялись, устаревший
ответ отбрасывается.
- Загрузка и замена файла считают сразу и подавляют ровно один авто-запуск
эффекта.
- Ошибка пересчёта — баннер сверху, прежний результат не сбрасывается.
## 5. Этапы и проверки
- **A. Декомпозиция** — перенос разметки в три карточки один к одному, поведение
не меняется. Проверка: `check/build`, визуально идентично.
- **B. Реактивность** — токен, задержка, чип пересчёта, минус кнопка. Проверка:
смоук §6 пп. 1–6.
- **C. Маска** — типы реестра, `colorMask` с тестами, переключатель в карточке
результата, пример на удалении цвета. Проверка: тесты + смоук §6 пп. 7–9.
- **D. Итог** — полный прогон `test/check/build`.
## 6. Смоук-чеклист
1. Загрузка файла → результат появляется сразу.
2. Изменить параметр → около 0.3 с и результат обновился сам.
3. Быстро изменить яркость несколько раз → одна финальная версия, промежуточные
не мелькают.
4. Во время пересчёта чип виден, предыдущий результат не исчезает.
5. Инструмент без параметров и информационная страница ведут себя как раньше.
6. Некорректные параметры (обрезка вне картинки) → баннер ошибки, прежний
результат цел.
7. Переключатель «Показать маску» у удаления цвета: включён — белым по чёрному
видно, что удалится; выключен — обычный полупрозрачный результат.
8. С включённой маской скачивание всё равно сохраняет прозрачный результат, а не
маску.
9. Замена исходника возвращает переключатель маски в выключенное состояние.
## 7. Критерии готовности
- `ToolPage` содержит только оркестрацию состояния и эффектов; вся разметка
панелей — в `components/tool/*`.
- Кнопки «Применить» нет ни на одной странице.
- Скачивание всегда соответствует `run`; экран может показывать `preview`.
- `test/check/build` зелёные, смоук §6 пройден.
## 8. Что сознательно не делаем
- Вынос вычислений в веб-воркер; настраиваемая задержка — фиксированные 300 мс.
- Редактор масок (кисть, слои, история) — только автогенерация из параметров
инструмента.
- Передача результата между инструментами — придёт вместе с многошаговым режимом
позже.
-118
View File
@@ -1,118 +0,0 @@
# План: раскладка «исходник слева — результат справа» + вставка изображения
> **СТАТУС: ВЫПОЛНЕН 2026-08-22.**
>
> **Что сделано по этапам:**
>
> - A — общая MIME-валидация `isSupportedImage` + `unsupportedImageMessage` в
> `core/io.ts`; `DropZone` переведён на них ✔
> - B — постоянная двухпанельная раскладка: одна карточка-«сцена» с равными
> половинами «Исходник | Результат», кнопки внутри своих половин, параметры —
> отдельной карточкой ниже; контент сайта растянут на всю ширину окна ✔
> - C — Ctrl+V: `<svelte:window onpaste>`, поиск image-item в буфере, общая
> валидация; текстовые вставки не затрагиваются ✔
> - D — drag-замена: компонент `DropOverlay` с вуалью-подсказкой и счётчиком
> `dragenter/dragleave` против мерцания; тот же счётчик в `DropZone` ✔
> - E — верификация: тесты 85/85, svelte-check 0 ошибок, статический билд с 11
> страницами; ручной смоук-чеклист §5 пройден ✔
>
> **Отступления от исходного плана в ходе реализации:** вместо пропа `overlay` у
> `DropZone` сделан отдельный компонент-обёртка `DropOverlay` (drag-события
> ловит контент под курсором); раскладка доработана по ревью — одна карточка
> равных половин вместо двух независимых колонок, сайт без ограничения ширины.
## 1. Целевое поведение
1. **Постоянная двухпанельная раскладка** страницы инструмента — отдельного
состояния «страница загрузки» больше нет:
- **Слева — исходник**, панель есть всегда. Не загружено → пустое дроп-поле
(drag&drop или клик). Загружено → превью исходника.
- **Справа — результат**: до загрузки — `EmptyState` «Результат появится
здесь»; во время обработки — `EmptyState` «Обработка…»; после — превью
результата (для info-инструмента — `InfoPanel`).
2. **Ctrl+V** — картинка из буфера обмена вставляется как исходник с любого
места страницы.
3. **Замена перетаскиванием** — новый файл, брошенный на левую панель с
загруженной картинкой, заменяет исходник (с оверлеем-подсказкой во время
перетаскивания).
4. **Кнопка «Заменить»** под превью исходника (переносится туда текущая
«Загрузить другое изображение»).
На мобильной ширине панели складываются в столбец: исходник сверху, результат
снизу.
## 2. Изменения по файлам
Новые компоненты не вводим — `EmptyState`, `Preview`, `DropZone` покрывают всё.
Меняем три файла:
- **`core/io.ts`** — экспорт `isSupportedImage(file): boolean`: общая
MIME-проверка на базе `ACCEPTED_IMAGE_TYPES`.
- **`DropZone.svelte`** — переходит на `isSupportedImage`; новый опциональный
проп `overlay` — режим «невидимая зона приёма поверх контента» для замены
перетаскиванием.
- **`ToolPage.svelte`** — постоянная сетка из двух панелей;
`<svelte:window onpaste>`; drag-оверлей левой панели; кнопки замены/сброса
переезжают в левую панель; правая панель — три состояния через `EmptyState`.
## 3. Этапы и проверки
- **A. Общая валидация** — `isSupportedImage` в `io.ts`, `DropZone` переведён на
неё. Проверка: `pnpm check`.
- **B. Раскладка** — две постоянные колонки в ToolPage; правая панель через
EmptyState; кнопки уходят в левую панель; обёртка `{#if !source}` на всю
страницу убирается. Проверка: визуальный осмотр всех инструментов,
`check/build`.
- **C. Ctrl+V** — `onpaste` на окне → поиск image-item в `clipboardData` → тот
же `handleFile`; не-изображения игнорируются молча. Проверка: ручной смоук со
скриншотом из буфера.
- **D. Drag-замена** — оверлей DropZone(overlay) при dragover на левой панели;
drop → `handleFile` нового файла. Проверка: ручной смоук «бросить файл на
загруженный исходник».
- **E. Итог** — полный ручной смоук-чеклист (§5). Проверка: `test/check/build`
зелёные.
## 4. Нюансы и решения
- **Мерцание dragleave** — классическая проблема оверлеев: используем счётчик
`dragenter/dragleave`, оверлей скрывается только когда счётчик обнулился и
произошёл `drop`/`dragend`.
- **Paste при фокусе в поле параметров** — обрабатываем всегда: если в буфере
именно изображение, пользователь очевидно хочет заменить исходник; текстовые
вставки обработчик не трогает.
- **Форматы буфера** — скриншоты приходят как PNG, часть приложений отдаёт
JPEG/WebP; все проходят общую `isSupportedImage`. GIF/BMP из буфера редки, но
тоже допустимы.
- **Info-инструменты** — ведут себя так же: слева исходник, справа после
загрузки появляется `InfoPanel`.
- **Состояние ошибки** — баннер остаётся сверху страницы; неудачная замена не
сбрасывает старый исходник.
## 5. Чеклист ручного смоука
1. Пустые обе панели → drag&drop файла слева → появился исходник и результат.
2. Клик по пустой левой панели → выбор файла.
3. Ctrl+V со скриншотом → исходник заменился, результат пересчитался.
4. Drag файла на загруженную левую панель → оверлей виден, после drop — замена.
5. Отмена перетаскивания (Esc / мимо) → оверлей скрылся, исходник не изменился.
6. Бросок неподдерживаемого файла (например .txt) → баннер ошибки, старый
исходник цел.
7. Кнопка «Заменить» открывает выбор файла.
8. png-info: работает та же схема, справа сводка.
9. Мобильная ширина: панели в столбец, порядок исходник → результат.
10. Все 11 инструментов отвечают на замену исходника корректным пересчётом.
## 6. Критерии готовности
- На странице инструмента нет состояния «только дропзона на всю страницу» —
раскладка двухпанельная всегда.
- Загрузка работает тремя способами: клик, drag&drop (включая замену), Ctrl+V.
- Ошибочные файлы и отмена перетаскивания не портят текущий исходник.
- `test/check/build` зелёные; чеклист §5 пройден.
## 7. Что сознательно не делаем (граница скоупа этого плана)
- Множественная загрузка / пакетная обработка — вне скоупа этого плана.
- Камера/скриншот-инструменты захвата — только буфер обмена.
- Прогресс-бары декодирования — файлы читаются быстро, достаточно индикации
«Обработка…».
-130
View File
@@ -1,130 +0,0 @@
# План: главная страница как рабочая область с цепочкой
> **СТАТУС: ВЫПОЛНЕН 2026-08-23.** Заменяет идею отдельной страницы рабочего
> пространства.
## 1. Модель
1. **Главная страница — это рабочая область.** Без выбранного инструмента на ней
большой поиск: начните вводить название — список отфильтровывается; выбор
разворачивает инструмент прямо здесь, без перехода.
2. Развернутый инструмент выглядит как привычная страница: слева исходник,
справа результат со своей кнопкой скачивания, параметры — в отдельной
карточке под сценой.
3. Под результатом рядом с кнопкой скачивания — кнопка **«⛓ Chain with…»**:
добавляет пустую секцию, внутри которой такой же поиск; выбор заполняет
секцию следующим инструментом. Так цепочка растёт сколько угодно.
4. Каждая заполненная секция цепи — полноценная: свои параметры, своё превью,
своя кнопка скачивания промежуточного результата. Крестик убирает секцию;
кнопка «✂ Remove chain» снимает всю цепь разом (без подтверждения).
5. Чекбокс «Показать маску» живёт в карточке параметров базового инструмента и
влияет только на его превью: в цепь всегда передаётся сам результат.
6. Сетка-каталог инструментов переезжает на отдельную страницу `/list-tools` —
для SEO и визуального поиска. Основная работа происходит на главной; в шапке
появляются ссылки на обе.
7. Прямые адреса `/tools/<id>` сохраняются: тот же развернутый инструмент для
глубоких ссылок.
8. Цепочка переживает перезагрузку (автосохранение в localStorage).
Экспорт/импорт файлом отложен до появления CLI/wasm-части.
## 2. Архитектура
- Страница инструмента уже собрана как самодостаточный компонент, принимающий
запись реестра, — он и становится встраиваемым блоком. Маршруты `/tools/[id]`
и главная используют один и тот же компонент: первая — по адресу из URL,
вторая — по выбору из поиска.
- Состояние главной: выбран ли инструмент; если да — рендерится блок
инструмента, целиком владеющий своим исходником, параметрами, результатом и
цепочкой.
- Смена инструмента на главной (через повторный поиск) сбрасывает блок целиком.
- Цепочка хранится массивом шагов; у пустого звена инструмент ещё не выбран —
такие звенья не сохраняются в localStorage и не участвуют в прогоне.
- Исполнение — последовательная свёртка: выход каждого звена — вход следующего;
результаты всех звеньев доступны их секциям. Общий авто-раннер даёт токен
отмены и задержку ~300 мс.
## 3. Переиспользование и доработки существующего
Готовое берётся как есть: `SourceCard`, `Preview`, `ParamForm`,
`DownloadButton`, `EmptyState`, модуль пайплайна, авто-раннер, `PNG_OUTPUT`.
Доработки:
- **Блок страницы инструмента** — расширяется секцией цепочки: список
заполненных звеньев, каждое со своими параметрами, превью и скачиванием, плюс
пустые звенья с поиском. Кнопки «⛓» и «✂ Remove chain» — рядом со скачиванием
базового результата.
- **`ParamsCard`** — опциональный блок «Показать маску» (флаг preview у
инструмента + двусторонний флаг).
- **Карточка результата** — упрощается: получает готовую картинку, маска уходит
в карточку параметров.
- **Существующая главная-каталог** — переезжает на маршрут `/list-tools` почти
без изменений.
Удаляется: черновой маршрут рабочего пространства и его компоненты
списка/селектора — вместо них новые, под вертикальную модель.
## 4. Новые компоненты
- **`ToolSearch.svelte`** — поиск с нечётким совпадением: поле ввода,
отфильтрованный список (регистронезависимая подпоследовательность по названию
и описанию, приоритет совпадению с начала), клавиатура ↑↓ Enter Esc; два
размера — герой на главной и компактный внутри пустого звена. Предлагает
только инструменты-трансформеры.
- **`ChainStep.svelte`** — секция одного звена: пустое звено показывает
компактный поиск; заполненное — заголовок с номером и крестиком, форму
параметров, превью результата, кнопку скачивания.
- **`ChainBar.svelte`** — строка под результатом базового инструмента: «⛓ Chain
with…» и «✂ Remove chain» (видна только при непустой цепи).
## 5. Этапы и проверки
- **A. Поиск и главная** — `ToolSearch`, герой поиска на главной, встраивание
блока инструмента, перенос каталога на `/list-tools`, ссылки в шапке.
Проверка: `check/build`, визуально.
- **B. Цепочка** — состояние цепи в блоке инструмента, пустые звенья с поиском,
прогон, промежуточные превью и скачивания, ошибки звеньев. Проверка: смоук §8
пп. 1–6.
- **C. Маска и упрощение** — блок маски в `ParamsCard`, упрощение карточки
результата. Проверка: смоук §8 п. 7.
- **D. Хранение и итог** — автосохранение и восстановление цепочки, удаление
чернового маршрута рабочего пространства, полный регресс каталога,
`test/check/build`.
## 6. Смоук-чеклист
1. На главной — большое поле поиска; ввод фильтрует список по мере набора; Enter
выбирает подсвеченный.
2. Выбранный инструмент разворачивается на той же странице: исходник, параметры,
результат.
3. «⛓ Chain with…» добавляет пустую секцию с компактным поиском; выбор заполняет
её.
4. У каждой секции своё превью и своё скачивание промежуточного результата.
5. Крестик убирает секцию; «✂ Remove chain» снимает все звенья разом.
6. Ошибка звена — баннер «Шаг N (имя): …»; последние успешные результаты секций
не исчезают.
7. «Показать маску» переключает превью базового инструмента; в цепь идёт
результат, скачивание базового отдаёт результат, не маску.
8. Перезагрузка восстанавливает цепочку заполненных звеньев; пустые звенья при
этом исчезают.
9. Быстрые правки подряд — один финальный пересчёт всей вертикали.
10. `/list-tools` открывает старый каталог по категориям; прямые адреса
`/tools/<id>` работают.
## 7. Критерии готовности
- Работа начинается с главной: поиск → инструмент → цепочка звеньями, каждый со
своим превью и скачиванием.
- Отдельной страницы рабочего пространства нет; каталог доступен на
`/list-tools`; прямые ссылки инструментов работают.
- Пустые звенья не попадают в сохранение и не ломают прогон.
- `test/check/build` зелёные, смоук §6 пройден.
## 8. Что сознательно не делаем
- Ветвления и прицепывание от середины цепи — новое звено всегда добавляется в
конец.
- Пипетка и маски внутри звеньев цепи — они остаются фичей базового инструмента.
- Экспорт и импорт цепочки файлом — отложено до появления CLI/wasm-части.
- Несколько параллельных инструментов на главной одновременно — один активный
блок за раз.
+77 -99
View File
@@ -1,22 +1,30 @@
# Backlog
> Статус: **active**. Продуктовые, UX- и инфраструктурные задачи. Здесь только
> **открытые** задачи: выполненные удаляются, а критичные для проекта факты из
> них переносятся в тематический документ (архитектура, quality gates, lint,
> e2e) по мере закрытия.
>
> «Что сознательно не вводим и почему» живёт в `docs/decisions.md`, архитектура
> — в `docs/architecture.md`. Raw-находки UX-аудита — `docs/tools-audit.md`, их
> разбивка на волны — `docs/plan-fields-audit-fixes.md`.
>
> Ids инструментов сверять с `web/src/lib/registry/`; полный список —
> `docs/tools-map.md`.
## lint и прочие правила
- [x] Кросс-языковой линтер словарей (warn-only: паритет ключей, пустые
значения, совпадение `{placeholder}`-переменных между всеми локалями) —
правило `i18n/dict-consistency`, детали в `web/eslint-plugins/README.md`.
Правило реализовано; Q2 перевёл i18n baseline к zero-warning через
policy-aware fallback.
- [x] Проверка текстов в svelte/ts/html — реализовано правилом
`i18n/no-hardcoded-user-text` (warn, baseline 0): ловит текст в шаблоне и
в пользовательских атрибутах, исключения — бренд, форматы, имена файлов,
registry-строки и `/kit`. Детали в `web/eslint-plugins/README.md`.
- [ ] расширить `i18n/no-hardcoded-user-text` на `web/src/routes/**`: сейчас
правило навешено только на `components/**` (без `/kit`), поэтому
захардкоженный текст в `routes/**` проходит молча (`web/eslint.config.js`,
`web/eslint-plugins/README.md`)
- [ ] `tseslint.config(...)` deprecated: сигнатура
`(...configs: InfiniteDepthConfigWithExtends[]): ConfigArray` помечена
устаревшей, нужен переход на `defineConfig` из `typescript-eslint` —
`web/eslint.config.js`.
- [ ] удалить мёртвые `tools.*.params` из root i18n после проверки потребителей;
до этого Q2 держит pattern-based policy-исключение
до этого действует pattern-based policy-исключение
(`web/eslint-plugins/README.md`, раздел `dict-consistency`)
## Мелочи всякие
@@ -90,14 +98,13 @@
## Переезд
- [x] добавить i18n в preview
- [ ] множественные числа (часть/части/частей; «N параметров», счётчики) —
`tPlural()` и формат `PluralForms`; детали и критерии — в этом backlog
## SEO/GEO
Общий план: `docs/plan-seo.md` (S0–S4). C17 выполнен; старт возможен — пока
только domain-free часть S1.
Общий план: `docs/plan-seo.md` (S0–S4). Переезд preview на реальные маршруты
выполнен; старт возможен — пока только domain-free часть S1.
- [ ] переименование бренда + домен: чек-лист в локальном плане
`docs/plan-domain.local.md` (вне git, имя домена в репо не публикуется;
@@ -125,38 +132,12 @@
## Качество production UI
Q5–Q6 и Q7–Q8 закрывают известные preview-проблемы, special fixtures, smoke,
layout/view-model инварианты, browser matrix, coverage-гейт и линтеры; активных
`test.fixme` в e2e больше нет.
Политика проверок — `docs/quality-gates.md`, e2e — `docs/testing-strategy.md`.
- [x] Генераторы auto-run без кнопки Generate — `web/e2e/generators.spec.ts`.
- [x] `resize-png` и `crop-png` используют размеры текущего source через общий
source-aware schema resolver — `web/e2e/pipeline.spec.ts`.
- [x] Reset сохраняет source/text, восстанавливает defaults и запускает deferred
auto-run — `web/e2e/pipeline.spec.ts`.
- [x] Ошибки хранят i18n key/vars и переводятся при выводе; изменение языка
обновляет alert — `web/e2e/i18n.spec.ts`.
- [x] Malformed/special PNG fixtures: CRC, truncated, palette и 16-bit проходят
через upload без краша — `web/e2e/png-fixtures.spec.ts`.
- [x] Registry-wide smoke покрывает все 122 инструмента по input/result-типу —
`web/e2e/tools-smoke.spec.ts`, `generators.spec.ts`,
`text-and-verdicts.spec.ts`.
- [x] Layout-группировка вынесена в `resolveLayoutGroups` и покрыта
unit-тестами; `SchemaFields.svelte` использует общий seam.
- [x] `SchemaPreview` view-model вынесена в `buildSchemaPreviewModel` и покрыта
unit-тестами для всех input/result-типов.
- [x] Browser matrix: полный Chromium, Firefox/WebKit для browser-critical
спецификаций и mobile Chromium 390×844 — `web/playwright.config.ts`.
- [x] Расхождение декодеров зафиксировано как ожидаемое поведение: Firefox и
WebKit декодируют `truncated-no-iend` и `idat-cut`, а WebKit ещё и
`crc-bad-idat`. Таблица движков живёт в `web/e2e/png-fixtures.spec.ts`,
продукт не ужесточается, и любая выгрузка битого файла обязана давать
alert, а не падение или «пустой» результат.
- [ ] Component-тесты `SchemaToolView`/`SchemaPreview` требуют отдельного
решения: node Vitest не имеет DOM/canvas, а e2e уже покрывает поведение.
- [ ] Ошибка декодирования изображения из `createImageBitmap` пока показывается
сырым сообщением браузера; нужен `ToolError`-маппер и i18n-ключ. Это
продуктовая задача, не Q6d.
сырым сообщением браузера; нужен `ToolError`-маппер и i18n-ключ.
## Идеи
@@ -178,7 +159,7 @@ layout/view-model инварианты, browser matrix, coverage-гейт и л
4. **Batch-обработка архивов выбранным chain** — оценка L. Вход — zip-архив
картинок (распаковка в браузере), прогон текущего или выбранного сохранённого
chain по каждому файлу, сборка результата обратно в zip для скачивания.
Зависит от п.4 (выбор цепочки) и от экспортного формата pipeline JSON;
Зависит от идеи 3 (выбор цепочки) и от экспортного формата pipeline JSON;
прогресс и ошибки — пофайлово.
5. **Избранные инструменты (fav tools)** — оценка S/M. Тоггл-звёздочка на
@@ -188,59 +169,48 @@ layout/view-model инварианты, browser matrix, coverage-гейт и л
6. **Конвертация формата — НЕ отдельный инструмент, а выбор в кнопке Download**
— оценка M/L. Инструменты `convert-png-to-jpg` / `convert-png-to-webp` (и др.
форматы) не должны жить как самостоятельные инструменты цепочки. Вместо этого
— бесшовное внедрение выбора формата/качества прямо в кнопку/диалог
**Download**: юзер скачивает результат в нужном формате (mime/ext/quality), а
pipeline при этом не усложняется лишним звеном. **Сделано (2026-09):**
механическая часть реализована в preview — `ToolEntry.output`
(`OutputFormat`: mime/ext/qualityParamId) в `web/src/lib/registry/types.ts`;
`SchemaToolView.download()` кодирует по `output` (jpg/webp/bmp уже настроены;
`png-to-bmp` переведён сюда же). Q6c покрывает этот фиксированный контракт
unit/E2E-проверками; переключатель форматов и quality остаётся отдельным
UX-райзом. **Осталось (UX-райз «Download»):**
форматы) не должны жить как самостоятельные инструменты цепочки. Механическая
часть уже есть: `ToolEntry.output` (`OutputFormat`: mime/ext/qualityParamId)
и кодирование по нему в `SchemaToolView.download()` — `docs/architecture.md`,
раздел 3. Осталось (UX-райз «Download»):
- выбор формата/качества прямо в кнопке/диалоге Download для любого
результата (не отдельными конвертерами);
- экспорт с лимитом размера → закрывает отложенный `reduce-to-size-png`
(целевой KB, бинарный поиск по k из квантования). Проработать вместе с
достройкой download-флоу нового UI.
7. **`watermark-image-png` (водяной знак картинкой)** — оценка M. Отложен из
миграции (`archive/plan-migrate-remaining.md`). Нужна overlay-механика:
второй источник изображения (getOverlay/store), параметры scale/opacity/
position/margin/aspect. Решается вместе с вопросом, как второй источник
картинки вписывается в schema-driven preview (`SchemaToolView`).
7. **`watermark-image-png` (водяной знак картинкой)** — оценка M. Нужна
overlay-механика: второй источник изображения (getOverlay/store), параметры
scale/opacity/position/margin/aspect. Решается вместе с вопросом, как второй
источник картинки вписывается в schema-driven preview (`SchemaToolView`).
8. **`png-info` (детальная информация о PNG)** — оценка M/L. Отложен из
миграции. Отдельный райз: exif-теги, редактирование, структурированный вывод;
нужен свой отдельный случай в UI, не «ещё один text/verdict». возможно нужна
вообще отдельная страница, несовместимая с другими инструментами: 1) она
работает не с изображением, а с мета данными из файла, 2) она не может быть
частью цепочки, т.к. результат не является изображением, 3) если надо
изменить теги, никакого форматирования и изменения в изображение вносить не
надо, только в мета информацияю
8. **`png-info` (детальная информация о PNG)** — оценка M/L. Отдельный райз:
exif-теги, редактирование, структурированный вывод; нужен свой отдельный
случай в UI, не «ещё один text/verdict». Возможно нужна вообще отдельная
страница, несовместимая с другими инструментами: 1) она работает не с
изображением, а с метаданными из файла, 2) она не может быть частью цепочки,
т.к. результат не является изображением, 3) если надо изменить теги, никакого
форматирования и изменения в изображение вносить не надо, только в
метаинформацию.
9. **Region-инструменты** (censor/erase/pixelate-area/blur-area/sharpen-area/
reverse-area) — ждут UI выделения области на превью. Из
`archive/plan-gap-waves.md` («вне очереди»).
reverse-area) — ждут UI выделения области на превью. Вне очереди.
10. **Мультифайловые инструменты-потребители** (gif-to-frames, separate-colors)
— механизм «результат = набор файлов» (1 → many) реализован: `FileResult` +
`result: "files"` + скачивание zip-архивом + грид-превью частей. Первый
потребитель — `split-into-parts-png`. Остальные подключаются по мере нужды.
— механизм «результат = набор файлов» (1 → many) уже есть
(`docs/architecture.md`, раздел 7), первый потребитель —
`split-into-parts-png`. Остальные подключаются по мере нужды.
11. **Анимационные** (slow-reveal/fade/scrolling) — выход не PNG; отдельное
решение о формате. Из `archive/plan-gap-waves.md`.
решение о формате.
12. **HARD-серия** (glitch, barcode, signature-extract, handwritten-digital). Из
`archive/plan-gap-waves.md`.
12. **HARD-серия** (glitch, barcode, signature-extract, handwritten-digital).
13. **Контраст текста на акцентных кнопках — частично закрыт**. Для dark-темы
`--color-main`/`--color-accent` подняты до tone 56, чтобы пройти текущий
порог `palette.test.ts` 4.0; targeted-тест проходит. Light-тема сохраняет
контраст ~4.1–4.3, что ниже AA 4.5 для обычного текста, поэтому после
завершения редизайна нужно повторно поднять тона и вернуть порог 4.5. Также
проверить `--color-danger` (dark, тон 55): текст на нём 4.93 — ок, но
light-danger tone 48 = 4.25 на границе.
13. **Контраст текста на акцентных кнопках — частично закрыт**. Тона
`--color-main`/`--color-accent` подняты под текущий порог `palette.test.ts`;
targeted-тест проходит. Light-тема всё ещё ниже AA 4.5 для обычного текста,
а `--color-danger` в light-теме на границе: после завершения редизайна
вернуть порог 4.5 и перепроверить оба токена.
14. **Отображение маски для инструментов (runMask/preview-mask)** — придумать и
реализовать процесс показа маски результата. **Контекст:** в старом
@@ -252,25 +222,32 @@ layout/view-model инварианты, browser matrix, coverage-гейт и л
возможность вернуть (или инструмент — объявить) `runMask` — ч/б изображение
маски изменений либо иной подход (отдельное поле `mask` + функция маски,
второй результат, overlay). Оценка — на усмотрение при проработке (S/M).
Пока живём без неё. **Идея 2:** каждый `run` возвращает два изображения -
для отображения (показывает маску или сам результат), и для передачи в
следующий инструмент (всегда результат. Оценка — на усмотрение при
Пока живём без неё. **Идея 2:** каждый `run` возвращает два изображения —
для отображения (показывает маску или сам результат) и для передачи в
следующий инструмент (всегда результат). Оценка — на усмотрение при
проработке (S/M). Пока живём без неё.
15. **Процесс работы с инструментами 1 → many и many → 1** — абстракция
результата «набор файлов» оформлена и работает: тип `FileResult`,
`result: "files"` в реестре, сериализация через executor, скачивание
zip-архивом, грид-превью частей. Реализовано на первом потребителе
`split-into-parts-png` (2026-09, `docs/archive/plan-split-into-parts.md`).
Q6b закрывает контракт ZIP/download: unit-тесты `web/src/lib/zip.test.ts` и
browser-сценарий `web/e2e/pipeline.spec.ts` проверяют имена частей и
содержимое архива. **1 → many:** подключение остальных потребителей
(gif-to-frames, separate-colors) при необходимости; формат/качество файлов
внутри zip — отложено до UX-райза «Download» (п.7). **many → 1** (вход =
архив/несколько картинок → единый результат): не реализовано. Зависит от
batch-обработки архивов (п.5 идей, roadmap фаза 8 «batch-страница»). Нужна
отдельная проработка входа (распаковка zip в браузере, порядок файлов,
поведение в пайплайне — такие инструменты терминальны, как и 1 → many).
15. **Набор файлов в пайплайне: 1 → many не должен быть терминальным** — оценка
L. **Сейчас** `execute` принимает один `source: PixelImage | undefined` и
возвращает один `ToolResult`, а `SchemaToolView` раскладывает вывод в
`result` / `fileResult` / `textResult` по `resultKind` — поэтому шаг с
`result: "files"` упирается в конец цепочки. **Направление:** следующий
инструмент применяется **ко всем картинкам** предыдущего результата
(fan-out), а many → 1 (вход = архив или несколько файлов) — обратная сторона
того же контракта. Терминальными остаются только результаты, которые не
картинка (text, verdict). Что обдумать:
- где проходит граница: один вызов `run(ctx)` на файл (имя части склеивается
в `FileResult.files`) или батч целиком на стороне исполнителя;
`executor/protocol.ts` `files` уже сериализуются, но при батче нужен
прогресс и отмена;
- частичные ошибки: один битый файл не должен ронять весь шаг — нужен
per-file отчёт, а не один `ToolError` на шаг;
- имена частей на каждом шаге и Download для набора (сейчас zip по
`tool.id`, `SchemaToolView.download()`);
- обратная сторона: распаковка zip в браузере, порядок файлов, поведение
many → 1 → many в одной цепочке. Зависит от идеи 10 (остальные потребители
1 → many) и идеи 4 (batch по архиву), `docs/roadmap.md` фаза 8
«batch-страница».
16. **Главная должна отличаться от каталога** — оценка S. Сейчас
`src/routes/+page.svelte` и `src/routes/list-tools/+page.svelte` побайтово
@@ -278,5 +255,6 @@ layout/view-model инварианты, browser matrix, coverage-гейт и л
рабочий маршрут: на него ссылается крошка в `+layout.svelte` и он открыт
e2e-сценариями `catalog.spec.ts`, `i18n.spec.ts`, `navigation.spec.ts`.
Замысел: главная — крупнее и с более простым поиском, без категорий; каталог
остаётся группировкой с фильтрами. Найдено измерением дублирования разметки
по 65 `.svelte`-файлам: файлы побайтово идентичны.
остаётся группировкой с фильтрами. Детектор дублирования разметки
сознательно не вводим — `docs/decisions.md`, раздел 1, поэтому расхождение
ловится на ревью.
+5 -36
View File
@@ -96,41 +96,10 @@
корректный (не «кракозябры»).
- [ ] remove-background на сложной полупрозрачности — края не «звенят».
## G. Закрытые Q5/Q6b/Q6c/Q6d/Q6e/Q6f.1/Q6f.2/Q6f.3-проблемы
> Q5/Q6b/Q6c/Q6d/Q6e/Q6f.1/Q6f.2/Q6f.3-проблемы закрыты автоматическими тестами;
> для ручной проверки остаются субъективные сценарии разделов A–F.
- [x] **Генераторы:** auto-run по дефолтам, результат и изменение параметра без
кнопки Generate. `web/e2e/generators.spec.ts`.
- [x] **resize-png/crop-png:** размеры текущего source используются по
умолчанию, alert не появляется. `web/e2e/pipeline.spec.ts`.
- [x] **Reset:** defaults восстанавливаются, source сохраняется, deferred
auto-run завершается результатом. `web/e2e/pipeline.spec.ts`.
- [x] **Локализация ошибок:** alert хранит key/vars, переводится при выводе и
обновляется при смене языка. `web/e2e/i18n.spec.ts`.
- [x] **FileResult → ZIP:** `split-into-parts-png` скачивает
`split-into-parts-png.zip` с именованными PNG-частями; содержимое и
сигнатуры проверяются `web/src/lib/zip.test.ts` и
`web/e2e/pipeline.spec.ts`.
- [x] **Output MIME/quality:** JPG/WebP/BMP имеют ожидаемые расширения и
сигнатуры, quality 10/90 меняет размер файла; `web/e2e/pipeline.spec.ts`.
- [x] **Malformed/special PNG:** CRC, truncated, palette и 16-bit fixtures
проходят через upload без краша; `web/e2e/png-fixtures.spec.ts`.
- [x] **Registry smoke:** все 122 production tools имеют специализированный
browser smoke по input/result-типу; `web/e2e/tools-smoke.spec.ts`,
`generators.spec.ts`, `text-and-verdicts.spec.ts`.
- [x] **Layout resolver:** `resolveLayoutGroups` покрыт unit-тестами; UI
группировка полей использует тот же seam.
- [x] **Preview model:** `buildSchemaPreviewModel` покрывает source/result
values, формат и `hasResult` для image/text/verdict/files.
- [x] **Browser matrix:** pipeline, verdicts и PNG-fixtures проходят в Firefox и
WebKit, mobile viewport 390×844 — в Chromium; отказ декодера битого PNG
зафиксирован per-engine. См. раздел 7 `docs/testing-strategy.md`.
## Как долго
- Полный прогон: ~40 минут (все разделы, включая мегапиксели и 4К) в двух
браузерах.
- Быстрый смоук (< 15 мин): A (кроме мегапикселей), B (выборочно), C, F
выборочно, G обязательно.
- Полный прогон: все разделы A–F, включая мегапиксели и 4К, в двух браузерах.
- Быстрый смоук: A (кроме мегапикселей), B (выборочно), C, F выборочно.
> Сценарии, закрытые e2e-тестами, в этот чек-лист не попадают: их покрытие видно
> по спецификациям в `web/e2e/`, политика e2e — `docs/testing-strategy.md`.
+155
View File
@@ -0,0 +1,155 @@
# Решения по гейтам и тестам
> Статус: **active**. Журнал решений «что сознательно НЕ вводим» и «как читаем
> результат CI». Только про гейты, линтеры, coverage, e2e и CI — **для обычных
> правок production-кода не нужен**. Читать, когда проектируется или меняется
> сам гейт, либо когда спрашивают «почему так, а не иначе». Оперативные команды
> и пороги — в `docs/quality-gates.md`, правила нагрузки на машину — в
> `docs/agent-workflow.md`, раздел 3.
Здесь лежат решения, чтобы не пересматривать их заново. Для каждого: причина и
то, что решение пересмотрело бы. В `docs/backlog.md` такие пункты не
дублируются: backlog живёт задачами, а не историей решений.
Числа ниже — разовые замеры, а не текущие значения: помечены датой. Актуальные
цифры даёт прогон (`docs/quality-gates.md`), по правилу `AGENTS.md` в документах
нет величин, которые меняются вместе с кодом.
## 1. Детектор дублирования разметки — не вводим
Ни свой AST-правило, ни готовая зависимость (jscpd, `eslint-plugin-sonarjs`).
- Замер по корпусу `.svelte` в `components/**` и `routes/**` (сентябрь 2026):
при пороге 5 нормализованных строк — одна осмысленная пара, при пороге 3 — 17
пар, и все они шум (`</label>`, `{/each}`, `{#if children}`). Семейство
schema-контролов делит максимум 4 строки — законное сходство by design.
- Причина: построчное сравнение Svelte-разметки принципиально ненадёжно —
одинаковый контейнер с разным содержимым даёт ложное срабатывание, а намеренно
общий класс — ложную тишину. Чтобы не ловить шум, порог пришлось бы поднять до
12+ строк, и тогда правило срабатывает ровно один раз.
- Контроль остаётся на ревью (расхождение видно в диффе) и на поведенческих
e2e-проверках; извлечение общей разметки закреплено конвенцией в `AGENTS.md`.
- Пересмотреть, если появится реально «уехавшая» копипаста в компонентах или
библиотека разрастётся на уровень композиций страниц.
## 2. Линтер согласованности комментариев — не вводим
Описания инструментов различаются осознанно, и ловить это текстовым правилом
дороже, чем смотреть на ревью. Действенная часть выполнена вручную: все
`TODO`/`FIXME` перенесены в backlog, незарегистрированных пометок в коде нет;
новые запрещены правилом в `AGENTS.md`.
## 3. Линтер «изменяемых чисел» в документации — не вводим
Правило «не фиксировать числа, которые меняются вместе с кодом» живёт в
`AGENTS.md` и держится на ревью. Автоматизация отклонена по замеру (сентябрь
2026): regex «число + существительное» на корпусе Markdown дал 15 совпадений, из
которых 14 — ложные («30 дней» политики архива, «9 опций» описания поля, «4
проекта» факт из `playwright.config.ts», цитата самого правила).
- Правило всё равно неполно: ловит «122 инструмента», но не «инструментов —
122», не «более 100», не «≈120»; на каждое обходное движение нужна правка
правила, а allowlist легитимных чисел становится вторым источником долга.
Худший исход — не «шумно», а «вроде гейт, а толку нет»: срабатывания на пустом
месте забейзелят, и через месяц правило перестанет ловить настоящие нарушения.
Видимое блокирующее правило, которое на деле ничего не проверяет, хуже
отсутствия правила.
- Соблюдение правила дешевле обхода: «список — в `docs/tools-map.md`» стоит
столько же символов, сколько цифра, но не требует обновления.
- Пересмотреть, если появится источник истины, из которого число можно
**вычислить** и сравнить автоматически, без разбора прозы. Такой случай есть и
решён отдельно — следующий пункт.
## 4. Сверка `docs/tools-map.md` с реестром — автоматизирована
Правило про числа не закрывает более опасный класс расхождений: документ не
устарел числом, а **перечисляет то, чего нет в коде**. В разделе «Реализовано»
карты лежали несуществующие id (`png-info`, `color-wheel-generator`,
`jpg-to-png`, `watermark-image-png`), которые агент принял бы за готовые
инструменты. Счёт при этом был верный — число такой класс не ловит.
Сверка множеств отвечает на вопрос однозначно и без allowlist, поэтому
автоматизирована: `web/src/lib/registry/tools-map-doc.test.ts` парсит раздел
«Реализовано» и сравнивает id с `TOOLS` в обе стороны (id без инструмента и
инструмент без записи), плюс ловит повтор id между буллетами. Формат строки
карты — всё до первого тире, id через `/` или `,` — закреплён в самом документе
и в тесте.
Чтобы сломанный парсер не выглядел как успех, у теста есть отдельная проверка
«разбор не пустой»: если формат карты снова поменяется, тест упадёт с сообщением
про парсер, а не молча перестанет что-либо проверять. Входит в обычный
`pnpm --dir web test`; новых гейтов и конфигов не добавляет.
## 5. Visual snapshots — не вводим
`toHaveScreenshot` сравнивает страницу с PNG-baseline из прошлого коммита, а не
с замыслом дизайна. При активно меняющемся визуальном языке это превращает
каждый дизайн-PR в регенерацию и ревью картинок, а baseline платформенные
(снимок с Windows не совпадёт на `ubuntu-latest`). Потребность закрывают
e2e-поведение, `lint:tokens` с token audit, i18n-гейт и ручной чеклист
`docs/checklist-manual-testing.md`.
Вернуться стоит, когда визуальный язык стабилизируется; тогда первым идёт `/kit`
в двух темах, а baseline рождаются на Linux.
## 6. Fail-режим для unused-token аудита — не вводим
Остаётся предупреждением: `verify` входит в deploy-workflow, поэтому стилевой
долг блокировал бы деплой сайта. Кроме того, аудит видит только ссылки
`var(--x)`, и чтение токена из JS дало бы ложное срабатывание. Правило «новый
токен — вместе с потребителем» живёт в `AGENTS.md`.
## 7. Per-project `expect.timeout` — не задаём
Firefox и WebKit отдают результат так же быстро, как Chromium: расхождения
объясняются памятью, а не скоростью движка. Таймаут под конкретную машину —
магическое число, которое придётся подстраивать на каждом раннере, поэтому
Playwright-конфиг остаётся с дефолтами, а проблема решается лимитом воркеров.
Практические правила — `docs/agent-workflow.md`, раздел 3.
## 8. Дублирование `verify` между workflow — сознательно
`Deploy to GitHub Pages` и `Quality` оба зовут `verify`: deploy проверяет
быстрый gate и собирает сайт, `Quality` прогоняет тот же `verify` и browser
matrix. По замеру (сентябрь 2026) `verify` — единицы минут, matrix — около трёх,
поэтому дублирование дешевле условных job-и, а matrix дёшево держать на каждом
PR. Актуальные длительности — в логе прогона. Ручной
`gh workflow run quality.yml` нужен только для перепрогона старого коммита.
## 9. Политика e2e в CI
Текущее состояние: `e2e` — non-blocking baseline, запускается на каждом пуше в
`main` и на каждом PR целиком (все проекты matrix). Ночного разделения нет: при
стоимости job около трёх минут оно не окупается, поэтому новые browser-critical
спецификации просто добавляются в `browserContractTests`.
`e2e` станет blocking, когда выполнятся три условия:
1. Условия matrix пересмотрены: решено, какие спецификации обязаны идти в
`browserContractTests` и `mobileTests`, нужен ли `png-fixtures` вне Chromium
и что делать с engine-специфичными расхождениями декодеров.
2. Накопились стабильные прогоны на раннере без падений по таймингу.
3. Вместе с `continue-on-error` снимается и сам trick с общим conclusion: иначе
зелёный прогон при красном `e2e` останется возможен.
Пока условия не выполнены, `e2e` остаётся non-blocking, а его статус читается по
отдельному job — см. следующий раздел.
## 10. Чтение результатов CI после пуша
Первый реальный прогон browser matrix происходит на runner, а не на локальной
машине: локально Firefox и WebKit проходят на 1 воркере, но состав, скорость и
память Linux-раннера отличаются. Поэтому после пуша разработчика:
1. дождаться завершения workflow и посмотреть, какие job вообще запустились;
2. смотреть статусы **отдельных job**, а не общий conclusion прогона: у `e2e`
стоит `continue-on-error: true`, поэтому прогон с упавшим e2e целиком
помечается как `success` — проверено на run `36225579363` (`E2E: failure` при
`conclusion: success`);
3. если `e2e` запускался и зелёный — сообщить фактические числа passed, failed,
skipped;
4. если `e2e` не запускался, сказать это прямо: matrix на раннере не проверена,
и её можно прогнать через `gh workflow run quality.yml`;
5. красный `e2e` только из-за ресурсов раннера фиксируется как ограничение
окружения: сначала проверяются один проект и один spec
(`docs/agent-workflow.md`, раздел 3), а не увеличиваются таймауты.
+20 -15
View File
@@ -3,7 +3,13 @@
> Статус: **draft** — на ревью.
>
> Источник: `docs/tools-audit.md`. Этот документ разбивает аудит на конкретные
> шаги, группирует по типу работы и фиксирует решения.
> шаги, группирует по типу работы и фиксирует решения. Порядок и правила
> проверки — `docs/quality-gates.md`; точки касания при добавлении вида поля —
> `docs/architecture.md`, раздел 4.
>
> **Ids инструментов в таблицах волны B сверены с реестром.** Названия из
> `tools-audit.md` вида `png-to-hsl` или `color-wheel-generator` в реестре нет
> (`hsl`, `color-wheel-png`).
## 0. Ключевое решение: select → buttons
@@ -116,7 +122,7 @@ buttons: <V extends string>(s: Omit<ButtonsSpec<V>, "kind">): Field<V> => ({
Каждый шаг — один коммит, < 500 строк. Меняем `field.select()` →
`field.buttons()` в перечисленных ниже схемах.
### B1. geometry.ts (8 инструментов)
### B1. geometry.ts
| Инструмент | Поле | Опции |
| ----------------------- | -------- | ----------------------------------------- |
@@ -132,7 +138,7 @@ buttons: <V extends string>(s: Omit<ButtonsSpec<V>, "kind">): Field<V> => ({
`field.select()`, т.к. 9 кнопок в ряд не поместятся. Аналогично `position9` (уже
отдельный компонент `PositionControl`).
### B2. color.ts (4 инструмента)
### B2. color.ts
| Инструмент | Поле | Опции |
| ------------------------ | --------- | ------------------------------------ |
@@ -144,7 +150,7 @@ buttons: <V extends string>(s: Omit<ButtonsSpec<V>, "kind">): Field<V> => ({
**Исключение:** channel spaces (hsl/hsv/... component) — 3-6 опций, но это
динамические инструменты, оставить `select`.
### B3. analyze.ts (5 инструментов)
### B3. analyze.ts
| Инструмент | Поле | Опции |
| ------------------------- | ---- | ------------------ |
@@ -156,13 +162,13 @@ buttons: <V extends string>(s: Omit<ButtonsSpec<V>, "kind">): Field<V> => ({
Все используют общий `maskBaseFields` — правка в одном месте.
### B4. filters.ts (1 инструмент)
### B4. filters.ts
| Инструмент | Поле | Опции |
| ------------- | ---- | ------------ |
| add-noise-png | mode | mono / color |
### B5. generate.ts (12 инструментов)
### B5. generate.ts
| Инструмент | Поле | Опции |
| ------------------ | ------------ | --------------------- |
@@ -183,10 +189,10 @@ buttons: <V extends string>(s: Omit<ButtonsSpec<V>, "kind">): Field<V> => ({
### C1. Source-aware defaults для dimension-полей
Q5 добавил общий `defaultFromSource` resolver в `registry-schema`. Для
`resize-png` и `crop-png` дефолт после загрузки равен размерам текущего source;
до загрузки используется положительный fallback `1×1`. Один-sided `0` у resize
сохраняет режим auto.
Source-aware defaults реализованы: общий resolver в `registry-schema`
(`applySourceDefaults`). Для `resize-png` и `crop-png` дефолт после загрузки
равен размерам текущего source; до загрузки используется положительный fallback
`1×1`. Один-sided `0` у resize сохраняет режим auto.
### C2. symmetric-copy-png: "keep side" не работает
@@ -201,9 +207,9 @@ Q5 добавил общий `defaultFromSource` resolver в `registry-schema`.
### C3. Source-aware bounds (отдельный следующий шаг)
Q5 реализовал только defaults dimension-полей через `SchemaContext` и
`defaultFromSource`. Динамические min/max для crop x/y, shift и resize остаются
отдельной задачей: bounds не следует смешивать с default resolver.
Реализованы только defaults dimension-полей через `SchemaContext` и
`applySourceDefaults`. Динамические min/max для crop x/y, shift и resize
остаются отдельной задачей: bounds не следует смешивать с default resolver.
**Что нужно спроектировать и завести:**
@@ -232,8 +238,7 @@ Q5 реализовал только defaults dimension-полей через `S
4. **Первые потребители:** crop (x/y → ±sourceWidth/sourceHeight), shift
(offsetX/offsetY), resize (max → maxSide).
Это отдельный refactor после Q6/Q7; source-aware defaults из Q5 не заменяют
bounds.
Это отдельный рефакторинг; source-aware defaults не заменяют bounds.
### C4. verify-is-png: неверное название
+72 -54
View File
@@ -1,10 +1,12 @@
# План: SEO + GEO — индексация и конкуренция в image tools
> **Статус (2026-09-11):** план к выполнению. Переезд `preview/*` на реальные
> маршруты выполнен (в `noindex` сохраняются до запуска S1). Сейчас применять
> можно domain-free часть S1. **Уточнено:** домен не был блокером C17 (см. §6).
> Задачи S1 разбиты на domain-free (можно делать параллельно с покупкой домена)
> и domain-required (после покупки и настройки домена на Vercel).
> **Статус (2026-09-11; уточнено 2026-09-27):** план к выполнению. Переезд
> `preview/*` на реальные маршруты выполнен. **`noindex` на страницах
> инструментов не выставляется** — снимать нечего, каталог открыт для
> индексации; запрет индексации держится только на служебных путях
> (`robots.txt`). Домен не был блокером переезда (см. §6). Задачи S1 разбиты на
> domain-free (можно делать параллельно с покупкой домена) и domain-required
> (после покупки и настройки домена).
>
> **Цель:** каждая страница инструмента — индексируемый лендинг под конкретный
> интент («crop png», «make png transparent», «jpg to png»…), конкуренция с
@@ -37,18 +39,21 @@
каждого инструмента — с селектором PNG/JPG/WebP/BMP (+quality для lossy). Райз
«выбор формата/качества в Download» вытащен из backlog №7 в S1f: механика уже
есть (`ToolEntry.output`), нужна как продуктовая основа «image
tools»-позиционирования. Отдельные страницы-конвертеры — только для
подтверждённых вордстатом интентов.
tools»-позиционирования. В направлении «из формата в PNG» отдельных
инструментов нет (загрузка принимает любой формат) — см. §5.
- **Слаги инструментов `-png` сохраняются.** Они держат низкоконкурентные
png-интенты; широкие интенты («crop image online») переносятся теми же
страницами контентом: H1 «Crop image», текст «works with PNG, JPG, WebP».
URL-алиасы без `-png` не создаются (канонический беспорядок).
- **Индексируемая локаль — EN** (базовая; конкурент англоязычный). RU-версия —
отдельно и позже: префикс `/ru/`, prerender обеих локалей, hreflang
(сознательно отложено в `archive/plan-i18n.md`). Пока язык переключается на
клиенте (localStorage) — для SEO не учитываем.
- **Прод-домен без `BASE_PATH`.** Сборка с базой (GitHub Pages `/repo/`) ломает
абсолютные URL, canonical и «корневость» страниц. Свой домен + корень.
(сознательно отложено). Пока язык переключается на клиенте (localStorage) —
для SEO не учитываем.
- **Прод-домен без `BASE_PATH`.** Сборка с базой (GitHub Pages `/repo/`, см.
`BASE_PATH` в `web/vite.config.ts` и `deploy.yml`) ломает абсолютные URL,
canonical и «корневость» страниц. Свой домен + корень. Текущий деплой — GitHub
Pages (`.github/workflows/deploy.yml`); перенос на свой домен выполняется
сменой origin в CI и `PUBLIC_SITE_ORIGIN`.
- **URL-схема инструментов:** короткие плоские адреса. После C17 страницы
инструментов находятся в корне на `/tools/[id]`, каталог — на `/list-tools`;
текущую схему не менять без отдельного SEO-плана: после индексации
@@ -66,28 +71,28 @@
- SvelteKit + `adapter-static`, `prerender = true` на весь сайт
(`web/src/routes/+layout.ts`) — весь контент в статичном HTML, ботам не нужен
JS. Это главный технический козырь.
- 122 инструмента в `web/src/lib/registry` с «SEO-шными» id (`crop-png`,
`add-border-png`, …) — URL уже повторяют паттерн конкурента.
- Инструменты в `web/src/lib/registry` с «SEO-шными» id (`crop-png`,
`add-border-png`, …) — URL уже повторяют паттерн конкурента. Актуальный
состав: `docs/tools-map.md`.
- На корневых страницах инструментов есть базовые `<title>` и `robots`; полный
SEO-слой (canonical, OG, content и JSON-LD) остаётся S1/S2.
- Каталог со ссылками на все инструменты (внутренняя перелинковка) и карта
инструментов против конкурента: `docs/tools-map.md` (у них 311, из них 107 —
нишевые клоны-серии Logo*/Icon*/Stamp* поверх обычных операций).
инструментов против конкурента: `docs/tools-map.md`.
- EN-тексты в реестре + i18n-словари ru/en.
Чего нет (пробелы закрываются этапами ниже):
| Пробел | Где сейчас | Этап |
| ----------------------------------------------------- | ---------------------------------------- | ---- |
| `sitemap.xml` | нет | S1 |
| `robots.txt` без `Sitemap:` | allow-all (`web/static/robots.txt`) | S1 |
| `rel=canonical` | нет | S1 |
| OG/Twitter-теги, OG-картинки | нет | S1 |
| `noindex, nofollow` на страницах инструментов preview | `web/src/routes/tools/[id]/+page.svelte` | S1 |
| Контент страницы (H1-текст, how-to, FAQ, related) | только title/description | S2 |
| JSON-LD (SoftwareApplication, FAQPage, Breadcrumb) | нет | S3 |
| `llms.txt`, хабы категорий с текстом | нет | S3 |
| GSC + приватная аналитика | нет | S4 |
| Пробел | Где сейчас | Этап |
| -------------------------------------------------- | ----------------------------------- | ---- |
| `sitemap.xml` | нет | S1 |
| `robots.txt` без `Sitemap:` | allow-all (`web/static/robots.txt`) | S1 |
| `rel=canonical` | нет | S1 |
| OG/Twitter-теги, OG-картинки | нет | S1 |
| Канонический домен в абсолютных URL | нет, деплой на GitHub Pages с базой | S1 |
| Контент страницы (H1-текст, how-to, FAQ, related) | только title/description | S2 |
| JSON-LD (SoftwareApplication, FAQPage, Breadcrumb) | нет | S3 |
| `llms.txt`, хабы категорий с текстом | нет | S3 |
| GSC + приватная аналитика | нет | S4 |
## 2. Конкурент (onlinepngtools.com)
@@ -109,8 +114,8 @@
## 3. Стратегия
1. **Не лобовая, а ядро интентов.** 80% объёма поиска приносят ~30–50 запросов.
Покрытие инструментов у нас почти полное (122); приоритет — контент и
качество страниц ядра, затем хвосты.
Покрытие инструментов у нас почти полное (список — `docs/tools-map.md`);
приоритет — контент и качество страниц ядра, затем хвосты.
2. **Страница = лендинг, а не «пустая коробка».** Шаблон контента: H1 → 1–2
абзаца «что делает» → как использовать (шаги) → параметры → FAQ (3–5 вопросов
уровня People-Also-Ask) → related tools по категории.
@@ -132,34 +137,35 @@
## 4. Этапы
### S1. Технический минимум (сразу после C17)
### S1. Технический минимум (закрывает блокер индексации)
Разбито на две группы: **domain-free** (делаем параллельно с покупкой домена) и
**domain-required** (после покупки и DNS-настройки).
#### S1-free: без домена (параллельно с quality-планом)
#### S1-free: без домена
- [ ] **[S1e]** OG-картинки per-tool: генерация PNG-карточек скриптом на
существующем ядре (мы сами умеем рисовать PNG) — имя инструмента + бренд,
файл `static/og/[id].png`, ссылка из `Seo.svelte`. Относительные пути
(`/og/crop-png.png`) работают без домена; при деплое на Vercel с
собственным доменом OG-картинки будут корректными.
(`/og/crop-png.png`) работают без домена; при деплое на собственном домене
OG-картинки будут абсолютными и корректными.
- [ ] **[S1f]** Download-кнопка с селектором формата PNG/JPG/WebP/BMP (+quality
для lossy) для всех инструментов — механика `ToolEntry.output` уже есть
(backlog №7, райз «выбор формата/качества»). Продуктовая основа «image
tools»-позиционирования: без неё сайт остаётся PNG-сайтом. Вести
параллельно с текущими quality-задачами.
- [ ] **[S1g]** Поведение конвертеров после S1f: не предлагать в меню форматов
формат исходника (disable) — чтобы «convert png to jpg» не мог сохранить в
png. Существующие страницы не трогать: `jpg-to-png`, `png-to-jpg`,
`png-to-webp`, `png-to-bmp` остаются самостоятельными лендингами со своим
контентом (§5).
формат исходника (disable) — чтобы `convert-png-to-jpg` не мог сохранить в
png. Существующие страницы не трогать: `jpg-to-png`, `convert-png-to-jpg`,
`convert-png-to-webp`, `png-to-bmp` остаются самостоятельными лендингами
со своим контентом (§5).
#### S1-domain: с доменом (после покупки + DNS на Vercel)
#### S1-domain: с доменом (после покупки + DNS)
- [ ] **[S1a]** Снять `noindex, no_follow` со страницы инструмента
(`tools/[id]/+page.svelte`) — делается одновременно с S1d (canonical)
чтобы Google сразу проиндексировал с правильным доменом.
- [ ] **[S1a]** Перевести прод на собственный домен: убрать `BASE_PATH` из
`deploy.yml`, задать `PUBLIC_SITE_ORIGIN`, перенаправить
`https://<юзер>/<repo>/…` на корневые URL (301). Делается одновременно с
S1d (canonical), чтобы Google проиндексировал сразу с правильным доменом.
- [ ] **[S1b]** Скрипт `web/scripts/gen-sitemap.mjs`: генерирует `sitemap.xml` в
`build/` из `TOOLS` реестра (`web/src/lib/registry`) + статических
страниц. Вызов из `pnpm build` после `vite build` (adapter-static пишет в
@@ -181,8 +187,8 @@
- [ ] **[S2a]** Схема данных: `seo`-блок per-tool (`longDescription`,
`howTo: string[]`, `faq: {q, a}[]`) — в i18n-словарях (ru/en), не в коде
реестра. Черновики — генерацией из схемы параметров инструмента, вычитка
вручную пачками по категориям (самый трудоёмкий этап: ~122 × 2 локали; RU
— отложить до решения о `/ru/`).
вручную пачками по категориям (самый трудоёмкий этап: объём растёт вместе
с числом инструментов × 2 локали; RU — отложить до решения о `/ru/`).
- [ ] **[S2b]** Компонент контента страницы инструмента (kit): H1, lead, how-to
(`ol`), описание параметров (из схемы), FAQ, related tools (перелинковка
внутри категории + 2–3 «соседних» интента).
@@ -237,15 +243,28 @@
## 5. Приоритизация инструментов (первая волна контента)
Источник — `docs/tools-map.md` (122 реализовано; у конкурента 311, из них 107 —
клоны-серии). Первая волна — топ-интенты, где конкуренты ранжируются, а у нас
уже есть инструмент и он не хуже:
Источник — `docs/tools-map.md` (наш состав и замер по конкуренту — §2). Ids ниже
— **фактические id реестра**; первая волна — топ-интенты, где конкуренты
ранжируются, а у нас уже есть инструмент и он не хуже:
- прозрачность/фон: create-transparent-png, remove-background, change-opacity
- конвертация: jpg-to-png, png-to-jpg, webp-to-png, svg-to-png, png-to-base64
- геометрия: crop, resize, rotate, circle-crop, round-corners, flip
- цвет/фильтры: grayscale, invert, pixelate, add-text, watermark
- генерация: gradient, empty-canvas
- прозрачность/фон: create-empty-png, remove-background-png, change-png-opacity,
set-alpha-channel-png
- конвертация: convert-png-to-jpg, convert-png-to-webp, png-to-bmp, svg-to-png,
png-to-base64
- геометрия: crop-png, resize-png, rotate-png, round-corners-png, flip-png,
change-aspect-ratio-png
- цвет/фильтры: grayscale-png, invert-colors-png, pixelate-png, add-text-png,
watermark-tile-png
- генерация: linear-gradient-png, create-empty-png
> **Открытый вопрос по интентам «из формата в PNG».** У конкурента есть
> `jpg-to-png`, `webp-to-png`, `gif-to-png` как отдельные страницы; у нас таких
> инструментов нет и создавать их не хочется — загрузка файла уже принимает
> любой формат, а конвертер «во что-то» закрывает обратное направление. Это
> high-volume интент без посадочной страницы. Решение (страница-обёртка на
> существующий инструмент / приём в H1 и тексте / ничего) требует вордстата и
> должно быть принято до старта S2, иначе первая волна контента закроет не ядро
> запросов.
Новая волна форматов (после S1f): jpg↔webp и другие конвертации «формат →
формат» — только по подтверждённому вордстату; базовые кейсы закрывает селектор
@@ -325,7 +344,7 @@ with PNG, JPG, WebP» в тексте + селектор формата в Downl
ключи localStorage (`easy-png-tools:*` — или мигрировать отдельной
мелочью), историю docs/archive.
- [ ] **[R2]** Купить выбранный домен (кандидаты и пробы — в локальном плане) и
настроить деплой на Vercel без `BASE_PATH` (§0). Домен нужен до S1-domain:
перевести прод на него без `BASE_PATH` (§0). Домен нужен до S1-domain:
canonical и sitemap генерятся от `PUBLIC_SITE_ORIGIN`.
- **Порядок:** R1+R2 можно делать параллельно с текущими quality-задачами (не
блокируют). R2 (домен) — блокер только для S1-domain (canonical + sitemap +
@@ -334,8 +353,8 @@ with PNG, JPG, WebP» в тексте + селектор формата в Downl
## 7. Что осознанно НЕ делаем
- Клонирование 311 URL конкурента (низкокачественные дубли + нет ресурсов на
контент) — вместо этого ядро + обоснованные хвосты.
- Клонирование всего каталога конкурента (низкокачественные дубли + нет ресурсов
на контент) — вместо этого ядро + обоснованные хвосты.
- RU-локаль в индексе до решения о структуре `/ru/` + hreflang.
- GA/реклама/трекинг — противоречит позиционированию.
- Дорвеи, программные зеркала, покупные ссылки.
@@ -352,6 +371,5 @@ with PNG, JPG, WebP» в тексте + селектор формата в Downl
- `docs/backlog.md` — пункты SEO/GEO в общем бэклоге.
- `docs/roadmap.md` — фазы 2–8 (ядро TS→Rust/wasm, CLI); фазы 9–10 — API и PWA.
- `docs/plan-platform.md` — планы API и PWA (общее ядро, оффлайн-режим).
- `docs/archive/plan-i18n.md` — отложенная многоязычная схема (будущий `/ru/`).
- `docs/plan-domain.local.md` — план переименования/домена; **в git не входит**
(`.gitignore`), лежит только локально.
+55 -169
View File
@@ -1,12 +1,12 @@
# Quality gates
> Статус: **active**. Каноническое описание быстрых проверок репозитория.
> Краткие обязательные команды остаются в `AGENTS.md`.
> Статус: **active**. Каноническое описание быстрых проверок: команды, coverage,
> warnings, CI. Почему гейт такой, а не иначе, — `docs/decisions.md`.
Команды выполняются из корня репозитория.
## Каноническая команда
Запускать из корня репозитория:
```text
pnpm verify
```
@@ -28,25 +28,40 @@ pnpm verify
## Coverage
`pnpm --dir web test:coverage` считает покрытие только по `src/lib` и падает
ниже порога. Scope и список исключений — в `web/vitest.config.ts`, обоснование —
в `docs/testing-strategy.md`.
ниже порога. Scope и список исключений — в `web/vitest.config.ts`.
| Метрика | Порог | Baseline |
| ---------- | ----- | -------- |
| statements | 90 | 91.77% |
| branches | 82 | 83.3% |
| functions | 72 | 74.31% |
| lines | 91 | 92.53% |
| Метрика | Порог |
| ---------- | ----- |
| statements | 90 |
| branches | 82 |
| functions | 72 |
| lines | 91 |
Порог — нижняя граница от текущего baseline, а не оценка качества: он блокирует
регрессию и не требует сначала закрывать весь долг. Локальный замер занимает ~30
с, поэтому отдельного прогона в `verify` нет — `test:coverage` заменяет `test`.
Порог — нижняя граница, а не оценка качества: он блокирует регрессию и не
требует сначала закрывать весь долг. Актуальные проценты даёт
`pnpm --dir web test:coverage`; здесь они не дублируются, чтобы число не
устарело молча (`AGENTS.md`). Локальный замер занимает единицы секунд, поэтому
отдельного прогона в `verify` нет — `test:coverage` заменяет `test`.
Исключения из scope и их причины:
| Исключение | Причина |
| ------------------------------------------- | ------------------------------------------- |
| `**/*.test.ts`, `**/test-helpers.ts` | сами тесты |
| `**/*.svelte.ts` | runes, не работают в node |
| `executor/executor.worker.ts` | entry worker, исполняется только в браузере |
| `components/define.ts` | type-only, нет исполняемого кода |
| `core/domText.ts`, `core/io.ts` | canvas, `createImageBitmap`, нужен браузер |
| `i18n/en.ts`, `i18n/ru.ts`, `tool-icons.ts` | данные, а не логика |
`registry/*` из scope НЕ исключены: низкое покрытие там означает непокрытую
логику, а не ограничение окружения. Включение браузерных модулей в scope —
отдельное решение, а не молчаливое расширение списка.
## E2E
E2E не входит в `verify`, потому что Playwright поднимает build и статический
сервер. Locator, fixture, i18n и `test.fixme`-политика описаны в
`docs/testing-strategy.md`:
сервер:
```text
pnpm --dir web test:e2e
@@ -55,104 +70,41 @@ pnpm --dir web test:e2e
Запускать его обязательно для изменений маршрутов, загрузки файла, результата,
download, темы или локализации. Изменения runtime fixtures/загрузки файла
проверяются отдельным `web/e2e/png-fixtures.spec.ts`; ручные бинарники из
`tests/fixtures/` не входят в `verify`. В текущем CI это отдельный non-blocking
baseline; после закрытия известных ошибок его можно сделать blocking.
`tests/fixtures/` не входят в `verify`.
Локально прогон идёт с `workers: 1` и заметно медленнее: это осознанный размен,
экономит память и оставляет компьютер отзывчивым. Playwright не умеет
per-project лимит воркеров, а browser matrix упирается в память, а не CPU.
Тяжёлые прогоны запускаются по команде пользователя — по одному, без фонового
запуска и циклов повторов; детали в `docs/agent-workflow.md`.
Локально прогон идёт с `workers: 1` и заметно медленнее. Правила нагрузки на
машину пользователя и диагностика флаков — `docs/agent-workflow.md`, раздел 3;
матрица проектов и ожидаемое поведение декодеров — `docs/testing-strategy.md`,
раздел 7; статус e2e в CI и порядок чтения прогона — `docs/decisions.md`,
разделы 9–10.
## Быстрые подмножества
| Изменение | Команда |
| ---------------------------- | ------------------------------------------------------- |
| Только Markdown | `pnpm check:docs` |
| TypeScript/Svelte | `pnpm --dir web check` и `pnpm --dir web test` |
| Lint-правило или lint-конфиг | `pnpm --dir web test:rules` и `pnpm --dir web lint:all` |
| Пользовательский поток | `pnpm --dir web test:e2e` |
| Fixtures/загрузка файла | `pnpm --dir web test:e2e` |
| Полный локальный gate | `pnpm verify` |
Матрица «изменение → минимальный набор проверок»:
| Изменение | Команда |
| ---------------------------------------------------- | ------------------------------------------------------- |
| Только Markdown | `pnpm check:docs` |
| TypeScript/Svelte | `pnpm --dir web check` и `pnpm --dir web test` |
| Lint-правило или lint-конфиг | `pnpm --dir web test:rules` и `pnpm --dir web lint:all` |
| Маршрут, загрузка, результат, download, тема, локаль | `pnpm --dir web test:e2e` |
| Перед ревью или merge | `pnpm verify` |
## Errors и warnings
- `errors` завершают соответствующий инструмент ненулевым кодом.
- После Q2 ESLint запускается с `--max-warnings=0`: новые ESLint warnings
блокируют gate.
- Token audit оставляет unused-token сообщение неблокирующим: после Q7a пыль
вычищена, решение о fail-режиме зафиксировано в Q7d — новый токен появляется
вместе с потребителем, но стиль не блокирует деплой.
- ESLint запускается с `--max-warnings=0`: новые ESLint warnings блокируют gate.
- Token audit оставляет unused-token сообщение неблокирующим: новый токен
появляется вместе с потребителем, но стиль не блокирует деплой (решение —
`docs/decisions.md`, раздел 6).
- Резолв `var()` в `app.css` — ошибка: несуществующий токен молча роняет
объявление, поэтому этот check failing, в отличие от unused.
- `lint:all` сейчас чист: 0 errors, 0 i18n warnings, 0 unused tokens.
- `dict-consistency` использует base-locale fallback для registry metadata и
pattern-based policy для временно неиспользуемых `tools.*.params`; это не
молчаливый `allowPaths`-список.
## Решения, принятые без автоматизации
Здесь лежат решения «что сознательно НЕ вводим», чтобы не пересматривать их
заново через полгода. Для каждого — причина и то, что пересмотрело бы решение. В
`docs/backlog.md` такие пункты не дублируются: backlog живёт задачами, а не
историей решений.
### Детектор дублирования разметки — не вводим
Ни свой AST-правило, ни готовая зависимость (jscpd, `eslint-plugin-sonarjs`).
- Измерение по 65 `.svelte` в `components/**` и `routes/**`: при пороге 5
нормализованных строк — одна осмысленная пара, при пороге 3 строки — 17 пар и
все они шум (`</label>`, `{/each}`, `{#if children}`). Семейство
schema-контролов делит максимум 4 строки — это законное сходство by design.
- Причина: построчное сравнение Svelte-разметки принципиально ненадёжно —
одинаковый контейнер с разным содержимым даёт ложное срабатывание, а намеренно
общий класс — ложную тишину. Чтобы не ловить шум, порог пришлось бы поднять до
12+ строк, и тогда правило срабатывает ровно один раз.
- Контроль остаётся на ревью (расхождение видно в диффе) и на поведенческих
e2e-проверках; извлечение общей разметки закреплено конвенцией в `AGENTS.md`.
- Пересмотреть, если появится реально «уехавшая» копипаста в компонентах или
библиотека разрастётся на уровень композиций страниц.
### Линтер согласованности комментариев — не вводим
Описания инструментов различаются осознанно, и ловить это текстовым правилом
дороже, чем смотреть на ревью. Действенная часть выполнена вручную: все
`TODO`/`FIXME` перенесены в backlog, незарегистрированных пометок в коде нет;
новые запрещены правилом в `AGENTS.md`.
### Visual snapshots — не вводим
`toHaveScreenshot` сравнивает страницу с PNG-baseline из прошлого коммита. При
активно меняющемся визуальном языке это превращает каждый дизайн-PR в
перегенерацию и ревью картинок, а baseline платформенные (снимок с Windows не
совпадёт на `ubuntu-latest`). Потребность закрывают e2e-поведение, `lint:tokens`
с token audit, i18n-гейт и ручной чеклист. Вернуться стоит, когда визуальный
язык стабилизируется; тогда первым идёт `/kit` в двух темах, а baseline
рождаются на Linux.
### Fail-режим для unused-token аудита — не вводим
Остаётся предупреждением: `verify` входит в deploy-workflow, поэтому стилевой
долг блокировал бы деплой сайта. Кроме того, аудит видит только ссылки
`var(--x)`, и чтение токена из JS дало бы ложное срабатывание. Правило «новый
токен — вместе с потребителем» живёт в `AGENTS.md`.
### Per-project `expect.timeout` — не задаём
Firefox и WebKit отдают результат так же быстро, как Chromium: расхождения
объясняются памятью, а не скоростью движка. Таймаут под конкретную машину —
магическое число, которое придётся подстраивать на каждом раннере, поэтому
`lint`/Playwright-конфиг остаётся с дефолтами, а проблема решается лимитом
воркеров.
### Coverage: браузерные модули вне scope
`src/lib/core/domText.ts` и `src/lib/core/io.ts` (canvas, `createImageBitmap`)
исключены из coverage-гейта: в node-Vitest они не исполнимы, и их включение
превращало бы метрику в шум. `registry/*` из scope НЕ исключены — низкое
покрытие там означает непокрытую логику, а не ограничение окружения. Список
исключений с причинами — в `docs/testing-strategy.md`.
`lint:all` может завершиться успешно при warnings — это не означает чистый
baseline. Агент показывает число errors, warnings и token warnings отдельно.
## CI
@@ -165,76 +117,10 @@ Firefox и WebKit отдают результат так же быстро, ка
`.github/workflows/deploy.yml` перед production build запускает тот же
`pnpm verify`, поэтому deploy не должен проходить при ошибке быстрых проверок.
### Что и когда запускается
| Workflow | Триггер | Jobs |
| ------------------------ | --------------------------------------- | --------------------------- |
| `Deploy to GitHub Pages` | push в `main`, dispatch | `verify`, `build`, `deploy` |
| `Quality` | push в `main`, `pull_request`, dispatch | `verify`, `e2e` |
Пуш в `main` запускает оба workflow: deploy проверяет быстрый gate и собирает
сайт, `Quality` прогоняет тот же `verify` и browser matrix. Дублирование
`verify` (~1 мин) сознательное: matrix на каждый пуш стоит ~3 мин, и условные
job-и ради минуты не окупаются. Browser matrix проверяется на каждом пуше, а
ручной `gh workflow run quality.yml` нужен только для перепрогона старого
коммита.
### Проверка результатов CI после пуша
Первый реальный прогон browser matrix происходит на runner, а не на локальной
машине: локально Firefox и WebKit проходят на 1 воркере, но состав, скорость и
память Linux-раннера отличаются. Поэтому после пуша разработчика:
1. дождаться завершения workflow и посмотреть, какие job вообще запустились;
2. смотреть статусы **отдельных job**, а не общий conclusion прогона: у `e2e`
стоит `continue-on-error: true`, поэтому прогон с упавшим e2e целиком
помечается как `success` — проверено на run `36225579363` (`E2E: failure` при
`conclusion: success`);
3. если `e2e` запускался и зелёный — сообщить фактические числа passed, failed,
skipped;
4. если `e2e` не запускался, сказать это прямо: matrix на раннере не проверена,
и её можно прогнать через `gh workflow run quality.yml`;
5. красный `e2e` только из-за ресурсов раннера фиксируется как ограничение
окружения: сначала проверяется один проект и один spec, а не увеличиваются
таймауты под конкретную машину.
### Измеренная стоимость прогона
Замер на ручном запуске `quality.yml` (276 e2e-тестов, 4 проекта,
ubuntu-latest):
| Этап | Время |
| ----------------- | -------- |
| `verify` job | ~1 мин |
| `e2e` job целиком | ~3.1 мин |
| из них сами тесты | ~2.2 мин |
| весь run | ~4.2 мин |
Browser matrix стоит около трёх минут, поэтому её дешёво держать на каждом PR.
Полный прогон локально на 1 воркере — заметно дольше (~7 мин), и именно поэтому
локальные правила нагрузки описаны в `docs/agent-workflow.md`.
## Политика e2e в CI
Текущее состояние: `e2e` — non-blocking baseline, запускается на каждом пуше в
`main` и на каждом PR целиком (все четыре проекта matrix). Ночного разделения
нет: при стоимости job около трёх минут оно не окупается, поэтому новые
browser-critical спецификации просто добавляются в `browserContractTests`.
`e2e` станет blocking, когда выполнятся три условия:
1. Условия matrix пересмотрены: решено, какие спецификации обязаны идти в
`browserContractTests` и `mobileTests`, нужен ли `png-fixtures` вне Chromium
и что делать с engine-специфичными расхождениями декодеров.
2. Накопились стабильные прогоны на раннере без падений по таймингу.
3. Вместе с `continue-on-error` снимается и сам trick с общим conclusion: иначе
зелёный прогон при красном `e2e` останется возможен.
Пока условия не выполнены, `e2e` остаётся non-blocking, а его статус читается по
отдельному job.
## Локальная диагностика
При медленном полном прогоне сначала запускай минимальную команду из таблицы, а
перед ревью или merge — `pnpm verify`. После каждого шага плана результат команд
и текущий baseline фиксируются в отчёте агента.
Результат после пуша читается **по отдельным job** — процедура в
`docs/decisions.md`, раздел 10.
+52 -59
View File
@@ -1,58 +1,53 @@
# План разработки: easy-png-tools
# План развития: easy-png-tools
> **Статус:** Фаза 1 (полноценный TS-сайт) в основном выполнена — сайт живёт,
> каталог переведён в типизированный `registry` (122 текущих инструмента), новый
> дизайн стал единственным production UI на корневых маршрутах; UI-v1 удалён в
> Q3. Tech debt не закрыт полностью; текущий baseline и состав quality-проверок
> — в `docs/README.md` и `docs/quality-gates.md`. Следующий этап: SEO S1-free
> (`plan-seo.md`) + баги и продуктовые задачи из `docs/backlog.md`. Домен нужен
> для SEO (`plan-seo.md`, S1-domain). Фазы 2–8 — будущие. Фазы 9–10 спланированы
> в `docs/plan-platform.md`.
> **Статус (2026-09):** Фаза 1 (полноценный TS-сайт) в основном выполнена — сайт
> живёт, каталог переведён в типизированный `registry`, новый дизайн стал
> единственным production UI на корневых маршрутах, UI-v1 удалён. Tech debt не
> закрыт полностью; состав quality-проверок — `docs/quality-gates.md`,
> архитектура кода — `docs/architecture.md`, список инструментов —
> `docs/tools-map.md`. Текущий фокус: SEO S1-free (`docs/plan-seo.md`) +
> продуктовые задачи из `docs/backlog.md`. Домен нужен для SEO (`plan-seo.md`,
> S1-domain). Фазы 2–8 — будущие; фазы 9–10 спланированы в
> `docs/plan-platform.md`.
>
> **Нумерация фаз этого документа не совпадает с волнами разработки** (`Q*`/`C*`
> в других документах) и не используется как идентификатор задач: ссылаться на
> фазу, а не на номер волны.
## 0. Решения
- **Набор PNG-утилит, всё в браузере** (SvelteKit + `adapter-static`), данные не
покидают машину.
- **Первый и главный приоритет — полностью рабочий TS-сайт**: максимум простых
инструментов уровня EASY, UX/UI, пайплайны (несколько инструментов подряд),
описания инструментов. Сайтом можно пользоваться уже после этой фазы.
- **Набор утилит для изображений, всё в браузере** (SvelteKit +
`adapter-static`), данные не покидают машину.
- **Первый и главный приоритет — полностью рабочий сайт**: максимум простых
инструментов, UX/UI, пайплайны (несколько инструментов подряд), описания
инструментов.
- **Никакого wasm/Rust на этом этапе.** TS-реализация на canvas + `ImageData` —
эталон алгоритмов, из которого позже генерируются эталонные файлы для проверки
Rust-порта. Rust-ядро, WASM и CLI — отдельные фазы позже, когда сайт живёт.
- **Пайплайн = JSON-список шагов** `{tool, params}`. Формат закладывается сейчас
и потом станет общим для браузера, wasm и CLI.
- **Реестр инструментов** (id, title, description, схема параметров, `run`) —
источник истины: из него генерируются страницы, формы, пайплайны. Позже —
общий формат для Rust-парсера.
- **Реестр инструментов** (`web/src/lib/registry/`: id, title, description,
category, schema, `run`) — источник истины: из него генерируются страницы,
формы, пайплайны. Позже — общий формат для Rust-парсера.
- Валидация будущего Rust-порта — эталонные файлы, сгенерированные из
TS-реализации. Сравнение: **бит-в-бит** для чистых пиксельных операций,
**perceptual diff** для canvas-зависимых.
- **Единая точка исполнения инструментов.** Страницы вызывают операции только
через `web/src/lib/tools/executor.ts::executeStep` — нигде напрямую
`tool.run`. Контракт уже асинхронный (`Promise<PixelImage>`), поэтому:
через `web/src/lib/executor/` — нигде напрямую `tool.run`. Контракт уже
асинхронный, поэтому:
- когда появятся тяжёлые MEDIUM-операции (свёртки, квантование), внутренности
`executeStep` переезжают в Web Worker — воркер импортирует тот же чистый
core как TS-фолбэк, алгоритмы не дублируются, вызывающий код не меняется;
исполнителя переезжают в Web Worker — воркер импортирует тот же чистый core
как TS-фолбэк, алгоритмы не дублируются, вызывающий код не меняется;
- в WASM-фазе тот же воркер принимает wasm-модуль и диспетчеризует по `toolId`
— свитч A/B из фазы 5 сводится к замене реализации внутри исполнителя;
- до появления MEDIUM-операций воркер не вводится: EASY-операции быстры и не
блокируют UI.
## 1. Структура репозитория (этап TS)
## 1. Где что лежит
```txt
easy-png-tools/
web/ # SvelteKit (adapter-static)
src/lib/core/ # TS-ядро: Image (ImageData) + операции (без DOM)
color.ts alpha.ts geometry.ts format.ts text.ts analyze.ts
src/lib/registry.ts # реестр инструментов: id, title, description, params, run
src/lib/pipeline.ts # пайплайн: шаги, применение, localStorage, экспорт/импорт JSON
src/lib/components/ # DropZone, Preview, ParamForm (из схемы), Download, PipelineSteps
src/routes/ # маршруты-утилиты (генерируются из реестра) + /workspace
docs/
core/ # (позже) Rust-крейт
tests/ # (позже) эталонные файлы и harness
```
Актуальная карта модулей, поток данных и рецепт добавления инструмента —
`docs/architecture.md`. Список реализованных инструментов — `docs/tools-map.md`,
открытые продуктовые задачи — `docs/backlog.md`.
## 2. Фазы
@@ -64,22 +59,21 @@ clamp-семантики (в TS это делает `Uint8ClampedArray` сам).
1.1 **Каркас и UX/UI.** SvelteKit + static adapter, дизайн-система (цвета,
типографика, компоненты), общий лейаут, шапка с навигацией по категориям.
1.2 **Ядро.** Тип `Image` (обёртка над `ImageData`), загрузка/декодирование
файла, кодирование и скачивание, библиотека операций
(color/alpha/geometry/format/text/analyze). Всё без DOM-зависимостей внутри
1.2 **Ядро.** Операции над `ImageData` (загрузка/декодирование файла,
кодирование и скачивание, библиотека операций
color/alpha/geometry/format/text/analyze). Всё без DOM-зависимостей внутри
`core`.
1.3 **Реестр инструментов.** Одна запись =
`{ id, title, description, category, params: ParamDef[], run }`. Универсальный
рендерер: страница инструмента и форма параметров строятся из записи реестра
`{ id, title, description, category, schema, run }`. Универсальный рендерер:
страница инструмента и форма параметров строятся из записи реестра
автоматически; маршруты-утилиты генерируются из реестра на билде. Новый
инструмент = новая запись + функция `run`.
инструмент = новая запись + функция `run` (рецепт — `docs/architecture.md` §8).
1.4 **Пайплайн-workspace** (`/workspace`): загрузил изображение → список шагов
(инструмент + его параметры) → последовательное применение с превью каждого шага
→ скачивание финального результата. Добавление/удаление/перестановка шагов.
Сохранение пайплайнов в localStorage, экспорт/импорт JSON. Общие компоненты:
DropZone, Preview, ParamForm, Download, PipelineSteps.
1.4 **Пайплайн-workspace**: загрузил изображение → список шагов (инструмент +
его параметры) → последовательное применение с превью каждого шага → скачивание
финального результата. Добавление/удаление шагов. Сохранение пайплайнов в
localStorage, экспорт/импорт JSON.
1.5 **Массовая реализация EASY-инструментов** по категориям:
@@ -105,17 +99,19 @@ wasm.
- **Checkpoint:** в `tests/reference/<op>/` лежат `input-*.png` +
`expected-*.png`.
### Фаза 3 — Playwright-тесты
### Фаза 3 — Эталонные файлы и UI-регресс
- `tools/gen-references.ts`: грузит страницу, гоняет TS-эталон, сохраняет
> UI-регресс на Playwright уже существует и закрывает фазу 3 частично
> (`web/e2e/`, browser matrix). Недостающая часть фазы — именно эталоны.
- Скрипт генерации эталонов: грузит страницу, гоняет TS-реализацию, сохраняет
`expected-*.png`.
- Регресс: изменение TS-ядра ловится тестами.
- **Checkpoint:** регресс-тесты падают при изменении выхода эталона.
### Фаза 4 — Rust-ядро
- Порт 1:1 чистых операций (первым делом те, что покрыты эталонными файлами) с
`Image` ↔ TS `ImageData`.
`PixelImage` ↔ TS `ImageData`.
- `clamp_to_u8` с семантикой `ToUint8Clamp` (round-half-to-even, ≥255 → 255) +
юнит-тесты на краях.
- `cargo test` читает эталонные файлы напрямую, **без браузера**.
@@ -179,8 +175,8 @@ Manifest + service worker, оффлайн-режим («работает без
- **Реестр разрастается, страницы дублируются** — один универсальный рендерер
страниц/форм из записей реестра; инструмент = данные + `run`.
- **Семантика пайплайна** — зафиксировать: каждый шаг применяется к результату
предыдущего; единый тип `Image` на всём пути; схема параметров на шаге.
- **Семантика пайплайна** — зафиксирована: каждый шаг применяется к результату
предыдущего; единый тип `PixelImage` на всём пути; схема параметров на шаге.
- **Canvas-операции не бит-в-бит** — класс perceptual закладывается на Фазе 2–3,
а не в конце.
- **WASM + Vite** — спайк на Фазе 5 до массовой интеграции (риск отложен
@@ -192,10 +188,7 @@ Manifest + service worker, оффлайн-режим («работает без
## 5. Команды верификации
```bash
pnpm dev # разработка (Фаза 1+, обёртка `pnpm --dir web dev`)
pnpm build # статический экспорт (обёртка `pnpm --dir web build`)
pnpm --dir web exec playwright test # эталоны + UI-регресс (Фаза 3+)
cargo test # Rust-ядро против эталонных файлов (Фаза 4+)
harness compare tests/reference # сквозная сверка native + wasm (Фаза 7)
```
Текущие команды и матрица «изменение → проверки» — `docs/quality-gates.md`
(`pnpm verify` — быстрый gate текущего этапа). Для будущих фаз добавляются
`cargo test` (Rust-ядро против эталонных файлов, фаза 4+) и
`harness compare tests/reference` (сквозная сверка native + wasm, фаза 7).
+14 -60
View File
@@ -68,24 +68,8 @@ browser-контракт: critical CRC и truncated отклоняются дв
`src/**/*.test.ts`; DOM, hydration и пользовательские потоки — в Playwright.
Component-тесты не дублируют e2e-контракт.
### Что попадает в coverage
Coverage-гейт считает только `src/lib` и только то, что node-Vitest способен
исполнить. Исключения в `web/vitest.config.ts` и их причины:
| Исключение | Причина |
| ------------------------------------------- | ------------------------------------------- |
| `**/*.test.ts`, `**/test-helpers.ts` | сами тесты |
| `**/*.svelte.ts` | runes, не работают в node |
| `executor/executor.worker.ts` | entry worker, исполняется только в браузере |
| `components/define.ts` | type-only, нет исполняемого кода |
| `core/domText.ts`, `core/io.ts` | canvas, `createImageBitmap`, нужен браузер |
| `i18n/en.ts`, `i18n/ru.ts`, `tool-icons.ts` | данные, а не логика |
`registry/*` в scope остаётся: низкое покрытие там — это непокрытая логика, а не
следствие окружения. Исключение браузерных модулей означает, что переход
браузерного кода в scope — отдельное решение, связанное с DOM-тестами или visual
snapshots, а не молчаливое расширение списка исключений.
Состав и причины исключений из coverage-гейта, а также пороги — в
`docs/quality-gates.md`; здесь они не дублируются.
## 5. Helpers и `test.fixme`
@@ -99,7 +83,7 @@ text — `renderText` + результат, generator — auto-run без upload
`test.fixme` допускается только для зарегистрированной проблемы в backlog или
checklist. В комментарии указывается ссылка на неё. После исправления тест и
ссылка обновляются вместе; в текущем Q6e baseline активных fixme нет.
ссылка обновляются вместе.
Если тест сравнивает два состояния одного элемента, он ждёт изменения значения
(`not.toHaveText`, `not.toHaveAttribute`), а не его видимости. Пересчёт идёт
@@ -108,21 +92,14 @@ checklist. В комментарии указывается ссылка на н
## 6. Проверки
Для изменений e2e выполняются:
```text
pnpm --dir web check
pnpm --dir web test:rules
pnpm --dir web lint:all
pnpm --dir web format:check
pnpm check:docs
pnpm --dir web test:e2e
pnpm verify
```
Матрица «изменение → минимальный набор проверок» и состав `pnpm verify` — в
`docs/quality-gates.md`. Для изменений e2e дополнительно уместны
`pnpm --dir web check` и `pnpm --dir web format:check`.
`verify` не включает Playwright: E2E остаётся отдельным тяжёлым gate. В CI
non-blocking E2E не считается заменой локальному smoke; перед ревью фиксируются
число passed, skipped и известные fixme.
число passed, skipped и известные fixme. Статус e2e в CI — `docs/decisions.md`,
раздел 9.
## 7. Browser matrix
@@ -148,32 +125,9 @@ non-blocking E2E не считается заменой локальному smo
| `idat-cut.png` | Chromium |
| ancillary CRC, tail, palette, 16-bit | ни один движок |
- Firefox и WebKit работают с теми же таймаутами, что и Chromium: движок не
причина расхождений, поэтому per-project `expect.timeout` не задаётся.
- `workers` — 1 локально и 2 в CI. Playwright не умеет per-project лимит
воркеров, а browser matrix ограничена памятью, а не CPU: при высокой
параллельности браузеры на машине с 16 GB RAM не успевают поднять worker, и
тесты падают без ошибок. Локальный matrix поэтому медленный, но не нагружает
компьютер.
- Локальный baseline: 276 тестов, 276 passed, 0 skipped, 0 failed.
- Coverage ограничена `src/lib` и не включает браузерные модули — см. раздел 4.
## 8. Visual snapshots
Сознательно не используются. `toHaveScreenshot` сравнивает страницу с
PNG-baseline из прошлого коммита, а не с замыслом дизайна, поэтому:
- визуальный язык в активной работе означает перегенерацию и ревью картинок в
каждом дизайн-изменяющем PR;
- baseline привязан к платформе (ОС, шрифты, рендеринг): снимок с Windows не
совпадёт на `ubuntu-latest`, и baseline должны рождаться на Linux;
- четыре проекта matrix плюс тема и viewport дают нелинейный рост числа файлов.
Потребность закрывают e2e-поведение, `lint:tokens` с token audit, i18n-гейт и
ручной чеклист `docs/checklist-manual-testing.md`; «элемент визуально наехал»
ловится чеклистом и ревью.
Возвращаться к snapshots стоит, когда визуальный язык стабилизируется, изменения
станут редкими, а чеклист перестанет справляться. Тогда первым идёт `/kit` в
двух темах, а baseline генерируются на Linux (docker-образ Playwright или
отдельный CI-job).
- `workers` — 1 локально и 2 в CI: Playwright не умеет per-project лимит
воркеров, а browser matrix ограничена памятью, а не CPU. Правила нагрузки на
машину и порядок диагностики флаков — `docs/agent-workflow.md`, раздел 3;
обоснование отсутствия per-project таймаутов — `docs/decisions.md`, раздел 7.
- Visual snapshots и per-project таймауты сознательно не используются —
`docs/decisions.md`, разделы 5 и 7.
+6
View File
@@ -1,5 +1,11 @@
# Аудит использования - UX\UI
> Статус: **active**. Сырые наблюдения по инструментам и полям — вход для
> `docs/plan-fields-audit-fixes.md`, который разбивает их на волны правок, и для
> `docs/backlog.md`. Здесь фиксируется, **что замечено**, а не порядок работы.
> Ids инструментов сверять с `web/src/lib/registry/` — исторические названия
> (`webp-to-png`, `color-wheel-generator`, `png-info`) в реестре отсутствуют.
## везде
- возможность переключать пиксели\проценты
+80 -73
View File
@@ -1,40 +1,53 @@
# Карта инструментов: реализовано · добавить · идеи
> Живой документ для сверки с каталогом. Формат записи:
> `id / название — параметры через запятую`. Источник сравнения:
> onlinepngtools.com (311 уникальных; 107 из них — нишевые клоны серий
> Logo*/Icon*/Stamp*/Signature* поверх обычных операций).
> Статус: **active**. Живой документ для сверки с каталогом.
>
> **Источник правды по id — реестр** `web/src/lib/registry/`. Этот список —
> навигационный: сверять перед использованием. Формат записи:
> `id — параметры через запятую`. Сравнение с каталогом источника (объём,
> нишевые клоны серий Logo*/Icon*/Stamp*/Signature*) — замер в
> `docs/plan-seo.md`, §2; здесь он не дублируется.
>
> Количество инструментов здесь не фиксируется — оно выводится из раздела 1 и из
> `TOOLS` в `registry/index.ts`, и дублировать его цифрой значит заставлять себя
> обновлять документ при каждом добавлении (`AGENTS.md`).
---
## 1. Реализовано
### Конвертация
Ids приведены к фактическим в `web/src/lib/registry/`.
- jpg-to-png — без параметров
- webp-to-png — без параметров
- gif-to-png — первый кадр
- bmp-to-png — без параметров
- ico-to-png — выбор размера кадра
**Формат строки (проверяется тестом
`web/src/lib/registry/tools-map-doc.test.ts`):** в буллите всё **до первого
тире** `—` — это id инструментов через `/` или `,`; всё после тире — свободное
описание параметров и не разбирается. Второй id нельзя оставлять после тире,
иначе он не попадёт в разбор и инструмент будет считаться неописанным.
### Конвертация (`convert.ts`)
Отдельных «из формата в PNG» инструментов **нет**: входной формат выбирается
загрузкой файла, любой инструмент принимает PNG/JPG/WebP/GIF/BMP. Все конвертеры
исходят из PNG.
- svg-to-png — width результата (текстовый вход: SVG-код)
- png-to-bmp — 24-бит, фон вместо альфы
- convert-png-to-jpg — background, quality
- convert-png-to-webp — quality
- svg-to-png — width результата
- png-to-base64 / base64-to-png — строка
- png-to-data-uri / data-uri-to-png — строка
- png-to-hex — rrggbbaa по строкам / hex-to-png — tokens + width
- png-to-bytes — десятичные RGBA-байты по строкам / bytes-to-png — tokens +
- png-to-hex / hex-to-png — rrggbbaa по строкам / tokens + width
- png-to-bytes / bytes-to-png — десятичные RGBA-байты по строкам / tokens +
width
- png-to-rgb-values — rgba(r,g,b,a) по пикселям / rgb-values-to-png — числа +
- png-to-rgb-values / rgb-values-to-png — rgba(r,g,b,a) по пикселям / числа +
width
- svg-to-png — width результата
- verify-is-png — текстовый источник (base64/data-uri), вердикт по сигнатуре
### Прозрачность
### Прозрачность (`alpha.ts`)
- change-png-opacity — percent
- set-alpha-channel-png — percent
- remove-alpha-channel-png — без параметров
- remove-alpha-channel-png — без параметров (фон `#ffffff` захардкожен, см.
`docs/plan-fields-audit-fixes.md` D1)
- extract-alpha-mask-png — без параметров
- invert-alpha-png — без параметров
- remove-background-png — color, tolerance, outerOnly, smooth
@@ -45,8 +58,6 @@
- harden-alpha-png — threshold
- feather-edges-png — radius (размытие только альфы)
- clean-edges-png — radius (defringe: RGB от ближайшего непрозрачного)
- watermark-image-png — вторая картинка-знак (загружается на странице), scale,
opacity, position, margin
- despeckle-alpha-png / close-holes-png — radius
- center-by-alpha-png — без параметров
- round-corners-png — radius
@@ -55,7 +66,7 @@
- star-mask-png — points, innerRadius, size, rotation, offsetX, offsetY
- wavy-mask-png — size, amplitude, waves, phase, offsetX, offsetY
### Цвет
### Цвет (`color.ts`)
- grayscale-png / invert-colors-png / sepia-png / auto-contrast-png — без
параметров
@@ -69,23 +80,17 @@
- temperature-png — percent
- gamma-png — value
- tint-png — color, strength
### Квантование и палитры
- quantize-png — colors (k, median-cut)
- decrease-color-count-png — maxColors (пресеты 2…256)
- custom-palette-png — colors (hex через запятую, ближайший цвет)
- dithering-png — colors (k), pattern (Floyd–Steinberg / Bayer 4×4)
### Разложение каналов
### Разложение каналов (`color.ts`)
- png-to-hsl / png-to-hsv / png-to-hsi — component (h/s/l и т.п.), display (gray
| space-as-rgb)
- png-to-cmyk — component (c/m/y/k), display
- png-to-ycbcr — component (y/cb/cr), display
- png-to-lab — component (l/a/b), display
- png-to-hsl, png-to-hsv, png-to-hsi, png-to-cmyk, png-to-ycbcr, png-to-lab —
component (h/s/l и т.п.), display (`gray` | `space-as-rgb`)
### Геометрия
### Геометрия (`geometry.ts`)
- resize-png — width/height (размеры source по умолчанию; 0=авто для одной
стороны), keepAspect
@@ -107,7 +112,7 @@
- swap-orientation-png — target (portrait/landscape)
- symmetric-copy-png — axis, keepSide
### Фильтры
### Фильтры (`filters.ts`)
- blur-png — radius
- sharpen-png — strength
@@ -118,12 +123,14 @@
- add-noise-png — amount, mode (mono/color), seed
- silhouette-png — color, threshold
### Анализ
### Анализ и вердикты (`analyze.ts`)
- png-info — размеры, альфа, число цветов
- png-file-size — без параметров (DOM-инструмент: кодирует в PNG и отдаёт
размер)
- png-is-transparent / png-is-grayscale / png-orientation — текстовый вердикт
- verify-is-png — текстовый источник (base64/data-uri), вердикт по сигнатуре
### Маски по свойствам пикселей
### Маски по свойствам пикселей (`analyze.ts`)
- show-transparent-png — color, opacity (подсветка прозрачных/полупрозрачных)
- show-grayscale-pixels-png — tolerance, mode (binary/highlight),
@@ -136,21 +143,29 @@
- extract-color-from-png — color, tolerance (оставить близкие, остальное
прозрачным)
### Генерация
### Генерация (`generate.ts`)
- create-empty-png — width, height, transparent, color
- single-color-png — width, height, color
- random-noise-png — width, height, seed
- linear-gradient-png — width, height, fromColor, toColor, direction
- text-to-png — text, fontSize, font, bold, color, transparentBg,
backgroundColor, padding
- emoji-to-png — emoji, size
- placeholder-png — width, height, backgroundColor, color, showText
- color-spectrum-png — width, height, direction, saturation, lightness
- random-colors-png — width, height, blockSize, seed
- draw-grid-png — width, height, cols, rows, lineWidth, color, transparentBg
- placeholder-png — width, height, backgroundColor, color, showText
- blend-two-png — pair (from/to), width
- step-colors-png — pair (from/to), steps, width
- emoji-to-png — emoji, size
- text-to-png — text, fontSize, font, bold, color, transparentBg,
backgroundColor, padding
- color-wheel-png — size, lightness
- complementary-png / triadic-png / tetradic-png / analogous-png /
monochromatic-png / shades-png — общая база палитры: baseColor, width, layout;
`shades-png` дополнительно count, depth
- mix-colors-png — colors, width
- sort-colors-png — colors, порядок (hue/brightness/saturation), width
### Текст
### Текст (`text.ts`)
- add-text-png — text, fontSize, color, font, bold, position (3×3), margin,
plate, plateColor, plateOpacity
@@ -159,28 +174,16 @@
- watermark-tile-png — text, fontSize, color, opacity, angle, stepX, stepY,
font, bold
### Палитры и цветовые утилиты
- color-wheel-generator — size, кольца/сектора, показ hex при клике (у нас —
статичный свотч-полотно)
- complementary-palette — baseColor
- monochromatic-palette — baseColor, count
- analogous-palette — baseColor, spread
- triadic-palette — baseColor
- tetradic-palette — baseColor
- similar-shades-palette — baseColor, count, range
- sort-colors — источник (палитра-картинка или список), порядок (hsl/luma)
- mix-colors — colors[], веса?
- average-color — colors[]; blend-two — a, b, steps; step-between — a, b, steps
(три частных случая одного движка)
---
## 2. Можно добавить — из onlinepngtools
Всё из этого списка **в реестре отсутствует**.
### Разложение каналов — остаток
- separate-colors — minShare слоя (MEDIUM, мультифайловый вывод → пока идея)
- separate-colors — minShare слоя (MEDIUM, мультифайловый вывод → см. идеи в
`docs/backlog.md`)
### Края и силуэт — остаток
@@ -197,8 +200,6 @@
- sort-pixels — blockSize, ключ (яркость/канал), направление (MEDIUM)
- slow-reveal / fade-in / fade-out / disappear — анимационные (→ идеи)
### Сжатие и качество — реализовано (см. Конвертация: compress, reduce-to-size; jpeg-artifacts; W8 quantize)
### Генераторы — остаток
- multi-color-gradient — список стопов (нужен новый тип параметра → MEDIUM-UI)
@@ -206,31 +207,37 @@
### Конвертеры — остаток
- png-to-gif — MEDIUM (однокадровый GIF-энкодер руками)
- gif-to-frames — MEDIUM (мультифайловый вывод → идея)
- gif-to-frames — MEDIUM (мультифайловый вывод → идеи)
- change-bit-depth — MEDIUM (пересборка PNG)
### Прочее единичное
### Сжатие и качество
- pick-a-color — пипетка уже есть в превью; отдельная страница не планируется
(покрыто)
- preview/test на цветных фонах — фича превью, не инструмент (решить позже)
- extract-barcode — HARD, вне планов
- compress, reduce-to-size, pick-a-color, extract-barcode — **не реализованы**;
заведены в `docs/backlog.md` (reduce-to-size зависит от UX-райза «Download»).
Пипетка для выбора цвета уже есть в превью, отдельная страница не планируется.
Извлечение штрихкодов — HARD, вне планов.
---
## 3. Идеи на рассмотрение (нужна архитектура или спорная ценность)
- **Region-инструменты** — требуется UI выделения прямоугольника/ластика на
превью: censor-region, erase-region, pixelate-area, blur-area, sharpen-area,
reverse-colors-area. Один раз делаем selection-компонент — получаем сразу
шесть инструментов.
- **Мультифайловый вывод** — механизм «результат = набор файлов» (zip)
реализован Остальные 1→many: gif-to-frames, separate-colors,
multiply-grid-as-files — подключаются по мере нужды.
Развёрнутые описания этих идей живут в `docs/backlog.md`, раздел «Идеи»; здесь
только список.
- **Region-инструменты** — нужен selection-компонент на превью: censor-region,
erase-region, pixelate-area, blur-area, sharpen-area, reverse-colors-area.
Один раз делаем selection — получаем шесть инструментов.
- **Мультифайловый выход** — механизм «результат = набор файлов» (zip)
реализован на `split-into-parts-png`. Остальные 1→many: gif-to-frames,
separate-colors, multiply-grid-as-files — подключаются по мере нужды.
- **Анимационные эффекты** — slow-reveal, fade-in/out, disappearing, scrolling:
это видео/GIF на выходе, а не PNG. Отдельное решение о формате результата.
- **HARD-хвост** — glitch-art, extract-signature, handwritten→digital,
extract-barcode.
- **Нишевые серии** (logo/icon/stamp/signature — 107 клонов у источника) —
- **Нишевые серии** (logo/icon/stamp/signature — замер в шапке документа) —
сознательно не копируем: это обычные операции над конкретным контентом, у нас
они доступны через базовые инструменты + цепочки.
- **Входные «из формата в PNG»** (jpg-to-png, webp-to-png) сознательно не
заведены: загрузка файла уже принимает любой поддержанный формат, а
инструмент-конвертер добавил бы лишнее звено цепочки. Отдельные страницы под
эти интенты — предмет решения в `docs/plan-seo.md` §5.