Compare commits

..
1 Commits
Author SHA1 Message Date
Ku6epXBOCTuK 299586fa87 feat: add redirect html 2026-10-13 08:05:16 +05:00
372 changed files with 64 additions and 47245 deletions
-64
View File
@@ -1,64 +0,0 @@
name: Deploy to GitHub Pages
on:
push:
branches:
- main
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: pages
cancel-in-progress: true
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: ^11.20.0
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 24
cache: pnpm
cache-dependency-path: web/pnpm-lock.yaml
- name: Configure Pages
uses: actions/configure-pages@v5
- name: Install dependencies
working-directory: web
run: pnpm install --frozen-lockfile
- name: Build
working-directory: web
run: pnpm build
env:
BASE_PATH: /easy-png-tools
- name: Upload Pages artifact
uses: actions/upload-pages-artifact@v3
with:
path: web/build
deploy:
needs: build
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Deploy
id: deployment
uses: actions/deploy-pages@v4
-15
View File
@@ -1,15 +0,0 @@
node_modules
# Reference source (legacy Next.js design in refs/): branding icons are not needed
refs/public/icon.svg
refs/public/icon-dark-32x32.png
refs/public/icon-light-32x32.png
refs/public/apple-icon.png
refs/public/placeholder-logo.svg
refs/public/placeholder-logo.png
refs/public/placeholder.svg
web/audit/cdp-audit*.txt
css-lint.txt
# Локальные планы с приватной информацией (выбранное имя домена) — не коммитить
docs/plan-domain.local.md
-7
View File
@@ -1,7 +0,0 @@
# Точечные исключения, без allowlist-`/*`: корневой `/*` глушил формат-он-сейв
# (весь `web/**` резолвился как ignored, дочерние конфиги не успевали примениться).
# Дочерние каталоги управляются своими конфигами/игнорами (web/, refs/).
node_modules
.git
refs-html
pnpm-lock.yaml
-3
View File
@@ -1,3 +0,0 @@
{
"proseWrap": "always"
}
-63
View File
@@ -1,63 +0,0 @@
# AGENTS.md
Инструкции для агентов (и IDE-ассистентов) по работе с этим репозиторием.
## Стек
- **SvelteKit** (`web/`) — основное приложение, Svelte 5 (runes).
- Менеджер пакетов: **pnpm**. Запускать команды из `web/`, либо через обёртки в
корне: `pnpm build`, `pnpm dev` (это `pnpm --dir web ...`). Не использовать
npm.
- Сборка: `pnpm --dir web build`, проверка типов:
`pnpm --dir web exec svelte-check --tsconfig ./tsconfig.json`, тесты:
`pnpm --dir web test` (Vitest), e2e: `pnpm --dir web test:e2e` (Playwright,
`web/e2e/`, свой webServer на `pnpm build` + `scripts/serve-static.mjs`, порт
4173).
- Форматирование: `pnpm format` в корне (оба прогона) или
`pnpm --dir web format` (только код web/). Проверка: `pnpm check:docs`
(docs) + `pnpm --dir web exec prettier --check .` (код).
- Линтинг: `pnpm --dir web lint` (ESLint). Полный прогон дизайн-проверок:
`pnpm --dir web lint:all` (ESLint + stylelint + токен-аудит). Каждый шаг
выполняется, даже если предыдущий упал. Текущие «ожидаемые долги» (hardcoded
размеры в kit до C19, не-Prefix токены в `app_v1.css`) — техдолг: чинить
только по заведённым tasks, не игнорировать правилом.
## Правила кода
Синтаксические ограничения описывать не нужно — они проверяются линтером
(весь список правил и токены-префиксы в `web/eslint-plugins/README.md`,
прогон: `pnpm --dir web lint` / `pnpm --dir web lint:all`). Здесь — только то,
что линтер не умеет.
### Svelte 5
Типизация пропсов через локальный `interface Props` закреплена правилом
`conventions/interface-props`; руками его нигде дублировать не надо.
### Сначала искать
Новая константа или тип не заводится, пока не произведён поиск существующего
определения (по ключу в соседних модулях и по всему `web/src/`). Это правило
в первую очередь для ИИ-агентов: дублирующее определение ухудшает правки в
нескольких местах и запутывает. (Синтаксис самих определений — под линтером:
строка `conventions/no-string-union-alias` и т.д.)
### Дизайн
Описание нового визуального языка — в `docs/plan-redesign.md`. Общие правила,
которые линтер не проверяет:
- Новый дизайн живёт в `web/src/app.css` (корневые маршруты), старый — в
`web/src/app_v1.css` (маршруты `/v1/*`).
- Все повторяющиеся визуальные элементы — отдельные компоненты в
`web/src/lib/components/`, даже «просто div с двумя стилями».
- Дизайн-токены (цвета, размеры, длительности, z-index) — только из
CSS-переменных; детали ограничений — в `web/eslint-plugins/README.md`.
### Изоляция веток old ↔ new
Старый (`v1/`) и новый UI изолированы: `isolation/no-mixed-imports` резолвит
каждый импорт до файла и запрещает смешивание.
- Trunk-based: коммиты делает разработчик после ревью, самому не коммитить.
Изменения делать небольшими (< ~500 строк), атомарными.
-21
View File
@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2026 Ku6epXBOCTuK
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+4 -89
View File
@@ -1,92 +1,7 @@
# easy-png-tools # Project renamed
Browser-based PNG utility toolkit — no server, no data upload, everything runs locally. "Easy png tools" now named "Easy image tools" and moved to a new domain.
Inspired by online services like onlinepngtools.com, but fully offline-capable and open source. [New repository link](https://github.com/Ku6epXBOCTuK/easy-image-tools)
## Features [New website link](https://easy-image-tools.com)
- **Pixel-level image processing** — format conversion, transparency/alpha channel, color transformations, geometry, filters/convolutions, morphology, quantization & palettes, image generation, text/watermarks, analysis & checks
- **Pipeline system (workspace)** — load image → chain multiple tool steps → sequential application → download result; pipelines saved to localStorage, exportable as JSON
- **Web Worker execution** — heavy operations run off the main thread with automatic fallback to direct calls
- **i18n** — full Russian and English localization with search matching
- **Dark/Light themes** — persisted to localStorage
- **Pure TypeScript core** — `lib/core/` operates on `ImageData`/`Uint8ClampedArray` with no DOM dependency
- **~30+ tools** across categories: transparency, color, geometry, filters, morphology, palettes, generation, text, analysis
## Tech Stack
- **SvelteKit** (Svelte 5, runes mode) with static adapter — pure static export, no server
- **Vite** + **TypeScript** (strict)
- **Vitest** for unit tests
- **ESLint** + **Prettier**
- **pnpm** (required, ^11.20.0)
- **Lucide** icons, **IBM Plex** fonts (self-hosted)
## Getting Started
### Prerequisites
- [pnpm](https://pnpm.io/) v11.20+
### Install & Run
```bash
pnpm install
pnpm dev
```
### Build
```bash
pnpm build
```
Static output is written to `web/build/`.
### Other Commands
| Command | Description |
| ------------------------ | ------------------------- |
| `pnpm --dir web test` | Run unit tests (Vitest) |
| `pnpm --dir web check` | Type-check (svelte-check) |
| `pnpm --dir web lint` | Lint (ESLint) |
| `pnpm --dir web format` | Format code (Prettier) |
| `pnpm format:docs` | Format docs (Prettier) |
| `pnpm --dir web preview` | Preview production build |
## Project Structure
```txt
easy-png-tools/
├── web/ # Main SvelteKit application
│ ├── src/
│ │ ├── lib/
│ │ │ ├── core/ # Pure TS image processing (ImageData-based)
│ │ │ ├── components/ # UI components
│ │ │ ├── tools/ # Pipeline, executor, overlay store
│ │ │ ├── i18n/ # Localization (ru/en)
│ │ │ └── registry.ts # Tool registry (source of truth)
│ │ └── routes/ # SvelteKit routes
│ └── build/ # Static export output
├── docs/ # Architecture docs, roadmap, plans
└── refs/ # Design reference (Next.js, not part of runtime)
```
## Architecture
The project is built around a **tool registry** (`registry.ts`) — each tool is a self-contained entry with `{id, title, description, category, params, run}`. Pages, forms, and pipelines are generated from this registry.
Core image processing lives in `lib/core/` and operates directly on `ImageData` objects with no browser API dependencies, making it portable to other environments (WASM, CLI) in the future.
## Roadmap
See [docs/roadmap.md](docs/roadmap.md) for the full development plan. Current phases:
1. **Phase 1** — TypeScript core + web UI (in progress)
2. **Phase 2** — Rust/WASM core for performance-critical operations
3. **Phase 3** — CLI tool
## License
[MIT](LICENSE)
-308
View File
@@ -1,308 +0,0 @@
# Анализ: набор PNG-утилит для браузера
Проект: **easy-png-tools** — набор PNG-утилит, работающий полностью в браузере.
Вдохновлён идеей онлайн-сервисов «все PNG-операции в одном месте»: никаких
загрузок на сервер, всё считается локально. Стек: **SvelteKit +
@sveltejs/adapter-static** (статический экспорт, никакого сервера). Все данные
обрабатываются на клиенте, ничего не уходит в сеть.
Источник идеи: типовой перечень утилит, который встречается в онлайн-наборах
PNG-инструментов (~296 операций).
---
## 1. Ключевые технологии браузера
| Задача | Средство |
| ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| Декодирование изображений | `<img>` / `createImageBitmap()` / `ImageDecoder` — браузер нативно декодирует PNG, JPEG, WebP, GIF, BMP, ICO, AVIF |
| Кодирование PNG/JPEG/WebP | `canvas.toBlob()` / `toDataURL()` — нативные кодировщики |
| Пиксельная обработка | `ImageData` + `Uint8ClampedArray` (RGBA), read-only canvas |
| Чтение/анализ PNG-структуры | Своя мини-библиотека чанков (IHDR/PLTE/tRNS) или `pngjs` |
| Сжатие/оптимизация PNG на байтовом уровне | `UPNG.js` (JS) либо `pngquant`/`oxipng` (WASM) |
| GIF-анимации (fade/reveal и т.п.) | `gifenc` / `gif.js` (JS), APNG — `UPNG` |
| Кодирование GIF/BMP на байтовом уровне | `gifenc`, простой ручной BMP-энкодер |
| Тяжёлая математика без блокировки UI | Web Workers, SharedArrayBuffer, WASM (zlib, квантование, ML) |
| Экономия памяти при больших изображениях | `ImageBitmap`, работа тайлами, downscale до декорирования |
**Вывод для архитектуры:** ~90% утилит — это одна и та же связка «декодировать →
прогнать через пиксельную функцию → закодировать». Разумно сделать общий ядро
(`core/pipeline.ts` + набор операций над `ImageData`) и тонькие страницы-обёртки
в SvelteKit. Каждая утилита = отдельный маршрут со своим UI (drag&drop слева,
превью, параметры, скачивание справа).
---
## 2. Уровень EASY — canvas + ImageData, простые попиксельные операции и композиция
Реализация: загрузка в canvas → `getImageData()` → цикл по пикселям /
перекомпоновка / отрисовка на выходной canvas → `toBlob('image/png')`. Без
сложных алгоритмов, каждая утилита — часы работы.
### 2.1 Конвертация форматов и текстовые представления
- **convert-png-to-jpg / convert-jpg-to-png / convert-webp-to-png /
convert-png-to-webp / convert-svg-to-png** — рисование на canvas + `toBlob()`
нужного типа
- **convert-bmp-to-png / convert-ico-to-png / convert-gif-to-png** (первый кадр)
— нативное декодирование браузером
- **convert-png-to-bmp** — BMP-формат простой, ~30 строк энкодера
- **convert-png-to-base64 / convert-base64-to-png / data-uri (в обе стороны)** —
`FileReader.readAsDataURL` + строка
- **convert-png-to-bytes / convert-bytes-to-png / convert-png-to-hex /
convert-hex-to-png / convert-png-to-rgb-values / convert-rgb-values-to-png** —
чтение/запись `ImageData` в текст
- **convert-png-to-gif** (один кадр) — `gifenc`, тривиально
### 2.2 Прозрачность и альфа-канал
- **create-transparent-png, remove-color-from-png(простая версия),
transparent-logo-maker, remove-logo/icon-background (по цвету)** — замена
цвета на прозрачность по порогу близости
- **remove-alpha-channel-from-png, fill-alpha-channel-in-png,
replace-alpha-channel-in-png, extract-alpha-channel-from-png,
invert-png-alpha-channel** — манипуляции только с A-компонентой
- **generate-png-alpha-mask, show-transparent-png-areas,
fill-transparent-png-pixels, convert-semi-transparent-png-to-opaque** — пороги
и заливки по альфе
- **harden-png-edges** — бинаризация альфы по порогу
### 2.3 Цвет: замена, тон, каналы, цветовые пространства
- **change-png-color, change-png-color-tone, change-png-opacity,
swap-png-colors, change-png-brightness, change-png-contrast,
convert-png-to-grayscale, add-sepia-tone-to-png, change-png-hue,
invert-png-colors, reverse-png-colors, convert-png-to-two-colors,
convert-png-to-black-and-white, create-monochrome-png,
generate-single-color-png** — попиксельные арифметики/преобразования
- **extract-png-color-channels, swap-rgba-color-channels,
split-png-into-rgb-components** — перестановка/выделение RGBA-каналов
- **convert-png-to-hsl/hsv/hsi/cmyk/ycbcr/lab-colors** — известные формулы
конвертации (Lab — чуть больше математики, но детерминировано)
- **generate-\*-color-palette
(complementary/monochromatic/analogous/triadic/tetradic),
generate-similar-color-shades, color-wheel-generator, mix-multiple-colors,
calculate-average-color, simulate-paint-colors, blend-two-colors,
step-between-two-colors, convert-color-names-to-png** — генераторы
палитр/смешение без работы с изображением
- **posterize-png, create-color-spectrum-png, create-multi-color-gradient-png,
generate-png-gradient-palette** — простые градиенты/квантование уровней
### 2.4 Геометрия, композиция, холст
- **resize-png, zoom-png, crop-png, cut-png, rotate-png, skew-png, shift-png,
change-png-canvas-size, change-png-aspect-ratio, fit-png-in-rectangle,
convert-landscape/portrait-png-to-portrait/landscape-png** — операции через
`CanvasRenderingContext2D` (`drawImage`, `rotate`, `setTransform`)
- **flip-png-horizontally/vertically, mirror-png, reverse-png** —
отражение/поворот на 180°
- **center-png, center-logo, center-icon** — центровка по bounding box
альфа-канала
- **duplicate-png, clone-png, multiply-png, create-png-tile,
create-symmetric-png, split-png-into-parts** — тайлинг/копирование
- **create-round-png, create-square-png, create-star-shaped-png,
create-wavy-shaped-png, round-png-corners** — маски-«штампы» на альфе
- **add-png-border, add-padding-to-png, add-space-around-png,
remove-padding-from-png, remove-space-around-png, remove-png-padding** —
расширение/усадка холста
- **add-png-background, add-signature/logo/icon/stamp-background** — подложка
под прозрачность
### 2.5 Текст, рамки, простые эффекты
- **add-text-to-png, convert-text-to-png, create-emoji-png, add-png-watermark,
wrap-png-in-polaroid, create-logo-from-text, create-icon-from-text/emoji,
create-custom-stamp, create-\*-stamp
(round/rect/green/red/number/date-time/monogram/from-text/emoji),
create-placeholder-png, create-empty-png, create-custom-png,
generate-colorful-png, generate-random-png, generate-white-noise-png,
generate-1x1-png, draw-png-grid** — рисование примитивов/случайных данных
- **add-noise-to-png, create-glitch-png, pixelate-png, censor-png,
erase-part-of-png, randomize-png-pixels** — шум/порча байтов/блоки/кисть
- **add-shadow-to-png** — смещение + лёгкое размытие + композиция
- **add-artifacts-to-png** — простой вариант: JPEG-раундтрип через canvas
### 2.6 Анализ и проверки
- **analyze-png, find-png-file-size, find-png-dimensions,
check-if-png-is-landscape-or-portrait, find-png-color-count,
check-if-png-is-grayscale, check-if-png-is-transparent,
verify-if-image-is-png, pick-png-color, pick-logo/icon-color, png-viewer,
test-png, preview-png-on-colorful-background,
test-png-on-multiple-backgrounds** — чтение заголовков/ImageData/гистограмм,
«пипетка» по клику
- **sort-colors** — сортировка списка палитры (не изображения)
### 2.7 Семейства signature/logo/icon/stamp — простые вариации
Почти все
«-maker/-color/-resize/-crop/-rotate/-bg/-space/-padding/-grayscale/-single-color/-white/-black/-semi-transparent/-shadow»
для signature/logo/icon/stamp — это переиспользование операций из разделов выше
с одним цветом/боксом. Отдельно перечислены только те, что требуют алгоритмов
(см. MEDIUM/HARD).
> Примерный объём: **≈180–190 утилит уровня EASY.**
---
## 3. Уровень MEDIUM — нужен настоящий алгоритм (свёртки, морфология, квантование, анимация)
Реализация: честные алгоритмы на `ImageData`, часто в Web Worker, при
необходимости — обвязка вокруг готовых JS-библиотек.
### 3.1 Фильтры и свёртки
- **blur-png, sharpen-png** — box/gaussian blur, ядро свёртки (упрощённо можно
`ctx.filter`, но качественно — своя свёртка)
- **refine-png-edges, feather-png-edges, clean-png-edges** — работа с
полупрозрачными краями: размытие альфы, де-контуринг, suppression
полупрозрачности
- **add-glow-effect-to-png** — расширение альфы + размытие + цветное наложение
### 3.2 Морфология и контуры (операции над альфа-маской)
- **add-stroke-to-png, add-outline-to-png, remove-stroke-from-png,
remove-outline-from-png, find-png-outline** — dilation/erosion по альфе,
извлечение контура
- **make-signature-thicker / make-signature-thinner, make-icon-thicker /
make-icon-thinner** — dilation/erosion
- **convert-outlined-icon-to-filled-icon** — flood-fill замкнутых областей;
**convert-filled-icon-to-outlined-icon** — эрозия
- **remove-png-border, remove-border-around-icon, remove-stamp-border** — детект
однотонной/декоративной рамки и вырезание
### 3.3 Квантование и цветовая кластеризация
- **quantize-png, decrease-png-color-count, set-custom-png-color-palette,
add-dithering-to-png** — median cut / octree / Wu + Floyd–Steinberg; готовые
JS-библиотеки (image-q и т.п.)
- **change-png-quality, create-low-quality-png** — симуляция потери качества
через квантование + JPEG-раундтрип
- **convert-png-to-color-blocks** — мультипликативный блок-эффект/квантование по
блокам
- **extract-png-color-palette, find-logo-color-scheme, extract-icon-colors** —
частотная гистограмма + выбор топ-N / кластеризация
- **create-unique/popular/multi-color/outlier-color-mask** — статистика
распределения цветов и выделение по маске
### 3.4 Выделение объектов по цвету / фону
- **remove-png-chroma-key** — порог по цвету + деспилл (убрать зелёную
окантовку)
- **remove-color-from-png (качественная версия), extract-color-from-png,
separate-png-colors, separate-logo-colors, separate-icon-colors** —
сегментация по близости цвета
- **convert-png-to-silhouette, convert-png-to-stencil** — порог + заливка
силуэта
- **extract-stamp-from-image (по цвету печати)** — хромакей/частотный фильтр по
цвету чернил
- **remove-png-background (вариант без ИИ)** — проверенный цветовой подход,
знакомый по популярным онлайн-наборам PNG-утилит. Работает хорошо на
однотонном/градиентном фоне и в ряде случаев удобнее ИИ. Опции и алгоритм:
- **Background Color** — выбор удаляемого цвета: пипеткой по изображению или
hex/RGB
- **Percentage (похожесть цвета)** — slider; 0% = убрать только точный
цвет, >0% = убрать соседние оттенки в окрестности цвета (в цветовом
пространстве)
- **Delete Outer Areas (галочка)** — режим «только внешние пиксели»: удаляются
только области, связанные с краями изображения (flood-fill/region-growing от
границ). Выключена = удалять по всему холсту. Именно эта опция позволяет
убрать фон, не задевая «пятна» того же цвета внутри объекта
- **Smooth Edge Line + Radius** — постобработка: полупрозрачная полоса
толщиной N px по границе удаления (сгладить переход)
- **Preview Deleted Background** — двухцветный предпросмотр маски (что
удаляется/что остаётся)
- Реализация: выбор цвета → порог по цветовому расстоянию → (опц.) flood-fill
от краёв → сглаживание краёв → маска поверх превью. Всё на `ImageData`, без
ML.
### 3.5 Геометрические искажения и пиксель-сортировка
- **add-whirl-to-png** — свирл/вихрь: обратное отображение координат
- **sort-png-pixels** — pixel sorting (сортировка строк/столбцов по
яркости/цвету)
- **upscale-png / downscale-png** — качественный ресемплинг (Lanczos, mipmap)
поверх базового canvas-resize
- **remove-signature-background, transparent-png-signature-maker,
clean-up-signature-edges, improve-low-quality-signature, clean-logo-edges,
clean-icon-edges, fix-low-quality-stamp,
convert-rubber-stamp-to-digital-stamp** — комбинация «цвет→прозрачность +
де-шум + морфология краёв»
- **convert-image/png/jpg/icon/logo/signature-to-stamp** — стилизация в
двухцветную «печать» (порог + зернистость + края)
### 3.6 Анимации и GIF-энкодинг
- **convert-png-to-gif, slowly-reveal-png, create-disappearing-png, fade-in-png,
fade-out-png, create-scrolling-png, create-png-sequence-from-gif** —
декомпозиция кадров GIF / генерация последовательности кадров +
`gifenc`/`UPNG` (APNG)
> Примерный объём: **≈71 утилита уровня MEDIUM.**
---
## 4. Уровень HARD — WASM, ML, длительная обработка, тонкая подгонка алгоритма
Реализация: WASM-модули, ML-модели в браузере, длительная обработка с
прогресс-барами.
- **compress-png / reduce-png-file-size / optimize-png** — сжатие на уровне
формата: нужен качественный PNG-энкодер с контролем zlib-фильтров/битовой
глубины и оптимизаторы. Решение: **pngquant / oxipng / zopfli (WASM)** +
подгонка под «качество→размер» (это главный кандидат на HARD — именно здесь
тюнинг бесконечен)
- **remove-png-background (вариант с ИИ)** — опциональное улучшение к
MEDIUM-версии: ML-сегментация (Rembg/DeepLab в WASM или ONNX) для произвольных
фото, где цветовой матчинг бессилен. Начинать можно без него — не-ИИ версия
уже даёт рабочий инструмент
- **extract-signature-from-image** — детект подписи на скане/фото с произвольным
фоном: де-шум, контраст, ML-детекция
- **convert-handwritten-signature-to-digital-signature** — очистка и «оцифровка»
почерка (ML, денойзинг, выравнивание)
- **extract-barcode-from-image** — декодирование штрих-кодов/QR: готовая
библиотека **zxing-wasm**, сопряжение с камерой/фото
> Итого **6 утилит уровня HARD** (все — опциональные улучшения/надстройки). Всё
> остальное (≈290) — EASY/MEDIUM.
---
## 5. Итоговая сводка
**EASY — ~185 утилит.** canvas + ImageData. Примеры: crop, resize, rotate,
конвертации, альфа-канал, палитры, текст, рамки, анализ.
**MEDIUM — ~71 утилита.** Алгоритмы, Web Workers, gifenc. Примеры: blur,
sharpen, морфология, квантование, дизеринг, pixel sorting, анимации GIF,
remove-background (без ИИ).
**HARD — 6 утилит.** WASM (pngquant/oxipng/zxing), ML (ONNX/Rembg). Примеры:
compress/optimize, remove-background (ИИ), extract-signature, barcode.
---
## 6. Рекомендации по реализации
1. **Единое ядро обработки** — `src/lib/core/`: загрузка файла → `ImageData`,
библиотека операций (`color.ts`, `alpha.ts`, `geometry.ts`, `filters.ts`,
`morphology.ts`, `quantize.ts`, `channels.ts`), экспорт через `toBlob`.
Каждая утилита — тонкая SvelteKit-страница + общие компоненты (DropZone,
Preview, Download).
2. **Порядок внедрения:** MVP = ~40 топовых EASY (crop, resize, rotate, convert,
alpha, grayscale, brightness, палитры, watermark, add-text, add-background,
pick-color, analyze). Затем остальные EASY → MEDIUM пакетами по темам
(фильтры, морфология, квантование, анимации). `remove-png-background` (без
ИИ) — отличная «витринная» MEDIUM-фича ранних этапов: не требует ML и
переиспользует `remove-color` + flood-fill + сглаживание краёв. HARD — в
последнюю очередь.
3. **Производительность:** вся пиксельная работа и тяжёлые алгоритмы — в Web
Worker; большие изображения декодировать сразу в downscale для превью;
прогресс-бары для MEDIUM/HARD.
4. **SEO/структура:** SvelteKit + static adapter — каждая утилита отдельный
маршрут (`/crop-png`, `/rotate-png`), что даёт удобные адреса и повторяет
распространённую структуру наборов PNG-инструментов.
5. **Семейства logo/icon/stamp/signature** — переиспользуют общие операции; в
коде это конфиг-опции той же страницы (например `/resize-png` и
`/resize-icon` = один компонент с разными подписями/дефолтами). Не плодить
дублирующий код.
-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-части.
- Несколько параллельных инструментов на главной одновременно — один активный
блок за раз.
-245
View File
@@ -1,245 +0,0 @@
# Backlog
## lint и прочие правила
- [x] Кросс-языковой линтер словарей (warn-only: паритет ключей, пустые
значения, совпадение `{placeholder}`-переменных между всеми локалями) —
правило `i18n/dict-consistency`, детали в §Фаза 8
`docs/plan-preview-i18n.md`
- [ ] проверка дублирования кода html - div с одинаковым классом и т.д. (глянуть
что за `SonarLint` или написать кастомный линтер)
- [ ] проверка текстов в svelte\ts\html - всё тексты должны быть через i18n
модуль
- [ ] консистентность комментариев - если есть описание к одному инструменту -
оно должно быть у всех или объяснено, почему этот инструмент такой
особенный и заслуживает комментарий
## Мелочи всякие
- [ ] Пересмотреть тесты - исправить всякие хрупкие, которые проверяют
количество инструментов, точное совпадение текстов, поиск по
`img[alt="Result image"]`, svelte классам и тому подобные
- [ ] нужен инструмент для удаления фона по выбранной точке - удалять внутри
выделенной области
- [ ] сделать конвертацию в смайлики\награды для твитча (6 размеров или выбор
какой размер и 2 сетки)
- [ ] неудобно настраивать ползунками, некоторые инструменты удобнее настроить
через drag-n-drop. звучит сложно, но надо придумать процесс
- [ ] можно ли в тестах получать i18n ключ вместо текста?
- [ ] убрать в тестах все нестандартные символы `→`, длинные тире и т.д.
- [ ] Пересмотреть прозрачность в инструментах - Create empty PNG - прозрачность
должны быть настраиваемой 0-100%, а не переключателем
- [ ] обдумать объединение пар `inputMode + source` и `resultKind + result` в
единую структуру, позволит упростить некоторые вещи - например
`sourceValue` \ `resultValue` в `SchemaPreview.svelte`
- [ ] Рассмотреть необходимость всех этих пропсов в `SchemaPreview.svelte` они
дублируют друг друга `ontextsource`, `onrendertext`, `oncopytext`,
`ondownloadtxt`, `ondownload`,
- [ ] Переделать внешний вид инструментов без настроек, например Remove alpha
channel PNG "remove-alpha-channel-png". Сейчас выглядит слишком пустым.
- [ ] Убрать описание формата в отображении исходник\результат - это должно быть
в кнопке Download
- [ ] Кнопка download должна сразу писать какой формат сейчас, быть с селектором
формата и подсвечивать предупреждение если формат не совместим (альфа
канал)
- [ ] Пересмотреть поля - Plate, Position, Font Style - сейчас второй label
- [ ] Font Style - обдумать выбор шрифта (google fonts???) как организовать
шрифты с кириллицей? сделать галочку "только с кириллицей"??? (решение о
наборе и лицензиях: `docs/plan-platform.md` §4 — только OFL/Apache с
паспортом, семейства с кириллицей)
- [ ] сделать загрузку картинки исходника по ctrl+v и drag-and-drop (dropzone)
- [ ] square-mask-png - проверить работу - параметры неверно считаются. width -
100, height - 100, x - 100, y - 100, обрезает 50% на 50% картинку
- [ ] прикрутить кастомный color picker (есть в old), добавить прозрачность
(range?)
- [ ] проверить настройки range slider на crop-png - слишком большие значения
- [ ] для range slider field - добавить поле для ввода цифр и кнопку сброса
значения
- [ ] для keep Aspect field - пересчитывать второе поле, учитывать какое было
изменено
- [ ] поле ввода текста - сделать по умолчанию нормального размера, а не одну
строку для svg - форматирование или подсветку синтаксиса??? для bin\hex -
форматирование по колонкам??? для hex\bytes\rgba - пример в placeholder
- [ ] обновить страницу kit или сделать storybook
- [ ] придумать процесс редактирования и настройки pipeline - drag-n-drop,
удаление, добавление инструментов. переход один инструмент - pipeline без
перезагрузки
- [ ] Слаги инструментов (`*-png`) — надо переименовть, они работают с любыми
изображениями. Надо сделать отдельный интерфейс маппинга инструмент ->
ссылка
## Переезд
- [x] добавить i18n в preview
- [ ] множественные числа (часть/части/частей; «N параметров», счётчики) —
`tPlural()` и формат `PluralForms`, детали в §Фаза 9
`docs/plan-preview-i18n.md`
## SEO/GEO
Общий план: `docs/plan-seo.md` (S0–S4). C17 выполнен; старт возможен — пока
только domain-free часть S1.
- [ ] переименование бренда + домен: чек-лист в локальном плане
`docs/plan-domain.local.md` (вне git, имя домена в репо не публикуется;
план-seo §6, R1/R2)
- [ ] download с селектором формата PNG/JPG/WebP/BMP для всех инструментов (райз
№7 → S1f)
- [ ] форматные страницы (`crop-jpeg`, …) — только по вордстату с уникальным
контентом; редиректы только для опечаток; клонов с canonical не делать
(план §5)
- [ ] sitemap.xml из реестра + `Sitemap:` в robots.txt (S1b/S1c)
- [ ] Seo.svelte: canonical + OG-теги, per-tool OG-картинки (S1d/S1e)
- [ ] контент страницы инструмента: seo-блок в словарях, H1/how-to/FAQ/related
(S2)
- [ ] JSON-LD (SoftwareApplication/FAQPage/Breadcrumb) + llms.txt (S3)
- [ ] GSC + приватная аналитика (Plausible/Umami/CF), дистрибуция (S4)
## Платформа (API/PWA)
Общий план: `docs/plan-platform.md`; WASM-ядро и CLI уже в `docs/roadmap.md`
(фазы 3–8) — здесь только новые треки.
- [ ] PWA: manifest + service worker, оффлайн-режим, установка (после C17 и S1;
C19 отложен — не блокер)
- [ ] API: спайк серверного ядра (нативный Rust vs edge-wasm), ключи, free tier
(после wasm-фаз 5–7)
## Баги preview
Зарегистрированы при написании e2e (`web/e2e/`, `pnpm --dir web test:e2e`) и
ручного чек-листа — см. `docs/checklist-manual-testing.md` (раздел G). Каждый
зафиксирован как `test.fixme`. После фикса бага: убрать соответствующий
`test.fixme` и отметить пункт G в чек-листе как пройденный.
1. **Генераторы без кнопки «Generate»** — все 21 инструмент категории GENERATE
открываются, но в UI нет кнопки Generate и полей схемы (только RU/EN/Reset);
результат через UI недостижим. В `SchemaToolView.svelte` `isGenerator`
определяется как `tool.generate && !tool.run`, кнопка рисуется только если
`ongenerate` прокинут, а для генераторов с дефолтным `input: "file"` — нет.
Ожидание: поля схемы + кнопка «Generate», результат-картинка по дефолтам.
Fixme: `web/e2e/generators.spec.ts`. Кнопки и не должно быть, все процессы
должны запускаться автоматически
2. **resize-png с дефолтами падает** — схема по умолчанию `size: 0×0` +
`keepAspect: true` → при загрузке ЛЮБОГО PNG сразу `errors.resizeSize`
(alert), результата нет. `registry-new/geometry.ts`. Ожидание: осмысленный
дефолт-размер (например, исходный) либо кнопка, а не авто-ошибка.
3. **crop-png с дефолтами падает** — аналогично: `width: 0, height: 0` → сразу
`errors.cropSize`. `registry-new/geometry.ts`. Ожидание: базовый кроп по
умолчанию либо внятное предзаполнение.
4. **Сырые ключи ошибок в UI** — alert на tool-странице показывает сырой ключ
i18n (`errors.resizeSize`), а не сообщение из `en.ts` / `ru.ts` (там
`resizeSize: "Width and/or height must be positive"`). Проверяется на
resize-png / crop-png. Ожидание: человекочитаемое сообщение на текущем языке.
Fixme: `web/e2e/known-issues.spec.ts` (тест «error message localized»).
## Идеи
1. **Менять первый инструмент цепочки** — оценка M. Сейчас базовый инструмент
зафиксирован при открытии страницы; хочется заменить его на другой без
разборки цепочки: шаги и их параметры сохраняются, вход перечитывается.
Открытый вопрос: что делать с несовместимыми параметрами (text-source ↔
file).
2. **Сворачивать инструменты в chain** — оценка S. Тоггл сворачивания звена до
заголовка «Шаг n: название» (превью скрываются). Состояние свёрнутости
помнить в workspace-pipeline.
3. **Несколько цепочек: сохранение и загрузка** — оценка M. Именованные цепочки
в localStorage (список, создать/переименовать/удалить), быстрое переключение.
Формат экспортного JSON расширить опциональным полем имени, старые файлы
читаются как безымянные.
4. **Batch-обработка архивов выбранным chain** — оценка L. Вход — zip-архив
картинок (распаковка в браузере), прогон текущего или выбранного сохранённого
chain по каждому файлу, сборка результата обратно в zip для скачивания.
Зависит от п.4 (выбор цепочки) и от экспортного формата pipeline JSON;
прогресс и ошибки — пофайлово.
5. **Избранные инструменты (fav tools)** — оценка S/M. Тоггл-звёздочка на
карточках каталога, результатах поиска и в шапке страницы инструмента; список
id в localStorage. Избранное показывается отдельной секцией сверху каталога и
поднимается в выдаче поиска (бонус к popularity при скоринге).
6. **Конвертация формата — НЕ отдельный инструмент, а выбор в кнопке Download**
— оценка M/L. Инструменты `convert-png-to-jpg` / `convert-png-to-webp` (и др.
форматы) не должны жить как самостоятельные инструменты цепочки. Вместо этого
— бесшовное внедрение выбора формата/качества прямо в кнопку/диалог
**Download**: юзер скачивает результат в нужном формате (mime/ext/quality), а
pipeline при этом не усложняется лишним звеном. **Сделано (2026-09):**
механическая часть реализована в preview — `ToolEntry.output`
(`OutputFormat`: mime/ext/qualityParamId) в `registry-new/types.ts`;
`SchemaToolView.download()` кодирует по `output` (jpg/webp/bmp уже настроены;
`png-to-bmp` переведён сюда же). **Осталось (UX-райз «Download»):**
- выбор формата/качества прямо в кнопке/диалоге Download для любого
результата (не отдельными конвертерами);
- экспорт с лимитом размера → закрывает отложенный `reduce-to-size-png`
(целевой KB, бинарный поиск по k из квантования). Проработать вместе с
достройкой download-флоу нового UI.
7. **`watermark-image-png` (водяной знак картинкой)** — оценка M. Отложен из
миграции (`archive/plan-migrate-remaining.md`). Нужна overlay-механика:
второй источник изображения (getOverlay/store), параметры scale/opacity/
position/margin/aspect. Решается вместе с вопросом, как второй источник
картинки вписывается в schema-driven preview (`SchemaToolView`).
8. **`png-info` (детальная информация о PNG)** — оценка M/L. Отложен из
миграции. Отдельный райз: exif-теги, редактирование, структурированный вывод;
нужен свой отдельный случай в UI, не «ещё один text/verdict». возможно нужна
вообще отдельная страница, несовместимая с другими инструментами: 1) она
работает не с изображением, а с мета данными из файла, 2) она не может быть
частью цепочки, т.к. результат не является изображением, 3) если надо
изменить теги, никакого форматирования и изменения в изображение вносить не
надо, только в мета информацияю
9. **Region-инструменты** (censor/erase/pixelate-area/blur-area/sharpen-area/
reverse-area) — ждут UI выделения области на превью. Из
`archive/plan-gap-waves.md` («вне очереди»).
10. **Мультифайловые инструменты-потребители** (gif-to-frames, separate-colors)
— механизм «результат = набор файлов» (1 → many) реализован: `FileResult` +
`result: "files"` + скачивание zip-архивом + грид-превью частей. Первый
потребитель — `split-into-parts-png`. Остальные подключаются по мере нужды.
11. **Анимационные** (slow-reveal/fade/scrolling) — выход не PNG; отдельное
решение о формате. Из `archive/plan-gap-waves.md`.
12. **HARD-серия** (glitch, barcode, signature-extract, handwritten-digital). Из
`archive/plan-gap-waves.md`.
13. **Контраст текста на акцентных кнопках — пересмотреть после готовности
сайта**. Текст `--color-background` на фоне `--color-main`/`--color-accent`
(тон 48) даёт контраст ~4.1–4.3 (light) — ниже AA 4.5 для обычного текста;
`palette.test.ts` держит для кнопок порог 4.0 (AA large-text) осознанно, как
компромисс текущего дизайна. После полного завершения редизайна пересмотреть
тона (например поднять tone кнопок до ~55) и вернуть порог 4.5. Также
проверить `--color-danger` (dark, тон 55): текст на нём 4.93 — ок, но
light-danger тон 48 = 4.25 на границе.
14. **Отображение маски для инструментов (runMask/preview-mask)** — придумать и
реализовать процесс показа маски результата. **Контекст:** в старом
контракте поле `preview` у `remove-color`/`remove-background` показывало
быстрый ч/б превью-маску (`colorMask`/`backgroundMaskPreview`) того, что
будет вырезано. При миграции на единый `run(ctx)` поле убрано (шаг 8,
`registry-new/alpha.ts`) — новый executor и schema-превью зовут `run()`
напрямую, маска пока не нужна. **Идея 1:** дать каждому `run` возможность
вернуть (или инструмент — объявить) `runMask` — ч/б изображение маски
изменений либо иной подход (отдельное поле `mask` + функция маски, второй
результат, overlay). Оценка — на усмотрение при проработке (S/M). Пока живём
без неё. **Идея 2:** каждый `run` возвращает два изображения - для
отображения (показывает маску или сам результат), и для передачи в следующий
инструмент (всегда результат. Оценка — на усмотрение при проработке (S/M).
Пока живём без неё.
15. **Процесс работы с инструментами 1 → many и many → 1** — абстракция
результата «набор файлов» оформлена и работает: тип `FileResult`,
`result: "files"` в реестре, сериализация через executor, скачивание
zip-архивом, грид-превью частей. Реализовано на первом потребителе
`split-into-parts-png` (2026-09, `docs/archive/plan-split-into-parts.md`).
**1 → many:** подключение остальных потребителей (gif-to-frames,
separate-colors) при необходимости; формат/качество файлов внутри zip —
отложено до UX-райза «Download» (п.7). **many → 1** (вход = архив/несколько
картинок → единый результат): не реализовано. Зависит от batch-обработки
архивов (п.5 идей, roadmap фаза 8 «batch-страница»). Нужна отдельная
проработка входа (распаковка zip в браузере, порядок файлов, поведение в
пайплайне — такие инструменты терминальны, как и 1 → many).
-104
View File
@@ -1,104 +0,0 @@
# Чек-лист ручного тестирования preview
> Вспомогательный документ к `docs/archive/plan-testing-preview.md` и e2e-тестам
> (`web/e2e/`, `pnpm --dir web test:e2e`). Автоматизированная часть закрывает
> «нет падений/ошибок»; здесь — то, что руками, и что Playwright не покрывает.
>
> Перед прогоном: `pnpm --dir web build`, поднять локально `web/` (dev или build
> → `node scripts/serve-static.mjs`).
>
> Критерий приёмки большинства пунктов: **нет зависит, нет «залипания»,
> сообщения об ошибках человекочитаемые**.
## A. Краевые PNG-файлы
> Фикстуры для этого раздела генерируются скриптом: `pnpm generate:fixtures` →
> `tests/fixtures/manual-edge-cases/`.
- [ ] Огромное изображение (мегапиксели) — загрузка не висит, прогресс есть.
- [ ] PNG 1×1 — инструменты не падают (проверено авто: flip-png, см. suite).
- [ ] PNG с полупрозрачностью/чётким альфа-краёв (чёрно-белая шахматка) —
remove-background, feather-edges, clean-edges дают корректные края.
- [ ] Палитровый PNG (индексированные цвета) — конвертируется/анализируется.
- [ ] Анимированный/APNG, если попадётся — не ломает конвейер.
- [ ] Повреждённый/не-PNG файл — понятная ошибка, нет краша (авто-чека в
pipeline.spec.ts).
- [ ] PNG с битой CRCh / 16-bit — как ведут себя анализаторы (ориентация,
размер, прозрачность).
### Файлы (`tests/fixtures/manual-edge-cases/`)
| Файл | Описание |
| ------------------------ | ------------------------------------------------------------ |
| `1x1.png` | Минимальное изображение 1×1, сплошной красный |
| `1x1-transparent.png` | 1×1, полностью прозрачный (alpha=0) |
| `2x2-extreme.png` | 2×2: чёрный, белый, полупрозрачный красный, прозрачный синий |
| `1024x1024-checker.png` | 1024×1024, шахматка ≈1 мегапиксель |
| `1920x1080-gradient.png` | 1920×1080, градиент ≈2 мегапикселя |
| `alpha-checkerboard.png` | 64×64, чёрно-белая шахматка с чередующейся альфой |
| `alpha-gradient.png` | 128×64, альфа-градиент 0→255 по ширине |
| `sparse-alpha.png` | 100×100, 10% пикселей с alpha=0 |
| `all-black-opaque.png` | 64×64, сплошной чёрный |
| `all-white-opaque.png` | 64×64, сплошной белый |
| `all-transparent.png` | 64×64, полностью прозрачное |
| `palette-4colors.png` | 32×32, палитровый 4 цвета (RGBW) |
| `palette-256.png` | 64×64, палитровый полная палитра 256 цветов |
| `16bit-subtle.png` | 32×32, 16-bit RGBA, тонкий градиент |
| `noise-pattern.png` | 100×100, шумовая текстура |
| `1x4000-strip.png` | 1×4000, длинная вертикальная полоса |
| `4000x1-strip.png` | 4000×1, широкая горизонтальная полоса |
| `corrupt-bad-crc.png` | 4×4, повреждённый CRC в IDAT |
| `truncated.png` | Обрезанный файл (только PNG-заголовок + часть IHDR) |
| `fake-png.txt` | Текстовый файл «.png» — не-PNG |
## B. Края параметров
- [ ] Экстремальные значения слайдеров (0 и max) в blur/sharpen/pixelate — не
«залипает» running.
- [ ] Пустые/NaN поля (напр. width/height) — санитизация (silent repair), нет
красного alert-текста в виде `errors.*`.
- [ ] Невалидные цвета в плашках/градиентах (мисс-спелл `#xyz`, короткие hex,
именные цвета) — silent repair или понятная ошибка.
- [ ] Пустой text-source на text-инструменте — run пропускается, ошибок нет
(кейс из плана п.14).
- [ ] Смена параметра после Reset — авто-перезапуск работает (Debounce 200 мс).
## C. Буфер обмена и drag-n-drop
- [ ] В реальном браузере кнопка copy на текстовом результате (png-to-base64 и
т.п.) кладёт текст в буфер (в Playwright не тестируем из-за permissions).
- [ ] Drag-n-drop файла на dropzone страницы инструмента (не только файл-диалог)
— UX-ощущения и корректность.
## D. Производительность и отзывчивость
- [ ] Долгие авто-рераны на больших картинках (jpeg-artifacts, dithering,
quantize на 4К) — UI не фризит, канселяция/дебаунс срабатывает.
- [ ] Скролл страницы при длинных настройках (например, text-инструменты) — нет
залипаний, панели не прыгают.
- [ ] Вкладка не «съедает» память при 10+ повторах blur/sharpen на мегапикселе
(утечек blob-URL/bitmap быть не должно).
## E. Визуальный слой (доделываем по ходу)
- [ ] Сетки/выравнивание на брейкпоинтах 640 / 800 / 1100 (mobile/tablet/
desktop) на всех 4 маршрутах.
- [ ] Тема light/dark: контраст вердиктов (Yes/No), статус «LIVE PREVIEW»,
мета-инфо бликов не даёт.
- [ ] Иконки в каталоге на всех категориях — не «бьются» (missing icon).
- [ ] Язык RU/EN на превью-страницах — переключение без регресса рендера.
## F. Субъективная корректность
- [ ] dithering / two-colors / quantize — результат «по ощущениям» соответствует
описанию.
- [ ] watermark-tile / add-text / date-stamp на кириллице — рендер текста
корректный (не «кракозябры»).
- [ ] remove-background на сложной полупрозрачности — края не «звенят».
## Как долго
- Полный прогон: ~40 минут (все разделы, включая мегапиксели и 4К) в двух
браузерах.
- Быстрый смоук (< 15 мин): A (кроме мегапикселей), B (выборочно), C, F
выборочно, G обязательно.
-388
View File
@@ -1,388 +0,0 @@
# План: исправления по аудиту UX/UI
> Статус: **draft** — на ревью.
>
> Источник: `docs/tools-audit.md`. Этот документ разбивает аудит на конкретные
> шаги, группирует по типу работы и фиксирует решения.
## 0. Ключевое решение: select → buttons
Существующий `Segmented.svelte` — компонент с группированными кнопками (общая
рамка, разделители между сегментами). Для параметров инструментов он не
подходит: при 3+ опциях сегменты сжимаются, текст не читается.
**Решение:** новый компонент `ButtonsControl` — раздельные outline-кнопки с
отступами. Каждая опция — отдельная `<button>` со своей рамкой, активная
подсвечивается заливкой (`--color-main`). Не зависит от `Segmented`.
### Компонент `ButtonsControl`
```svelte
<!-- web/src/lib/components/fields/schema/ButtonsControl.svelte -->
<div class="buttons" role="group">
{#each options as opt}
<button
class="btn"
class:selected={value === opt.value}
aria-pressed={value === opt.value}
onclick={() => select(opt.value)}
>{opt.label}</button>
{/each}
</div>
```
```css
.buttons {
display: flex;
flex-wrap: wrap;
gap: var(--space-m);
}
.btn {
padding: var(--space-m) var(--space-l);
border: var(--size-border) solid var(--color-border);
border-radius: var(--radius-s);
font: var(--font-size-s) var(--font-mono);
color: var(--color-text-muted);
background: transparent;
cursor: pointer;
}
.btn.selected {
background: var(--color-main);
color: var(--color-background);
border-color: var(--color-main);
}
```
### Схема: `field.buttons()`
Новый тип в `registry-schema.ts`:
```ts
export interface ButtonsSpec<V extends string = string> extends FieldSpecBase {
kind: "buttons";
default: V;
options: { value: V; label: string }[];
}
```
Фабрика:
```ts
buttons: <V extends string>(s: Omit<ButtonsSpec<V>, "kind">): Field<V> => ({
spec: { kind: "buttons", ...s },
}),
```
Регистрация: добавить `"buttons": {} as ButtonsSpec` в `fieldSpecs`, добавить
`ButtonsControl` в `FIELDS` в `SchemaFields.svelte`, добавить дефолт/санитайз в
`sanitizeSchemaParams` и `defaultSchemaParams`.
---
## 1. Волна A — инфраструктура (1 коммит)
### Шаг A1. Новый тип `buttons` в схеме
Файлы:
- `web/src/lib/registry-schema.ts` — `ButtonsSpec`, фабрика `field.buttons()`,
кейс в `sanitizeSchemaParams`, кейс в `defaultSchemaParams`
- `web/src/lib/components/fields/schema/ButtonsControl.svelte` — новый компонент
- `web/src/lib/components/SchemaFields.svelte` — импорт + запись в `FIELDS`
### Шаг A2. Seed → input + randomize
Для `field.number()` с `kind: "seed"` (или новый подтип `field.seed()`) —
рендерить поле ввода + кнопку «Random» вместо range slider.
Решение: новый вид `field.seed()` (наследует от number, но рендерится иначе).
Или проще: опциональное поле `variant?: "seed"` в `NumberSpec` — тогда
`RangeControl` покажет input + кнопку.
Решение: **`variant: "seed"` в `NumberSpec`** — меньше новых типов.
Файлы:
- `web/src/lib/registry-schema.ts` — `NumberSpec.variant?: "seed"`
- `web/src/lib/components/fields/schema/RangeControl.svelte` — условный рендер
- `web/src/lib/registry/filters.ts` — `randomizePixels`, `addNoise`: поле seed →
`{ ...field.number(...), spec: { ...spec, variant: "seed" } }` (или
пересоздать через `field.seed()`)
---
## 2. Волна B — select → buttons (по файлам)
Каждый шаг — один коммит, < 500 строк. Меняем `field.select()` →
`field.buttons()` в перечисленных ниже схемах.
### B1. geometry.ts (8 инструментов)
| Инструмент | Поле | Опции |
| ----------------------- | -------- | ----------------------------------------- |
| rotate-png | angle | 90° / 180° / 270° |
| flip-png | axis | horizontal / vertical |
| swap-orientation-png | target | portrait / landscape |
| symmetric-copy-png | axis | vertical / horizontal |
| symmetric-copy-png | keepSide | left / right / top / bottom |
| change-aspect-ratio-png | ratio | 1:1 / 4:3 / 3:4 / 3:2 / 2:3 / 16:9 / 9:16 |
| change-aspect-ratio-png | mode | crop / pad |
**Исключение:** `change-canvas-size-png` (anchor, 9 опций) — оставить
`field.select()`, т.к. 9 кнопок в ряд не поместятся. Аналогично `position9` (уже
отдельный компонент `PositionControl`).
### B2. color.ts (4 инструмента)
| Инструмент | Поле | Опции |
| ------------------------ | --------- | ------------------------------------ |
| dithering-png | pattern | floyd-steinberg / bayer |
| extract-channel-png | channel | red / green / blue |
| swap-channels-png | pair | r-g / r-b / g-b |
| decrease-color-count-png | maxColors | 2 / 4 / 8 / 16 / 32 / 64 / 128 / 256 |
**Исключение:** channel spaces (hsl/hsv/... component) — 3-6 опций, но это
динамические инструменты, оставить `select`.
### B3. analyze.ts (5 инструментов)
| Инструмент | Поле | Опции |
| ------------------------- | ---- | ------------------ |
| show-transparent-png | mode | binary / highlight |
| show-grayscale-pixels-png | mode | (тот же) |
| show-color-pixels-png | mode | (тот же) |
| light-pixel-mask-png | mode | (тот же) |
| dark-pixel-mask-png | mode | (тот же) |
Все используют общий `maskBaseFields` — правка в одном месте.
### B4. filters.ts (1 инструмент)
| Инструмент | Поле | Опции |
| ------------- | ---- | ------------ |
| add-noise-png | mode | mono / color |
### B5. generate.ts (12 инструментов)
| Инструмент | Поле | Опции |
| ------------------ | ------------ | --------------------- |
| color-spectrum-png | direction | horizontal / vertical |
| step-colors-png | layout | grid / strip |
| complementary-png | layout | grid / strip |
| triadic-png | layout | grid / strip |
| tetradic-png | layout | grid / strip |
| analogous-png | layout | grid / strip |
| monochromatic-png | layout | grid / strip |
| shades-png | layout | grid / strip |
| sort-colors-png | layout | grid / strip |
| mix-colors-png | (нет select) | — |
---
## 3. Волна C — обязательные фиксы (баги + дефолты)
### C1. resize-png: дефолт 0×0 → осмысленный
`geometry.ts`: `width: 0, height: 0` → `width: 512, height: 512` (или
`width: imgWidth, height: imgHeight` — но дефолт в схеме не может знать размер
изображения). **Решение:** дефолт `512×512`.
### C2. symmetric-copy-png: "keep side" не работает
Баг в `run()`: `keepSide` не учитывает `axis`. При `axis: "vertical"` (удвоение
ширины) работают только `left`/`right`, `top`/`bottom` не имеют смысла. Нужно:
либо фильтровать опции `keepSide` в зависимости от `axis` (reactive schema),
либо нормализовать в `run()`.
**Решение:** reactive — при смене `axis` сбрасывать `keepSide` на допустимое
значение. Пока достаточно в `run()`: если `axis === "vertical"` и `keepSide` ∈
{top, bottom} → заменить на `left`.
### C3. Механизм проброса размеров картинки в схему
**Проблема.** Поля, значения которых зависят от размеров исходника (crop x/y,
resize, offsets), объявлены в статической схеме (`-100000…100000`), а реальная
картинка обычно 800–4000px. Диапазон надо считать от фактического размера
изображения, а не от потолков.
**Что нужно спроектировать и завести:**
1. **Где берём размер.** `SchemaToolView` уже знает размер после `decodeFile()`
→ `PixelImage` (width/height). Сейчас он никуда не пробрасывается — надо
прокинуть в `SchemaFields` и дальше в контролы, например через context или
проп `sourceSize`.
2. **Как схеме описать зависимость.** В спеках добавить ссылку на размер
источника вместо жёстких чисел. Кандидат:
```ts
x: field.slider({
label: "fields.x",
bound: "sourceWidth", // min/max = ±sourceWidth
default: 0,
}),
```
`BoundSpec = "sourceWidth" | "sourceHeight" | "minSide" | "maxSide"`,
применимо к `NumberSpec`, `SliderSpec`, `OffsetSpec`, `DimensionSpec`.
3. **Что делает RangeControl.** При `bound` вычисляет min/max из текущего
размера источника; при смене картинки диапазон пересчитывается, текущее
значение пере-клампится. Дефолт и `sanitize` остаются на статической основе
(или тоже учат bound) — решить при проектировании.
4. **Первые потребители:** crop (x/y → ±sourceWidth/sourceHeight), shift
(offsetX/offsetY), resize (max → maxSide).
Это отдельный шаг плана: **завести механизм**, спроектировать на ревью интерфейс
спека (`bound` vs `range: (size) => [min, max]`), затем применить к crop и
остальным полям размерности. Пока механизма нет — поля crop остаются как есть.
### C4. verify-is-png: неверное название
Проверяет текст, не PNG-файл. Переименовать:
- `id` → `verify-is-png-data` (или оставить для обратной совместимости)
- `title` → "Verify PNG data"
- `description` → уточнить
**Решение:** поменять title и description, id не трогать (url-dependent).
---
## 4. Волна D — обязательные фиксы (новые параметры)
### D1. remove-alpha-channel-png: выбор цвета фона
Сейчас захардкожен `#ffffff`. Добавить `field.color({ default: "#ffffff" })`.
Файл: `alpha.ts`, схема `removeAlphaChannelSchema`.
### D2. extract-alpha-mask-png: галочка «инвертировать»
Добавить `field.checkbox({ label: "fields.invertMask", default: false })`. В
`run()`: если `invert`, вызвать `invertAlpha()` после `extractAlphaMask()`.
Файл: `alpha.ts`.
### D3. change-canvas-size-png: position9 вместо select
`anchor` сейчас `field.select()` с 9 опциями → `field.position9()`.
Файл: `geometry.ts`. Уже есть `PositionControl`.
### D4. Generators: добавить height
Инструменты без `size`/`height`:
- blend-two-png — добавить
`height: field.slider({ min: 1, max: 1024, default: 512 })`
- step-colors-png — аналогично
- complementary, triadic, tetradic, analogous, monochromatic, shades,
sort-colors — все используют `paletteBaseSchema` (общий объект), добавить
`height` туда
- mix-colors-png — добавить height
Файл: `generate.ts`.
### D5. add-border-png: прозрачность цвета
Текущий `field.color()` не поддерживает alpha. Пока что: оставить как есть
(прозрачность не поддерживается нативным color picker). Отметить в аудите как
blocked.
---
## 5. Волна E — полировка (некритично)
### E1. Плейсхолдер "нет параметров"
Инструменты с пустой схемой (`EmptyParams`) выглядят странно. Добавить
`EmptyState` или подсказку "No parameters — click Run".
Файл: `SchemaToolView.svelte` — показать подсказку если `schema.fields` пуст.
### E2. quantize-png: пресеты
`field.slider` с max 64 → добавить пресеты (8, 16, 32, 64) как кнопки под
слайдером. Либо `field.buttons()` с 4 опциями вместо слайдера.
### E3. trim-empty-space-png: 0-254 → проценты
Текущий `min: 0, max: 254` — нестандартно. Добавить переключатель px/% (аналог
шага A1 — `variant: "alpha-threshold"` в NumberSpec).
### E4. add-text-png: plate offset
Plate снизу имеет большой отступ — добавить галочку "compact plate" или
уменьшить дефолтный padding.
### E5. emoji-to-png: выбор эмодзи
Поле `text` → добавить группу часто используемых эмодзи как кнопки- пресеты над
полем ввода.
---
## 6. Верификация
После каждой волны:
1. `pnpm --dir web build` — без ошибок
2. `pnpm --dir web exec svelte-check --tsconfig ./tsconfig.json` — без ошибок
3. `pnpm --dir web lint` — без ошибок (кроме ожидаемых долгов)
4. Ручная проверка в браузере: параметры рендерятся, инструменты работают
---
## 7. Не покрыто / требует решения
Эти пункты из аудита не попали в волны A–E. Требуют либо дополнительного
исследования, либо нормативного решения, либо отложены.
### 7.1 Требуют normative решения
- **Pixel ↔ Percent** (circle-mask, square-mask, star-mask, wavy-mask — %→px,
round-corners — px→%): нужен компонент переключателя единиц в слайдере.
Отложен до волны E, т.к. требует правки `RangeControl` + всех схем.
- **Smoothing checkbox** для масок (circle, square, star, wavy, round-corners,
invert-alpha): нужен `field.checkbox("smoothing")` + правка core (shapes.ts,
alpha). Отложен — требует визуального тестирования.
- **Vignette**: выбор центра + цвет + feather — 3 новых параметра. Требует
normative решения по UI.
- **Remove-background**: выбор точки удаления — либо параметр, либо отдельный
инструмент.
- **Precision/quality**: select в resize (bilinear/bicubic/nearest) — нужен ли
выбор алгоритма?
- **Placeholders**: "нет параметров" — какой текст/компонент показывать.
### 7.2 Операционные (тест-кейсы, описания)
- find-contour-png: расширять на толщину линии (требует правки core)
- make-thicker-png: расширять картинку на толщину линии
- make-thinner-png: уменьшать от краев, fix выступа
- feather-edges-png: расширять картинку на толщину линии
- clean-edges-png: найти тест-кейс, возможно увеличить максимум
- despeckle-alpha-png: найти более заметный тест-кейс
- close-holes-png: проверить поведение на больших полупрозрачных областях
- auto-contrast-png: найти тестовый кейс
- sharpen-png: добавить тест-кейс
- center-by-alpha-png: найти тест-кейс
- png-is-grayscale: улучшить видимость результата
### 7.3 Дизайн-вопросы
- add-padding-png vs add-border-png: разница неочевидна, нужен review
- change-aspect-ratio-png: не видно что меняется — рамка результата
- watermark-tile-png: заполняет не полностью
- sepia-png: нужна ли настройка силы эффекта?
- posterize-png: настройка для каждого канала отдельно?
- pixelate-png: разные алгоритмы?
- swap-channels-png: обдумать flow через более простые инструменты
### 7.4 Аудит полей (компоненты)
- Range slider: поле ввода + кнопки +/- + кнопка сброса
- Color picker: пипетка с картинки, прозрачность
- Width/height с keep-aspect-ratio: единый компонент
- Position offset: отдельные слайдеры x/y (уже сделано в `OffsetControl`)
-132
View File
@@ -1,132 +0,0 @@
# План: платформа — общее ядро, API и PWA
> **Статус (2026-09-09):** план-набросок. WASM-ядро и CLI уже описаны в
> `docs/roadmap.md` (фазы 3–8) и здесь не дублируются — этот план добавляет то,
> чего в roadmap нет: **API** (фаза 9, возможная монетизация) и **PWA** (фаза
> 10, оффлайн + установка). Оба стартуют после wasm-фаз; PWA не ждёт API и может
> идти раньше.
## 0. Решения
- **Одно ядро — три потребителя.** Rust-крейт `core/` (roadmap фаза 3) даёт:
wasm-пакет для сайта (фаза 5), нативный CLI (фаза 6), серверный слой API (фаза
9). Единый контракт — JSON-пайплайн `{tool, params}` (заложен в roadmap §0);
эталонные файлы и harness (фазы 2, 4, 7) гарантируют одинаковый результат во
всех рантаймах.
- **API — нативный Rust на сервере, не wasm.** В браузере wasm нужен ради
доставки и песочницы; на сервере крейт линкуется напрямую — быстрее и проще.
Исключение — edge-воркеры (см. спайк ниже): там исполняется тот же
wasm-артефакт фазы 5.
- **PWA не трогает SEO-схему.** Сайт остаётся полностью prerendered-статикой;
service worker добавляет оффлайн-слой поверх и не отвечает за HTML ботовам.
- **Монетизация не ломает позиционирование.** Браузерные инструменты — бесплатны
и без трекинга; API — отдельный продукт для автоматизации/бизнеса. Файлы в API
обрабатываются в памяти: без хранения и логирования содержимого — продолжение
«privacy»-истории.
## 1. Фаза 9 — API (после фаз 5–7)
- [ ] **[A1]** Спайк сервера: (а) axum/actix + нативный `core/` на VPS против
(б) wasm-артефакт фазы 5 на edge (CF Workers). Сравнить: стоимость, лимиты
памяти/времени на больших картинках, сложность деплоя и rate-limiting.
Решение зафиксировать здесь.
- [ ] **[A2]** Контракт: `POST /v1/process` — multipart (image) + JSON-пайплайн
→ изображение/архив; лимиты размера, коды ошибок; OpenAPI-спека.
- [ ] **[A3]** Ключи и лимиты: Bearer-ключи, free tier (N запросов/мес), rate
limiting по ключу; самостоятельная выдача/ротация ключей в кабинете —
позже.
- [ ] **[A4]** Биллинг: Stripe/LemonSqueezy (merchant of record), 2–3 тира по
запросам/размеру файла. Только после стабильного free tier.
- [ ] **[A5]** Документация как лендинг: `/api` — индексируемая страница
(туториал, примеры curl/JS), OpenAPI, ссылка из `llms.txt`. Dev-аудитория
— источник бэклинков (план-seo S4c).
- Гейт: один и тот же пайплайн даёт пиксельно идентичный результат в браузере,
CLI и API (harness фазы 7 расширяется третьим рантаймом).
## 2. Фаза 10 — PWA (после C17 и S1; C19 не блокер)
- [ ] **[P1]** Манифест: имя (бренд после R1 из plan-seo §6), иконки 192/512 +
maskable, theme-color из дизайн-токенов, `display: standalone`.
- [ ] **[P2]** Service worker (SvelteKit `src/service-worker.ts`): precache
app-shell; immutable-ассеты (build-hash) — cache-first; навигация по
инструментам — stale-while-revalidate; offline-fallback страница. Спайком
оценить вес полного precache: страницы маленькие, тяжёлое тащить по
требованию.
- [ ] **[P3]** Оффлайн-UX: индикатор «offline — работаете локально», своя кнопка
установки (на `beforeinstallprompt`), стратегия обновления версии (prompt
vs skipWaiting — решить на спайке).
- [ ] **[P4]** SEO-безопасность: SW не перехватывает запросы ботов и не меняет
HTML-ответы; canonical/prerendered-страницы не затрагиваются. Проверка:
Lighthouse PWA + SEO зелёные после внедрения.
- Гейт: каталог и топ-инструменты полностью работают в airplane-mode; повторный
визит без сети открывается мгновенно.
## 3. Порядок и связи
- WASM-ядро, эталоны, CLI — `docs/roadmap.md` фазы 2–8 (не дублируются).
- PWA — после C17 (один дизайн, реальные маршруты) и после S1 `plan-seo.md`; C19
отложен и PWA не ждёт (не кешировать noindex-версии); API — после фаз 5–7.
- Синергия с дистрибуцией: «works offline, installable» и публичный API —
аргументы для Product Hunt / alternative.to / GitHub (plan-seo S4c) и для
GEO-текстов (`llms.txt`, описания категорий).
## 4. Шрифты: лицензирование и DOM-free рендер текста
Текст — единственный класс операций, который roadmap (фаза 8) держит в TS из-за
canvas. Для CLI/API его всё равно придётся рендерить в Rust, поэтому решение:
**перевести text-инструменты в ядро** (в браузере canvas-рендер остаётся
TS-фолбэком для A/B). Два вопроса — легальность шрифтов в дистрибутиве и рендер
без DOM.
### Лицензирование (решить до выбора шрифтов, backlog «Font Style»)
- [ ] **[F1]** В набор — только лицензии, допускающие **коммерческое
использование и встраивание в продукт** (сайт + PWA + CLI + API): база —
SIL OFL 1.1 (можно редистрибутировать и использовать коммерчески; нельзя
продавать сами шрифты; требование — сохранять лицензию и имена при
редистрибуции) и Apache 2.0 (Roboto, Open Sans, часть Noto).
- [ ] **[F2]** Запрещённые категории: «free for personal use», shareware, шрифты
без явной лицензии, MS core fonts (лицензия привязана к поставке с
Windows-ПО). Отдельно проверять: бесплатность «для веба на одном домене» ≠
право класть TTF в дистрибутив CLI/API — у OFL/Apache таких ограничений
нет, у прочих «free»-лицензий бывают.
- [ ] **[F3]** Паспорт шрифта в `core/fonts/<name>/` (или эквивалент): файл(ы),
LICENSE, источник, версия, хеш; CI-проверка «у каждого файла шрифта есть
паспорт». Браузер грузит шрифты из бандла, не из ОС и не с
`fonts.googleapis.com`-CDN (сторонний запрос = утечка IP пользователя,
противоречит «no tracking»), — одинаковый результат у всех пользователей и
во всех рантаймах.
- [ ] **[F4]** Кириллица: приоритет семействам с полным покрытием — Liberation
Sans, PT Sans/PT Serif, Noto Sans, Inter, Roboto (OFL/Apache), DejaVu Sans
(пермиссивная лицензия Bitstream Vera: редистрибуция и коммерческое
использование разрешены) — закрывает и backlog-вопрос про «только с
кириллицей».
- **Стартовый набор** (все OFL/Apache, с кириллицей; состав не догма —
расширяется по запросам, каждый проходит паспорт F3):
- базовый sans: **Inter** (дефолт add-text), **PT Sans** / **Golos Text**
(профессиональная русская типографика), **Noto Sans** (фолбэк покрытия);
- serif: **PT Serif**, **Lora**, **Playfair Display** (display);
- mono: **JetBrains Mono** (дефолт), **Roboto Mono**;
- display/акцент: **Oswald** (узкий), **Rubik** (округлый), **Comfortaa**,
**Caveat** (рукописный), **Amatic SC** (маркерный);
- кириллицу проверить по файлу при добавлении: Bebas Neue, Pacifico
(добавлялась поздними версиями). Источник файлов — только Google Fonts /
Fontsource (версионированные пакеты под паспорт F3); случайные «free
font»-сайты — нет. Вес: woff2-сабсеты latin+cyr лениво в браузере, TTF в CLI
(~2–4 МБ суммарно), в wasm не зашивать — грузить из статики по требованию.
### Рендер без DOM (спайк — фаза 5–6, реализация — фаза 8)
- [ ] **[F5]** Спайк стека: растеризация глифов (`ab_glyph`/`rusttype`) +
шейпинг (`swash`/`harfrust`; для латиницы/кириллицы сложный шейпинг не
критичен, сложные письменности — осознанно не в первой версии) → отрисовка
в наш `Image`. Эталон — canvas-рендер TS (perceptual-класс по roadmap §3):
совпадать обязаны позиция/выравнивание/переносы, пиксели будут отличаться
хинтингом/антиалиасингом.
- [ ] **[F6]** Единый текстовый API для TS-core и Rust-core: параметры текста —
из JSON-схемы инструмента, шрифт — **id из реестра шрифтов** (не системное
имя), метрики базовой линии считать в ядре, а не через canvas
`measureText` (иначе TS и Rust разъедутся).
- Гейт: один и тот же текстовый шаг пайплайна даёт в браузере (wasm), CLI и API
визуально идентичный результат; в дистрибутиве нет ни одного файла шрифта без
паспорта лицензии.
-330
View File
@@ -1,330 +0,0 @@
# План: i18n в preview (новый UI)
> Статус: готов к реализации (вопросы §5 решены). Соответствует беклог-задаче
> «добавить i18n в preview» (`docs/backlog.md`). Реализация не начата.
## 1. Что получается
Новый UI (`web/src/routes/**` кроме `v1`, `web/src/lib/components/**` кроме
`v1`) переходит на существующий модуль `lib/i18n`: переключатель RU/EN в шапке
работает по-настоящему, все строки локализованы, ошибки уже локализуются.
Инфраструктура i18n уже есть и проверена (v1-ветка) — задача в том, чтобы
подключить к ней preview и закрыть пробелы словарей.
До начала локализации **v1 изолируется**: у v1 появляется полная замороженная
копия `lib/i18n` → `lib/v1/i18n`, у нового UI — своя независимая ветка
`lib/i18n`, которую дальше меняем без оглядки на v1 (Фаза 0). Переключённая
локаль (localStorage) остаётся общей для обеих версий — язык переживает переход
между страницами.
## 2. Текущее состояние (исследование)
### Уже есть
- `lib/i18n/locale.svelte.ts` — состояние локали (runes), персист в
localStorage, обновление `document.documentElement.lang`.
- `lib/i18n/t.ts` — перевод по точечному ключу с фолбэком на базовую локаль и
интерполяцией `{var}`.
- `lib/i18n/dict.ts` — тип `Dict`, `BASE_LOCALE = "en"` (база — английский, ru —
перевод), `LOCALES = ["ru", "en"]`.
- `lib/i18n/ru.ts` (~1351 строка) и `en.ts` (188 строк) — полные переводы
инструментов v1 по id (title/description/params/options/results).
- `lib/i18n/tool-strings.ts` — хелперы для v1
(`toolTitle/toolDescription/ paramLabel/optionLabel/searchDoc`).
- `lib/i18n/matching.ts` — кросс-языковой поиск (ё→е, диакритика, скоринг).
- Тесты: `i18n.test.ts`, `tools-i18n.test.ts`, `matching.test.ts`,
`smoke-i18n.test.ts`, `search-coverage.test.ts`.
- `+layout.svelte` уже вызывает `initLocale()` на клиенте.
### Пробелы в новом UI
- `LangToggle.svelte` — заглушка с локальным `let lang = $state("RU")`, не
связана с `locale.svelte.ts`.
- Новый UI почти целиком на хардкод-английском:
- ~150+ строк по ~30 компонентам и маршрутам (заголовки, кнопки, аria,
плейсхолдеры, подписи секций);
- 122 инструмента с английскими `title`/`description` в `lib/registry/*.ts`;
- ~60+ подписей опций select;
- ~30 заголовков layout-групп (`title: "Canvas"`, `"Background"`, …);
- вердикты анализаторов — raw English строки (`"Yes — valid PNG signature."`),
при том что v1 возвращает ключи словаря (`"verifyYes"`) и рендерит их через
`tools[id].results`;
- `labelOf(id)` в `SchemaFields.svelte` генерирует подписи полей из field ID
(keepAspect → "Keep Aspect");
- категории в `catalog.ts` (`GROUP_LABELS`) — English uppercase, хотя в `dict`
уже есть ключи `categories.X`.
- Из всех новых компонентов `t()` вызывает только `SchemaToolView.svelte`, и
только для ошибок (`ToolError.key`).
### Совпадение регистров
- Новый registry и старый v1 registry используют **одинаковые id** инструментов
(resize-png, flip-png, …) — переводы title/description из v1 `ru.ts` служат
исходником для новой ветки `lib/i18n/ru.ts` (копирование данных при наполнении
Фазы 2, не общий модуль).
- **Расхождение**: param ids в ru.ts (`width`/`height`/`keepAspect`) не равны
field ids новоно schema (`size` dimension, `anchor`). Нужны новые переводы
полей.
- `ru.ts` tools[id].results для анализаторов уже содержит ключи (`verifyYes`,
`grayscaleNo`, `orientationPortrait` и т.д.).
### Архитектурные ограничения
- Изоляция old↔new (`isolation/no-mixed-imports`): сейчас `lib/i18n/` — shared.
По решению задачи (Фаза 0) i18n **форкается**: v1 получает полную копию
`lib/v1/i18n`, а `lib/i18n` переходит в ветку «new» (правило изоляции запретит
старому коду импортить новый i18n). До форка в общем модуле два скрытых
v1-импорта: `dict.ts` тянет `CategoryId` из `../v1/categories`,
`tool-strings.ts` — `$lib/v1/registry`; после форка оба живут только в копии.
Новому UI нужен параллельный `schema-tool-strings.ts` поверх `$lib/registry`.
- Tone detection в `SchemaTextResult.svelte`: `/^(yes|true)/i` по value —
сломается при русских вердиктах («Да», «Нет»).
- `tools-i18n.test.ts` покрывает только v1 TOOLS; нужен аналог для нового
registry.
- `known-issues.spec.ts`:
`test.fixme("error message is localized, not a raw i18n key")`.
## 3. Как делаем
Принципы:
- Мелкие атомарные коммиты (правило AGENTS.md); самому не коммитить — коммитит
разработчик после ревью.
- Изменения «снизу вверх»: сначала общий слой i18n, потом словари, потом UI.
- Каждый этап заканчивается зелёными `pnpm --dir web lint` +
`pnpm --dir web check` + тесты.
- Решения по §5 зафиксированы и учитываются с первых правок словарей.
## 4. Фазы
### Фаза 0: Форк i18n для v1 + подготовка общего слоя
Выполняется строго до остальных фаз, без изменения поведения UI. Цель — два
независимых i18n-слоя: v1 получает замороженную копию, новый UI — свою ветку,
которую дальше меняем свободно. На выходе фазы — зелёные `lint`, `check`,
`test`, `test:rules`.
- **0.1** Форк: скопировать `web/src/lib/i18n/` → `web/src/lib/v1/i18n/` (полный
снимок: `dict.ts`, `en.ts`, `ru.ts`, `t.ts`, `locale.svelte.ts`,
`matching.ts`, `tool-strings.ts` + все 5 тестов). v1 работает дальше только с
копией — поздние правки нового i18n её не затрагивают.
- **0.2** Привести копию к v1-канону (относительные пути от нового места):
- копия `dict.ts`: импорт `CategoryId` `../v1/categories` → `../categories` (=
`lib/v1/categories`);
- копии тестов (`tools-i18n.test.ts`, `search-coverage.test.ts`,
`smoke-i18n.test.ts`): `../v1/registry` → `../registry` (=
`lib/v1/registry`);
- импорт `$lib/v1/registry` в копии `tool-strings.ts` остаётся валидным;
- перепроверить остальные относительные и `$lib`-пути в копии.
- **0.3** Переключить импорты старого UI: все `$lib/i18n/*` в `routes/v1/**` и
`lib/v1/components/**` (39 импортов в 23 файлах) → `$lib/v1/i18n/*`.
- **0.4** Очистить новый `lib/i18n/` от v1-специфики, иначе isolation поймает
new→old:
- удалить `tool-strings.ts` (завязан на v1-типы `ParamDef`/`ToolEntry`; для
нового UI вместо него `schema-tool-strings.ts`, см. 0.8);
- удалить v1-центричные тесты `tools-i18n.test.ts`, `search-coverage.test.ts`,
`smoke-i18n.test.ts` (они импортируют `../v1/registry`; живут только в
копии);
- остаются: `dict.ts`, `en.ts`, `ru.ts`, `t.ts`, `locale.svelte.ts`,
`matching.ts`, `i18n.test.ts`, `matching.test.ts`.
- **0.5** Обновить правило изоляции и конфиги:
- `isolation/no-mixed-imports.js`: добавить `lib/i18n/**` в `DEFAULT_NEW`
(копия по-прежнему old через `lib/v1/**`);
- `eslint-plugins/__tests__/no-mixed-imports.test.ts`: кейсы «old → i18n
чисто» (строки 83–89) и «lib/i18n — shared» (строки 99–105) обновить под
новую классификацию: old→новый i18n = нарушение; добавить фикстуру
`__fixtures__/src/lib/v1/i18n/t.ts` для кейса old-копия;
- `eslint.config.js`: в `allowDefaultProject` добавить фикстуру
`eslint-plugins/__fixtures__/src/lib/v1/i18n/t.ts`; поправить комментарий
изоляции «Общее (core/, i18n/, theme)» → «Общее (core/, theme)»;
- `eslint-plugins/README.md`: убрать i18n из списка shared (§ Isolation).
- **0.6** Новый `lib/i18n/dict.ts`: `CategoryId` переключить с
`../v1/categories` на `../categories` (новый структурно-идентичный тип) —
скрытых v1-зависимостей в новом i18n не остаётся.
- **0.7** Проверка форка: `pnpm --dir web lint`, `check`, `test` (v1-копия
тестов подхватывается `vitest include: src/**/*.test.ts`), `test:rules`;
смоук: v1-страницы и оба переключателя RU/EN работают как раньше, языки обеих
версий согласованы.
- **0.8** Добавить `lib/i18n/schema-tool-strings.ts` поверх `$lib/registry`
(новый тип `ToolEntry`):
- `toolTitle(tool)`, `toolDescription(tool)` с фолбэком на registry English;
- `fieldLabel(field, id)` — перевод по ключу из `fields`-секции словаря (см.
решение C), фолбэк на `labelOf(id)`;
- `groupLabel(title)` — перевод по ключу из `groups`-секции словаря (см.
решение A);
- `verdictText(toolId, key)` — рендер вердикта по ключу;
- `verdictTone(key)` — `success`|`danger`|`info` по имени ключа
(`*Yes`/`*No`/`*Portrait`/`*Landscape`) — не по строке.
- **0.9** Расширить тип `Dict` (только новая ветка `lib/i18n`):
- секция `preview` **не заводится**: строки нового UI живут в собственных
категориях — расширяем существующие (`catalog`, `header`, `dropZone`,
`sourceCard`, `resultCard`, `paramsCard`, `textResult`, `ui`) и заводим две
маленькие новые: `actions` (Generate / Open image / Download result…) и
`textSource` (placeholder, Try sample…);
- новая глобальная секция `fields: Record<string, string>` — переводы подписей
полей по ключу из схемы (решение C);
- новая глобальная секция `groups: Record<string, string>` — переводы
заголовков групп полей (решение A).
### Фаза 1: Словари — наполнение (только новая ветка `lib/i18n`, v1 не трогаем)
- **1.1** Заполнить `en.ts` и `ru.ts` секциями `actions`, `textSource`,
`fields`, `groups` и ключами недостающих строк в существующих секциях.
- **1.2** Проинвентаризировать разницу между 122 инструментами нового registry и
ключами `ru.tools`: для инструментов без перевода (нет в v1) добавить
title/description в ru.ts новой ветки (по категориям — несколько коммитов).
- **1.3** Добавить в `ru.ts` ключи `fields`/`groups` для используемых в схемах
ключей полей и заголовков групп (по ходу проставления ключей в schema, Фаза
5).
- **1.4** Добавить `tools[id].options[fieldId][value]` для select-полей, чьи
опции нужно локализовать.
- **1.5** Тест полноты (задел на Фазу 6): для каждого ключа label в schema есть
перевод в обоих словарях.
### Фаза 2: LangToggle → реальный locale
- **2.1** `LangToggle.svelte`: убрать локальный `lang`, подключить
`getLocale()`, `setLocale()`, `LOCALES`. Модель — language switch из
`routes/v1/+layout.svelte`.
- **2.2** Проверить e2e `navigation.spec.ts` «language toggle marks the active
button» — должен проходить без изменений.
### Фаза 3: Routes + layout
- **3.1** `+layout.svelte`: breadcrumbs через `t()`, динамический `lang`.
- **3.2** `+page.svelte`, `list-tools/+page.svelte`: footer-строка →
`t("header.footerNote")`.
- **3.3** `tools/[id]/+page.svelte`: «Tool not found», fallback title → `t()`.
### Фаза 4: Компоненты — основной блок (коммиты кучками по смыслу)
Все текстовые подписи в компонентах — информативные, сохраняем и переводим на
`t`/хелперы. Каждый коммит: замена строк + lint/check.
- `SchemaToolView.svelte`: title/description →
`toolTitle()`/`toolDescription()`, перевести `TOOL SETTINGS`, «Configure
output» и «This tool has no schema yet.».
- `SchemaSourceTile.svelte`: перевести figcaption `SOURCE`, «choose an
image»/«no source…» и alt.
- `SchemaResultTile.svelte`: перевести figcaption `RESULT`, «no result yet» и
alt; суффикс «parts» — в `PreviewTile`.
- `SchemaPreview.svelte`: перевести панельный лейбл `PREVIEW PANEL` и
meta-капшены `SOURCE / RESULT / FORMAT`; значения «parts»/«text» → `t()`.
- `SchemaActions.svelte`: Generating…, Generate, Open image, Download result.
- `SchemaFields.svelte`: Reset, «updates automatically», `labelOf(field.id)` →
`fieldLabel(field)`.
- `SchemaTextSource.svelte`: placeholder, Try sample, Render text.
- `SchemaTextResult.svelte`: перевести лейбл `RESULT` у вердикта, Copy /
Download .txt; tone detection → `verdictTone()`.
- `CatalogToolbar.svelte`: фильтры → `t("categories.all")` +
`t("categories.X")`, поисковый placeholder.
- `CatalogHeader.svelte`: перевести heading/lead и счётчик «TOOLS AVAILABLE».
- `CatalogGroup.svelte`: перевести суффикс счётчика «TOOLS».
- `Dropzone.svelte`: текст зоны → `t("dropZone.pickDefault")`.
- Schema field controls (по одному или группой): Width/Height, From/To, Backing
plate, Opacity, Font/Size/Color/Bold, Sans/Serif/Mono, Angle, «+ Add color»,
«Remove color».
- `TopNav.svelte`, `ThemeToggle.svelte`, `CodeBlock.svelte`, `StepCard.svelte`,
`Footer.svelte`.
### Фаза 5: Вердикты + ключи в schema
- **5.1** Анализаторы в `registry/analyze.ts`: возвращают ключи (`"verifyYes"`
вместо raw English).
- **5.2** `SchemaPreview.svelte`: рендер вердикта через
`verdictText(tool.id, key)`.
- **5.3** Layout groups в `registry/*.ts` (решение A): `title` → ключ
(`title: "groups.canvas"`); в `SchemaFields.svelte` — `groupLabel(title)`. В
`Dict` — глобальная секция `groups`.
- **5.4** Лейблы полей в `registry/*.ts` (решение C): на поле в схеме
проставляется `label`-ключ (`field.number({ label: "fields.thickness", … })`),
вывод в UI — `fieldLabel(field)` = `t(label)`. В `Dict` — глобальная секция
`fields`.
- **5.5** Категории в `catalog.ts`: `GROUP_LABELS` → `t("categories.X")` (ключи
уже есть в dict).
### Фаза 6: Тесты
- **6.1** `tools-i18n.test.ts` — полнота словарей для текущего registry:
title/description ru для всех инструментов, каждый `label`-ключ схемы есть в
`fields`, каждый `groups`-ключ есть в `groups`, без лишних ключей.
- **6.2** Фикс `known-issues.spec.ts` test.fixme — ошибки локализуются.
- **6.3** В новой ветке `lib/i18n` появится собственный `smoke-i18n.test.ts`
(аналог v1: переключение локали, `html lang`, контент без смеси языков) —
v1-копия теста не трогается.
### Фаза 7: Кросс-языковой поиск
Переключить поиск в новом каталоге с `toLowerCase()` на
`scoreDoc()`/`normalizeForSearch()` из `matching.ts` (входит в объём, см.
решение D). Последний шаг, аккуратный отдельный коммит.
### Фаза 8: Линтер словарей (i18n key linter)
Замена ручной проверки ключей на автоматическую. Инфраструктура уже есть:
`eslint-plugins/` умеет читать соседние файлы (`isolation/no-mixed-imports`) и
гоняться фикстурами (`test:rules`).
- **8.1** Новое правило `i18n/dict-consistency` в `web/eslint-plugins/`.
Применяется к файлам-словарям (конвенция: экспортируют `Dict`, лежат в
`lib/i18n/`); новые локали подхватываются автоматически — правка правила не
нужна.
- **8.2** Проверки — **только `warn`, никогда `error`** (пропущенный перевод не
валит сборку):
- **Паритет дерева ключей**: каждый ключ (dot-path `a.b.c`) присутствует во
всех остальных локалях, не только против BASE. Нет —
`missing key "x.y" in en`.
- **Пустые значения**: `""` и строка из одних пробелов —
`empty value "x.y" in en`.
- **Паритет плейсхолдеров**: у одинакового ключа набор `{name}` одинаковый во
всех локалях — поймать потерянную при переводе переменную.
- **8.3** Цель — zero-warn. Сейчас `en.ts` неполный (в `tools` только
результаты-помощники, остальной английский — из реестра), поэтому после
включения будет много warn'ов: доводим `en.ts` до полноты `ru.ts` (секции +
`tools` полностью, source — registry-English, без дрейфа). До достижения цели
допускается allowlist-механизм в ядре правила для осознанных отклонений.
- **8.4** Тесты правила — фикстуры в `web/eslint-plugins/__fixtures__/`, док в
`web/eslint-plugins/README.md`; включение в `eslint.config.js` как `"warn"`.
### Фаза 9: Множественные числа (plural-формы)
- **9.1** Тип значения словаря расширяем: `string | PluralForms`, где
`PluralForms = { other: string } & Partial<{ one, few, many }>` по
CLDR-правилам локали (ru: обязательны `one/few/many`; `other` — всегда, как
фолбэк). Новая функция `tPlural(path, n)` в `lib/i18n/`: выбирает категорию
через `Intl.PluralRules(locale).select(n)` и подставляет её с интерполяцией
`{n}`.
- **9.2** Первые потребители: «часть/части/частей» (split-into-parts-png),
счётчики инструментов (`catalog.toolsCount`), «N параметров», «N файлов».
- **9.3** Паритет в линтере (Фаза 8): если ключ — plural-объект в одной локали,
он обязан быть plural-объектом во всех остальных (тип-паритет; string в одной
из локалей — warn). Набор форм между локалями не сравнивается дословно — он
проверяется по правилам конкретной локали (`other` обязателен; en: one/other,
ru: one/few/many).
- **9.4** Открытый вопрос на этапе реализации: неоднозначность «объект-секция»
(`Record<string, string>`) vs plural-объект в дереве словаря. Варианты:
отдельная секция `plurals` в словаре, конвенция имён ключей, вывод типа из
контекста. Фаза начинается с проработки этого вопроса, не с кода.
## 5. Решения
- **A. Layout groups**: заголовки групп храним i18n-ключами прямо в схеме
(`title: "groups.canvas"`), перевод — в глобальной секции `groups` словаря
(`Dict.groups: Record<string, string>`). В UI — `groupLabel(title)` =
`t(title)` с фолбэком на сырой ключ.
- **C. Лейблы полей**: в схеме задаём i18n-ключ
(`field.number({ label: "fields.thickness", … })`), в UI выводится перевод —
`fieldLabel(field)` = `t(label)`. Ключи — глобальная секция `fields` словаря,
фолбэк на текущий `labelOf(id)` для несопоставленных.
- **D. Объём**: переводим всё сразу — и каркас, и контент реестра, и вердикты, и
кросс-языковой поиск (Фаза 7). Крупные изменения кода разбиваем на небольшие
атомарные шаги/коммиты.
## 6. Проверки
- `pnpm --dir web lint`
- `pnpm --dir web exec svelte-check --tsconfig ./tsconfig.json`
- `pnpm --dir web test`
- `pnpm --dir web test:rules` (если менялся eslint/isolation-конфиг)
- Смоук: переключатель RU/EN на новом UI; страницы каталога/инструмента, ошибка,
вердикт — без смеси языков; `html lang` следует за локалью.
-447
View File
@@ -1,447 +0,0 @@
# План: переезд на новый дизайн (refs) — параллельная сборка
> Статус: C17 **выполнен** (`docs/archive/plan-c17.md`), C18 **выполнен**.
> Остались C20 (шаг 6, проверка нового дизайна) — **следующий этап**. C19/C21
> (шаг 7, удаление старого) — **отложены**, нет ценности. Они больше не
> привязаны к «верности против рефа»: `docs/archive/plan-design-fix.md` закрыт,
> аудит против рефа прекращён. Tech debt закрыт
> (`docs/archive/plan-tech-debt.md`). Backlog №14 (переезд старых файлов в
> `old/`) — **выполнено**. Покупка домена НЕ была блокером C17: домен нужен для
> S1 (`plan-seo.md`) — canonical, sitemap, снятие `noindex`.
>
> Источники: `refs/` — Next/React-референс (правда по пикселям и интерактиву),
> `refs-html/` — статические HTML-снимки для быстрого просмотра в браузере.
>
> **Стратегия изменена.** Вместо in-place рестайла (старый план A–F, правка
> общих компонентов на месте с алиасами токенов) — **параллельная сборка**:
> новый дизайн строится отдельно, изолируется от старого, проверяется целиком,
> затем становится основным и старый удаляется. Старое приложение остаётся
> рабочим и шippable на всём протяжении перехода.
## 0. Почему меняем стратегию
- **Старый план (A–F):** правим `ui/*` и страницы на месте, держим старые токены
как алиасы, чтобы ничего не сломалось до этапа F. Риск: общие компоненты
редактируются вживую → регресс старого UI во время работы, новый дизайн нельзя
принять изолированно.
- **Новый подход (5 фаз):** (1) строим новый ui-kit, (2) строим новые страницы
рядом, не трогая старые, (3) проверяем новый дизайн, (4) делаем его основным,
(5) удаляем старый. Ниже фазы 1–5 — это требуемые шаги.
- **Главный технический риск и его решение.** Сейчас `web/src/app.css`
импортируется в **корневом** `web/src/routes/+layout.svelte:2` и потому
грузится на ВСЕХ маршрутах, включая новые. Решение — **не префиксовать новый
CSS (никакого `.ds2`), а разнести глобальные стили по layout'ам без
наследования**: (а) убрать `import '../app.css'` из корневого layout; (б)
старые маршруты обернуть группой `(old)/` с собственным layout, импортирующим
`app.css`; (в) новые маршруты (`preview/*`) — собственный layout,
импортирующий `design2.css`. SvelteKit делает code-splitting CSS по маршрутам,
а стили компонентов скоупятся сами — поэтому глобальные стили каждого дизайна
живут только в своей ветке, и префикс-обёртка `.ds2` не нужна. Старые `ui/` и
страницы не трогаются до фазы 5.
## 1. Источники и их оценка
### `refs-html/` — витрина разметки
Покрытие достаточное:
| Файл | Что показывает | Оценка |
| ------------------------- | ----------------------------------------- | ---------------- |
| `index.html` | Главная: pipeline-воркспейс + превью | ✅ |
| `demo.html` | Зеркало `index.html` (идентичны) | ⚠️ дубликат |
| `gradient.html` | Инструмент «Градиент»: настройки + превью | ✅ (тёмная тема) |
| `background-remover.html` | Инструмент «Удаление фона»: source/result | ✅ (тёмная тема) |
| `list-tools.html` | Каталог инструментов | ✅ |
- **Статика без JS** — интерактив (тумблеры/слайдеры/перетаскивание) брать из
кода `refs/`, не из HTML. HTML — только визуальная правда по разметке/классам.
- По 4 файлам собран инвентарь классов — это и есть спецификация kit'а:
`app-shell`, `page-grid`, `eyebrow`, `brand-mark`, `topbar`, `footer`,
`panel-heading`, `setting-group`, `settings-footer`, `settings-panel`,
`segmented`, `status`, `step-card` (`step-index`/`step-heading`/`step-type`/
`step-body`/`drag`), `color-field` (`swatch`/`native-color`),
`preview-toolbar`/`checker`
(`tile-canvas`/`remover-canvas`/`comparison-grid`), `result-meta`/`meta-row`,
`tool-card`/`image-card` (`tool-icon`/`tool-index`/ `tool-copy`),
`code-block`, `icon-btn`, `download-btn`, `reset-btn`, `lede`, `auto-note`,
`label`, `toggle`/`toggle-group`, `direction-grid`, `gradient-bar`.
- **Дыры в refs-html:** нет состояний empty/error/loading, нет глобальных
модалок/тултипов, i18n не представлен (все лейблы — английские брендовые).
Решения — см. §9.
## 2. Токены (фундамент нового дизайна)
Живут в `src/lib/styles/design2.css` как обычный `:root { … }` (плюс
`[data-theme='dark'] { … }`). Файл грузится **только** layout'ом новой ветки
(§5), поэтому глобально не конфликтует со старым `app.css`. Маппинг старых
токенов `web/src/app.css` → новые:
| Старый | Новый | Light | Dark |
| -------------- | -------------- | --------- | ------------------------ |
| `--bg` | `--background` | `#eef1f4` | `#11171d` |
| `--surface` | `--panel` | `#f8fafb` | `#182129` |
| `--text` | `--foreground` | `#17212b` | `#e8eef2` |
| `--text-muted` | `--muted` | `#6d7883` | `#91a0ac` |
| `--border` | `--line` | `#cbd3da` | `#33414c` |
| `--accent` | `--blue` | `#1769d2` | `#54a2ff` |
| — | `--cyan` | `#00a8c7` | `#00a8c7` |
| — | `--amber` | `#bd7411` | `#bd7411` |
| — | `--success` | `#25a96a` | `#25a96a` (статус-точки) |
| `--danger` | без изменений | `#e5484d` | `#ff7479` |
Прочее:
- Радиусы: один `--radius: 4px` (мягкость старой темы уходит).
- Шрифты: `IBM Plex Sans` + `IBM Plex Mono`, self-host через `@fontsource`
(offline-first, CDN нельзя). Mono — рабочий шрифт для лейблов/метаданных/
цифр; Sans — заголовки и текст. Задаются на `.app-shell` (корень новой ветки),
чтобы не зависеть от старых глобальных правил `body`.
- Фон-сетка (32px blueprint-grid) и шахматка канвасов — утилиты `.page-grid` и
`.checker-canvas` внутри `.ds2`, не токены.
- Тёмная тема — на `[data-theme='dark']` (инлайн-скрипт уже есть в `app.html`);
класс `.dark-mode` из рефа не переносим, используем наш механизм. На
переходный период **никаких алиасов старых токенов** (старые страницы сами
живут на своём `app.css`).
## 3. Базовые примитивы дизайн-системы (все — компоненты)
Правило фазы 1: **любой повторяющийся паттерн — компонент**, даже «просто div с
двумя стилями». Никакой копипасты классов между страницами. Каждый примитив ниже
— отдельный `.svelte` в `src/lib/components/kit/`.
- `MonoLabel` — микро-лейбл: mono 10px, uppercase, letter-spacing .1em, muted;
слот-модификатор accent (синие eyebrow/version).
- `StatusDot` / `StatusLine` — точка `--success` + подпись («AUTO PIPELINE»,
«LIVE PREVIEW»).
- `Panel` / `PanelHeading` / `SettingGroup` / `SettingsFooter` — панель с
1px-бордером, составные части с разделителями.
- `CheckerCanvas` — рабочая поверхность превью: тёмная подложка + шахматка.
- `MetaRow` / `MetaList` — пары «CAPTION значение».
- `Segmented` — сегмент-переключатель (язык, тип градиента, пресеты).
- `Badge` — тег типа шага (BACKGROUND/TRANSFORM/STYLE), чип «✓ AUTO».
- `StepCard` — нумерованная карточка этапа: индекс `01`, drag-handle, Badge
типа, заголовок, кнопка удаления; слот для параметров.
- `ToolCard` — карточка каталога (icon, index, title, copy, arrow).
- `ImageCard` / `PreviewTile` — label сверху + canvas-превью.
- `AppShell` — корневой контейнер `.app-shell` + `.page-grid` (обёртка новой
ветки маршрутов); задаёт шрифты и фон нового дизайна.
- `BlueprintGrid` — фоновая сетка (используется в `AppShell`).
- `IconButton`, `Icon` (обёртка `lucide-svelte`, 16–17px, muted).
## 4. Минимальный UI-kit (`src/lib/components/kit/`)
Контролы, поверх примитивов §3. API проектируем сразу под новый дизайн (обратной
совместимости со старым `ui/` не нужно — старые страницы его продолжают
использовать).
| Компонент | Назначение |
| ------------------------- | ----------------------------------------------------------------- |
| `Button.svelte` | `primary` (solid blue) / `secondary` (outline) / `ghost` + `icon` |
| `Field.svelte` | обёртка: `MonoLabel` + слот значения/контрола |
| `SliderField.svelte` | range + `<output>`, опц. строка подсказок |
| `ColorField.svelte` | swatch + hex-input + нативный picker |
| `TextField.svelte` | текстовый ввод в стиле дизайна |
| `SelectField.svelte` | селект в стиле дизайна |
| `CheckboxField.svelte` | чекбокс → switch-toggle 32×18 |
| `Toggle.svelte` | переключатель (для toggle-group) |
| `SegmentedControl.svelte` | на базе `Segmented` — RU/EN, Linear/Radial, пресеты |
| `Badge.svelte` | на базе `Badge` |
| `MetaList.svelte` | на базе `MetaRow` |
| `EmptyState.svelte` | пустое состояние под новый стиль |
| `CodeBlock.svelte` | блок кода (gradient-art / direction-grid) |
| `DownloadButton.svelte` | кнопка скачивания (можно переиспользовать идею старой) |
Иконки: `lucide-svelte` (те же пути, MIT), 16–17px, stroke, muted.
## 5. Изоляция старого и нового (критично)
Без префиксов и обёрток — только за счёт того, какие layout грузят какой CSS.
- **Корневой layout `src/routes/+layout.svelte`** перестаёт импортировать
`../app.css`. В нём остаётся только общая логика (init темы/locale) и
нейтральный `<div>{children}</div>` без глобальных стилей дизайна.
- **Старая ветка `src/routes/(old)/+layout.svelte`** импортирует `app.css` и
содержит текущую шапку/подвал (разметку из нынешнего корневого layout). Все
старые страницы переносятся внутрь `(old)/` (группа в скобках не меняет URL):
`(old)/+page.svelte`, `(old)/demo/...`, `(old)/list-tools/...`,
`(old)/tools/[id]/...`.
- **Новая ветка `src/routes/preview/+layout.svelte`** импортирует `design2.css`
и рендерит `<AppShell>`. Сюда же — новые страницы (§6).
- Благодаря code-splitting CSS в SvelteKit: на старых маршрутах грузится только
`app.css`, на `preview/*` — только `design2.css`. Скоуп стилей
Svelte-компонентов добавляет второй уровень изоляции. Никаких `.ds2`.
- Переключатель для ручной проверки: временный флаг `previewDesign` в
`localStorage` + кнопка в старой шапке («Preview v2»), ведущая на `/preview`.
На фазе 4 флаг убираем.
> Почему не `(new)`-группа с теми же путями вместо `preview/*`: две группы не
> могут одновременно определять один и тот же URL (`(old)/+page.svelte` и
> `(new)/+page.svelte` оба → `/` = конфликт маршрутов). Поэтому новая ветка
> живёт под префиксом `preview/`, а на фазе 4 файлы переносятся на реальные пути
> и группа `(old)` удаляется.
>
> Это совпадает с рекомендованным в документации SvelteKit паттерном: «put your
> entire app inside one or more groups except the routes that should not inherit
> the common layouts» — старое целиком в `(old)`, новое снаружи. Синтаксис
> `@`-breakout (`+layout@.svelte`, `+page@.svelte`) здесь не требуется:
> «выломаться» можно лишь до корня, а нам нужно, чтобы именно у корня НЕ было
> `app.css`; если позже захотим держать новые маршруты внутри группы, `@`
> пригодится для сброса родительского layout.
## 6. Маршрутизация параллельных страниц
Новые страницы живут в **отдельной ветке `preview/*`**, не трогая старые:
```txt
src/routes/+layout.svelte # нейтральный, БЕЗ app.css
src/routes/(old)/+layout.svelte # импорт app.css + старая шапка/подвал
src/routes/(old)/+page.svelte # / (старое)
src/routes/(old)/demo/+page.svelte # /demo
src/routes/(old)/list-tools/...
src/routes/(old)/tools/[id]/...
src/routes/preview/+layout.svelte # импорт design2.css, <AppShell>
src/routes/preview/+page.svelte # /preview — воркспейс (index/demo)
src/routes/preview/demo/+page.svelte # /preview/demo
src/routes/preview/list-tools/+page.svelte
src/routes/preview/tools/[id]/+page.svelte
```
- Каждая новая страница импортирует **только** `kit/*`. Старые страницы не
изменяются (только перемещены в `(old)/`).
- Фаза 4: содержимое `preview/*` переносится на реальные маршруты (move-файлы),
группа `(old)/` удаляется, `design2.css` подключается в корневом
`+layout.svelte` (как новый основной слой), временный флаг/кнопка убираются.
## 7. Фазы выполнения (1–5 по запросу)
Каждая фаза заканчивается зелёными `test`/`check`/`build` и ревью в браузере
(обе темы) против `refs-html/`.
- **Фаза 1 — Фундамент + UI-kit.** Токены §2, `design2.css` по §5, все примитивы
§3 и контролы §4 как отдельные компоненты. Критерий: демо-страница kit'а
(`/preview` или временная) показывает все состояния (light/dark,
hover/disabled, все варианты Button/Segmented/Badge); unit-тесты форм не
падают; **старые страницы визуально не изменились** (изоляция работает).
- **Фаза 2 — Новые страницы.** Под `preview/*` (§6): воркспейс, demo,
list-tools, tool-page (двухпанельная раскладка из рефа: settings-panel +
preview-panel, step-cards для chain). Критерий: разметка соответствует
`refs-html/index.html`, `gradient.html`, `background-remover.html`,
`list-tools.html`; интерактив (слайдеры/тоглы/загрузка/автозапуск) работает в
новом стиле; старые маршруты продолжают работать как раньше.
- **Фазa 3 — Проверка нового дизайна.** Смоук §8 (обе темы, узкий экран, 800px),
скриншоты против `refs-html/`, прогон старого тест-сьюта для доказательства
отсутствия регресса, RU/EN-локализация новых лейблов. Критерий: визуальное
совпадение с рефом, зелёные тесты, записанные скриншоты приняты.
- **Фаза 4 — Сделать новый дизайн основным.** Перенос `preview/*` → реальные
маршруты; `design2.css` подключается в корневом `+layout.svelte`; удаление
preview-группы и временного флага/кнопки. Старые страницы теперь не
используются (мёртвый код), но физически ещё в репо. Критерий: реальные
маршруты отдают новый дизайн, тесты зелёные, билд без предупреждений.
- **Фаза 5 — Удалить старый дизайн.** Удаление старых страниц, старого `ui/`,
старого `app.css` (и его токенов), неиспользуемых классов; наведение порядка в
`components/` (имя каталога и подпапок — решается на фазе 1; к C17 `kit/` уже
разложен в `components/`). Финальный проход по брейкпоинтам 1200/1100/800/480.
Критерий: `grep` по старым токенам/классам пуст, build чистый, старый дизайн
нигде не упоминается.
Оценка объёма: Фаза 1 — фундамент+kit (~1 день), Фаза 2 — страницы (~1–2 дня),
Фаза 3 — проверка (~полдня), Фаза 4–5 — переключение и чистка (~полдня).
## 8. Смоук (после фазы 3, контроль на фазе 4)
1. Светлая и тёмная тема: сетка фона, панели, шахматка канвасов читаются,
контраст mono-лейблов достаточный.
2. Инструмент с параметрами (градиент): slider/color/toggle в стиле
setting-groups, автозапуск не регрессировал.
3. Удаление фона: сравнение source/result, маска, пипетка работают на новой
шахматке.
4. Chain из 3+ инструментов: step-cards рендерятся, удаление/сброс работают.
5. Узкий экран 800px: настройки над превью, sticky выключается как в рефе.
6. RU/EN: новые лейблы локализованы; mono-лейблы рефа — либо ключи i18n, либо
осознанно английские брендовые элементы (см. §9).
## 9. Открытые вопросы
- **Имя финального каталога компонентов:** решено на C1 — писали в `kit/`, C17
разложил его в `components/` (подпапки fields/layout/ui). На C19 — только
навести порядок внутри `components/`, переименование не требуется.
- **Изоляция через layout:** `app.css` вынесен из корневого layout в
`(old)/+layout.svelte`; новый `design2.css` — только в `preview/+layout`.
Проверить на фазе 1, что на `/preview/*` не попадают стили старого дизайна
(DevTools → вкладка Styles, нет правил из `app.css`).
- **Переключатель для проверки:** рекомендую `localStorage`-флаг + кнопка
«Preview v2» в старой шапке + группа `preview/*`. Убрать на фазе 4.
- **Mono-лейблы рефа на английском** (`PROCESSING PIPELINE`): завести i18n-
ключи, но допустить часть английских брендовых («приборная панель»).
- **Версия в шапке/футере:** из `package.json` через `define` или захардкодить —
мелочь, решить при сборке topbar.
- **`refs/` и `refs-html/` — одноразовый источник, НЕ поддерживаемый.** После
внедрения нового дизайна все правки дизайна делаются сразу в продакшен-коде
(`design2.css` + `kit/`), а не в `refs/`. Механизм синхронизации «ref → код»
не нужен и не создаётся. `refs/` и `refs-html/` можно удалить из репо на фазе
5 (по желанию), они не являются частью рантайма.
## 10. План исполнения (мелкие коммиты)
Ветка `feat/redesign`. Каждый шаг — отдельный коммит, цель < ~500 строк. Перед
стартом: `test`/`build` зелёные на базе (якорь регрессии).
### Шаг 0. Изоляция layout (без дизайна)
- [x] **[C1]** Сделать корневой `routes/+layout.svelte` нейтральным: убрать
`import '../app.css'`, оставить только init темы/locale и
`<div>{children}</div>`.
- [x] **[C2]** Создать `routes/(old)/+layout.svelte` (импорт `app.css` + старая
шапка/подвал) и перенести старые страницы (`+page`, `demo`, `list-tools`,
`tools/[id]`) в `(old)/`. URL не меняются. Проверка: старый сайт
идентичен. _(файлы — move, объём строк минимален)_
### Шаг 1. Фундамент нового дизайна
- [x] **[C3]** Создать `src/lib/styles/design2.css` (токены `:root` +
`[data-theme='dark']` по §2) и подключить `@fontsource/ibm-plex-sans` +
`@fontsource/ibm-plex-mono` (self-host, offline-first). подключить шрифты
в `design2.css` (self-host, offline-first).
- [x] **[C4]** _(слит в C3: шрифты нельзя подключить до создания `design2.css`)_
`[data-theme='dark']` (таблица §2). Без утилитарных классов.
- [x] **[C5]** Примитивы batch 1: `AppShell`, `BlueprintGrid`, `CheckerCanvas`,
`MonoLabel`, `StatusDot`/`StatusLine`.
- [x] **[C6]** Примитивы batch 2: `Panel`, `PanelHeading`, `SettingGroup`,
`SettingsFooter`, `MetaRow`/`MetaList`, `Segmented`, `Badge`, `StepCard`,
`ToolCard`, `ImageCard`/`PreviewTile`.
- [x] **[C7]** Контролы: `Icon`, `IconButton`, `Button`, `Field`, `SliderField`,
`ColorField`, `TextField`, `SelectField`, `CheckboxField`, `Toggle`,
`SegmentedControl` (= `Segmented` из C6), `EmptyState`, `CodeBlock`,
`DownloadButton`.
### Шаг 2. Витрина kit (проверка компонентов)
- [x] **[C8]** `preview/+layout.svelte` (импорт `design2.css` + `<AppShell>` +
переключатель темы) и временная `preview/kit` страница со всеми
состояниями (light/dark, hover/disabled, варианты Button/Segmented/Badge и
пр.)
### Шаг 3. Новые страницы
- [x] **[C9]** `/preview` — воркспейс (eyebrow, h1, lede, поиск, «открыть
последний»). Поиск фильтрует `TOOLS` из `$lib/registry`; карточки
результата пока ведут на `#` (реальные `/preview/tools/[id]` подключим на
C12, иначе prerender падает на 404).
- [x] **[C10]** `/preview/list-tools` — каталог карточек (группировка по
`CATEGORIES`, `ToolCard`). Ссылки пока `#` (реальные `/preview/tools/[id]`
подключим на C12).
- [x] **[C11]** `/preview/demo` — зеркало воркспейса (4 шага + панель превью),
статичный приёмочный стенд.
- [x] **[C12]** `/preview/tools/[id]` — data-driven settings-panel +
preview-panel (gradient live, остальные — плейсхолдер); `NumberField`,
`entries()` для prerender, ToolCard-ссылки подключены.
- [x] **[C13]** `/preview/tools/[id]` — comparison-grid для file-source
инструментов (`Dropzone` + before/after `CheckerCanvas`, `result-meta`);
охватывает background-remover и прочие.
- [x] **[C14]** `/preview/tools/[id]` — исполняемая цепочка: `StepCard`-шаги
(параметры inline) + «Add step» (chainable), живой прогон пайплайна на
исходнике, превью каждого шага + итог + Download.
### Шаг 4. Переключатель и проверка
- [x] **[C15]** В старой шапке ссылка «Preview v2» → `/preview`, ставит
`localStorage['easy-png-tools:preview-v2']='1'`.
- [x] **[C16]** База под дизайн: рабочий переключатель тем (persist в
localStorage, применяется на `<html>`; проверено — фон
`#eef1f4`↔`#11171d`), `TopBar` приведён к виду `demo.html` (бренд + синий
хлебный крош `/* страница */`, безрамочные иконки, язык — active =
`--foreground`/`--background`), страницы `/preview/*` на всю ширину (убран
`max-width`). Точечное выравнивание к рефу отложено: реф-дизайн
неконсистентен. (доказательство отсутствия регресса). Фиксы по результатам
— отдельными мелкими коммитами.
### Шаг 5. Сделать новый дизайн основным
> C17 — **выполнен** (`docs/archive/plan-c17.md`): старый `(old)/` → `/v1/`,
> preview-страницы переехали в корень, `app.css` стал корневым. C18 (правки
> импортов/редиректов после переноса, проверка билда) — **выполнен**. Остались:
> C19 (удаление старого, раскрытие `recommended` на весь код), C20, C21.
- [x] **[C17]** Перенести `preview/*` → реальные маршруты (move), удалить
`(old)/`, корневой `+layout.svelte` импортирует `design2.css` + новый
shell. Убрать флаг/кнопку. **Снять `noindex, nofollow`** со страницы
инструмента (`preview/tools/[id]/+page.svelte`) — страницы становятся
боевыми лендингами ровно в момент переезда (SEO: `plan-seo.md`, S1a).
**Временное решение:** если домен ещё не куплен, `noindex` можно оставить
до S1 — тогда C17 делается без снятия `noindex`, а снятие происходит
вместе с подключением canonical в S1. **И обновить `newCode` в
`web/eslint.config.js`:** заменить `src/routes/preview/**` на
`src/routes/**` (после переезда все маршруты — новый дизайн), компонентный
glob `src/lib/components/kit/**` пока оставить. _(Итог: `newCode` обновлён
(`components/**`, `routes/**`), `noindex` оставлен до S1 — домен ещё не
куплен. Детали: `docs/archive/plan-c17.md`.)_
- [x] **[C18]** Правки импортов/редиректов после переноса, проверка билда.
_(Билд собирается без ошибок, линт/свелт-чека нет — выполнен.)_
### Шаг 6. Проверка нового дизайна
- [ ] **[C20]** Финальный проход по брейкпоинтам 1200/1100/800/480; build без
предупреждений; `grep` по старым токенам/классам пуст. Доказательство
отсутствия регресса: старый `v1/` работает и не затронут.
### Шаг 7. Опциональная чистка (отложен, нет срока)
- [ ] **[C21]** Удалить `refs/` и `refs-html/` из репо — они больше не источник
правды.
- [ ] **[C19]** Удалить старое: маршруты `routes/v1/**`, код `lib/v1/**`,
`app_v1.css` (и его токены), неиспользуемые классы. **И раскрыть ESLint
`recommended` на весь код** — убрать scoped-блок `newCode` в
`web/eslint.config.js`, прогнать `lint` и починить всплывшие ошибки.
_Примечание: доменные модули (`lib/core`, `lib/registry`, тесты и т.п.) —
это не «старый дизайн», у них свой линт-долг; расширение `recommended` на
них может выдать много ошибок. Решить на C19: либо чиним сразу, либо
расширяем scoped только на `src/routes/**` + `src/lib/components/**`._
- [ ] Навести порядок в `components/` (согласовать подпапки, финальное имя
каталога).
> **Удаление v1 не решает никаких текущих задач** и не даёт пользовательской
> ценности. Старый код лежит мёртвым, не мешает, не ломает билд. Удалять стоит
> только когда нет горящих задач и хочется навести порядок.
>
> Если какой-то коммит тянет за собой > 500 строк (например, C6/C7), разбивать
> на под-коммиты по 2–4 компонента. Тесты/билд прогонять после каждого коммита,
> чтобы регрессия локализовалась одним шагом назад.
>
> **Атомарность и точки «поломки» между коммитами.**
>
> - C1+C2 уже сделаны вместе (связаны: нейтральный root без `(old)`-layout
> ломает старый сайт). Аналогично связанных пар больше нет.
> - C3 и C4 слиты: шрифты подключаются внутри `design2.css`, поэтому файл должен
> существовать в том же коммите.
> - Коммиты **C3→C16 не трогают старый сайт** `(old)/` — они только добавляют
> новые файлы/маршруты, сломать старый сайт на них нельзя.
> - **C17 — атомарный переключатель**: перенос `preview/*` на реальные пути +
> удаление `(old)/` + смена root-layout — в ОДНОМ коммите. Снятие `noindex`
> также в C17, **если домен уже куплен**; иначе — отложить на S1. Не дробить
> сам C17.
> - Очерёдность зависимостей: `design2.css` (C3) и kit-компоненты (C5–C7) должны
> существовать до страниц, которые их импортируют (C8–C14).
## Бэклог полировки (после C9–C21, отдельным проходом)
Конкретные расхождения витрины `preview/kit` с `refs-html/`. Править системно в
конце, а не по ходу сборки базы.
- **Toggle** в дизайне прямоугольный, у нас — закруглённый. Сделать
прямоугольным (или уточнить радиус у свича отдельно от общих токенов).
- **Радиусы бордеров** не совпадают: в `refs` радиусы вообще 0, а мы взяли
ненулевой `--radius` для всех бордеров. Проверить токен `--radius` против
референсов; возможно он должен быть 0 (или близко), а скругления оставить
только там, где они реально есть в дизайне.
- **DownloadButton**: нет отступа между текстом и размером файла (`size`-чип).
Добавить `gap`/отступ внутри кнопки.
- **Поля ввода (controls)**: выглядят без паддингов (возможно артефакт самой
витрины, но визуально плохо). Проверить паддинги в `TextField`/`SelectField`/
`ColorField`/`SliderField` против референса и поправить.
- **Segmented**: на витрине последняя кнопка растянута и нечитаемо, что это
сегмент. Проверить, как `Segmented` выглядит в реальных раскладках (C9–C14);
возможно нужен фиксированный/авто размер сегментов или перенос.
-360
View File
@@ -1,360 +0,0 @@
# План: SEO + GEO — индексация и конкуренция в image tools
> **Статус (2026-09-11):** план к выполнению. C17 выполнен — `preview/*`
> переехали на реальные маршруты (в `noindex` сохраняются до запуска S1, см.
> `plan-redesign.md` C17). Сейчас применять можно domain-free часть S1.
> **Уточнено:** домен не был блокером C17 (см. §6). Задачи S1 разбиты на
> domain-free (можно делать параллельно с покупкой домена) и domain-required
> (после покупки и настройки домена на Vercel).
>
> **Цель:** каждая страница инструмента — индексируемый лендинг под конкретный
> интент («crop png», «make png transparent», «jpg to png»…), конкуренция с
> onlinepngtools.com за ядро запросов + попадание в ответы генеративных систем
> (ChatGPT, Perplexity, AI Overviews) — GEO.
>
> Задача: «для каждого инструмента — своя страница, и можно реально
> конкурировать с onlinepngtools».
>
> **Позиционирование (уточнено 2026-09-09):** сайт — не «PNG-инструменты», а
> **image tools с PNG-ядром**: любой инструмент работает с PNG/JPG/WebP/GIF/BMP
> на входе и отдаёт результат в PNG/JPG/WebP/BMP через селектор в кнопке
> Download. Дифференциатор — прозрачность/альфа/фон и no-upload-приватность.
> Референс-конкурент по ядру — onlinepngtools.com (см. §2). Долгосрочно ядро
> переезжает на wasm и обрастает API + PWA (оффлайн/установка) —
> `docs/roadmap.md` фазы 3–10, `docs/plan-platform.md`; для SEO это аргументы
> дистрибуции (S4c) и GEO-факты, схему сайта они не меняют.
## 0. Решения
- **Бренд: переименовать до индексации** (новое имя и домен сознательно не
записаны в этом репозитории — см. §6 и локальный план
`docs/plan-domain.local.md`, исключён из git: выбранное имя не должно утекать
в историю коммитов до покупки домена). Легенда честная: «started as PNG tools,
now all formats» — это контент-плюс, а не баг. Названия инструментов
(`crop-png`, …) и localStorage-ключи не переименовывать (см. решение про слаги
ниже). Точка истины по бренду: константа в i18n + README; хардкод в
компонентах недопустим.
- **Форматы: формат = выбор в UI, не отдельные страницы.** Download-кнопка
каждого инструмента — с селектором PNG/JPG/WebP/BMP (+quality для lossy). Райз
«выбор формата/качества в Download» вытащен из backlog №7 в S1f: механика уже
есть (`ToolEntry.output`), нужна как продуктовая основа «image
tools»-позиционирования. Отдельные страницы-конвертеры — только для
подтверждённых вордстатом интентов.
- **Слаги инструментов `-png` сохраняются.** Они держат низкоконкурентные
png-интенты; широкие интенты («crop image online») переносятся теми же
страницами контентом: H1 «Crop image», текст «works with PNG, JPG, WebP».
URL-алиасы без `-png` не создаются (канонический беспорядок).
- **Индексируемая локаль — EN** (базовая; конкурент англоязычный). RU-версия —
отдельно и позже: префикс `/ru/`, prerender обеих локалей, hreflang
(сознательно отложено в `archive/plan-i18n.md`). Пока язык переключается на
клиенте (localStorage) — для SEO не учитываем.
- **Прод-домен без `BASE_PATH`.** Сборка с базой (GitHub Pages `/repo/`) ломает
абсолютные URL, canonical и «корневость» страниц. Свой домен + корень.
- **URL-схема инструментов:** короткие плоские адреса. После C17 вынести
страницы инструментов в корень: `/crop-png` (как у конкурента), каталог —
`/tools`. Переезд `/preview/tools/[id]` → `/[id]` сделать **одновременно с
C17** (в одном атомарном коммите): после индексации переименование URL = 301 и
потеря позиций.
- **Без GA и рекламных скриптов:** аналитика — self-hosted Plausible/Umami или
Cloudflare Web Analytics. «No tracking» — часть позиционирования, её нельзя
подрывать собственными скриптами.
- **Честные методы.** Никакого клонированного текста, дорвеев, PBN. Контент — по
каждому инструменту, с ручной вычиткой.
## 1. Аудит (2026-09-09)
Фундамент уже есть:
- SvelteKit + `adapter-static`, `prerender = true` на весь сайт
(`web/src/routes/+layout.ts`) — весь контент в статичном HTML, ботам не нужен
JS. Это главный технический козырь.
- 121 инструмент в `registry-new` с «SEO-шными» id (`crop-png`,
`add-border-png`, …) — URL уже повторяют паттерн конкурента.
- Уникальные `<title>` + `<meta description>` на страницах инструментов старой
ветки; механика переносится на новую при C17.
- Каталог со ссылками на все инструменты (внутренняя перелинковка) и карта
инструментов против конкурента: `docs/tools-map.md` (у них 311, из них 107 —
нишевые клоны-серии Logo*/Icon*/Stamp* поверх обычных операций).
- EN-тексты в реестре + i18n-словари ru/en.
Чего нет (пробелы закрываются этапами ниже):
| Пробел | Где сейчас | Этап |
| ----------------------------------------------------- | ----------------------------------- | ------ |
| `sitemap.xml` | нет | S1 |
| `robots.txt` без `Sitemap:` | allow-all (`web/static/robots.txt`) | S1 |
| `rel=canonical` | нет | S1 |
| OG/Twitter-теги, OG-картинки | нет | S1 |
| `noindex, nofollow` на страницах инструментов preview | `preview/tools/[id]/+page.svelte` | S1/C17 |
| Контент страницы (H1-текст, how-to, FAQ, related) | только title/description | S2 |
| JSON-LD (SoftwareApplication, FAQPage, Breadcrumb) | нет | S3 |
| `llms.txt`, хабы категорий с текстом | нет | S3 |
| GSC + приватная аналитика | нет | S4 |
## 2. Конкурент (onlinepngtools.com)
Снято с живого сайта (2026-09-09):
- ~311 URL в sitemap, все плоские (`/crop-png`, `/create-transparent-png`);
title вида «Crop a PNG – Online PNG Maker».
- Страница инструмента: h1 + 2–3 абзаца («World's simplest…», import/crop
примеры), примеры «before/after», how-to-блок, огромные футерные перелинковки
(серии Logo*/Icon*/Stamp*), страницы-хабы, price-wall.
- Сила: возраст домена, ссылочная масса, бренд, охват, серии-клоны под длинные
хвосты.
- Слабость (наш контраргумент): реклама, ежедневные лимиты free-плана, платное
коммерческое использование, апселл-модалки, страницы по ~450 КБ.
- Наш ответ в контенте каждой страницы: **no upload (всё локально в браузере),
no signup, no limits, бесплатно в т.ч. для коммерческого использования, open
source**.
## 3. Стратегия
1. **Не лобовая, а ядро интентов.** 80% объёма поиска приносят ~30–50 запросов.
Покрытие инструментов у нас почти полное (121); приоритет — контент и
качество страниц ядра, затем хвосты.
2. **Страница = лендинг, а не «пустая коробка».** Шаблон контента: H1 → 1–2
абзаца «что делает» → как использовать (шаги) → параметры → FAQ (3–5 вопросов
уровня People-Also-Ask) → related tools по категории.
3. **CWV как измеримое преимущество.** Лёгкие быстрые страницы против ~450-КБ
страниц конкурента; после редизайна не потерять скорость.
4. **GEO.** LLM-системы цитируют страницы с прямыми определениями, списками,
таблицами и уникальными фактами. Всё это уже есть в данных реестра — отдавать
в HTML-структуре, без воды.
5. **Информационный контур — потом, не вместо.** Гайды по форматам (PNG/JPEG/
WebP: история, модификации, lossy/lossless, прозрачность, «png vs jpeg») дают
E-E-A-T, перехватывают сравнительные запросы и служат хабами внутренних
ссылок на инструменты. Но это этап S3e и только после того, как написано ядро
S2: транзакционная страница приносит конверсию, информационная — только
трафик. Тонкие FAQ-страницы «про всё» не заводить — несколько сильных
материалов с ссылками на 3–5 инструментов каждый.
6. **Дистрибуция.** GitHub-репо (звёзды/форки = сигналы), alternative.to,
Product Hunt, Reddit по интенту — реальный путь набрать первые бэклинки без
бюджетов.
## 4. Этапы
### S1. Технический минимум (сразу после C17)
Разбито на две группы: **domain-free** (делаем параллельно с покупкой домена) и
**domain-required** (после покупки и DNS-настройки).
#### S1-free: без домена (параллельно с C18–C19)
- [ ] **[S1e]** OG-картинки per-tool: генерация PNG-карточек скриптом на
существующем ядре (мы сами умеем рисовать PNG) — имя инструмента + бренд,
файл `static/og/[id].png`, ссылка из `Seo.svelte`. Относительные пути
(`/og/crop-png.png`) работают без домена; при деплое на Vercel с
собственным доменом OG-картинки будут корректными.
- [ ] **[S1f]** Download-кнопка с селектором формата PNG/JPG/WebP/BMP (+quality
для lossy) для всех инструментов — механика `ToolEntry.output` уже есть
(backlog №7, райз «выбор формата/качества»). Продуктовая основа «image
tools»-позиционирования: без неё сайт остаётся PNG-сайтом. Вести
параллельно с C18–C19.
- [ ] **[S1g]** Поведение конвертеров после S1f: не предлагать в меню форматов
формат исходника (disable) — чтобы «convert png to jpg» не мог сохранить в
png. Существующие страницы не трогать: `jpg-to-png`, `png-to-jpg`,
`png-to-webp`, `png-to-bmp` остаются самостоятельными лендингами со своим
контентом (§5).
#### S1-domain: с доменом (после покупки + DNS на Vercel)
- [ ] **[S1a]** Снять `noindex, no_follow` со страницы инструмента
(`tools/[id]/+page.svelte`) — делается одновременно с S1d (canonical)
чтобы Google сразу проиндексировал с правильным доменом.
- [ ] **[S1b]** Скрипт `web/scripts/gen-sitemap.mjs`: генерирует `sitemap.xml` в
`build/` из `TOOLS` реестра (`registry-new`) + статических страниц. Вызов
из `pnpm build` после `vite build` (adapter-static пишет в `build/`,
дописываем файл там же). Origin — из env (`PUBLIC_SITE_ORIGIN`), учёт
опционального `BASE_PATH`.
- [ ] **[S1c]** `robots.txt`: `Sitemap:` + закрыть служебные маршруты (`/kit` до
C19; после — вернуть минимальный allow-all).
- [ ] **[S1d]** Компонент `Seo.svelte` (kit): canonical (абсолютный URL по
актуальному маршруту), OG-теги (title/description/type/url/image),
twitter:card. Подключить в root-layout. **Требует `PUBLIC_SITE_ORIGIN`**
для абсолютных URL в canonical и OG:url.
> **Порядок S1:** S1e/S1f/S1g → покупка домена → S1a+S1b+S1c+S1d. Гейты: build
> зелёный; в `build/` лежит корректный `sitemap.xml` со всеми инструментами;
> canonical/OG присутствуют на всех страницах; e2e зелёные.
### S2. Контентный слой (параллельно C18–C19, волнами по категориям)
- [ ] **[S2a]** Схема данных: `seo`-блок per-tool (`longDescription`,
`howTo: string[]`, `faq: {q, a}[]`) — в i18n-словарях (ru/en), не в коде
реестра. Черновики — генерацией из схемы параметров инструмента, вычитка
вручную пачками по категориям (самый трудоёмкий этап: ~121 × 2 локали; RU
— отложить до решения о `/ru/`).
- [ ] **[S2b]** Компонент контента страницы инструмента (kit): H1, lead, how-to
(`ol`), описание параметров (из схемы), FAQ, related tools (перелинковка
внутри категории + 2–3 «соседних» интента).
- [ ] **[S2c]** Хлебные крошки (Каталог → Категория → Инструмент) в UI.
- [ ] **[S2d]** Первая волна контента — топ-30 инструментов ядра (приоритет по
вордстату: transparent png, jpg→png, compress, resize, crop, rotate,
circle crop, add text, pixelate, grayscale…).
- Гейты: на каждой странице инструмента — уникальный текст ≥ 300 слов, H1
совпадает с интентом, FAQ ≥ 3, related ≥ 3; текст в статике (prerender), не в
рантайме.
### S3. Структурированные данные + GEO
- [ ] **[S3a]** JSON-LD: `SoftwareApplication` (+ `offers` price 0) и `FAQPage`
на страницах инструментов; `WebSite` + `Organization` на главной;
`BreadcrumbList` на всех внутренних. Учесть: Google ограничил rich results
по FAQ/HowTo для мелких сайтов — разметка нужна прежде всего для
LLM-парсинга, она безвредна.
- [ ] **[S3b]** `llms.txt` в статике: короткий манифест сайта + список
инструментов с одним предложением на каждый (генерится из реестра тем же
скриптом, что sitemap).
- [ ] **[S3c]** Хабы категорий с текстом: 2–3 абзаца на категорию (что за
группа, когда какие инструменты применять), не голые списки карточек.
- [ ] **[S3d]** GEO-редактура: каждое описание начинается прямым определением
(«Crop PNG is a free tool that cuts a rectangular area from a PNG image
right in your browser»), списки/таблицы для фактов, без вводных «в
современном мире…». Проверка: страница отвечает на запрос одним абзацем
«без скролла».
- [ ] **[S3e]** _(опционально)_ Информационные гайды по форматам: 3–4 сильных
материала (`/guides/png-format`, `/guides/jpeg-vs-png`, …): история и
модификации формата, lossy/lossless, прозрачность, когда какой формат
выбирать. Каждый — с ссылками на 3–5 связанных инструментов (хабы
перелинковки), FAQ из гайдов продублирован как FAQPage-разметка. Приоритет
ниже S2/S3a–d: только когда ядро написано и есть ресурс.
- Гейты: валидация JSON-LD (Rich Results Test без ошибок); `llms.txt` актуален
при добавлении инструментов (генерация, а не ручное редактирование).
### S4. Измерение и запуск
- [ ] **[S4a]** Google Search Console: собственность на домен, отправка sitemap,
мониторинг индексации и запросов. Бесплатно и обязательно.
- [ ] **[S4b]** Аналитика без трекинга: self-hosted Plausible/Umami либо
Cloudflare Web Analytics. GA/ЯМ не ставим (позиционирование).
- [ ] **[S4c]** Дистрибуция: закрепить GitHub-репо (README со скриншотами),
подача на alternative.to (страница против конкурента), Product Hunt,
тематические Reddit/HN-треды. Бэклинки — главный дефицит для конкуренции.
- [ ] **[S4d]** _(опционально, позже)_ Сравнительные посадочные «vs …» (без
негатива: «no limits, no signup, no upload») — по мере роста доверия
домена.
- [ ] **[S4e]** Контроль CWV: Lighthouse на страницы ядра после каждого крупного
этапа редизайна; цель — зелёные метрики на мобильных.
## 5. Приоритизация инструментов (первая волна контента)
Источник — `docs/tools-map.md` (121 реализовано; у конкурента 311, из них 107 —
клоны-серии). Первая волна — топ-интенты, где конкуренты ранжируются, а у нас
уже есть инструмент и он не хуже:
- прозрачность/фон: create-transparent-png, remove-background, change-opacity
- конвертация: jpg-to-png, png-to-jpg, webp-to-png, svg-to-png, png-to-base64
- геометрия: crop, resize, rotate, circle-crop, round-corners, flip
- цвет/фильтры: grayscale, invert, pixelate, add-text, watermark
- генерация: gradient, empty-canvas
Новая волна форматов (после S1f): jpg↔webp и другие конвертации «формат →
формат» — только по подтверждённому вордстату; базовые кейсы закрывает селектор
формата в Download (S1f), страницы заводим лишь там, где интент стабильно
ищется.
- **crop-png: H1 «Crop image online — free, no upload»**, текст «works with PNG,
JPG, WebP» — широкий интент обслуживается контентом страницы с png-слагом,
слаг при этом остаётся якорем дешёвого png-интента.
### Форматные варианты страниц (`crop-jpeg`, `crop-webp`, …)
Три возможных механики — важно не перепутать:
1. **301-редирект `/crop-jpeg` → `/crop-png`** — законно только для опечаток и
альтернативных написаний (`crop-jpg` → `crop-jpeg`) и переименований
существующих страниц. Для самостоятельного запроса редирект — это отказ от
страницы: Google перестаёт показывать редирект, ранжировать будет только
цель. Как посадочная под «crop jpeg» не работает вообще.
2. **Зеркало с canonical на `/crop-png`** — технически легально, но бесполезно:
canonical-дубль в индекс не попадает, видимости не добавляет, а массовое
производство таких зеркал выглядит как накрутка площади и при ошибке в
canonical превращается в дорвейный кластер. Не делаем.
3. **Самостоятельная страница = самостоятельный контент.** Заводить `/crop-jpeg`
можно только если: (а) интент подтверждён вордстатом, (б) на странице
собственный текст — H1 «Crop JPEG», FAQ про качество/артефакты/ когда
выбирать JPEG вместо PNG, отличия от png-кейса, (в) текст написан или выверен
человеком, а не получен подстановкой формата в шаблон. Реестром
автоматизировать **данные** (тот же tool, другой дефолтный output) — можно;
автоматизировать **текст** — нельзя: массовая подстановка {tool}×{format} в
шаблон — doorway-паттерн, риск deindex всего сайта.
Как правильно получать видимость под «jpeg»/«webp» без клонов: широкие
формулировки закрывает контент существующей страницы (H1 «Crop image», «works
with PNG, JPG, WebP» в тексте + селектор формата в Download, S1f). Отдельные
форматные страницы — точечные исключения по вордстату, не серия.
### Convert-инструменты: страницы и «универсальный конвертер»
Разводить два разных интента:
- **«convert png to jpg»** — интент «страница-конвертер»: пользователь ищет
конкретную пару форматов (лучшее качество конверсии, меньше ошибок), это
готовые лендинги. Существующие (`jpg-to-png`, `png-to-jpg`, `png-to-webp`,
`png-to-bmp`, `svg-to-png`, …) **оставить и наполнить контентом как все**:
свой H1 («Convert PNG to JPG — free, no upload»), FAQ про
качество/прозрачность/фон, related. Селектор формата (S1f) их **не заменяет**
— он про «уже открыл инструмент и хочу другой формат на выходе».
- **«convert image» / «image converter»** — интент «швейцарский нож». Под него —
**один будущий инструмент `/convert-image`**: сначала пустой **core-tool** в
`core/` — функция ре-энкода («копия с перекодированием»: декодирует любой
поддержанный исходник в ImageData и кодирует в выбранный формат; поверх неё
S1f строит селектор формата). `ToolEntry` и страницу добавлять **только вместе
с собственным текстом** («Convert any image to PNG, JPG or WebP in your
browser») — core в коде не обязывает к странице. Этот же шаблон потом
закрывает форматные волны (jpg→webp и т.п.) без новых страниц.
Не делать: серию `convert-jpeg`/`convert-webp`-клонов с подстановкой формата
(doorway, см. выше) и «универсальный конвертер» как единственную страницу вместо
форматных лендингов — теряется ядро «convert png to jpg»-запросов.
Правило добавления новых инструментов: брать из хвоста конкурента только те
интенты, которые подтверждены вордстатом, и сразу с контентом S2 (страница без
текста — потраченный URL).
## 6. Переименование бренда и домен (сделать до S1-domain, дёшево сейчас)
Новое имя и конкретный домен **не записаны в этом репозитории** — детальный
чек-лист в `docs/plan-domain.local.md` (исключён из git через `.gitignore`),
чтобы выбранное имя не попало в историю коммитов до покупки домена.
- [ ] **[R1]** Механический переезд бренда на новое имя по чек-листу из
локального плана: GitHub-репо + `git remote set-url`, README,
`package.json` (корень + `web/`), `TopBar.svelte` / `Footer.svelte`,
`favicon.svg` (title в SVG), i18n-строки (`en.ts`, `ru.ts`: defaultTitle,
pageTitle, metaDescription). НЕ трогать: слаги инструментов (`*-png`),
ключи localStorage (`easy-png-tools:*` — или мигрировать отдельной
мелочью), историю docs/archive.
- [ ] **[R2]** Купить выбранный домен (кандидаты и пробы — в локальном плане) и
настроить деплой на Vercel без `BASE_PATH` (§0). Домен нужен до S1-domain:
canonical и sitemap генерятся от `PUBLIC_SITE_ORIGIN`.
- **Порядок:** R1+R2 можно делать параллельно с C17/C18/C19 (не блокируют). R2
(домен) — блокер только для S1-domain (canonical + sitemap + снятие
`noindex`). После индексации переименование = переезд с 301, потеря позиций и
ссылок — не делать. Поэтому R1/R2 — до S1-domain, но не обязательно до C17.
## 7. Что осознанно НЕ делаем
- Клонирование 311 URL конкурента (низкокачественные дубли + нет ресурсов на
контент) — вместо этого ядро + обоснованные хвосты.
- RU-локаль в индексе до решения о структуре `/ru/` + hreflang.
- GA/реклама/трекинг — противоречит позиционированию.
- Дорвеи, программные зеркала, покупные ссылки.
- Массово-сгенерированные форматные клоны (`crop-jpeg`/`crop-webp` как копии
`crop-png` с подстановкой формата в шаблонный текст) — doorway-паттерн, риск
deindex всего сайта. Форматная страница — только уникальный контент + вордстат
(§5); редиректы — только для опечаток/переименований.
- URL-алиасы без `-png` для существующих инструментов и зеркальные
страницы-конвертеры «на каждый формат» без вордстата.
## 8. Связанные документы
- `docs/plan-redesign.md` — C17 (снятие noindex + переезд маршрутов), C18–C21.
- `docs/tools-map.md` — карта «наш инструмент ↔ интент конкурента».
- `docs/backlog.md` — пункты SEO/GEO в общем бэклоге.
- `docs/roadmap.md` — фазы 2–8 (ядро TS→Rust/wasm, CLI); фазы 9–10 — API и PWA.
- `docs/plan-platform.md` — планы API и PWA (общее ядро, оффлайн-режим).
- `docs/archive/plan-i18n.md` — отложенная многоязычная схема (будущий `/ru/`).
- `docs/plan-domain.local.md` — план переименования/домена; **в git не входит**
(`.gitignore`), лежит только локально.
-201
View File
@@ -1,201 +0,0 @@
# План разработки: easy-png-tools
> **Статус:** Фаза 1 (полноценный TS-сайт) в основном выполнена — сайт живёт,
> каталог переведён в типизированный `registry` (121/125), новый дизайн по
> `plan-redesign.md` стал основным на корневых маршрутах (C17/C18 выполнены),
> старый UI — на `/v1/*` (удаление отложено, ценности не даёт — C19 в бэклоге).
> Tech debt закрыт. Следующий этап: C20 (проверка нового дизайна,
> `plan-redesign.md`, §10) + SEO S1-free (`plan-seo.md`) + баги. Домен нужен для
> SEO (`plan-seo.md`, S1-domain). Фазы 2–8 — будущие. Фазы 9–10 спланированы в
> `docs/plan-platform.md`.
## 0. Решения
- **Набор PNG-утилит, всё в браузере** (SvelteKit + `adapter-static`), данные не
покидают машину.
- **Первый и главный приоритет — полностью рабочий TS-сайт**: максимум простых
инструментов уровня EASY, UX/UI, пайплайны (несколько инструментов подряд),
описания инструментов. Сайтом можно пользоваться уже после этой фазы.
- **Никакого wasm/Rust на этом этапе.** TS-реализация на canvas + `ImageData` —
эталон алгоритмов, из которого позже генерируются эталонные файлы для проверки
Rust-порта. Rust-ядро, WASM и CLI — отдельные фазы позже, когда сайт живёт.
- **Пайплайн = JSON-список шагов** `{tool, params}`. Формат закладывается сейчас
и потом станет общим для браузера, wasm и CLI.
- **Реестр инструментов** (id, title, description, схема параметров, `run`) —
источник истины: из него генерируются страницы, формы, пайплайны. Позже —
общий формат для Rust-парсера.
- Валидация будущего Rust-порта — эталонные файлы, сгенерированные из
TS-реализации. Сравнение: **бит-в-бит** для чистых пиксельных операций,
**perceptual diff** для canvas-зависимых.
- **Единая точка исполнения инструментов.** Страницы вызывают операции только
через `web/src/lib/tools/executor.ts::executeStep` — нигде напрямую
`tool.run`. Контракт уже асинхронный (`Promise<PixelImage>`), поэтому:
- когда появятся тяжёлые MEDIUM-операции (свёртки, квантование), внутренности
`executeStep` переезжают в Web Worker — воркер импортирует тот же чистый
core как TS-фолбэк, алгоритмы не дублируются, вызывающий код не меняется;
- в WASM-фазе тот же воркер принимает wasm-модуль и диспетчеризует по `toolId`
— свитч A/B из фазы 5 сводится к замене реализации внутри исполнителя;
- до появления MEDIUM-операций воркер не вводится: EASY-операции быстры и не
блокируют UI.
## 1. Структура репозитория (этап TS)
```txt
easy-png-tools/
web/ # SvelteKit (adapter-static)
src/lib/core/ # TS-ядро: Image (ImageData) + операции (без DOM)
color.ts alpha.ts geometry.ts format.ts text.ts analyze.ts
src/lib/registry.ts # реестр инструментов: id, title, description, params, run
src/lib/pipeline.ts # пайплайн: шаги, применение, localStorage, экспорт/импорт JSON
src/lib/components/ # DropZone, Preview, ParamForm (из схемы), Download, PipelineSteps
src/routes/ # маршруты-утилиты (генерируются из реестра) + /workspace
docs/
core/ # (позже) Rust-крейт
tests/ # (позже) эталонные файлы и harness
```
## 2. Фазы
### Фаза 1 — Полноценный TS-сайт (единственный приоритет)
**Цель: сайт, которым можно пользоваться.** Без wasm, без Rust, без
clamp-семантики (в TS это делает `Uint8ClampedArray` сам).
1.1 **Каркас и UX/UI.** SvelteKit + static adapter, дизайн-система (цвета,
типографика, компоненты), общий лейаут, шапка с навигацией по категориям.
1.2 **Ядро.** Тип `Image` (обёртка над `ImageData`), загрузка/декодирование
файла, кодирование и скачивание, библиотека операций
(color/alpha/geometry/format/text/analyze). Всё без DOM-зависимостей внутри
`core`.
1.3 **Реестр инструментов.** Одна запись =
`{ id, title, description, category, params: ParamDef[], run }`. Универсальный
рендерер: страница инструмента и форма параметров строятся из записи реестра
автоматически; маршруты-утилиты генерируются из реестра на билде. Новый
инструмент = новая запись + функция `run`.
1.4 **Пайплайн-workspace** (`/workspace`): загрузил изображение → список шагов
(инструмент + его параметры) → последовательное применение с превью каждого шага
→ скачивание финального результата. Добавление/удаление/перестановка шагов.
Сохранение пайплайнов в localStorage, экспорт/импорт JSON. Общие компоненты:
DropZone, Preview, ParamForm, Download, PipelineSteps.
1.5 **Массовая реализация EASY-инструментов** по категориям:
- конвертация форматов (png/jpg/webp/bmp/base64/gif) и текстовые представления;
- прозрачность и альфа-канал;
- цвет: замена, тон, каналы, оттенки серого, инверсии;
- геометрия и холст: resize, crop, rotate, flip, border, padding, background;
- текст и простые эффекты: watermark, add-text, рамки, шум, pixelate;
- анализ: размеры, палитра, проверки, просмотр. Приоритет — ширина, не глубина:
как можно больше простых инструментов.
1.6 **Полировка.** Описания и подсказки для всех инструментов, состояния
загрузки/ошибок, доступность, пустые состояния.
**Checkpoint:** загрузил PNG → применил пайплайн из N шагов → сохранил и вернул
пайплайн → скачал результат. Все инструменты 1.5 работают на сайте. Ни одного
wasm.
### Фаза 2 — Тестовые PNG и генератор эталонов
- Фикстурные входы: градиенты/паттерны/шум + пара «настоящих» PNG.
- Правило: сравниваем **распакованные пиксели**, не байты файла.
- **Checkpoint:** в `tests/reference/<op>/` лежат `input-*.png` +
`expected-*.png`.
### Фаза 3 — Playwright-тесты
- `tools/gen-references.ts`: грузит страницу, гоняет TS-эталон, сохраняет
`expected-*.png`.
- Регресс: изменение TS-ядра ловится тестами.
- **Checkpoint:** регресс-тесты падают при изменении выхода эталона.
### Фаза 4 — Rust-ядро
- Порт 1:1 чистых операций (первым делом те, что покрыты эталонными файлами) с
`Image` ↔ TS `ImageData`.
- `clamp_to_u8` с семантикой `ToUint8Clamp` (round-half-to-even, ≥255 → 255) +
юнит-тесты на краях.
- `cargo test` читает эталонные файлы напрямую, **без браузера**.
- **Checkpoint:** `cargo test` зелёный на всех эталонных файлах.
### Фаза 5 — WASM на сайте
- Спайк WASM + Vite (проверить интеграцию) → wasm-bindgen-биндинги.
- Свитч A/B: TS-реализация заменяется на wasm-вызов.
- Playwright-проверка: wasm-результат == эталон.
- **Checkpoint:** сайт работает через WASM; A/B TS vs WASM идентично.
### Фаза 6 — Rust-CLI
- CLI: обход папки, `--pipeline pipeline.json`, запись результатов.
- Воспроизведение эталонов из консоли.
- **Checkpoint:** CLI выдаёт те же пиксели, что сохранены в эталонных файлах.
### Фаза 7 — Сквозная проверка идентичности
- Матрица: **эталонные файлы (из TS) ↔ Rust-native ↔ Rust-wasm**, одна команда
`harness compare`.
- **Checkpoint:** сходимость по всем покрытым операциям во всех трёх рантаймах.
### Фаза 8 — Масштабирование и продвинутое
- Порт оставшихся EASY → MEDIUM (свёртки, морфология, квантование; кодеки через
крейт `image`).
- Canvas-зависимые операции (текст, градиенты, штампы, `ctx.filter`): остаются в
TS с perceptual-классом либо реимплементация в Rust. Для текста выбран курс на
реимплементацию в Rust (рендер без DOM + шрифты OFL с лицензионными
паспортами) — `docs/plan-platform.md`, §4.
- batch-страница: дроп папки → wasm по всем файлам → zip. localStorage-пресеты
(тот же JSON, что у CLI).
- HARD: compress/optimize через oxipng/zopfli (в Rust — нативно).
### Фаза 9 — API (монетизация) — см. `docs/plan-platform.md`
Серверная обёртка над тем же ядром (нативный Rust или wasm на edge — решает
спайк), ключи и free tier, платные лимиты, индексируемая документация `/api`.
Чек-лист и решения — `docs/plan-platform.md`, §1.
### Фаза 10 — PWA — см. `docs/plan-platform.md`
Manifest + service worker, оффлайн-режим («работает без сети — файлы не покидают
устройство»), установка как приложение. Wasm-фаз не требует, идёт после C17/C19
и S1 из `plan-seo.md`. Чек-лист — `docs/plan-platform.md`, §2.
## 3. Правила сравнения (для фаз 4–7)
| Класс операции | Критерий |
| ------------------------------------------------------------------------------ | --------------------------------------------------------------------------- |
| Чистые пиксельные: цвет, альфа, порог, свёртки, морфология, квантование | **бит-в-бит** |
| Canvas-растеризация: текст, градиенты, антиалиасинг, `drawImage`, `ctx.filter` | **perceptual diff** (допуск: % изменившихся пикселей / цветовое расстояние) |
| PNG-декод | бит-в-бит (lossless) |
| JPEG-декод | perceptual (разные декодеры дают лёгкий дрейф) |
| Кодирование | сравниваем декодированные пиксели, не байты файла |
| compress/optimize | своя метрика: размер + визуальная дельта |
## 4. Риски и контрмеры
- **Реестр разрастается, страницы дублируются** — один универсальный рендерер
страниц/форм из записей реестра; инструмент = данные + `run`.
- **Семантика пайплайна** — зафиксировать: каждый шаг применяется к результату
предыдущего; единый тип `Image` на всём пути; схема параметров на шаге.
- **Canvas-операции не бит-в-бит** — класс perceptual закладывается на Фазе 2–3,
а не в конце.
- **WASM + Vite** — спайк на Фазе 5 до массовой интеграции (риск отложен
сознательно: сайт уже живёт на TS).
- **Расползание реестра при будущем Rust-порте** — схема параметров в JSON, её
можно парсить и из Rust.
- **Разные декодеры PNG** — фикстурные PNG генерировать самим, чтобы не тащить
чужие артефакты.
## 5. Команды верификации
```bash
pnpm dev # разработка (Фаза 1+, обёртка `pnpm --dir web dev`)
pnpm build # статический экспорт (обёртка `pnpm --dir web build`)
pnpm --dir web exec playwright test # эталоны + UI-регресс (Фаза 3+)
cargo test # Rust-ядро против эталонных файлов (Фаза 4+)
harness compare tests/reference # сквозная сверка native + wasm (Фаза 7)
```
-77
View File
@@ -1,77 +0,0 @@
# Хранить данные для сортировки по популярности инструментов
Идея выложить на Vercel и использовать Vercel KV (на базе Upstash Redis) — это
отличный, рабочий и очень быстрый в реализации вариант. Для 150 инструментов
объем данных будет крошечным, но здесь есть пара критически важных нюансов по
лимитам, о которых нужно знать на старте, чтобы бесплатный тариф не отключился в
первый же месяц. [1]
## ⚠️ Подводный камень: Лимиты Vercel KV на Free-тарифе
На бесплатном тарифе (Hobby) для Vercel KV действуют следующие ограничения: [2]
- 30 000 запросов в месяц (суммарно чтение + запись).
- Ограничение около 3 000 запросов в день. [2, 3]
Если делать запрос к базе на каждый просмотр страницы («посещение»), лимит в 30
тысяч улетит мгновенно при первой же паре сотен посетителей в день.
## 💡 Как сделать это правильно и бесплатно
Чтобы не выйти за рамки лимитов, архитектуру нужно построить экономно:
## 1. Чтение: Сортировка на этапе сборки (ISR / SSG)
Не нужно запрашивать KV из Edge-функции при каждом открытии сайта пользователем.
- Используйте Incremental Static Regeneration (ISR) (если вы делаете на Next.js)
или раз в сутки запускайте Cron-сид (Vercel Cron).
- Сайт раз в несколько часов запрашивает весь список из KV (всего 1 запрос на
получение общей структуры), сортирует инструменты и кеширует эту страницу как
статическую.
- Результат: 100 000 пользователей увидят отсортированный список, а вы потратите
всего пару запросов к KV в день.
## 2. Запись: Защита от накрутки и экономия («Добавление в избранное»)
«Избранное» — идеальная метрика для KV, так как её нажимают редко. А вот
«Посещения» лучше отправлять не на каждый клик, а пачками (батчингом) или
использовать для просмотров бесплатную Vercel Web Analytics (там лимит 25 000
событий в месяц обновляется отдельно).
---
## 💻 Оптимальная структура данных в Redis (KV)
Вместо 150 отдельных ключей, храните всё в одной хэш-таблице (Hash) или
сортированном множестве (Sorted Set). Это сэкономит количество запросов. Вариант
структуры через Hash:
```js
// Запись / инкремент (в API роуте Vercel)
import { kv } from "@vercel/kv";
//Когда пользователь зашел на инструмент 'crop-png'
await kv.hincrby("tools:visits", "crop-png", 1);
// Когда добавил в избранное
await kv.hincrby("tools:favorites", "crop-png", 1);
```
Как забрать всё одним запросом для сортировки:
```js
// Получаем сразу все просмотры (вернет объект вида { 'crop-png': '120', 'resize-png': '85' })
const allVisits = await kv.hgetall("tools:visits");
const allFavorites = await kv.hgetall("tools:favorites");
// Дальше соединяем в массив на клиенте/сервере и сортируем:
// Популярность = Просмотры + (Избранное * 5) <- так лайки весят больше
```
## ⚡ Альтернатива: Cloudflare Workers + KV
Если вы боитесь упереться в 30 000 запросов на Vercel, рассмотрите связку
Cloudflare Workers + Workers KV. Там на бесплатном тарифе дают: [4]
- 100 000 запросов на запись в сутки.
- 10 000 000 (10 млн) запросов на чтение в сутки.
- Для пет-проекта этого хватит навсегда, даже если вы будете делать запросы
напрямую с фронтенда без кеширования.
-157
View File
@@ -1,157 +0,0 @@
# Аудит использования - UX\UI
## везде
- возможность переключать пиксели\проценты
- инструменты без параметров выглядят странно, пересмотреть, может добавить
какую-то надпись "нет параметров"
- удаление\маски и т.д. - надо добавить галочку сглаживание
- производительность: почему flip заметно тормозит, но нет индикации работы
инструмента (также sharpen-png)
- производительность: исследовать, есть ли смысл на инструментах геометрии
выводить рамку результата до полноценной работы (например на skew \ rotate
будет удобнее)
## аудит инструментов
### transparency
- add-stroke-png: добавить галочку "расширить на толщину линии"
- find-contour-png: если толщина линии более 1px - круг боле с искажениями, надо
расширять на толщину линии
- circle-mask-png: размеры заданы в процентах, надо задавать в пикселях
- square-mask-png: размеры заданы в процентах, надо задавать в пикселях
- star-mask-png: размеры заданы в процентах, надо задавать в пикселях, проверить
форму звезды - сейчас выпуклые края, радиус задан до 100%, а более 50 - смысле
нет
- wavy-mask-png:размеры заданы в процентах, надо задавать в пикселях,
пересмотреть форму и параметры. как будто неудобно конфигурировать
- remove-alpha-channel-png: выбор цвета фона, как будет работать с
полупрозрачной картинкой?
- extract-alpha-mask-png: галочка инвертировать маску
- round-corners-png: размеры заданы в пикселях, должна быть возможность задать в
процентах, нету сглаживания по вырезанным пикселям
- invert-alpha-png: края со сглаживанием странно обрабатываются, может
галочка\слайдер обрезать сглаживание?
- remove-background-png: возможность выбора точки, от которой удалять (или
отдельный инструмент?)
- make-thicker-png: расширять картинку на толщину линии
- make-thinner-png: уменьшать от краев, если картинка начинается от края, то
остается выступ
- feather-edges-png: расширять картинку на толщину линии, (выбор цвета возможен?
другой инструмент?)
- clean-edges-png: непонятный инструмент (слишком маленький максимум?). найти
тест кейс для него
- despeckle-alpha-png: найти более заметный тест-кейс
- close-holes-png: проверить, как должен работать, картинки с большой
полупрозрачной областью - закрывает всё, по описанию должен закрывать только
небольшие дырки
### colors
- two-colors-png: в range slider добавить описание что это luminance threshold
- gamma-png: проверить правильность работы - яркое изображение выглядит не
контрастным, проверить максимум и минимум
- temperature-png: проверить правильность работы - не слишком ли отдает
желтым\синим?
- quantize-png: максимум 64 - не слишком мало? сделать кнопки с пресетами
- custom-palette-png: копирует two-colors-png, только ввод через текстовое поле
и нет выбора порога luminance
- dithering-png: проверить алгоритм Floyd–Steinberg на малом количестве цветов
- sepia-png: не нужна настройка силы эффекта?
- extract-channel-png: мульти вывод сразу в 3 канала?
- swap-channels-png: обдумать описание подобных инструментов через flow более
простых?
- black-and-white-png: аналог two-colors-png, только без выбора цветов
- posterize-png: не нужен вариант настройки для каждого канала отдельно?
- auto-contrast-png: найти тестовый кейс
- decrease-color-count-png: сделать кнопки вместо селекта
- png-to-hsl: проверить, что `space as rgb` должна выдавать одинаковые
результаты, вне зависимости от выбора h s l. может выбрать отдельные кнопки?
- png-to-hsv: тоже самое ^
- png-to-hsi: тоже самое ^, нужно ли разделять на отдельные инструменты?
- png-to-cmyk: тоже самое ^
- png-to-lab: тоже самое ^
### geometry
- add-border-png: прозрачность цвета (аудит полей)
- fit-on-background-png: цвет
- change-canvas-size-png: использовать position9 вместо селекта
- resize-png: по умолчанию размеры 0х0, надо поменять; выбор метода
масштабирования
- crop-png: offset - слишком большие значения
- rotate-png: кнопки вместо select
- flip-png: кнопки вместо select
- add-padding-png: чем отличается от add-border-png?
- center-by-alpha-png: найти тест кейс
- zoom-png: выбор точки центра
- trim-empty-space-png: выбор от 0 до 254 (надо везде привести к единому стилю -
переключение 0-100 проценты и 0-255)
- change-aspect-ratio-png: не видно что меняется, добавить рамку вокруг
результата, а не вокруг div
- swap-orientation-png: кнопки вместо select
- symmetric-copy-png: кнопки вместо select, два выбора в селекте `keep side` не
работают (в зависимости от выбора `axis`)
- shift-png: цвет (прозрачность)
- split-into-parts-png: результат с несколькими файлами, обдумать процесс
### filters
- vignette-png: выбор точки центра, цвет, кроме силы нужен еще параметр прироста
силы (как называется?)
- pixelate-png: разные алгоритмы? выглядит не очень. тест кейс?
- randomize-pixels-png: настройка размеров другим вариантом - на сколько кусков
делить. seed - поменять
- add-noise-png: кнопки вместо select, seed - поменять
- sharpen-png: добавить тест кейс
- silhouette-png: аналог extract-alpha-mask-png?
### text
- add-text-png: plate снизу большой отступ, сделать галочку для этого (некоторые
буквы будут обрезаться - y, g)
- watermark-tile-png: заполняет не полностью
### analyze
- extract-color-from-png: надо описать или дать выбор на основе какого параметра
работает threshold
- show-transparent-png: кнопки вместо select
- show-grayscale-pixels-png: кнопки вместо select
- show-color-pixels-png: кнопки вместо select
- light-pixel-mask-png: кнопки вместо select
- verify-is-png: название неверное, в описании указано изображение, а фактически
проверяется текст
- png-is-grayscale: результат плохо виден, нужно ли копирование на полный
вердикт? может отдельные поля - 0\1, yes\no?
### generators
- color-spectrum-png: кнопки вместо select
- draw-grid-png: выбор цвета фона
- placeholder-png: с галочкой "показать текст" нет заполнения фона
- blend-two-png: высота не задается
- step-colors-png: кнопки вместо select, высота не задается, не слишком ли
маленький max steps?
- emoji-to-png: выбор эмодзи
- color-wheel-png: сглаживание краев
- complementary-png: кнопки вместо select(?), не меняется, всего 2 цвета, высота
не задается
- triadic-png: кнопки вместо select, высота не задается
- tetradic-png: кнопки вместо select, высота не задается
- analogous-png: кнопки вместо select, высота не задается
- monochromatic-png: кнопки вместо select, высота не задается
- shades-png: кнопки вместо select, высота не задается
- mix-colors-png: высота не задается
- sort-colors-png: кнопки вместо select, высота не задается
## аудит полей
- position offset: должны быть отдельные range slider на x и y
- range slider: поле ввода, кнопки + и -, кнопка сброса на умолчание
- range slider with presets: слайдер, как выше, добавочные кнопки с пресетами
- color picker: пикер с картинки (как исходника, так и результата), прозрачность
- ввод высоты и ширины с keep aspect ratio должен быть единым компонентом
- поле seed (random) - range slider не нужен, поле для ввода и "randomize"
кнопка
-235
View File
@@ -1,235 +0,0 @@
# Карта инструментов: реализовано · добавить · идеи
> Живой документ для сверки с каталогом. Формат записи:
> `id / название — параметры через запятую`. Источник сравнения:
> onlinepngtools.com (311 уникальных; 107 из них — нишевые клоны серий
> Logo*/Icon*/Stamp*/Signature* поверх обычных операций).
---
## 1. Реализовано
### Конвертация
- jpg-to-png — без параметров
- webp-to-png — без параметров
- gif-to-png — первый кадр
- bmp-to-png — без параметров
- ico-to-png — выбор размера кадра
- png-to-bmp — 24-бит, фон вместо альфы
- convert-png-to-jpg — background, quality
- convert-png-to-webp — quality
- svg-to-png — width результата
- png-to-base64 / base64-to-png — строка
- png-to-data-uri / data-uri-to-png — строка
- png-to-hex — rrggbbaa по строкам / hex-to-png — tokens + width
- png-to-bytes — десятичные RGBA-байты по строкам / bytes-to-png — tokens +
width
- png-to-rgb-values — rgba(r,g,b,a) по пикселям / rgb-values-to-png — числа +
width
- svg-to-png — width результата
- verify-is-png — текстовый источник (base64/data-uri), вердикт по сигнатуре
### Прозрачность
- change-png-opacity — percent
- set-alpha-channel-png — percent
- remove-alpha-channel-png — без параметров
- extract-alpha-mask-png — без параметров
- invert-alpha-png — без параметров
- remove-background-png — color, tolerance, outerOnly, smooth
- remove-color-from-png — targetColor, tolerance
- add-stroke-png — color, thickness
- find-contour-png — color, thickness
- make-thicker-png / make-thinner-png — radius
- harden-alpha-png — threshold
- feather-edges-png — radius (размытие только альфы)
- clean-edges-png — radius (defringe: RGB от ближайшего непрозрачного)
- watermark-image-png — вторая картинка-знак (загружается на странице), scale,
opacity, position, margin
- despeckle-alpha-png / close-holes-png — radius
- center-by-alpha-png — без параметров
- round-corners-png — radius
- circle-mask-png — size (диаметр, % меньшей стороны), offsetX, offsetY
- square-mask-png — widthPct, heightPct, offsetX, offsetY
- star-mask-png — points, innerRadius, size, rotation, offsetX, offsetY
- wavy-mask-png — size, amplitude, waves, phase, offsetX, offsetY
### Цвет
- grayscale-png / invert-colors-png / sepia-png / auto-contrast-png — без
параметров
- adjust-brightness-contrast-png — brightness, contrast
- change-png-hue — degrees
- extract-channel-png — channel (r/g/b)
- swap-channels-png — pair (r-g/r-b/g-b)
- black-and-white-png — threshold
- posterize-png — levels
- two-colors-png — lightColor, darkColor, threshold
- temperature-png — percent
- gamma-png — value
- tint-png — color, strength
### Квантование и палитры
- quantize-png — colors (k, median-cut)
- decrease-color-count-png — maxColors (пресеты 2…256)
- custom-palette-png — colors (hex через запятую, ближайший цвет)
- dithering-png — colors (k), pattern (Floyd–Steinberg / Bayer 4×4)
### Разложение каналов
- png-to-hsl / png-to-hsv / png-to-hsi — component (h/s/l и т.п.), display (gray
| space-as-rgb)
- png-to-cmyk — component (c/m/y/k), display
- png-to-ycbcr — component (y/cb/cr), display
- png-to-lab — component (l/a/b), display
### Геометрия
- resize-png — width (0=авто), height (0=авто), keepAspect
- crop-png — x, y, width, height
- rotate-png — angle (90/180/270)
- flip-png — axis (h/v)
- skew-png — degX, degY
- rotate-free-png — angle
- zoom-png — scale
- shift-png — offsetX, offsetY, color фона
- add-padding-png — padding, transparent, color
- add-border-png — thickness, color
- fit-on-background-png — width, height, transparent, color
- tile-png — columns, rows
- split-into-parts-png — columns, rows (мультифайловый вывод, zip)
- trim-empty-space-png — threshold (альфа)
- change-canvas-size-png — width, height, anchor (3×3)
- change-aspect-ratio-png — ratio (пресеты), mode (crop/pad)
- swap-orientation-png — target (portrait/landscape)
- symmetric-copy-png — axis, keepSide
### Фильтры
- blur-png — radius
- sharpen-png — strength
- vignette-png — strength
- jpeg-artifacts-png — quality (имитация пережатия jpg/webp)
- pixelate-png — blockSize (закрывает и их Color Blocks)
- randomize-pixels-png — blockSize, seed
- add-noise-png — amount, mode (mono/color), seed
- silhouette-png — color, threshold
### Анализ
- png-info — размеры, альфа, число цветов
- png-is-transparent / png-is-grayscale / png-orientation — текстовый вердикт
### Маски по свойствам пикселей
- show-transparent-png — color, opacity (подсветка прозрачных/полупрозрачных)
- show-grayscale-pixels-png — tolerance, mode (binary/highlight),
highlightColor, highlightOpacity
- show-color-pixels-png — tolerance, mode, highlightColor, highlightOpacity
- light-pixel-mask-png — threshold, mode, highlightColor, highlightOpacity
- dark-pixel-mask-png — threshold, mode, highlightColor, highlightOpacity
- unique-color-mask-png — rarity (макс. повторов), mode, highlightColor,
highlightOpacity
- extract-color-from-png — color, tolerance (оставить близкие, остальное
прозрачным)
### Генерация
- create-empty-png — width, height, transparent, color
- single-color-png — width, height, color
- random-noise-png — width, height, seed
- linear-gradient-png — width, height, fromColor, toColor, direction
- text-to-png — text, fontSize, font, bold, color, transparentBg,
backgroundColor, padding
- emoji-to-png — emoji, size
- placeholder-png — width, height, backgroundColor, color, showText
- color-spectrum-png — width, height, direction, saturation, lightness
- random-colors-png — width, height, blockSize, seed
- draw-grid-png — width, height, cols, rows, lineWidth, color, transparentBg
### Текст
- add-text-png — text, fontSize, color, font, bold, position (3×3), margin,
plate, plateColor, plateOpacity
- date-stamp-png — format, fontSize, color, font, bold, position, margin, plate,
plateColor, plateOpacity
- watermark-tile-png — text, fontSize, color, opacity, angle, stepX, stepY,
font, bold
### Палитры и цветовые утилиты
- color-wheel-generator — size, кольца/сектора, показ hex при клике (у нас —
статичный свотч-полотно)
- complementary-palette — baseColor
- monochromatic-palette — baseColor, count
- analogous-palette — baseColor, spread
- triadic-palette — baseColor
- tetradic-palette — baseColor
- similar-shades-palette — baseColor, count, range
- sort-colors — источник (палитра-картинка или список), порядок (hsl/luma)
- mix-colors — colors[], веса?
- average-color — colors[]; blend-two — a, b, steps; step-between — a, b, steps
(три частных случая одного движка)
---
## 2. Можно добавить — из onlinepngtools
### Разложение каналов — остаток
- separate-colors — minShare слоя (MEDIUM, мультифайловый вывод → пока идея)
### Края и силуэт — остаток
- glow — radius, color, intensity
- shadow — offsetX, offsetY, blur, color, alpha
### Эффекты — остаток
- censor-region / erase-region — область (MEDIUM: нужен UI выделения → см. идеи)
- whirl — угол, центр, радиус (MEDIUM)
### Сортировка/блоки пикселей
- sort-pixels — blockSize, ключ (яркость/канал), направление (MEDIUM)
- slow-reveal / fade-in / fade-out / disappear — анимационные (→ идеи)
### Сжатие и качество — реализовано (см. Конвертация: compress, reduce-to-size; jpeg-artifacts; W8 quantize)
### Генераторы — остаток
- multi-color-gradient — список стопов (нужен новый тип параметра → MEDIUM-UI)
### Конвертеры — остаток
- png-to-gif — MEDIUM (однокадровый GIF-энкодер руками)
- gif-to-frames — MEDIUM (мультифайловый вывод → идея)
- change-bit-depth — MEDIUM (пересборка PNG)
### Прочее единичное
- pick-a-color — пипетка уже есть в превью; отдельная страница не планируется
(покрыто)
- preview/test на цветных фонах — фича превью, не инструмент (решить позже)
- extract-barcode — HARD, вне планов
---
## 3. Идеи на рассмотрение (нужна архитектура или спорная ценность)
- **Region-инструменты** — требуется UI выделения прямоугольника/ластика на
превью: censor-region, erase-region, pixelate-area, blur-area, sharpen-area,
reverse-colors-area. Один раз делаем selection-компонент — получаем сразу
шесть инструментов.
- **Мультифайловый вывод** — механизм «результат = набор файлов» (zip)
реализован Остальные 1→many: gif-to-frames, separate-colors,
multiply-grid-as-files — подключаются по мере нужды.
- **Анимационные эффекты** — slow-reveal, fade-in/out, disappearing, scrolling:
это видео/GIF на выходе, а не PNG. Отдельное решение о формате результата.
- **HARD-хвост** — glitch-art, extract-signature, handwritten→digital,
extract-barcode.
- **Нишевые серии** (logo/icon/stamp/signature — 107 клонов у источника) —
сознательно не копируем: это обычные операции над конкретным контентом, у нас
они доступны через базовые инструменты + цепочки.
+60
View File
@@ -0,0 +1,60 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Redirecting...</title>
<script>
const NEW_DOMAIN = "https://easy-image-tools.com";
const OLD_REPO_NAME = "easy-png-tools";
</script>
<style>
html,
body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
background-color: #ddd;
color: #222;
}
@media (prefers-color-scheme: dark) {
html,
body {
background-color: #222;
color: #ddd;
}
}
</style>
<script>
(function () {
let currentPath = window.location.pathname;
if (currentPath.startsWith("/" + OLD_REPO_NAME)) {
currentPath = currentPath.substring(OLD_REPO_NAME.length + 1);
}
const newUrl =
NEW_DOMAIN +
currentPath +
window.location.search +
window.location.hash;
window.location.replace(newUrl);
})();
</script>
<noscript>
<meta
http-equiv="refresh"
content="0; url=https://easy-image-tools.com"
/>
<link rel="canonical" href="https://easy-image-tools.com" />
</noscript>
</head>
<body>
<p>
"Easy png tools" now named "Easy image tools" and moved to a new domain.
Please follow this link: <a href="https://easy-image-tools.com"></a>.
</p>
</body>
</html>
-29
View File
@@ -1,29 +0,0 @@
{
"name": "easy-png-tools",
"version": "0.1.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "pnpm --dir web test",
"dev": "pnpm --dir web dev",
"build": "pnpm --dir web build",
"format": "pnpm format:docs && pnpm --dir web format",
"format:docs": "prettier --write docs --log-level warn",
"check:docs": "prettier --check docs",
"generate:fixtures": "node tests/fixtures/manual-edge-cases/generate.mjs"
},
"keywords": [],
"author": "Ku6epXBOCTuK",
"license": "MIT",
"devEngines": {
"packageManager": {
"name": "pnpm",
"version": "^11.20.0",
"onFail": "download"
}
},
"type": "module",
"devDependencies": {
"prettier": "^3.9.6"
}
}
-223
View File
@@ -1,223 +0,0 @@
---
lockfileVersion: '9.0'
importers:
.:
configDependencies: {}
packageManagerDependencies:
'@pnpm/exe':
specifier: ^11.20.0
version: 11.25.0
pnpm:
specifier: ^11.20.0
version: 11.25.0
packages:
'@pnpm/exe@11.25.0':
resolution: {integrity: sha512-X19R2uC+VAJ4UJQE9c/PCdOXbDaWnZtad7WUrTHBFQuMVaK9MAHbyO/WgahQCuRtTmJkLbxcWKKCk+JeTYFm/g==}
hasBin: true
'@pnpm/linux-arm64@11.25.0':
resolution: {integrity: sha512-ra8akqhzsbcOhKSJ9fFV8H+Oc9uGQAcp/XmIBEdT+8hPPoZCit3+RNCHmg8bLoNvpSLtRvZE0WFCMj7WyzxeBA==}
cpu: [arm64]
os: [linux]
'@pnpm/linux-x64@11.25.0':
resolution: {integrity: sha512-pQl/L10diKQCbF73viRrtVU8qVWMTudUCSG1uZ+EWBNKtU9Sbxe6Q378diXDb1uTwSgUVMQoypxlC1MTzh7qvQ==}
cpu: [x64]
os: [linux]
'@pnpm/linuxstatic-arm64@11.25.0':
resolution: {integrity: sha512-cYcrbB/xN1N6/5VUlFuHblb1gNyJgZv1CF5Pk1T0sHJxQMY4DFJV3OqHVAgahxyUfSTaQcVLvH1H2JFvd/+sgw==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@pnpm/linuxstatic-x64@11.25.0':
resolution: {integrity: sha512-HXDtaeQod16DDMUUcVLIMxvEc1jKn7IYsNPwbwAPs/Je/d3umRPJi3Ejjdn6e9qpXN6Oon+yvSsJr7B9oDt6KA==}
cpu: [x64]
os: [linux]
libc: [musl]
'@pnpm/macos-arm64@11.25.0':
resolution: {integrity: sha512-m/eAgEqKhiSexGxWPHNXNnSRI0hudymg6K7LbrU2EoDs9IgJ28OKEz9mGxMzhkYBweEquR4k5teMNes/aToy9w==}
cpu: [arm64]
os: [darwin]
'@pnpm/win-arm64@11.25.0':
resolution: {integrity: sha512-oAeECbtZ+eJziaBmPUkwJM8Dx7KVQ5FO367AXTjD2HGfB5ob1Bcu5hoUF5RBTgDBiP9fRQ1u13uAEpqar/6NLA==}
cpu: [arm64]
os: [win32]
'@pnpm/win-x64@11.25.0':
resolution: {integrity: sha512-8/n+wCc0a8TRYTMFqti93Vh0cscdJ25xfMyYSDdXrLUh2ccxSFuS+SE7cNPjd/nOXoFAJvdW0kwfbyRPLa16/w==}
cpu: [x64]
os: [win32]
'@reflink/reflink-darwin-arm64@0.1.19':
resolution: {integrity: sha512-ruy44Lpepdk1FqDz38vExBY/PVUsjxZA+chd9wozjUH9JjuDT/HEaQYA6wYN9mf041l0yLVar6BCZuWABJvHSA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
'@reflink/reflink-darwin-x64@0.1.19':
resolution: {integrity: sha512-By85MSWrMZa+c26TcnAy8SDk0sTUkYlNnwknSchkhHpGXOtjNDUOxJE9oByBnGbeuIE1PiQsxDG3Ud+IVV9yuA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
'@reflink/reflink-linux-arm64-gnu@0.1.19':
resolution: {integrity: sha512-7P+er8+rP9iNeN+bfmccM4hTAaLP6PQJPKWSA4iSk2bNvo6KU6RyPgYeHxXmzNKzPVRcypZQTpFgstHam6maVg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@reflink/reflink-linux-arm64-musl@0.1.19':
resolution: {integrity: sha512-37iO/Dp6m5DDaC2sf3zPtx/hl9FV3Xze4xoYidrxxS9bgP3S8ALroxRK6xBG/1TtfXKTvolvp+IjrUU6ujIGmA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@reflink/reflink-linux-x64-gnu@0.1.19':
resolution: {integrity: sha512-jbI8jvuYCaA3MVUdu8vLoLAFqC+iNMpiSuLbxlAgg7x3K5bsS8nOpTRnkLF7vISJ+rVR8W+7ThXlXlUQ93ulkw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@reflink/reflink-linux-x64-musl@0.1.19':
resolution: {integrity: sha512-e9FBWDe+lv7QKAwtKOt6A2W/fyy/aEEfr0g6j/hWzvQcrzHCsz07BNQYlNOjTfeytrtLU7k449H1PI95jA4OjQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [musl]
'@reflink/reflink-win32-arm64-msvc@0.1.19':
resolution: {integrity: sha512-09PxnVIQcd+UOn4WAW73WU6PXL7DwGS6wPlkMhMg2zlHHG65F3vHepOw06HFCq+N42qkaNAc8AKIabWvtk6cIQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
'@reflink/reflink-win32-x64-msvc@0.1.19':
resolution: {integrity: sha512-E//yT4ni2SyhwP8JRjVGWr3cbnhWDiPLgnQ66qqaanjjnMiu3O/2tjCPQXlcGc/DEYofpDc9fvhv6tALQsMV9w==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
'@reflink/reflink@0.1.19':
resolution: {integrity: sha512-DmCG8GzysnCZ15bres3N5AHCmwBwYgp0As6xjhQ47rAUTUXxJiK+lLUxaGsX3hd/30qUpVElh05PbGuxRPgJwA==}
engines: {node: '>= 10'}
detect-libc@2.1.2:
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
engines: {node: '>=8'}
pnpm@11.25.0:
resolution: {integrity: sha512-XN6SW08HX3Jetx+64YpC/+eEUkeJ8ZthxzHLhyHsKKruFg4BqNWvT+2ypCzb8wDv4j2zVrDUoXtNY+EfirfJVg==}
engines: {node: '>=22.13'}
hasBin: true
snapshots:
'@pnpm/exe@11.25.0':
dependencies:
'@reflink/reflink': 0.1.19
detect-libc: 2.1.2
optionalDependencies:
'@pnpm/linux-arm64': 11.25.0
'@pnpm/linux-x64': 11.25.0
'@pnpm/linuxstatic-arm64': 11.25.0
'@pnpm/linuxstatic-x64': 11.25.0
'@pnpm/macos-arm64': 11.25.0
'@pnpm/win-arm64': 11.25.0
'@pnpm/win-x64': 11.25.0
'@pnpm/linux-arm64@11.25.0':
optional: true
'@pnpm/linux-x64@11.25.0':
optional: true
'@pnpm/linuxstatic-arm64@11.25.0':
optional: true
'@pnpm/linuxstatic-x64@11.25.0':
optional: true
'@pnpm/macos-arm64@11.25.0':
optional: true
'@pnpm/win-arm64@11.25.0':
optional: true
'@pnpm/win-x64@11.25.0':
optional: true
'@reflink/reflink-darwin-arm64@0.1.19':
optional: true
'@reflink/reflink-darwin-x64@0.1.19':
optional: true
'@reflink/reflink-linux-arm64-gnu@0.1.19':
optional: true
'@reflink/reflink-linux-arm64-musl@0.1.19':
optional: true
'@reflink/reflink-linux-x64-gnu@0.1.19':
optional: true
'@reflink/reflink-linux-x64-musl@0.1.19':
optional: true
'@reflink/reflink-win32-arm64-msvc@0.1.19':
optional: true
'@reflink/reflink-win32-x64-msvc@0.1.19':
optional: true
'@reflink/reflink@0.1.19':
optionalDependencies:
'@reflink/reflink-darwin-arm64': 0.1.19
'@reflink/reflink-darwin-x64': 0.1.19
'@reflink/reflink-linux-arm64-gnu': 0.1.19
'@reflink/reflink-linux-arm64-musl': 0.1.19
'@reflink/reflink-linux-x64-gnu': 0.1.19
'@reflink/reflink-linux-x64-musl': 0.1.19
'@reflink/reflink-win32-arm64-msvc': 0.1.19
'@reflink/reflink-win32-x64-msvc': 0.1.19
detect-libc@2.1.2: {}
pnpm@11.25.0: {}
---
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
devDependencies:
prettier:
specifier: ^3.9.6
version: 3.9.6
packages:
prettier@3.9.6:
resolution: {integrity: sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==}
engines: {node: '>=14'}
hasBin: true
snapshots:
prettier@3.9.6: {}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 225 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 203 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 290 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 601 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 B

-1
View File
@@ -1 +0,0 @@
this is definitely not a png file
-329
View File
@@ -1,329 +0,0 @@
import { deflateSync } from "node:zlib";
import { writeFileSync, mkdirSync } from "node:fs";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = dirname(fileURLToPath(import.meta.url));
const CRC_TABLE = (() => {
const table = new Uint32Array(256);
for (let n = 0; n < 256; n++) {
let c = n;
for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
table[n] = c >>> 0;
}
return table;
})();
function crc32(data) {
let c = 0xffffffff;
for (let i = 0; i < data.length; i++)
c = CRC_TABLE[(c ^ data[i]) & 0xff] ^ (c >>> 8);
return (c ^ 0xffffffff) >>> 0;
}
function chunk(type, data) {
const out = Buffer.alloc(8 + data.length + 4);
out.writeUInt32BE(data.length, 0);
out.write(type, 4, "ascii");
data.copy(out, 8);
out.writeUInt32BE(crc32(out.subarray(4, 8 + data.length)), 8 + data.length);
return out;
}
function makePng(width, height, pixelAt) {
const sig = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
const ihdr = Buffer.alloc(13);
ihdr.writeUInt32BE(width, 0);
ihdr.writeUInt32BE(height, 4);
ihdr[8] = 8; // bit depth
ihdr[9] = 6; // color type RGBA
ihdr[10] = 0; // compression
ihdr[11] = 0; // filter
ihdr[12] = 0; // interlace
const stride = width * 4 + 1;
const raw = Buffer.alloc(height * stride);
for (let y = 0; y < height; y++) {
raw[y * stride] = 0; // filter: none
for (let x = 0; x < width; x++) {
const [r, g, b, a] = pixelAt(x, y);
const p = y * stride + 1 + x * 4;
raw[p] = r;
raw[p + 1] = g;
raw[p + 2] = b;
raw[p + 3] = a;
}
}
return Buffer.concat([
sig,
chunk("IHDR", ihdr),
chunk("IDAT", deflateSync(raw)),
chunk("IEND", Buffer.alloc(0)),
]);
}
function makePalettePng(width, height, palette, pixelAt) {
const sig = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
const ihdr = Buffer.alloc(13);
ihdr.writeUInt32BE(width, 0);
ihdr.writeUInt32BE(height, 4);
ihdr[8] = 8; // bit depth
ihdr[9] = 3; // color type: indexed
ihdr[10] = 0;
ihdr[11] = 0;
ihdr[12] = 0;
// PLTE: palette entries (R,G,B × count), padded to multiple of 3
const plteData = Buffer.alloc(256 * 3);
for (let i = 0; i < palette.length && i < 256; i++) {
plteData[i * 3] = palette[i][0];
plteData[i * 3 + 1] = palette[i][1];
plteData[i * 3 + 2] = palette[i][2];
}
// Raw image data: 1 byte per pixel (index)
const stride = width + 1;
const raw = Buffer.alloc(height * stride);
for (let y = 0; y < height; y++) {
raw[y * stride] = 0;
for (let x = 0; x < width; x++) {
raw[y * stride + 1 + x] = pixelAt(x, y);
}
}
return Buffer.concat([
sig,
chunk("IHDR", ihdr),
chunk("PLTE", plteData),
chunk("IDAT", deflateSync(raw)),
chunk("IEND", Buffer.alloc(0)),
]);
}
function make16bitPng(width, height, pixelAt) {
const sig = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
const ihdr = Buffer.alloc(13);
ihdr.writeUInt32BE(width, 0);
ihdr.writeUInt32BE(height, 4);
ihdr[8] = 16; // bit depth
ihdr[9] = 6; // color type RGBA
ihdr[10] = 0;
ihdr[11] = 0;
ihdr[12] = 0;
const stride = width * 8 + 1;
const raw = Buffer.alloc(height * stride);
for (let y = 0; y < height; y++) {
raw[y * stride] = 0;
for (let x = 0; x < width; x++) {
const [r, g, b, a] = pixelAt(x, y);
const p = y * stride + 1 + x * 8;
raw.writeUInt16BE(r, p);
raw.writeUInt16BE(g, p + 2);
raw.writeUInt16BE(b, p + 4);
raw.writeUInt16BE(a, p + 6);
}
}
return Buffer.concat([
sig,
chunk("IHDR", ihdr),
chunk("IDAT", deflateSync(raw)),
chunk("IEND", Buffer.alloc(0)),
]);
}
function makeCorruptPng() {
const sig = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
const ihdr = Buffer.alloc(13);
ihdr.writeUInt32BE(4, 0);
ihdr.writeUInt32BE(4, 4);
ihdr[8] = 8;
ihdr[9] = 2; // RGB
ihdr[10] = 0;
ihdr[11] = 0;
ihdr[12] = 0;
const stride = 4 * 3 + 1;
const raw = Buffer.alloc(4 * stride);
for (let y = 0; y < 4; y++) {
raw[y * stride] = 0;
for (let x = 0; x < 4; x++) {
const p = y * stride + 1 + x * 3;
raw[p] = 255;
raw[p + 1] = 0;
raw[p + 2] = 0;
}
}
const idat = chunk("IDAT", deflateSync(raw));
// Corrupt CRC: flip a byte in the CRC field
idat[idat.length - 1] ^= 0xff;
return Buffer.concat([sig, chunk("IHDR", ihdr), idat, chunk("IEND", Buffer.alloc(0))]);
}
function makeTruncatedPng() {
return Buffer.from([137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82]);
}
const outDir = join(__dirname);
// 1. PNG 1×1
writeFileSync(join(outDir, "1x1.png"), makePng(1, 1, () => [255, 0, 0, 255]));
// 2. PNG 1×1 fully transparent
writeFileSync(
join(outDir, "1x1-transparent.png"),
makePng(1, 1, () => [0, 0, 0, 0]),
);
// 3. Large 1024×1024 checkerboard (≈1 megapixel RGBA)
writeFileSync(
join(outDir, "1024x1024-checker.png"),
makePng(1024, 1024, (x, y) =>
x % 2 === y % 2 ? [255, 0, 0, 255] : [255, 255, 255, 255],
),
);
// 4. 1920×1080 gradient (≈2 megapixels, tests larger displays)
writeFileSync(
join(outDir, "1920x1080-gradient.png"),
makePng(1920, 1080, (x, y) => [
Math.floor((x / 1920) * 255),
Math.floor((y / 1080) * 255),
128,
255,
]),
);
// 5. Transparency checkerboard (black/white with alpha pattern)
writeFileSync(
join(outDir, "alpha-checkerboard.png"),
makePng(64, 64, (x, y) => {
const alpha = (x + y) % 3 === 0 ? 0 : 255;
const base = (x % 2 === y % 2) ? 0 : 255;
return [base, base, base, alpha];
}),
);
// 6. Palette / indexed-color PNG (4 colors: red, green, blue, white)
const palette = [
[255, 0, 0],
[0, 255, 0],
[0, 0, 255],
[255, 255, 255],
];
writeFileSync(
join(outDir, "palette-4colors.png"),
makePalettePng(32, 32, palette, (x, y) => {
const idx = (Math.floor(x / 8) + Math.floor(y / 8)) % palette.length;
return idx;
}),
);
// 7. Palette with many colors (256-color full palette)
const fullPalette = Array.from({ length: 256 }, (_, i) => [
i,
(i * 3) % 256,
(i * 7) % 256,
]);
writeFileSync(
join(outDir, "palette-256.png"),
makePalettePng(64, 64, fullPalette, (x, y) => (x + y * 64) % 256),
);
// 8. 16-bit RGBA PNG (subtle gradient to test high bit depth)
writeFileSync(
join(outDir, "16bit-subtle.png"),
make16bitPng(32, 32, (x, y) => [
Math.floor((x / 32) * 65535),
Math.floor((y / 32) * 65535),
32768,
65535,
]),
);
// 9. Corrupt PNG (bad CRC in IDAT)
writeFileSync(join(outDir, "corrupt-bad-crc.png"), makeCorruptPng());
// 10. Truncated PNG (only header, no data)
writeFileSync(join(outDir, "truncated.png"), makeTruncatedPng());
// 11. Non-PNG file disguised as PNG
writeFileSync(
join(outDir, "fake-png.txt"),
Buffer.from("this is definitely not a png file"),
);
// 12. All-black fully opaque
writeFileSync(
join(outDir, "all-black-opaque.png"),
makePng(64, 64, () => [0, 0, 0, 255]),
);
// 13. All-white fully opaque
writeFileSync(
join(outDir, "all-white-opaque.png"),
makePng(64, 64, () => [255, 255, 255, 255]),
);
// 14. All-transparent
writeFileSync(
join(outDir, "all-transparent.png"),
makePng(64, 64, () => [0, 0, 0, 0]),
);
// 15. Semi-transparent gradient (alpha 0→255 across width)
writeFileSync(
join(outDir, "alpha-gradient.png"),
makePng(128, 64, (x, y) => [
0,
100,
255,
Math.floor((x / 128) * 255),
]),
);
// 16. Noise pattern (random-ish pixels)
writeFileSync(
join(outDir, "noise-pattern.png"),
makePng(100, 100, (x, y) => {
const v = ((x * 7919 + y * 104729) % 256);
return [v, (v * 3) % 256, (v * 7) % 256, 255];
}),
);
// 17. Minimal size: 2×2 with extreme colors
writeFileSync(
join(outDir, "2x2-extreme.png"),
makePng(2, 2, (x, y) => {
if (x === 0 && y === 0) return [0, 0, 0, 255];
if (x === 1 && y === 0) return [255, 255, 255, 255];
if (x === 0 && y === 1) return [255, 0, 0, 128];
return [0, 0, 255, 0];
}),
);
// 18. Long strip (1×4000) — tests scroll / vertical tools
writeFileSync(
join(outDir, "1x4000-strip.png"),
makePng(1, 4000, (x, y) => [y % 256, (y * 2) % 256, (y * 3) % 256, 255]),
);
// 19. Wide strip (4000×1) — tests horizontal handling
writeFileSync(
join(outDir, "4000x1-strip.png"),
makePng(4000, 1, (x, y) => [x % 256, (x * 2) % 256, (x * 3) % 256, 255]),
);
// 20. PNG with very high alpha variance (sparse transparency)
writeFileSync(
join(outDir, "sparse-alpha.png"),
makePng(100, 100, (x, y) => {
const sparse = ((x * 31 + y * 17) % 100) < 10;
return [sparse ? 0 : 100, sparse ? 0 : 200, 255, sparse ? 0 : 255];
}),
);
console.log("Generated 20 edge case PNG files in:", outDir);
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 879 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 722 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 B

-28
View File
@@ -1,28 +0,0 @@
node_modules
# Output
.output
.vercel
.netlify
.wrangler
/.svelte-kit
/build
# OS
.DS_Store
Thumbs.db
# Env
.env
.env.*
!.env.example
!.env.test
# Vite
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
# Playwright
/test-results
/playwright-report
/e2e-results
-1
View File
@@ -1 +0,0 @@
engine-strict=true
-9
View File
@@ -1,9 +0,0 @@
node_modules
build
.svelte-kit
dist
pnpm-lock.yaml
static
*.log
.DS_Store
audit
-23
View File
@@ -1,23 +0,0 @@
{
"useTabs": true,
"singleQuote": false,
"trailingComma": "all",
"printWidth": 80,
"proseWrap": "always",
"endOfLine": "lf",
"plugins": ["prettier-plugin-svelte"],
"overrides": [
{
"files": "*.md",
"options": {
"useTabs": false
}
},
{
"files": "*.svelte",
"options": {
"parser": "svelte"
}
}
]
}
-3
View File
@@ -1,3 +0,0 @@
{
"recommendations": ["svelte.svelte-vscode"]
}
-45
View File
@@ -1,45 +0,0 @@
# sv
Everything you need to build a Svelte project, powered by
[`sv`](https://github.com/sveltejs/cli).
## Creating a project
If you're seeing this, you've probably already done this step. Congrats!
```sh
# create a new project
npx sv create my-app
```
To recreate this project with the same configuration:
```sh
# recreate this project
pnpm dlx sv@0.17.0 create --template minimal --types ts --install pnpm web
```
## Developing
Once you've created a project and installed dependencies with `npm install` (or
`pnpm install` or `yarn`), start a development server:
```sh
npm run dev
# or start the server and open the app in a new browser tab
npm run dev -- --open
```
## Building
To create a production version of your app:
```sh
npm run build
```
You can preview the production build with `npm run preview`.
> To deploy your app, you may need to install an
> [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.
-55
View File
@@ -1,55 +0,0 @@
import { expect, test } from "playwright/test";
import { expectNoErrors, trackErrors } from "./helpers/page";
const TOTAL = 122;
const GROUPS = 8;
test("catalog shows total and all groups", async ({ page }) => {
const sink = trackErrors(page);
await page.goto("/list-tools");
await expect(page.locator(".catalog-total b")).toHaveText(String(TOTAL));
await expect(page.locator(".catalog-group")).toHaveCount(GROUPS);
expectNoErrors(sink);
});
test("catalog search narrows results", async ({ page }) => {
await page.goto("/list-tools");
await expect(async () => {
await page.getByRole("textbox", { name: "Search tools" }).fill("resize");
await expect(page.locator(".tool-card")).toHaveCount(1);
}).toPass();
await expect(page.locator(".tool-card").first()).toContainText("Resize PNG");
});
test("category filter shows only matching group and resets on ALL", async ({
page,
}) => {
await page.goto("/list-tools");
const allButtons = page.locator(".tool-card");
const allCount = await allButtons.count();
expect(allCount).toBe(TOTAL);
await expect(async () => {
await page.getByRole("button", { name: "CONVERT" }).click();
await expect(page.locator(".catalog-group")).toHaveCount(1);
}).toPass();
await expect(page.locator(".catalog-group")).toContainText("Convert");
const convertCards = await page.locator(".tool-card").count();
expect(convertCards).toBeGreaterThan(0);
expect(convertCards).toBeLessThan(allCount);
await expect(async () => {
await page.getByRole("button", { name: "ALL" }).click();
await expect(page.locator(".catalog-group")).toHaveCount(GROUPS);
}).toPass();
await expect(page.locator(".tool-card")).toHaveCount(allCount);
});
test("workspace search matches a tool by title", async ({ page }) => {
await page.goto("/");
await expect(async () => {
await page.getByRole("textbox", { name: "Search tools" }).fill("flip");
await expect(page.locator(".tool-card")).toHaveCount(1);
}).toPass();
await expect(page.locator(".tool-card").first()).toContainText("Flip PNG");
});
-24
View File
@@ -1,24 +0,0 @@
import { expect, test } from "playwright/test";
import { expectNoErrors, openTool, trackErrors } from "./helpers/page";
test.describe("generators — known UI gap (#checklist, п.1)", () => {
// FIXME: Генераторы (21/121) открываются, но в UI нет ни полей схемы, ни кнопки
// «Generate» — только RU/EN/Reset. Пока баг открыт — fixme с ожидаемым
// сценарием; после фикса убрать fixme.
test.fixme("single-color-png renders Generate controls and produces a result", async ({
page,
}) => {
await openTool(page, "single-color-png");
const generate = page.getByRole("button", { name: "Generate" });
await expect(generate).toBeVisible();
await generate.click();
await expect(page.locator('img[alt="Result image"]')).toBeVisible();
});
test("create-empty-png page opens without errors", async ({ page }) => {
const sink = trackErrors(page);
await openTool(page, "create-empty-png");
await expect(page.locator("h1")).toContainText("Create");
expectNoErrors(sink);
});
});
-120
View File
@@ -1,120 +0,0 @@
import { deflateSync } from "node:zlib";
export interface SourceFile {
name: string;
mimeType: string;
buffer: Buffer;
}
const CRC_TABLE = (() => {
const table = new Uint32Array(256);
for (let n = 0; n < 256; n++) {
let c = n;
for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
table[n] = c >>> 0;
}
return table;
})();
function crc32(data: Uint8Array): number {
let c = 0xffffffff;
for (let i = 0; i < data.length; i++)
c = CRC_TABLE[(c ^ data[i]) & 0xff] ^ (c >>> 8);
return (c ^ 0xffffffff) >>> 0;
}
function chunk(type: string, data: Buffer): Buffer {
const out = Buffer.alloc(8 + data.length + 4);
out.writeUInt32BE(data.length, 0);
out.write(type, 4, "ascii");
data.copy(out, 8);
out.writeUInt32BE(crc32(out.subarray(4, 8 + data.length)), 8 + data.length);
return out;
}
/** Строит валидный RGBA PNG в рантайме (8-bit, фильтр type 0, deflate/zlib). */
export function makePng(
width: number,
height: number,
pixelAt: (x: number, y: number) => [number, number, number, number],
): Buffer {
const sig = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
const ihdr = Buffer.alloc(13);
ihdr.writeUInt32BE(width, 0);
ihdr.writeUInt32BE(height, 4);
ihdr[8] = 8;
ihdr[9] = 6;
const stride = width * 4 + 1;
const raw = Buffer.alloc(height * stride);
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const [r, g, b, a] = pixelAt(x, y);
const p = y * stride + 1 + x * 4;
raw[p] = r;
raw[p + 1] = g;
raw[p + 2] = b;
raw[p + 3] = a;
}
}
return Buffer.concat([
sig,
chunk("IHDR", ihdr),
chunk("IDAT", deflateSync(raw)),
chunk("IEND", Buffer.alloc(0)),
]);
}
const checker = (x: number, y: number): [number, number, number, number] =>
x % 2 === y % 2 ? [255, 0, 0, 255] : [255, 255, 255, 255];
const alphaGrid = (x: number, y: number): [number, number, number, number] =>
(x + y) % 3 === 0 ? [0, 0, 0, 0] : [0, 128, 255, 255];
const solidRed = (): [number, number, number, number] => [255, 0, 0, 255];
export function asSourceFile(buffer: Buffer, name: string): SourceFile {
return { name, mimeType: "image/png", buffer };
}
export const opaquePng: SourceFile = asSourceFile(
makePng(64, 48, checker),
"opaque.png",
);
export const transparentPng: SourceFile = asSourceFile(
makePng(64, 64, alphaGrid),
"transparent.png",
);
export const onePixelPng: SourceFile = asSourceFile(
makePng(1, 1, solidRed),
"1px.png",
);
export const landscapePng: SourceFile = asSourceFile(
makePng(64, 16, checker),
"landscape.png",
);
export const largePng: SourceFile = asSourceFile(
makePng(256, 256, checker),
"large.png",
);
export const corruptPng: SourceFile = {
name: "corrupt.png",
mimeType: "image/png",
buffer: Buffer.from("this is definitely not a png file", "utf8"),
};
export const tinyBase64 = makePng(8, 8, solidRed).toString("base64");
export const svgMarkup =
'<svg xmlns="http://www.w3.org/2000/svg" width="10" height="6">' +
'<rect width="10" height="6" fill="#ff0000"/></svg>';
const PIXEL_BYTES = (r: number, g: number, b: number, a: number): string =>
`${r} ${g} ${b} ${a}`;
/** Строка для bytes-to-png / rgb-values-to-png, ровно 32 пикселя в ширину по умолчанию. */
export function pixelRow(
count: number,
rgba: [number, number, number, number],
): string {
return Array.from({ length: count }, () => PIXEL_BYTES(...rgba)).join(" ");
}
-52
View File
@@ -1,52 +0,0 @@
import { expect, type Page } from "playwright/test";
import type { SourceFile } from "./fixtures";
export type ErrorSink = { errors: string[] };
export function trackErrors(page: Page): ErrorSink {
const errors: string[] = [];
page.on("pageerror", (e) => errors.push(`pageerror: ${e.message}`));
page.on("console", (m) => {
if (m.type() === "error") errors.push(`console: ${m.text()}`);
});
return { errors };
}
export async function openTool(page: Page, id: string): Promise<void> {
await page.goto(`/tools/${id}`);
await expect(page.locator(".schema-tool h1")).toBeVisible();
}
export async function uploadImage(page: Page, file: SourceFile): Promise<void> {
await page.locator('.actions input[type="file"]').setInputFiles({
name: file.name,
mimeType: file.mimeType,
buffer: file.buffer,
});
}
export async function metaValue(page: Page, caption: string): Promise<string> {
const row = page.locator(".meta-row", { hasText: caption });
await expect(row).toBeVisible();
const value = await row.locator(".meta-value").textContent();
return value?.trim() ?? "";
}
export async function suggestedDownloadName(
page: Page,
selector: string,
): Promise<string> {
const [download] = await Promise.all([
page.waitForEvent("download"),
page.locator(selector).click(),
]);
return download.suggestedFilename();
}
export async function expectNoErrorAlert(page: Page): Promise<void> {
await expect(page.locator('[role="alert"]')).toHaveCount(0);
}
export async function expectNoErrors(sink: ErrorSink): Promise<void> {
expect(sink.errors, "нет console/page errors").toEqual([]);
}
-48
View File
@@ -1,48 +0,0 @@
import { expect, test } from "playwright/test";
import { opaquePng } from "./helpers/fixtures";
import { openTool, uploadImage } from "./helpers/page";
test("поиск каталога находит по названию из другой локали", async ({
page,
}) => {
await page.goto("/list-tools");
const input = page.locator(".catalog-search input");
const flipCard = page.locator('a.tool-card[href*="flip-png"]');
await input.fill("отразить");
await expect(flipCard).toContainText("Flip PNG");
await page.getByRole("button", { name: "RU", exact: true }).click();
await expect(flipCard).toContainText("Отразить PNG");
await input.fill("flip");
await expect(flipCard).toContainText("Отразить PNG");
});
test("RU/EN: переключение переводит заголовок инструмента", async ({
page,
}) => {
await openTool(page, "resize-png");
const ruBtn = page.getByRole("button", { name: "RU", exact: true });
const enBtn = page.getByRole("button", { name: "EN", exact: true });
await expect(async () => {
await ruBtn.click();
await expect(page.locator(".schema-tool h1")).toHaveText(
"Изменить размер PNG",
);
}).toPass();
await expect(ruBtn).toHaveAttribute("aria-pressed", "true");
await enBtn.click();
await expect(page.locator(".schema-tool h1")).toHaveText("Resize PNG");
});
// Валится на известном баге resize-png (дефолтные 0-параметры дают ошибку при
// аплоаде, см. known-issues.spec.ts). Здесь он используется как детерминированная
// ошибка: важно, что её текст локализован, а не сырой ключ.
// TODO: найти другую ошибку, эта будет исправлена при фиксе resize-png
test("ошибка run'а локализуется, не сырой i18n-ключ", async ({ page }) => {
await openTool(page, "resize-png");
await uploadImage(page, opaquePng);
const alert = page.locator("[role='alert']");
await expect(alert).toBeVisible();
const text = (await alert.textContent()) ?? "";
expect(text).not.toMatch(/^errors\./);
expect(text.length).toBeGreaterThan(3);
});
-29
View File
@@ -1,29 +0,0 @@
import { expect, test } from "playwright/test";
import { opaquePng } from "./helpers/fixtures";
import { openTool, uploadImage } from "./helpers/page";
// FIXME: В файле зарегистрированы баги preview (см. docs/checklist-manual-testing.md
// → «Известные баги preview»). Тела assert'ят ОЖИДАЕМОЕ поведение. Статус fixme
// означает «мы знаем, что сейчас падает»; когда баг починят — убрать fixme и
// тест станет зелёным «сам по себе». (Тест про локализацию ошибок переехал в
// i18n.spec.ts — он больше не issue.)
test.describe("known bugs — documented as fixme", () => {
test.fixme("resize-png: upload produces a resized result (no alert)", async ({
page,
}) => {
await openTool(page, "resize-png");
await uploadImage(page, opaquePng);
await expect(page.locator("[role='alert']")).toHaveCount(0);
await expect(page.locator('img[alt="Result image"]')).toBeVisible();
});
test.fixme("crop-png: upload produces a cropped result (no alert)", async ({
page,
}) => {
await openTool(page, "crop-png");
await uploadImage(page, opaquePng);
await expect(page.locator("[role='alert']")).toHaveCount(0);
await expect(page.locator('img[alt="Result image"]')).toBeVisible();
});
});
-57
View File
@@ -1,57 +0,0 @@
import { expect, test } from "playwright/test";
import { expectNoErrors, trackErrors } from "./helpers/page";
const ROUTES = ["/", "/list-tools", "/kit", "/tools/flip-png"];
for (const route of ROUTES) {
test(`page ${route} loads without errors`, async ({ page }) => {
const sink = trackErrors(page);
const resp = await page.goto(route);
expect(resp?.status()).toBe(200);
await page.waitForLoadState("networkidle");
expectNoErrors(sink);
});
}
test("unknown tool route responds 404 on static build", async ({ page }) => {
const resp = await page.goto("/tools/definitely-not-a-tool");
expect(resp?.status()).toBe(404);
});
// FIXME:
// Error: expect(received).toBe(expected) // Object.is equality
// Expected: "dark"
// Received: null
// 36 | expect(["light", "dark"]).toContain(before);
// 37 | expect(["light", "dark"]).toContain(after);
// > 38 | expect(stored).toBe(after);
test.fixme("theme toggle flips preview theme and persists to localStorage", async ({
page,
}) => {
await page.goto("/");
const root = page.locator("main.preview-root");
const before = await root.getAttribute("data-theme");
await expect(async () => {
await page.getByRole("button", { name: "Toggle theme" }).click();
const after = await root.getAttribute("data-theme");
expect(after).not.toBe(before);
}).toPass();
const after = await root.getAttribute("data-theme");
const stored = await page.evaluate(() =>
localStorage.getItem("easy-png-tools:theme"),
);
expect(["light", "dark"]).toContain(before);
expect(["light", "dark"]).toContain(after);
expect(stored).toBe(after);
});
test("language toggle marks the active button", async ({ page }) => {
await page.goto("/");
const group = page.getByRole("group", { name: "Language" });
await expect(async () => {
await group.getByRole("button", { name: "EN" }).click();
await expect(page.locator(".lang-btn.active")).toHaveText("EN");
}).toPass();
});
-116
View File
@@ -1,116 +0,0 @@
import { expect, test } from "playwright/test";
import {
corruptPng,
landscapePng,
largePng,
onePixelPng,
opaquePng,
} from "./helpers/fixtures";
import {
expectNoErrorAlert,
expectNoErrors,
metaValue,
openTool,
suggestedDownloadName,
trackErrors,
uploadImage,
} from "./helpers/page";
test("flip-png: full flow - upload, result, meta, download", async ({
page,
}) => {
const sink = trackErrors(page);
await openTool(page, "flip-png");
await uploadImage(page, landscapePng);
await expect(page.locator('img[alt="Result image"]')).toBeVisible();
await expect(page.locator('img[alt="Result image"]')).toHaveAttribute(
"src",
/^(blob:|data:image\/png;base64,)/,
);
const resultMeta = await metaValue(page, "RESULT");
expect(resultMeta).toBe("64 × 16 px");
const downloadName = await suggestedDownloadName(
page,
'button[aria-label="Download result"]',
);
expect(downloadName).toBe("flip-png.png");
await expectNoErrorAlert(page);
expectNoErrors(sink);
});
test("convert-png-to-jpg: produces a downloadable jpg", async ({ page }) => {
const sink = trackErrors(page);
await openTool(page, "convert-png-to-jpg");
await uploadImage(page, opaquePng);
await expect(page.locator('img[alt="Result image"]')).toBeVisible();
const downloadName = await suggestedDownloadName(
page,
'button[aria-label="Download result"]',
);
expect(downloadName).toBe("convert-png-to-jpg.jpg");
await expectNoErrorAlert(page);
expectNoErrors(sink);
});
test.fixme("reset clears result but keeps source", async ({ page }) => {
await openTool(page, "flip-png");
await uploadImage(page, opaquePng);
await expect(page.locator('img[alt="Result image"]')).toBeVisible();
await page.getByRole("button", { name: "Reset" }).click();
await expect(page.locator('img[alt="Result image"]')).toHaveCount(0);
await expect(page.locator(".empty")).toContainText("no result yet");
await expect(page.locator('img[alt="source"]')).toBeVisible();
});
test("blur: changing slider updates result image", async ({ page }) => {
await openTool(page, "blur-png");
await uploadImage(page, opaquePng);
await expect(page.locator('img[alt="Result image"]')).toBeVisible();
const src1 = await page
.locator('img[alt="Result image"]')
.getAttribute("src");
await expect(async () => {
await page.locator('input[type="range"]').fill("20");
await expect(page.locator('img[alt="Result image"]')).not.toHaveAttribute(
"src",
src1 ?? "",
);
}).toPass();
});
test("runs without Web Worker (fallback)", async ({ browser }) => {
const context = await browser.newContext();
await context.addInitScript(() => {
Object.defineProperty(window, "Worker", {
value: undefined,
configurable: true,
});
});
const page = await context.newPage();
const sink = trackErrors(page);
await openTool(page, "flip-png");
await uploadImage(page, landscapePng);
await expect(page.locator('img[alt="Result image"]')).toBeVisible();
await expectNoErrorAlert(page);
expectNoErrors(sink);
await context.close();
});
test("invalid file upload shows error, no crash", async ({ page }) => {
const sink = trackErrors(page);
await openTool(page, "flip-png");
await uploadImage(page, corruptPng);
await expect(page.locator('[role="alert"]')).toBeVisible();
await expect(page.locator('img[alt="Result image"]')).toHaveCount(0);
expectNoErrors(sink);
});
for (const fixture of [onePixelPng, largePng]) {
test(`handles ${fixture.name} correctly`, async ({ page }) => {
const sink = trackErrors(page);
await openTool(page, "flip-png");
await uploadImage(page, fixture);
await expect(page.locator('img[alt="Result image"]')).toBeVisible();
expectNoErrors(sink);
});
}
-102
View File
@@ -1,102 +0,0 @@
import { expect, test } from "playwright/test";
import {
opaquePng,
pixelRow,
svgMarkup,
tinyBase64,
transparentPng,
} from "./helpers/fixtures";
import {
expectNoErrorAlert,
expectNoErrors,
openTool,
trackErrors,
uploadImage,
} from "./helpers/page";
for (const [id, input] of [
["base64-to-png", tinyBase64],
["hex-to-png", "ff0000ff"],
["bytes-to-png", pixelRow(32, [255, 0, 0, 255])],
[
"rgb-values-to-png",
Array.from({ length: 32 }, () => "rgba(255,0,0,255)").join(" "),
],
["svg-to-png", svgMarkup],
] as const) {
test(`text input → ${id} produces result image`, async ({ page }) => {
const sink = trackErrors(page);
await expect(async () => {
await openTool(page, id);
await page.locator(".text-source textarea").fill(input);
await page.getByRole("button", { name: "Render text" }).click();
await expect(page.locator('img[alt="Result image"]')).toBeVisible();
}).toPass({ timeout: 25_000 });
await expectNoErrorAlert(page);
expectNoErrors(sink);
});
}
test("png-to-base64 shows decoded text result", async ({ page }) => {
const sink = trackErrors(page);
await openTool(page, "png-to-base64");
await uploadImage(page, opaquePng);
const code = page.locator(".result-pre code");
await expect(code).toBeVisible();
const text = (await code.textContent()) ?? "";
expect(text.length).toBeGreaterThan(20);
await expectNoErrorAlert(page);
expectNoErrors(sink);
});
for (const [input, expected] of [
[tinyBase64, "Yes — this is a valid PNG signature."],
["aGVsbG8=", "No — the signature does not match a PNG file."],
] as const) {
test(`verify-is-png verdict: ${expected}`, async ({ page }) => {
await expect(async () => {
await openTool(page, "verify-is-png");
await page.locator(".text-source textarea").fill(input);
await page.getByRole("button", { name: "Render text" }).click();
await expect(page.locator(".verdict-text")).toContainText(expected);
}).toPass({ timeout: 25_000 });
});
}
test("png-is-transparent: opaque image → 'No'", async ({ page }) => {
await openTool(page, "png-is-transparent");
await uploadImage(page, opaquePng);
await expect(page.locator(".verdict-text")).toContainText(
"No — all pixels are fully opaque.",
);
});
test("png-is-transparent: transparent image → 'Yes'", async ({ page }) => {
await openTool(page, "png-is-transparent");
await uploadImage(page, transparentPng);
await expect(page.locator(".verdict-text")).toContainText(
"Yes — there are transparent or semi-transparent pixels.",
);
});
test("png-is-grayscale: colored image → 'No'", async ({ page }) => {
await openTool(page, "png-is-grayscale");
await uploadImage(page, opaquePng);
await expect(page.locator(".verdict-text")).toContainText(
"No — colored pixels were found.",
);
});
test("png-orientation: landscape image → Landscape", async ({ page }) => {
await openTool(page, "png-orientation");
await uploadImage(page, opaquePng); // 64×48 → landscape
await expect(page.locator(".verdict-text")).toContainText("Landscape");
});
test("png-file-size: returns a size in KB", async ({ page }) => {
await openTool(page, "png-file-size");
await uploadImage(page, opaquePng);
const verdict = page.locator(".verdict-text");
await expect(verdict).toBeVisible();
await expect(verdict).toContainText("KB");
});
-124
View File
@@ -1,124 +0,0 @@
import { expect, test } from "playwright/test";
import type { SourceFile } from "./helpers/fixtures";
import { opaquePng, transparentPng } from "./helpers/fixtures";
import {
expectNoErrorAlert,
expectNoErrors,
openTool,
trackErrors,
uploadImage,
} from "./helpers/page";
type Kind = "image" | "text-out" | "verdict";
const CASES: { id: string; kind: Kind; file?: SourceFile }[] = [
// ── convert ────────────────────────────────────────────────
{ id: "convert-png-to-jpg", kind: "image" },
{ id: "convert-png-to-webp", kind: "image" },
{ id: "png-to-bmp", kind: "image" },
{ id: "png-to-base64", kind: "text-out" },
// ── alpha ──────────────────────────────────────────────────
{ id: "remove-background-png", kind: "image", file: transparentPng },
{ id: "remove-color-from-png", kind: "image", file: transparentPng },
{ id: "round-corners-png", kind: "image", file: transparentPng },
{ id: "add-stroke-png", kind: "image", file: transparentPng },
{ id: "circle-mask-png", kind: "image", file: transparentPng },
{ id: "wavy-mask-png", kind: "image", file: transparentPng },
{ id: "find-contour-png", kind: "image", file: transparentPng },
{ id: "feather-edges-png", kind: "image", file: transparentPng },
{ id: "clean-edges-png", kind: "image", file: transparentPng },
{ id: "make-thicker-png", kind: "image", file: transparentPng },
{ id: "make-thinner-png", kind: "image", file: transparentPng },
{ id: "despeckle-alpha-png", kind: "image", file: transparentPng },
{ id: "close-holes-png", kind: "image", file: transparentPng },
{ id: "harden-alpha-png", kind: "image", file: transparentPng },
{ id: "invert-alpha-png", kind: "image", file: transparentPng },
{ id: "set-alpha-channel-png", kind: "image", file: transparentPng },
{ id: "extract-alpha-mask-png", kind: "image", file: transparentPng },
{ id: "remove-alpha-channel-png", kind: "image", file: transparentPng },
{ id: "square-mask-png", kind: "image", file: transparentPng },
{ id: "star-mask-png", kind: "image", file: transparentPng },
// ── color ──────────────────────────────────────────────────
{ id: "grayscale-png", kind: "image" },
{ id: "invert-colors-png", kind: "image" },
{ id: "sepia-png", kind: "image" },
{ id: "quantize-png", kind: "image" },
{ id: "dithering-png", kind: "image" },
{ id: "decrease-color-count-png", kind: "image" },
{ id: "two-colors-png", kind: "image" },
{ id: "tint-png", kind: "image" },
{ id: "auto-contrast-png", kind: "image" },
{ id: "png-to-hsl", kind: "image" },
{ id: "png-to-cmyk", kind: "image" },
// ── geometry ───────────────────────────────────────────────
{ id: "flip-png", kind: "image" },
{ id: "rotate-png", kind: "image" },
{ id: "add-border-png", kind: "image" },
{ id: "tile-png", kind: "image" },
{ id: "change-canvas-size-png", kind: "image" },
{ id: "trim-empty-space-png", kind: "image" },
{ id: "swap-orientation-png", kind: "image" },
{ id: "skew-png", kind: "image" },
{ id: "zoom-png", kind: "image" },
{ id: "center-by-alpha-png", kind: "image" },
{ id: "change-aspect-ratio-png", kind: "image" },
{ id: "symmetric-copy-png", kind: "image" },
{ id: "shift-png", kind: "image" },
// ── filters ────────────────────────────────────────────────
{ id: "blur-png", kind: "image" },
{ id: "sharpen-png", kind: "image" },
{ id: "pixelate-png", kind: "image" },
{ id: "add-noise-png", kind: "image" },
{ id: "vignette-png", kind: "image" },
{ id: "silhouette-png", kind: "image" },
{ id: "randomize-pixels-png", kind: "image" },
{ id: "jpeg-artifacts-png", kind: "image" },
// ── text ───────────────────────────────────────────────────
{ id: "add-text-png", kind: "image" },
{ id: "date-stamp-png", kind: "image" },
{ id: "watermark-tile-png", kind: "image" },
// ── analyze (image output) ─────────────────────────────────
{ id: "extract-color-from-png", kind: "image", file: transparentPng },
{ id: "show-transparent-png", kind: "image", file: transparentPng },
{ id: "light-pixel-mask-png", kind: "image", file: transparentPng },
{ id: "extract-channel-png", kind: "image", file: transparentPng },
{ id: "unique-color-mask-png", kind: "image", file: transparentPng },
{ id: "show-grayscale-pixels-png", kind: "image", file: transparentPng },
{ id: "show-color-pixels-png", kind: "image", file: transparentPng },
{ id: "dark-pixel-mask-png", kind: "image", file: transparentPng },
// ── analyze (verdict output) ───────────────────────────────
{ id: "png-is-transparent", kind: "verdict" },
{ id: "png-is-grayscale", kind: "verdict" },
{ id: "png-orientation", kind: "verdict" },
{ id: "png-file-size", kind: "verdict" },
];
test.describe("smoke: tools produce output without errors", () => {
for (const { id, kind, file: fixture } of CASES) {
test(`tool ${id} (${kind})`, async ({ page }) => {
const sink = trackErrors(page);
const outputLocator = (): ReturnType<typeof page.locator> => {
switch (kind) {
case "image":
return page.locator('img[alt="Result image"]');
case "text-out":
return page.locator(".result-pre code");
case "verdict":
return page.locator(".verdict-text");
}
};
// toPass: устойчивость к hydration-рейсу (ввод до гидрации SvelteKit
// может не обработаться с первого раза).
await expect(async () => {
await openTool(page, id);
await uploadImage(page, fixture ?? opaquePng);
await expect(outputLocator()).toBeVisible({ timeout: 8_000 });
}).toPass({ timeout: 25_000 });
await expectNoErrorAlert(page);
expectNoErrors(sink);
});
}
});
-180
View File
@@ -1,180 +0,0 @@
# ESLint-плагины и дизайн-токены: детали
Эта часть репозитория подробно описана здесь, а не в корневом `AGENTS.md`:
рутина-разработка её не требует, детали нужны только при правке самих
линт-правил или конфигов.
## Структура
- `web/eslint-plugins/` — локальные ESLint-плагины.
- `design-tokens/` — правила для `<style>`-блоков svelte-компонентов.
- `isolation/` — правило изоляции old/new UI.
- `conventions/` — конвенции кода (интерфейс пропсов, запрет union-алиасов).
- `i18n/` — кросс-языковой линтер словарей (`dict-consistency`).
- `__tests__/` — юнит-тесты (Vitest + `RuleTester`/`Linter`).
- `__fixtures__/` — фикстуры для тестов: миниатюрный `src/app.css` (словарь
токенов), файлы для изоляционных тестов и мини-словари `lib/i18n/`.
- `web/scripts/`:
- `lint-all.mjs` — оркестратор `lint:all`.
- `check-tokens.mjs` + `token-audit/` — токен-аудит по `app.css`.
- `postcss-hct.mjs` — постcss-плагин эммита `hct()` в sRGB-hex.
- `web/stylelint.config.js`, `web/postcss.config.js` — конфиги stylelint/css.
## Конфигурация ESLint (`web/eslint.config.js`)
Устроена инкрементально:
- На **весь код** — парсинг TS/Svelte + правило
`@typescript-eslint/consistent-type-imports` (запрет инлайн-тип-импортов,
`prefer: 'type-imports'`).
- Полные `recommended`-наборы (`eslint` + `typescript-eslint` +
`eslint-plugin-svelte`) навешаны **только на новый код**
(`src/lib/components/**`, `src/routes/**`), чтобы старый код не засыпался
предсуществующими ошибками. Старый код (C17) — `src/lib/v1/**`,
`src/routes/v1/**`. Когда старый дизайн удалён (C19), scoped-блок убирается и
`recommended` включается на весь код.
- **Scoped-пути двигаются вместе с папками** (см. `plan-redesign.md` §10): не
оставлять устаревшие пути в `eslint.config.js` (C17 уже двигал
`routes/preview/**` → `routes/**` и `components/kit/**` → `components/**`; на
C19 при наведении порядка в `components/` paths останутся `components/**` +
`routes/**`).
- Для `*.svelte` выключен `prefer-const` (пропсы в Svelte 5 пишутся через
`let`).
- `allowDefaultProject` перечисляет test-файлы и фикстуры точечно (glob'ы с `**`
там запрещены tseslint) — при добавлении файлов в `__tests__/` /
`__fixtures__/` дописать их туда же.
- design-tokens правила применяются к тем же scoped-путям
(`src/lib/components/**`, `src/routes/**`).
- isolation-правило включено на весь `**/*.{ts,svelte}`.
## Правила плагина design-tokens
- `design-tokens/no-hardcoded-in-svelte` — запрет прямых цветов/размеров/
длительностей/z-index и `color-mix()` (результат токенизировать в CSS).
Исключения размеров: `0`, `0px`, `1px`, проценты и unitless (`line-height`).
- `design-tokens/no-category-mismatch` — size-свойство не может использовать
color-токен (`--color-*`/`--brand-*`) и наоборот.
- `design-tokens/no-token-definition-in-svelte` — определение `--x:` в
компоненте не может содержать примитив (hex/rgb/oklch/px/rem); допустимы
производные от токенов (`var()`, `calc()`, unitless-числа).
- `design-tokens/no-undefined-in-svelte` — `var(--x)` в `<style>` должен быть
определён в `src/app.css` (словарь токенов) или локально в компоненте. Файл
читается один раз и кэшируется (не `glob`-зависим).
(ESLint лезет в `<style>`-блоки через постпрефес postcss AST от
`svelte-eslint-parser`; stylelint прогоняется по всем CSS-файлам.)
## Правило плагина i18n (`i18n/dict-consistency`)
Кросс-языковой линтер словарей `lib/i18n/` (план: `docs/plan-preview-i18n.md`
§Фаза 8). Сравнивает каждый словарь со **всеми** остальными локалями (не только
с BASE). Список локалей берётся из `LOCALES` в `lib/i18n/dict.ts` — новый
словарь подхватывается без правки правила.
- **warn-only, никогда `error`** — пропущенный перевод не валит сборку.
- Диагностика живёт там, где фикс: каждый файл репортит **только свои**
расхождения:
- **паритет ключей**: правило собирает union всех ключей из словарей-соседей
(с диска) и для текущего файла репортит те, которых в нём нет — ровно одно
предупреждение на разрыв, независимо от числа локалей, где ключ есть;
пропавшая целая секция репортится один раз (не по каждому потомку);
- **пустые значения**: `""` и строка из одних пробелов;
- **паритет плейсхолдеров**: у одинакового ключа набор `{name}` сравнивается с
каноническим (мажоритарный по локалям, при равенстве — первый по `LOCALES`)
— ловит потерянную при переводе переменную ровно один раз, даже когда
отклоняется одна-единственная локаль.
- Словари-соседи читаются с диска и кэшируются на процесс линта (как
`no-undefined-in-svelte`); каждый парсится `@typescript-eslint/parser`
(синтакс, без типов).
- Опция `allowPaths: string[]` — dot-path ключи, исключаемые из всех проверок
(осознанные отклонения до достижения zero-warn).
- Тесты: `__tests__/dict-consistency.test.ts`, фикстуры-словари в
`__fixtures__/src/lib/i18n/` (`en`, `ru`, `de` — консистентные, zero-warn) — в
тестах подаются модифицированные варианты `en.ts`/`ru.ts` против эталонных на
диске.
## Токены-префиксы (целевой словарь дизайна)
- Цвета: `--color-*`, бренд `--brand-main` / `--brand-alt` — единственные две
переменные, которым разрешено быть hex/rgb, остальные цвета — только `hct()` и
только в app.css.
- Размеры: `--space-*`, `--text-*` (font-size), `--radius-*`, `--size-*`.
- Брейкпоинты: `@custom-media --bp-mobile (max-width: 640px)` /
`--bp-tablet (800px)` / `--bp-desktop (1100px)` — объявляются в app.css,
используются как `@media (--bp-*)`. CSS-переменные в `@media` не работают,
поэтому отдельных `--bp-*` токенов нет; раскрытие делает postcss-плагин
`postcss-custom-media` (конфиг `web/postcss.config.js`, определения
подтягиваются через `@csstools/postcss-global-data`).
- z-index: `--z-*`; длительности/анимации: `--duration-*`, `--ease-*`.
Нюансы stylelint:
- Правило `custom-property-pattern` тестирует паттерн **без** `--` (`--x` →
`x`), а `declaration-property-value-disallowed-list` — целиком с `--`.
- `custom-property-empty-line-before` перенастроен (`after-custom-property` в
`ignore`, не в `except`): пустые строки между подряд идущими токенами
**свободны** и `--fix` их не удаляет — можно группировать цветовые и размерные
токены в app.css отдельными блоками.
## Токен-аудит (`check-tokens.mjs`)
Лёгкий оркестратор поверх `web/scripts/token-audit/*`. Проверяет:
- **Parity**: каждый цветовой токен из `:root` обязан иметь пару в
`[data-theme="dark"]` и наоборот. Производные токены (значение содержит
`var()`, напр. `hct(from var(--...))`) из пары исключены — они наследуют тему
автоматически.
- **hct-only для цветов**: ЛЮБОЕ цветовое значение в app.css обязано быть
`hct(...)` — и литерал, и производное `hct(from var(...) h c t)`. Исключения:
только `--brand-main` / `--brand-alt` (seed-токены, любая форма) и
`color-mix(...)` (единственный легальный способ смешать два токена).
`oklch()/rgb()/#hex` в `--color-*` запрещены.
Выводит **варнинг** о неиспользуемых токенах app.css (определены, но нигде не
используются) — выход он не меняет. Провалом (exit 1) считаются только parity и
hct-авторство.
## Тесты кастомных линт-правил
Линт-правила покрыты юнит-тестами (Vitest, `test:rules`):
- `__tests__/design-tokens.test.ts` — три чистых правила
(`no-hardcoded-in-svelte`, `no-category-mismatch`,
`no-token-definition-in-svelte`) через `RuleTester` со строковыми кейсами;
`no-undefined-in-svelte` — через `Linter` API, т.к. читает словарь токенов из
`__fixtures__/src/app.css` (не из реального `src/app.css`).
- `__tests__/no-mixed-imports.test.ts` — isolation-правило через `Linter` API с
`cwd` на `__fixtures__`: правило резолвит импорты по реальным файлам, поэтому
цели импортов обязаны существовать на диске.
- Хелпер `__tests__/helpers.ts` собирает `Linter` с `cwd = __fixtures__` —
`process.cwd()` не трогается, реальные `src/` не читаются.
Запуск: `pnpm --dir web test:rules` (`vitest run eslint-plugins/__tests__`).
Полный `pnpm --dir web test` тоже их гоняет.
Фикстуры живут в `__fixtures__/` и сами прогоняются линтом (`eslint .`), поэтому
добавление/правка стабов — тоже работа с валидным кодом.
## Isolation-правило (`isolation/no-mixed-imports`)
**Полная взаимная изоляция** старого (v1) и нового (корень) UI. В отличие от
`no-restricted-imports`, правило **резолвит** каждый импорт (и `$lib/...`, и
относительные `./`/`../`) до реального файла и классифицирует стороны по
фактическому пути, поэтому относительным импортом правило не обойти.
- Старое: `routes/v1/**`, `lib/v1/**`.
- Новое: `routes/**` без `v1/**`; корневой `lib` целиком (`lib/**`) без
`lib/v1/**` и без общих пакетов.
- Общее (разрешено обоим) — только служебные пакеты: `core/`, `theme`. v1 не
высовывается: из `lib/v1/**` наружу разрешены импорты лишь в общие пакеты и в
собственный форк i18n (`lib/v1/i18n/**`).
- Плагин **конфигурируем** (опции `old`/`new` + `root`/`alias` в
`eslint.config.js`): перенос старых файлов в папки `old/` — это правка
glob-паттернов в настройке, а не код правила.
- Изоляция уже достигнута: старый `lib/v1/registry.ts`/`registry-helpers.ts` не
тянут `registry-schema` (пилоты add-border/add-stroke работают через
`params`), `registry.ts` не импортирует `ToolSchema`; новый `list-tools`
использует `$lib/tool-icons.ts`, а не старые `lib/v1/tools/tool-icons.ts`.
Линтер только показывает ошибки, старый код (непрефиксованные токены, hex в
`app_v1.css`) — известный техдолг, его НЕ чинить и не игнорировать правилами.
@@ -1,11 +0,0 @@
:root {
--color-fg: #101010;
--space-1: calc(var(--size-1) / 2);
--z-nav: 100;
--duration-fast: 120ms;
--text-md: 0.875rem;
}
@@ -1 +0,0 @@
<!-- Fixture stub for lint-rule tests: content is ignored, must exist for import resolution. -->
@@ -1 +0,0 @@
<!-- Fixture stub for lint-rule tests: content is ignored, must exist for import resolution. -->
@@ -1 +0,0 @@
// Fixture stub for lint-rule tests: content is ignored, must exist for import resolution.
@@ -1,24 +0,0 @@
import type { Dict } from "./dict";
export const de: Dict = {
header: {
workspace: "Arbeitsbereich",
catalog: "Katalog",
},
home: {
hero: "Hallo {name}!",
restoreLast: "{title} wiederherstellen",
},
errors: {
sourceRequired: "Quelle erforderlich",
},
tools: {
addBorder: {
title: "Rahmen hinzufügen",
description: "Zeichnet einen {kind}-Rahmen.",
results: {
done: "Rahmen {what}",
},
},
},
};
@@ -1,16 +0,0 @@
export const LOCALES = ["en", "ru", "de"] as const;
export const BASE_LOCALE: "en" = "en";
export type Dict = {
header: Record<string, string>;
home: Record<string, string>;
errors: Record<string, string>;
tools: Record<string, ToolStrings>;
};
export type ToolStrings = {
title?: string;
description?: string;
results?: Record<string, string>;
};
@@ -1,24 +0,0 @@
import type { Dict } from "./dict";
export const en: Dict = {
header: {
workspace: "Workspace",
catalog: "Catalog",
},
home: {
hero: "Hello {name}!",
restoreLast: "Restore {title}",
},
errors: {
sourceRequired: "Source image required",
},
tools: {
addBorder: {
title: "Add border",
description: "Draws a {kind} frame.",
results: {
done: "Border {what}",
},
},
},
};
@@ -1,24 +0,0 @@
import type { Dict } from "./dict";
export const ru: Dict = {
header: {
workspace: "Рабочая область",
catalog: "Каталог",
},
home: {
hero: "Привет, {name}!",
restoreLast: "Вернуть {title}",
},
errors: {
sourceRequired: "Нужен исходник",
},
tools: {
addBorder: {
title: "Добавить рамку",
description: "Рисует рамку {kind}.",
results: {
done: "Рамка {what}",
},
},
},
};
@@ -1 +0,0 @@
// Fixture stub for lint-rule tests: content is ignored, must exist for import resolution.
@@ -1 +0,0 @@
// Fixture stub for lint-rule tests: content is ignored, must exist for import resolution.
@@ -1 +0,0 @@
// Fixture stub for lint-rule tests: content is ignored, must exist for import resolution.
@@ -1,2 +0,0 @@
// Fixture stub for lint-rule tests: content is ignored (the test supplies the
// source code), this file only has to EXIST for import resolution.
@@ -1 +0,0 @@
<!-- Fixture stub for lint-rule tests: content is ignored, must exist for import resolution. -->

Some files were not shown because too many files have changed in this diff Show More