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
+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-части.
- Несколько параллельных инструментов на главной одновременно — один активный
блок за раз.