style: format docs markdown

This commit is contained in:
2026-09-09 04:13:13 +05:00
parent 46da54140c
commit 64e35f4fdf
36 changed files with 2092 additions and 1187 deletions
+5
View File
@@ -0,0 +1,5 @@
# Allowlist: prettier в корне форматирует только markdown docs/.
# web/ и refs/ имеют свои тулинги, refs-html/ — генерат: всё остальное
# в корне (включая появившееся позже) игнорируется целиком.
/*
!/docs
+3
View File
@@ -0,0 +1,3 @@
{
"proseWrap": "always"
}
+6 -2
View File
@@ -11,9 +11,13 @@
- Сборка: `pnpm --dir web build`, проверка типов: `pnpm --dir web exec svelte-check --tsconfig ./tsconfig.json`, - Сборка: `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, тесты: `pnpm --dir web test` (Vitest), e2e: `pnpm --dir web test:e2e` (Playwright,
`web/e2e/`, свой webServer на `pnpm build` + `scripts/serve-static.mjs`, порт 4173). `web/e2e/`, свой webServer на `pnpm build` + `scripts/serve-static.mjs`, порт 4173).
- Форматирование: `pnpm --dir web format` (Prettier + `prettier-plugin-svelte`, - Форматирование: два независимых прогона. Корневой prettier форматирует
только markdown `docs/` (`.prettierrc` + allowlist `.prettierignore` в корне,
`pnpm format:docs`, проверка — `pnpm check:docs`). Код `web/` форматируется
отдельно: `pnpm --dir web format` (Prettier + `prettier-plugin-svelte`,
конфиг `web/.prettierrc`, игнор `web/.prettierignore`). Проверка без записи: конфиг `web/.prettierrc`, игнор `web/.prettierignore`). Проверка без записи:
`pnpm --dir web exec prettier --check .`. `pnpm --dir web exec prettier --check .`. Оба прогона сразу: `pnpm format`
в корне.
- Линтинг: `pnpm --dir web lint` (ESLint, flat-конфиг `web/eslint.config.js`). - Линтинг: `pnpm --dir web lint` (ESLint, flat-конфиг `web/eslint.config.js`).
Устроен инкрементально: Устроен инкрементально:
- На **весь код** — парсинг TS/Svelte + правило - На **весь код** — парсинг TS/Svelte + правило
+2 -1
View File
@@ -51,7 +51,8 @@ Static output is written to `web/build/`.
| `pnpm --dir web test` | Run unit tests (Vitest) | | `pnpm --dir web test` | Run unit tests (Vitest) |
| `pnpm --dir web check` | Type-check (svelte-check) | | `pnpm --dir web check` | Type-check (svelte-check) |
| `pnpm --dir web lint` | Lint (ESLint) | | `pnpm --dir web lint` | Lint (ESLint) |
| `pnpm --dir web format` | Format (Prettier) | | `pnpm --dir web format` | Format code (Prettier) |
| `pnpm format:docs` | Format docs (Prettier) |
| `pnpm --dir web preview` | Preview production build | | `pnpm --dir web preview` | Preview production build |
## Project Structure ## Project Structure
+208 -74
View File
@@ -1,9 +1,13 @@
# Анализ: набор PNG-утилит для браузера # Анализ: набор PNG-утилит для браузера
Проект: **easy-png-tools** — набор PNG-утилит, работающий полностью в браузере. Вдохновлён идеей онлайн-сервисов «все PNG-операции в одном месте»: никаких загрузок на сервер, всё считается локально. Проект: **easy-png-tools** — набор PNG-утилит, работающий полностью в браузере.
Стек: **SvelteKit + @sveltejs/adapter-static** (статический экспорт, никакого сервера). Все данные обрабатываются на клиенте, ничего не уходит в сеть. Вдохновлён идеей онлайн-сервисов «все PNG-операции в одном месте»: никаких
загрузок на сервер, всё считается локально. Стек: **SvelteKit +
@sveltejs/adapter-static** (статический экспорт, никакого сервера). Все данные
обрабатываются на клиенте, ничего не уходит в сеть.
Источник идеи: типовой перечень утилит, который встречается в онлайн-наборах PNG-инструментов (~296 операций). Источник идеи: типовой перечень утилит, который встречается в онлайн-наборах
PNG-инструментов (~296 операций).
--- ---
@@ -21,63 +25,121 @@
| Тяжёлая математика без блокировки UI | Web Workers, SharedArrayBuffer, WASM (zlib, квантование, ML) | | Тяжёлая математика без блокировки UI | Web Workers, SharedArrayBuffer, WASM (zlib, квантование, ML) |
| Экономия памяти при больших изображениях | `ImageBitmap`, работа тайлами, downscale до декорирования | | Экономия памяти при больших изображениях | `ImageBitmap`, работа тайлами, downscale до декорирования |
**Вывод для архитектуры:** ~90% утилит — это одна и та же связка «декодировать → прогнать через пиксельную функцию → закодировать». Разумно сделать общий ядро (`core/pipeline.ts` + набор операций над `ImageData`) и тонькие страницы-обёртки в SvelteKit. Каждая утилита = отдельный маршрут со своим UI (drag&drop слева, превью, параметры, скачивание справа). **Вывод для архитектуры:** ~90% утилит — это одна и та же связка «декодировать →
прогнать через пиксельную функцию → закодировать». Разумно сделать общий ядро
(`core/pipeline.ts` + набор операций над `ImageData`) и тонькие страницы-обёртки
в SvelteKit. Каждая утилита = отдельный маршрут со своим UI (drag&drop слева,
превью, параметры, скачивание справа).
--- ---
## 2. Уровень EASY — canvas + ImageData, простые попиксельные операции и композиция ## 2. Уровень EASY — canvas + ImageData, простые попиксельные операции и композиция
Реализация: загрузка в canvas → `getImageData()` → цикл по пикселям / перекомпоновка / отрисовка на выходной canvas → `toBlob('image/png')`. Без сложных алгоритмов, каждая утилита — часы работы. Реализация: загрузка в canvas → `getImageData()` → цикл по пикселям /
перекомпоновка / отрисовка на выходной canvas → `toBlob('image/png')`. Без
сложных алгоритмов, каждая утилита — часы работы.
### 2.1 Конвертация форматов и текстовые представления ### 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-png-to-jpg / convert-jpg-to-png / convert-webp-to-png /
- **convert-bmp-to-png / convert-ico-to-png / convert-gif-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-bmp** — BMP-формат простой, ~30 строк энкодера
- **convert-png-to-base64 / convert-base64-to-png / data-uri (в обе стороны)** — `FileReader.readAsDataURL` + строка - **convert-png-to-base64 / convert-base64-to-png / data-uri (в обе стороны)** —
- **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` в текст `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`, тривиально - **convert-png-to-gif** (один кадр) — `gifenc`, тривиально
### 2.2 Прозрачность и альфа-канал ### 2.2 Прозрачность и альфа-канал
- **create-transparent-png, remove-color-from-png(простая версия), transparent-logo-maker, remove-logo/icon-background (по цвету)** — замена цвета на прозрачность по порогу близости - **create-transparent-png, remove-color-from-png(простая версия),
- **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-компонентой transparent-logo-maker, remove-logo/icon-background (по цвету)** — замена
- **generate-png-alpha-mask, show-transparent-png-areas, fill-transparent-png-pixels, convert-semi-transparent-png-to-opaque** — пороги и заливки по альфе цвета на прозрачность по порогу близости
- **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** — бинаризация альфы по порогу - **harden-png-edges** — бинаризация альфы по порогу
### 2.3 Цвет: замена, тон, каналы, цветовые пространства ### 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** — попиксельные арифметики/преобразования - **change-png-color, change-png-color-tone, change-png-opacity,
- **extract-png-color-channels, swap-rgba-color-channels, split-png-into-rgb-components** — перестановка/выделение RGBA-каналов swap-png-colors, change-png-brightness, change-png-contrast,
- **convert-png-to-hsl/hsv/hsi/cmyk/ycbcr/lab-colors** — известные формулы конвертации (Lab — чуть больше математики, но детерминировано) convert-png-to-grayscale, add-sepia-tone-to-png, change-png-hue,
- **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** — генераторы палитр/смешение без работы с изображением invert-png-colors, reverse-png-colors, convert-png-to-two-colors,
- **posterize-png, create-color-spectrum-png, create-multi-color-gradient-png, generate-png-gradient-palette** — простые градиенты/квантование уровней 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 Геометрия, композиция, холст ### 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`) - **resize-png, zoom-png, crop-png, cut-png, rotate-png, skew-png, shift-png,
- **flip-png-horizontally/vertically, mirror-png, reverse-png** — отражение/поворот на 180° change-png-canvas-size, change-png-aspect-ratio, fit-png-in-rectangle,
- **center-png, center-logo, center-icon** — центровка по bounding box альфа-канала convert-landscape/portrait-png-to-portrait/landscape-png** — операции через
- **duplicate-png, clone-png, multiply-png, create-png-tile, create-symmetric-png, split-png-into-parts** — тайлинг/копирование `CanvasRenderingContext2D` (`drawImage`, `rotate`, `setTransform`)
- **create-round-png, create-square-png, create-star-shaped-png, create-wavy-shaped-png, round-png-corners** — маски-«штампы» на альфе - **flip-png-horizontally/vertically, mirror-png, reverse-png** —
- **add-png-border, add-padding-to-png, add-space-around-png, remove-padding-from-png, remove-space-around-png, remove-png-padding** — расширение/усадка холста отражение/поворот на 180°
- **add-png-background, add-signature/logo/icon/stamp-background** — подложка под прозрачность - **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 Текст, рамки, простые эффекты ### 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-text-to-png, convert-text-to-png, create-emoji-png, add-png-watermark,
- **add-noise-to-png, create-glitch-png, pixelate-png, censor-png, erase-part-of-png, randomize-png-pixels** — шум/порча байтов/блоки/кисть 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-shadow-to-png** — смещение + лёгкое размытие + композиция
- **add-artifacts-to-png** — простой вариант: JPEG-раундтрип через canvas - **add-artifacts-to-png** — простой вариант: JPEG-раундтрип через canvas
### 2.6 Анализ и проверки ### 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/гистограмм, «пипетка» по клику - **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** — сортировка списка палитры (не изображения) - **sort-colors** — сортировка списка палитры (не изображения)
### 2.7 Семейства signature/logo/icon/stamp — простые вариации ### 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). Почти все
«-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.** > Примерный объём: **≈180–190 утилит уровня EASY.**
@@ -85,54 +147,96 @@
## 3. Уровень MEDIUM — нужен настоящий алгоритм (свёртки, морфология, квантование, анимация) ## 3. Уровень MEDIUM — нужен настоящий алгоритм (свёртки, морфология, квантование, анимация)
Реализация: честные алгоритмы на `ImageData`, часто в Web Worker, при необходимости — обвязка вокруг готовых JS-библиотек. Реализация: честные алгоритмы на `ImageData`, часто в Web Worker, при
необходимости — обвязка вокруг готовых JS-библиотек.
### 3.1 Фильтры и свёртки ### 3.1 Фильтры и свёртки
- **blur-png, sharpen-png** — box/gaussian blur, ядро свёртки (упрощённо можно `ctx.filter`, но качественно — своя свёртка) - **blur-png, sharpen-png** — box/gaussian blur, ядро свёртки (упрощённо можно
- **refine-png-edges, feather-png-edges, clean-png-edges** — работа с полупрозрачными краями: размытие альфы, де-контуринг, suppression полупрозрачности `ctx.filter`, но качественно — своя свёртка)
- **refine-png-edges, feather-png-edges, clean-png-edges** — работа с
полупрозрачными краями: размытие альфы, де-контуринг, suppression
полупрозрачности
- **add-glow-effect-to-png** — расширение альфы + размытие + цветное наложение - **add-glow-effect-to-png** — расширение альфы + размытие + цветное наложение
### 3.2 Морфология и контуры (операции над альфа-маской) ### 3.2 Морфология и контуры (операции над альфа-маской)
- **add-stroke-to-png, add-outline-to-png, remove-stroke-from-png, remove-outline-from-png, find-png-outline** — dilation/erosion по альфе, извлечение контура - **add-stroke-to-png, add-outline-to-png, remove-stroke-from-png,
- **make-signature-thicker / make-signature-thinner, make-icon-thicker / make-icon-thinner** — dilation/erosion remove-outline-from-png, find-png-outline** — 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** — детект однотонной/декоративной рамки и вырезание - **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 Квантование и цветовая кластеризация ### 3.3 Квантование и цветовая кластеризация
- **quantize-png, decrease-png-color-count, set-custom-png-color-palette, add-dithering-to-png** — median cut / octree / Wu + FloydSteinberg; готовые JS-библиотеки (image-q и т.п.) - **quantize-png, decrease-png-color-count, set-custom-png-color-palette,
- **change-png-quality, create-low-quality-png** — симуляция потери качества через квантование + JPEG-раундтрип add-dithering-to-png** — median cut / octree / Wu + FloydSteinberg; готовые
- **convert-png-to-color-blocks** — мультипликативный блок-эффект/квантование по блокам JS-библиотеки (image-q и т.п.)
- **extract-png-color-palette, find-logo-color-scheme, extract-icon-colors** — частотная гистограмма + выбор топ-N / кластеризация - **change-png-quality, create-low-quality-png** — симуляция потери качества
- **create-unique/popular/multi-color/outlier-color-mask** — статистика распределения цветов и выделение по маске через квантование + 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 Выделение объектов по цвету / фону ### 3.4 Выделение объектов по цвету / фону
- **remove-png-chroma-key** — порог по цвету + деспилл (убрать зелёную окантовку) - **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** — порог + заливка силуэта - **remove-color-from-png (качественная версия), extract-color-from-png,
- **extract-stamp-from-image (по цвету печати)** — хромакей/частотный фильтр по цвету чернил separate-png-colors, separate-logo-colors, separate-icon-colors** —
- **remove-png-background (вариант без ИИ)** — проверенный цветовой подход, знакомый по популярным онлайн-наборам PNG-утилит. Работает хорошо на однотонном/градиентном фоне и в ряде случаев удобнее ИИ. Опции и алгоритм: сегментация по близости цвета
- **Background Color** — выбор удаляемого цвета: пипеткой по изображению или hex/RGB - **convert-png-to-silhouette, convert-png-to-stencil** — порог + заливка
- **Percentage (похожесть цвета)** — slider; 0% = убрать только точный цвет, >0% = убрать соседние оттенки в окрестности цвета (в цветовом пространстве) силуэта
- **Delete Outer Areas (галочка)** — режим «только внешние пиксели»: удаляются только области, связанные с краями изображения (flood-fill/region-growing от границ). Выключена = удалять по всему холсту. Именно эта опция позволяет убрать фон, не задевая «пятна» того же цвета внутри объекта - **extract-stamp-from-image (по цвету печати)** — хромакей/частотный фильтр по
- **Smooth Edge Line + Radius** — постобработка: полупрозрачная полоса толщиной N px по границе удаления (сгладить переход) цвету чернил
- **Preview Deleted Background** — двухцветный предпросмотр маски (что удаляется/что остаётся) - **remove-png-background (вариант без ИИ)** — проверенный цветовой подход,
- Реализация: выбор цвета → порог по цветовому расстоянию → (опц.) flood-fill от краёв → сглаживание краёв → маска поверх превью. Всё на `ImageData`, без ML. знакомый по популярным онлайн-наборам 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 Геометрические искажения и пиксель-сортировка ### 3.5 Геометрические искажения и пиксель-сортировка
- **add-whirl-to-png** — свирл/вихрь: обратное отображение координат - **add-whirl-to-png** — свирл/вихрь: обратное отображение координат
- **sort-png-pixels** — pixel sorting (сортировка строк/столбцов по яркости/цвету) - **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** — комбинация «цвет→прозрачность + де-шум + морфология краёв» - **upscale-png / downscale-png** — качественный ресемплинг (Lanczos, mipmap)
- **convert-image/png/jpg/icon/logo/signature-to-stamp** — стилизация в двухцветную «печать» (порог + зернистость + края) поверх базового 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-энкодинг ### 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) - **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.** > Примерный объём: **≈71 утилита уровня MEDIUM.**
@@ -140,35 +244,65 @@
## 4. Уровень HARD — WASM, ML, длительная обработка, тонкая подгонка алгоритма ## 4. Уровень HARD — WASM, ML, длительная обработка, тонкая подгонка алгоритма
Реализация: WASM-модули, ML-модели в браузере, длительная обработка с прогресс-барами. Реализация: WASM-модули, ML-модели в браузере, длительная обработка с
прогресс-барами.
- **compress-png / reduce-png-file-size / optimize-png** — сжатие на уровне формата: нужен качественный PNG-энкодер с контролем zlib-фильтров/битовой глубины и оптимизаторы. Решение: **pngquant / oxipng / zopfli (WASM)** + подгонка под «качество→размер» (это главный кандидат на HARD — именно здесь тюнинг бесконечен) - **compress-png / reduce-png-file-size / optimize-png** — сжатие на уровне
- **remove-png-background (вариант с ИИ)** — опциональное улучшение к MEDIUM-версии: ML-сегментация (Rembg/DeepLab в WASM или ONNX) для произвольных фото, где цветовой матчинг бессилен. Начинать можно без него — не-ИИ версия уже даёт рабочий инструмент формата: нужен качественный PNG-энкодер с контролем zlib-фильтров/битовой
- **extract-signature-from-image** — детект подписи на скане/фото с произвольным фоном: де-шум, контраст, ML-детекция глубины и оптимизаторы. Решение: **pngquant / oxipng / zopfli (WASM)** +
- **convert-handwritten-signature-to-digital-signature** — очистка и «оцифровка» почерка (ML, денойзинг, выравнивание) подгонка под «качество→размер» (это главный кандидат на HARD — именно здесь
- **extract-barcode-from-image** — декодирование штрих-кодов/QR: готовая библиотека **zxing-wasm**, сопряжение с камерой/фото тюнинг бесконечен)
- **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. > Итого **6 утилит уровня HARD** (все — опциональные улучшения/надстройки). Всё
> остальное (≈290) — EASY/MEDIUM.
--- ---
## 5. Итоговая сводка ## 5. Итоговая сводка
**EASY — ~185 утилит.** canvas + ImageData. **EASY — ~185 утилит.** canvas + ImageData. Примеры: crop, resize, rotate,
Примеры: crop, resize, rotate, конвертации, альфа-канал, палитры, текст, рамки, анализ. конвертации, альфа-канал, палитры, текст, рамки, анализ.
**MEDIUM — ~71 утилита.** Алгоритмы, Web Workers, gifenc. **MEDIUM — ~71 утилита.** Алгоритмы, Web Workers, gifenc. Примеры: blur,
Примеры: blur, sharpen, морфология, квантование, дизеринг, pixel sorting, анимации GIF, remove-background (без ИИ). sharpen, морфология, квантование, дизеринг, pixel sorting, анимации GIF,
remove-background (без ИИ).
**HARD — 6 утилит.** WASM (pngquant/oxipng/zxing), ML (ONNX/Rembg). **HARD — 6 утилит.** WASM (pngquant/oxipng/zxing), ML (ONNX/Rembg). Примеры:
Примеры: compress/optimize, remove-background (ИИ), extract-signature, barcode. compress/optimize, remove-background (ИИ), extract-signature, barcode.
--- ---
## 6. Рекомендации по реализации ## 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). 1. **Единое ядро обработки**`src/lib/core/`: загрузка файла → `ImageData`,
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 — в последнюю очередь. библиотека операций (`color.ts`, `alpha.ts`, `geometry.ts`, `filters.ts`,
3. **Производительность:** вся пиксельная работа и тяжёлые алгоритмы — в Web Worker; большие изображения декодировать сразу в downscale для превью; прогресс-бары для MEDIUM/HARD. `morphology.ts`, `quantize.ts`, `channels.ts`), экспорт через `toBlob`.
4. **SEO/структура:** SvelteKit + static adapter — каждая утилита отдельный маршрут (`/crop-png`, `/rotate-png`), что даёт удобные адреса и повторяет распространённую структуру наборов PNG-инструментов. Каждая утилита — тонкая SvelteKit-страница + общие компоненты (DropZone,
5. **Семейства logo/icon/stamp/signature** — переиспользуют общие операции; в коде это конфиг-опции той же страницы (например `/resize-png` и `/resize-icon` = один компонент с разными подписями/дефолтами). Не плодить дублирующий код. 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` = один компонент с разными подписями/дефолтами). Не плодить
дублирующий код.
+78 -63
View File
@@ -1,103 +1,118 @@
# Demo fidelity: /preview/demo vs refs-html/demo.html # Demo fidelity: /preview/demo vs refs-html/demo.html
> ⚠️ **УТРАТИЛ АКТУАЛЬНОСТЬ — архивная копия.** Актуальный план фикса верности и > ⚠️ **УТРАТИЛ АКТУАЛЬНОСТЬ — архивная копия.** Актуальный план фикса верности и
> текущий статус пунктов B/F — в `docs/plan-design-fix.md` (Этап 1). Аудит верности > текущий статус пунктов B/F — в `docs/plan-design-fix.md` (Этап 1). Аудит
> теперь автоматизирован (`pnpm audit` → `web/audit/audit-report.md`; пиксель-дифф не > верности теперь автоматизирован (`pnpm audit` → `web/audit/audit-report.md`;
> используется); темы в `refs-html` переключаются (скрипт инжектится в > пиксель-дифф не используется); темы в `refs-html` переключаются (скрипт
> `scripts/extract-static.mjs`). Ниже — исходный текст на момент написания. > инжектится в `scripts/extract-static.mjs`). Ниже — исходный текст на момент
> написания.
Метод: Playwright, viewport 1440×900, скриншоты обеих страниц + замер вычисленных Метод: Playwright, viewport 1440×900, скриншоты обеих страниц + замер
стилей ключевых элементов + пиксель-дифф (без чтения картинки моделью). вычисленных стилей ключевых элементов + пиксель-дифф (без чтения картинки
Пиксель-дифф: **24.9%** (наш скролл 1731px, реф 1579px). Зоны различий размазаны моделью). Пиксель-дифф: **24.9%** (наш скролл 1731px, реф 1579px). Зоны различий
по всей высоте → править надо глобально, не точечно. размазаны по всей высоте → править надо глобально, не точечно.
Теперь процесс автоматизирован: `pnpm audit` (Playwright) пишет Теперь процесс автоматизирован: `pnpm audit` (Playwright) пишет
`web/audit/audit-report.md` — light/dark токены + поэлементный дифф стилей по тексту. `web/audit/audit-report.md` — light/dark токены + поэлементный дифф стилей по
Пиксель-дифф убран (неудобен для работы). Фиксы можно делать в любой момент и тексту. Пиксель-дифф убран (неудобен для работы). Фиксы можно делать в любой
перепроверять аудитом. момент и перепроверять аудитом.
## Баги (объективно, правим в любом случае) ## Баги (объективно, правим в любом случае)
- **B1. Кнопки/инпуты рендерятся шрифтом Arial, а не IBM Plex Sans.** - **B1. Кнопки/инпуты рендерятся шрифтом Arial, а не IBM Plex Sans.**
Доказательство: пробы `Reset pipeline``fontFamily: Arial`. Причина: у `button/input` Доказательство: пробы `Reset pipeline``fontFamily: Arial`. Причина: у
не задан `font-family: inherit`. Фикс: добавить в reset `design2.css` `button/input` не задан `font-family: inherit`. Фикс: добавить в reset
`button, input, select, textarea { font-family: inherit; }`. Влияет на все страницы. (P1) `design2.css` `button, input, select, textarea { font-family: inherit; }`.
- **B2. Двойной топбар.** Страница демо рендерит свой `.topbar` (brand / AUTO PIPELINE / Влияет на все страницы. (P1)
help / theme / lang), который перекрывается глобальным `fixed` Header из - **B2. Двойной топбар.** Страница демо рендерит свой `.topbar` (brand / AUTO
`preview/+layout.svelte` (в нём только theme-toggle). В рефе — один топбар. PIPELINE / help / theme / lang), который перекрывается глобальным `fixed`
Контент демо-топбара (brand, статус, язык) визуально спрятан за фикс-хедером. Header из `preview/+layout.svelte` (в нём только theme-toggle). В рефе — один
Фикс: убрать кастомный `.topbar` из демо и сделать хедер лейаута реф-стайл топбаром топбар. Контент демо-топбара (brand, статус, язык) визуально спрятан за
(brand + version + status + help + theme + lang) — улучшит и C9/C10. (P1) фикс-хедером. Фикс: убрать кастомный `.topbar` из демо и сделать хедер лейаута
реф-стайл топбаром (brand + version + status + help + theme + lang) — улучшит
и C9/C10. (P1)
## Расхождения дизайна (нужно решение) ## Расхождения дизайна (нужно решение)
- **F1. Фон холста.** Реф: плоский серый `#EEF1F4`. Наш: белый + blueprint-сетка. - **F1. Фон холста.** Реф: плоский серый `#EEF1F4`. Наш: белый +
Решение: совпасть с рефом (убрать сетку на демо) или оставить сетку как сигнатуру нового дизайна. blueprint-сетка. Решение: совпасть с рефом (убрать сетку на демо) или оставить
- **F2. Бейдж типа шага** (BACKGROUND/TRANSFORM/STYLE). Реф: приглушённый серый `#6D787F`. сетку как сигнатуру нового дизайна.
Решено C16: `StepCard` рендерит `type` как приглушённый моно-текст (не синий бейдж); - **F2. Бейдж типа шага** (BACKGROUND/TRANSFORM/STYLE). Реф: приглушённый серый
индекс шага (`step-index`) — синий `#1769D2`, как в рефе. `#6D787F`. Решено C16: `StepCard` рендерит `type` как приглушённый моно-текст
- **F3. Подписи превью-тайлов** (SOURCE / STEP 01 / FINAL OUTPUT). Реф: синий `#1769D2`, (не синий бейдж); индекс шага (`step-index`) — синий `#1769D2`, как в рефе.
weight 400. Решено C16: `PreviewTile` label — синий `#1769D2`. - **F3. Подписи превью-тайлов** (SOURCE / STEP 01 / FINAL OUTPUT). Реф: синий
- **F4. Раскладка preview-stack.** Реф: одна колонка (5 строк, выше). Наш: 2 колонки (grid). `#1769D2`, weight 400. Решено C16: `PreviewTile` label — синий `#1769D2`.
Решение: одна колонка (как реф) или оставить 2 колонки как выбор нового дизайна. - **F4. Раскладка preview-stack.** Реф: одна колонка (5 строк, выше). Наш: 2
колонки (grid). Решение: одна колонка (как реф) или оставить 2 колонки как
выбор нового дизайна.
- **F5. Вес лейблов контролов.** Реф: 400. Наш (`MonoLabel`): 500. Мелко. - **F5. Вес лейблов контролов.** Реф: 400. Наш (`MonoLabel`): 500. Мелко.
- **F6. Мета-подписи** (DIMENSIONS/FORMAT/SIZE): реф fs 9px, наш 10px. Мелко. - **F6. Мета-подписи** (DIMENSIONS/FORMAT/SIZE): реф fs 9px, наш 10px. Мелко.
- **F7. Копирайт:** `Add tool` у нас `+ Add tool`; кнопка `Download result` сверить стиль - **F7. Копирайт:** `Add tool` у нас `+ Add tool`; кнопка `Download result`
(в рефе синяя с белым текстом 478×42). Пробы `Download result`/`Add tool`/`DIRECTION` сверить стиль (в рефе синяя с белым текстом 478×42). Пробы
вернули NOT FOUND — скорее артефакт замера (вложенный текст/чип размера), проверить визуально. `Download result`/`Add tool`/`DIRECTION` вернули NOT FOUND — скорее артефакт
замера (вложенный текст/чип размера), проверить визуально.
- **F8. h1 line-height** чуть выше (наш 150px vs реф 115px) — мелкий зазор. - **F8. h1 line-height** чуть выше (наш 150px vs реф 115px) — мелкий зазор.
- **F9. Вертикальный ритм:** контент на ~152px выше, зазоры между секциями отличаются - **F9. Вертикальный ритм:** контент на ~152px выше, зазоры между секциями
(pipeline-head y 434 vs 400 и т.д.) — подровнять паддинги под реф. отличаются (pipeline-head y 434 vs 400 и т.д.) — подровнять паддинги под реф.
- **F10. Внутренние отступы preview-панели** (Hide intermediate x 907 vs 1218) — паддинг панели. - **F10. Внутренние отступы preview-панели** (Hide intermediate x 907 vs 1218) —
паддинг панели.
## Темы (отложено — F1-F4) ## Темы (отложено — F1-F4)
Серый фон `#EEF1F4` — это **светлая** тема. Уточнение по F1: наш токен `--background` Серый фон `#EEF1F4` — это **светлая** тема. Уточнение по F1: наш токен
уже равен `#EEF1F4` (совпадает с рефом); видимое отличие — только blueprint-сетка `--background` уже равен `#EEF1F4` (совпадает с рефом); видимое отличие — только
поверх фона, а не «белый vs серый». То есть F1 = «сетка против плоского», не цвет фона. blueprint-сетка поверх фона, а не «белый vs серый». То есть F1 = «сетка против
плоского», не цвет фона.
Перед тем как сверять цвета бейджей/тайлов/раскладок (F2-F4), нужно сначала Перед тем как сверять цвета бейджей/тайлов/раскладок (F2-F4), нужно сначала
зафиксировать палитры light/dark как единый источник токенов. Сейчас они есть в зафиксировать палитры light/dark как единый источник токенов. Сейчас они есть в
`web/src/lib/styles/design2.css` (`:root` + `[data-theme="dark"]`), но не сверены с рефом. `web/src/lib/styles/design2.css` (`:root` + `[data-theme="dark"]`), но не
сверены с рефом.
**Как проверить темы в refs-html:** **Как проверить темы в refs-html:**
- Открыть `refs-html/demo.html` (и соседние `gradient.html`, `background-remover.html`) - Открыть `refs-html/demo.html` (и соседние `gradient.html`,
через Playwright/браузер. `background-remover.html`) через Playwright/браузер.
- Нажать theme-toggle в топбаре рефа (иконка луны/солнца) — переключает тему - Нажать theme-toggle в топбаре рефа (иконка луны/солнца) — переключает тему
(скорее всего меняет атрибут/класс на `<html>` или `.page` и переопределяет CSS-переменные). (скорее всего меняет атрибут/класс на `<html>` или `.page` и переопределяет
- Снять скриншоты светлой и тёмной темы, замерить computed-стиль `:root` переменных CSS-переменные).
рефа (через `getComputedStyle(document.documentElement).getPropertyValue('--...')`) - Снять скриншоты светлой и тёмной темы, замерить computed-стиль `:root`
и сопоставить с нашими токенами в `design2.css`. переменных рефа (через
`getComputedStyle(document.documentElement).getPropertyValue('--...')`) и
сопоставить с нашими токенами в `design2.css`.
- Рефы, похоже, не доделаны по тёмной теме — зафиксировать, какие именно токены - Рефы, похоже, не доделаны по тёмной теме — зафиксировать, какие именно токены
меняются и где расхождения. На основе этого выровнять наши токены, затем вернуться меняются и где расхождения. На основе этого выровнять наши токены, затем
к F2 (бейдж типа), F3 (тайлы), F4 (раскладка preview-stack). вернуться к F2 (бейдж типа), F3 (тайлы), F4 (раскладка preview-stack).
Решение по F1-F4 принимается только после сверки токенов светлой/тёмной темы с рефом. Решение по F1-F4 принимается только после сверки токенов светлой/тёмной темы с
рефом.
**Состояние тем (уточнение):** наша dark-тема реализована в `design2.css` **Состояние тем (уточнение):** наша dark-тема реализована в `design2.css`
(`[data-theme="dark"]`), но пока подключена только на preview-маршрутах (старые (`[data-theme="dark"]`), но пока подключена только на preview-маршрутах (старые
страницы грузят `app.css`). Этого достаточно для сверки: токены, отличные от light, страницы грузят `app.css`). Этого достаточно для сверки: токены, отличные от
соберём по всем `refs-html/*.html` (не только `demo.html`), остальные токены light, соберём по всем `refs-html/*.html` (не только `demo.html`), остальные
не меняются. Light-токены уже совпадают с рефом (`--background`, `--panel`, токены не меняются. Light-токены уже совпадают с рефом (`--background`,
`--foreground`, `--muted`, `--line`, `--blue` равны). `--panel`, `--foreground`, `--muted`, `--line`, `--blue` равны).
## Состояние (C16) ## Состояние (C16)
Цветовые расхождения против `refs-html/demo.html` устранены: стек шрифтов сверен с Цветовые расхождения против `refs-html/demo.html` устранены: стек шрифтов сверен
рефом (`"IBM Plex Sans", sans-serif` / `"IBM Plex Mono", monospace`), бренд топбара — с рефом (`"IBM Plex Sans", sans-serif` / `"IBM Plex Mono", monospace`), бренд
`--foreground`, индекс шага — синий, тип шага и подпись плитки — как в рефе, топбара — `--foreground`, индекс шага — синий, тип шага и подпись плитки — как в
`Segmented` — 10px/32px. Аудит: токены light 10 / dark 16, элементы 47. Остаток — рефе, `Segmented` — 10px/32px. Аудит: токены light 10 / dark 16, элементы 47.
sub-pixel (10px→9px лейблы, letter-spacing) и структурные (`display`/`rect` от наших Остаток — sub-pixel (10px→9px лейблы, letter-spacing) и структурные
flex/grid раскладок против статичного мока); визуально незначимы, правим точечно при (`display`/`rect` от наших flex/grid раскладок против статичного мока);
желании. Старые тесты (583) — без регрессий. визуально незначимы, правим точечно при желании. Старые тесты (583) — без
регрессий.
## План действий (предлагаю) ## План действий (предлагаю)
> Актуальный пошаговый план фикса вынесен в `docs/plan-design-fix.md` > Актуальный пошаговый план фикса вынесен в `docs/plan-design-fix.md` (Этапы
> (Этапы 0–1: рефакторинг + верность). Ниже — исходные наброски. > 0–1: рефакторинг + верность). Ниже — исходные наброски.
1. Сразу поправить B1 + B2 (чёткие баги, улучшают все страницы). 1. Сразу поправить B1 + B2 (чёткие баги, улучшают все страницы).
2. По F1–F4 — решение за тобой (совпадать с рефом дословно ИЛИ оставить как сигнатуру нового дизайна). 2. По F1–F4 — решение за тобой (совпадать с рефом дословно ИЛИ оставить как
3. F5–F10 — мелочи, подчистить в том же проходе, если решаем «совпадать с рефом». сигнатуру нового дизайна).
3. F5–F10 — мелочи, подчистить в том же проходе, если решаем «совпадать с
рефом».
4. Переснять скриншоты и перепроверить пиксель-дифф. 4. Переснять скриншоты и перепроверить пиксель-дифф.
+54 -23
View File
@@ -4,20 +4,36 @@
## 1. Цели и принципы ## 1. Цели и принципы
1. **Один источник правды** — все визуальные константы живут в токенах `web/src/app.css`; ни один компонент не пишет «магические» цвета/размеры напрямую. 1. **Один источник правды** — все визуальные константы живут в токенах
2. **Никакого дублирования** — повторяющийся визуальный рецепт оформляется один раз: либо токен, либо глобальный утилитарный класс, либо компонент в `ui/`. `web/src/app.css`; ни один компонент не пишет «магические» цвета/размеры
3. **Простые компоненты важнее умных** — если проще сделать отдельные `SliderField` и `TextField`, делаем два маленьких компонента, а не один конфигурируемый монстр. напрямую.
4. **Scoped-стили — только для раскладки** конкретного компонента (сетка, отступы между блоками). Внешний вид контролов и текстов — из дизайн-системы. 2. **Никакого дублирования** — повторяющийся визуальный рецепт оформляется один
раз: либо токен, либо глобальный утилитарный класс, либо компонент в `ui/`.
3. **Простые компоненты важнее умных** — если проще сделать отдельные
`SliderField` и `TextField`, делаем два маленьких компонента, а не один
конфигурируемый монстр.
4. **Scoped-стили — только для раскладки** конкретного компонента (сетка,
отступы между блоками). Внешний вид контролов и текстов — из дизайн-системы.
## 2. Аудит: что дублируется прямо сейчас ## 2. Аудит: что дублируется прямо сейчас
- **«Приглушённый текст»** (`--text-muted` + размер 0.85–0.95rem) — повторён в семи местах: `ToolPage` (.description, .hint), главная (.lead, .card-desc), лейаут (.nav-link, footer), `Preview` (.dims), `ParamForm` (label), `InfoPanel` (dt). - **«Приглушённый текст»** (`--text-muted` + размер 0.85–0.95rem) — повторён в
- **«Заголовок секции капсом»** (uppercase, letter-spacing, muted) — точная копипаста в `ToolPage` и на главной. семи местах: `ToolPage` (.description, .hint), главная (.lead, .card-desc),
- **Базовые инпуты** (padding, border, radius) — живут только внутри `ParamForm`; любые новые типы контролов (слайдеры, пипетка цвета) пришлось бы копировать вручную. лейаут (.nav-link, footer), `Preview` (.dims), `ParamForm` (label),
- **Блок «label + отступы»** — дублировался бы в каждом `*Field`; решается обёрткой `Field.svelte`. `InfoPanel` (dt).
- **«Панель»** (surface + border + radius) — три ручные реализации одного паттерна: `DropZone`, карточки главной, строки `InfoPanel`. - **«Заголовок секции капсом»** (uppercase, letter-spacing, muted) — точная
- **Кнопки** — глобальные классы `.primary`/`.secondary` без компонента; состояния «занят/недоступен» каждый описывает заново. копипаста в `ToolPage` и на главной.
- **Ошибка-баннер** — только в `ToolPage`, а понадобится минимум ещё раз в workspace. - **Базовые инпуты** (padding, border, radius) — живут только внутри
`ParamForm`; любые новые типы контролов (слайдеры, пипетка цвета) пришлось бы
копировать вручную.
- **Блок «label + отступы»** — дублировался бы в каждом `*Field`; решается
обёрткой `Field.svelte`.
- **«Панель»** (surface + border + radius) — три ручные реализации одного
паттерна: `DropZone`, карточки главной, строки `InfoPanel`.
- **Кнопки** — глобальные классы `.primary`/`.secondary` без компонента;
состояния «занят/недоступен» каждый описывает заново.
- **Ошибка-баннер** — только в `ToolPage`, а понадобится минимум ещё раз в
workspace.
## 3. Токены (расширение `app.css`) ## 3. Токены (расширение `app.css`)
@@ -55,11 +71,13 @@
- `.text-muted` — приглушённый текст (размер наследуется); - `.text-muted` — приглушённый текст (размер наследуется);
- `.text-caption` — мелкий текст `--text-s`; - `.text-caption` — мелкий текст `--text-s`;
- `.heading-section` — заголовок секции капсом (вместо двух копипаст); - `.heading-section` — заголовок секции капсом (вместо двух копипаст);
- `.panel` — поверхность с бордером/радиусом (DropZone, карточки, InfoPanel-строки); - `.panel` — поверхность с бордером/радиусом (DropZone, карточки,
InfoPanel-строки);
- `.error-banner` — баннер ошибки; - `.error-banner` — баннер ошибки;
- `.visually-hidden` — скрытый, но читаемый скринридерами. - `.visually-hidden` — скрытый, но читаемый скринридерами.
Элементные стили из текущего `app.css` (голые `input, select { ... }`) убрать — внешний вид контролов переезжает в компоненты `ui/`. Элементные стили из текущего `app.css` (голые `input, select { ... }`) убрать —
внешний вид контролов переезжает в компоненты `ui/`.
## 5. Состав UI-компонентов (`web/src/lib/components/ui/`) ## 5. Состав UI-компонентов (`web/src/lib/components/ui/`)
@@ -76,15 +94,23 @@
| `ColorField.svelte` | `id`, `label`, `value = $bindable()` | color-ветка `ParamForm`; точка будущего расширения пипеткой | | `ColorField.svelte` | `id`, `label`, `value = $bindable()` | color-ветка `ParamForm`; точка будущего расширения пипеткой |
| `EmptyState.svelte` | `title`, `hint`, слот действия | «Обработка…», будущие пустые панели исходника/результата | | `EmptyState.svelte` | `title`, `hint`, слот действия | «Обработка…», будущие пустые панели исходника/результата |
`Field` — чисто раскладочная обёртка (`<div class="field"><label for>…</label><slot /></div>`), ничего не знает о типе контрола. На ней построены `TextField`, `SliderField`, `SelectField`, `ColorField`. Исключение — `CheckboxField`: у чекбокса label оборачивает контрол (кликабельная строка), поэтому у него своя раскладка. `Field` — чисто раскладочная обёртка
(`<div class="field"><label for>…</label><slot /></div>`), ничего не знает о
типе контрола. На ней построены `TextField`, `SliderField`, `SelectField`,
`ColorField`. Исключение — `CheckboxField`: у чекбокса label оборачивает контрол
(кликабельная строка), поэтому у него своя раскладка.
## 6. Миграция существующего ## 6. Миграция существующего
1. `ParamForm` — переписывается на `Field` + `*Field`-компоненты; собственная стилизация контролов и label удаляется. 1. `ParamForm` — переписывается на `Field` + `*Field`-компоненты; собственная
стилизация контролов и label удаляется.
2. `DownloadButton` и кнопки `ToolPage` — на `Button`. 2. `DownloadButton` и кнопки `ToolPage` — на `Button`.
3. `DropZone`, карточки главной, строки `InfoPanel` — на `.panel` + свои раскладочные стили. 3. `DropZone`, карточки главной, строки `InfoPanel` — на `.panel` + свои
4. `ToolPage`, главная, лейаут — приглушённые тексты на `.text-*`, заголовки секций на `.heading-section`, баннер на `.error-banner`. раскладочные стили.
5. Из `app.css` удалить глобальные классы кнопок и голые стили инпутов после перехода. 4. `ToolPage`, главная, лейаут — приглушённые тексты на `.text-*`, заголовки
секций на `.heading-section`, баннер на `.error-banner`.
5. Из `app.css` удалить глобальные классы кнопок и голые стили инпутов после
перехода.
## 7. Порядок работ ## 7. Порядок работ
@@ -97,13 +123,18 @@
## 8. Критерии готовности ## 8. Критерии готовности
- Ни один компонент не задаёт цвет/радиус/шрифт мимо токенов или классов системы. - Ни один компонент не задаёт цвет/радиус/шрифт мимо токенов или классов
системы.
- Все интерактивные контролы на страницах — экземпляры `ui/*`. - Все интерактивные контролы на страницах — экземпляры `ui/*`.
- `grep -E '#[0-9a-f]{3,6}' web/src/lib/components` не находит ничего, кроме токенов в `app.css`. - `grep -E '#[0-9a-f]{3,6}' web/src/lib/components` не находит ничего, кроме
- Визуальной регрессии нет (ручная сверка всех страниц до/после), тесты и билд зелёные. токенов в `app.css`.
- Визуальной регрессии нет (ручная сверка всех страниц до/после), тесты и билд
зелёные.
## 9. Что сознательно не делаем ## 9. Что сознательно не делаем
- Никаких CSS-фреймворков и препроцессоров — обычный CSS, как решено на старте. - Никаких CSS-фреймворков и препроцессоров — обычный CSS, как решено на старте.
- Никаких тем/тёмного режима — но токены сразу семантические, чтобы тема добавилась заменой значений `:root`. - Никаких тем/тёмного режима — но токены сразу семантические, чтобы тема
- Никаких сложных составных компонентов (модалки, таблицы) — они появятся, когда потребуется реальная задача. добавилась заменой значений `:root`.
- Никаких сложных составных компонентов (модалки, таблицы) — они появятся, когда
потребуется реальная задача.
+116 -45
View File
@@ -4,38 +4,59 @@
> >
> **Что сделано по плану:** > **Что сделано по плану:**
> >
> - Шаг 0 — каркас: SvelteKit + adapter-static, design-токены на CSS, лейаут с навигацией по категориям ✔ > - Шаг 0 — каркас: SvelteKit + adapter-static, design-токены на CSS, лейаут с
> - Шаг 1 — ядро: `PixelImage`, декодирование файлов, кодирование PNG/JPEG/WebP, скачивание > навигацией по категориям
> - Шаг 2 операции ядра: color / geometry / alpha / analyze, чистые функции без DOM ✔ > - Шаг 1 — ядро: `PixelImage`, декодирование файлов, кодирование PNG/JPEG/WebP,
> скачивание ✔
> - Шаг 2 — операции ядра: color / geometry / alpha / analyze, чистые функции
> без DOM ✔
> - Шаг 3 — реестр из 11 инструментов + санитизация параметров ✔ > - Шаг 3 — реестр из 11 инструментов + санитизация параметров ✔
> - Шаг 4 — универсальные UI-компоненты (DropZone, Preview, ParamForm, DownloadButton, InfoPanel, ToolPage) ✔ > - Шаг 4 — универсальные UI-компоненты (DropZone, Preview, ParamForm,
> - Шаг 5 — маршруты: `/tools/[id]` с prerender всех страниц, главная-каталог по категориям > DownloadButton, InfoPanel, ToolPage)
> - Шаг 5 — маршруты: `/tools/[id]` с prerender всех страниц, главная-каталог по
> категориям ✔
> - Шаги 6–7 — обработка ошибок, состояния, полировка; верификация ✔ > - Шаги 6–7 — обработка ошибок, состояния, полировка; верификация ✔
> >
> **Checkpoint пройден:** все 11 инструментов вручную проверены в браузере (загрузка → применение → скачивание), включая найденный и исправленный баг resize с сохранением пропорций. Тесты 75/75, svelte-check 0 ошибок, статический экспорт собирается. > **Checkpoint пройден:** все 11 инструментов вручную проверены в браузере
> (загрузка → применение → скачивание), включая найденный и исправленный баг
> resize с сохранением пропорций. Тесты 75/75, svelte-check 0 ошибок,
> статический экспорт собирается.
> >
> **Попутные улучшения сверх плана:** корневой скрипт `pnpm dev`; `sanitizeParams` против пустых/невалидных значений полей; семантика «вписать в размеры» для resize. > **Попутные улучшения сверх плана:** корневой скрипт `pnpm dev`;
> `sanitizeParams` против пустых/невалидных значений полей; семантика «вписать в
> размеры» для resize.
## 0. Зафиксированные решения ## 0. Зафиксированные решения
- **Объём MVP:** только одиночные инструменты «загрузил → настроил → скачал». Пайплайн-workspace (`/workspace`) вне скоупа этого плана. - **Объём MVP:** только одиночные инструменты «загрузил → настроил → скачал».
- **Набор инструментов первого среза:** ~11 базовых EASY (см. §3). Достаточно, чтобы проверить насквозь ядро → реестр → универсальную страницу. Пайплайн-workspace (`/workspace`) вне скоупа этого плана.
- **Стили:** обычный CSS без фреймворка — design-токены на CSS custom properties, ноль зависимостей. - **Набор инструментов первого среза:** ~11 базовых EASY (см. §3). Достаточно,
- **Стек:** SvelteKit + `@sveltejs/adapter-static` + TypeScript, пакетный менеджер pnpm (уже зафиксирован в корневом `package.json`). Весь код сайта — в `web/`. чтобы проверить насквозь ядро → реестр → универсальную страницу.
- **Ядро без DOM:** все пиксельные операции — чистые функции над `PixelImage`; DOM/canvas допускается только в модуле IO и компонентах превью. - **Стили:** обычный CSS без фреймворка — design-токены на CSS custom
properties, ноль зависимостей.
- **Стек:** SvelteKit + `@sveltejs/adapter-static` + TypeScript, пакетный
менеджер pnpm (уже зафиксирован в корневом `package.json`). Весь код сайта — в
`web/`.
- **Ядро без DOM:** все пиксельные операции — чистые функции над `PixelImage`;
DOM/canvas допускается только в модуле IO и компонентах превью.
--- ---
## 1. Шаг 0 — Каркас проекта ## 1. Шаг 0 — Каркас проекта
1. Создать SvelteKit-приложение в `web/`: `npx sv create web` (шаблон minimal, TypeScript, без лишних аддонов). 1. Создать SvelteKit-приложение в `web/`: `npx sv create web` (шаблон minimal,
TypeScript, без лишних аддонов).
2. `pnpm install` внутри `web/`. 2. `pnpm install` внутри `web/`.
3. `svelte.config.js`: заменить адаптер на `@sveltejs/adapter-static`. 3. `svelte.config.js`: заменить адаптер на `@sveltejs/adapter-static`.
4. `src/routes/+layout.ts`: `export const prerender = true;` — весь сайт prerenderится в статику. 4. `src/routes/+layout.ts`: `export const prerender = true;` — весь сайт
5. `src/app.css`: дизайн-токены (палитра, типографика, отступы, радиусы через CSS custom properties) + минимальный сброс стилей. prerenderится в статику.
6. `src/routes/+layout.svelte`: шапка с названием сайта и навигацией по категориям инструментов (категории — константа из реестра), футер. 5. `src/app.css`: дизайн-токены (палитра, типографика, отступы, радиусы через
CSS custom properties) + минимальный сброс стилей.
6. `src/routes/+layout.svelte`: шапка с названием сайта и навигацией по
категориям инструментов (категории — константа из реестра), футер.
**Проверка:** `pnpm dev` открывает пустой каркас; `pnpm build && pnpm preview` отдаёт статический экспорт. **Проверка:** `pnpm dev` открывает пустой каркас; `pnpm build && pnpm preview`
отдаёт статический экспорт.
--- ---
@@ -46,28 +67,42 @@
- `types.ts` - `types.ts`
```ts ```ts
export type PixelImage = { width: number; height: number; data: Uint8ClampedArray }; // RGBA, как ImageData export type PixelImage = {
width: number;
height: number;
data: Uint8ClampedArray;
}; // RGBA, как ImageData
``` ```
- `io.ts` — единственное место ядра, где разрешён canvas/DOM: - `io.ts` — единственное место ядра, где разрешён canvas/DOM:
- `decodeFile(file: File): Promise<PixelImage>` — `createImageBitmap(file)` → отрисовка на canvas → `getImageData`; - `decodeFile(file: File): Promise<PixelImage>` — `createImageBitmap(file)` →
- `encode(img: PixelImage, mime: 'image/png' | 'image/jpeg' | 'image/webp', quality?: number): Promise<Blob>` — `putImageData` → `canvas.toBlob`; отрисовка на 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`. - `downloadBlob(blob: Blob, filename: string): void`.
**Проверка:** юнит-смоук — декодировать фикстурный PNG, закодировать обратно, размеры совпадают. **Проверка:** юнит-смоук — декодировать фикстурный PNG, закодировать обратно,
размеры совпадают.
--- ---
## 3. Шаг 2 — Операции ядра (чистые функции) ## 3. Шаг 2 — Операции ядра (чистые функции)
Файлы в `web/src/lib/core/`, каждая функция: `(img, params) => new PixelImage`, без мутации входа: Файлы в `web/src/lib/core/`, каждая функция: `(img, params) => new PixelImage`,
без мутации входа:
- `color.ts`: `grayscale(img)` (luma), `invert(img)`, `brightnessContrast(img, brightness 100..100, contrast 100..100)`. - `color.ts`: `grayscale(img)` (luma), `invert(img)`,
- `geometry.ts`: `flip(img, axis)`, `rotate90(img, turns)`, `crop(img, x, y, w, h)` — индексная математика; `resize(img, w, h)` — билинейная интерполяция на TS (не `drawImage`, чтобы ядро осталось DOM-free и воспроизводимым бит-в-бит). `brightnessContrast(img, brightness 100..100, contrast 100..100)`.
- `alpha.ts`: `removeColorToAlpha(img, hex, tolerancePercent)` — цветовое расстояние → альфа 0. - `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 }`. - `analyze.ts`: `imageInfo(img)` → `{ width, height, hasAlpha, colorCount }`.
**Проверка:** vitest на маленьких матрицах пикселей (2×2, 3×3): flip, rotate, crop, grayscale — ожидаемые значения руками. **Проверка:** vitest на маленьких матрицах пикселей (2×2, 3×3): flip, rotate,
crop, grayscale — ожидаемые значения руками.
--- ---
@@ -77,8 +112,22 @@
```ts ```ts
export type ParamDef = 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: "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: "checkbox"; default: boolean }
| { id: string; label: string; type: "color"; default: string }; | { id: string; label: string; type: "color"; default: string };
@@ -88,11 +137,15 @@ export type ToolEntry = {
description: string; description: string;
category: CategoryId; category: CategoryId;
params: ParamDef[]; params: ParamDef[];
run: (img: PixelImage, params: Record<string, unknown>) => Promise<PixelImage> | PixelImage; run: (
img: PixelImage,
params: Record<string, unknown>,
) => Promise<PixelImage> | PixelImage;
}; };
``` ```
Категории (константа там же): `convert`, `alpha`, `color`, `geometry`, `analyze`. Категории (константа там же): `convert`, `alpha`, `color`, `geometry`,
`analyze`.
### Состав первого среза — 11 инструментов ### Состав первого среза — 11 инструментов
@@ -110,9 +163,11 @@ export type ToolEntry = {
| 10 | `remove-color-from-png` | alpha | hex color, tolerance % | | 10 | `remove-color-from-png` | alpha | hex color, tolerance % |
| 11 | `png-info` | analyze | — особый случай: не картинка, а текстовая сводка | | 11 | `png-info` | analyze | — особый случай: не картинка, а текстовая сводка |
Для `png-info` в записи реестра добавляется флаг `resultType: 'info'`; остальные — `'image'`. Для `png-info` в записи реестра добавляется флаг `resultType: 'info'`; остальные
— `'image'`.
**Проверка:** импорт реестра в тесте — все id уникальны, у каждого `params` имеют дефолты, `run` определён. **Проверка:** импорт реестра в тесте — все id уникальны, у каждого `params`
имеют дефолты, `run` определён.
--- ---
@@ -120,33 +175,45 @@ export type ToolEntry = {
`web/src/lib/components/`: `web/src/lib/components/`:
- `DropZone.svelte` — drag&drop + клик; эмитит `File`; проверка MIME (PNG/JPEG/WebP/GIF/BMP). - `DropZone.svelte` — drag&drop + клик; эмитит `File`; проверка MIME
- `Preview.svelte` — рендер `PixelImage` на canvas с fit-масштабированием; шахматный фон под прозрачность. (PNG/JPEG/WebP/GIF/BMP).
- `ParamForm.svelte` — генерирует контролы из `ParamDef[]`, двусторонний `bind:values`. - `Preview.svelte` — рендер `PixelImage` на canvas с fit-масштабированием;
- `DownloadButton.svelte` — encode выбранного формата + `downloadBlob`, имя `<tool-id>.<ext>`. шахматный фон под прозрачность.
- `ParamForm.svelte` — генерирует контролы из `ParamDef[]`, двусторонний
`bind:values`.
- `DownloadButton.svelte` — encode выбранного формата + `downloadBlob`, имя
`<tool-id>.<ext>`.
- `InfoPanel.svelte` — текстовый вывод для `resultType: 'info'`. - `InfoPanel.svelte` — текстовый вывод для `resultType: 'info'`.
- `ToolPage.svelte` — оркестратор страницы инструмента; состояния `idle → loaded → processing → done | error`; раскладка: слева DropZone, справа превью + форма параметров + кнопки «Применить» / «Скачать». - `ToolPage.svelte` — оркестратор страницы инструмента; состояния
`idle → loaded → processing → done | error`; раскладка: слева DropZone, справа
превью + форма параметров + кнопки «Применить» / «Скачать».
**Правило:** новый инструмент = новая запись в реестре (+ функция операции). Компоненты и маршрут не трогаем. **Правило:** новый инструмент = новая запись в реестре (+ функция операции).
Компоненты и маршрут не трогаем.
--- ---
## 6. Шаг 5 — Маршруты ## 6. Шаг 5 — Маршруты
- `/` (`src/routes/+page.svelte`) — сетка карточек инструментов, сгруппированных по категориям; данные из реестра. - `/` (`src/routes/+page.svelte`) — сетка карточек инструментов, сгруппированных
по категориям; данные из реестра.
- `/tools/[id]` — универсальная страница: - `/tools/[id]` — универсальная страница:
- `+page.ts`: `entries()` возвращает все `id` из реестра (обязательное условие prerender при adapter-static); - `+page.ts`: `entries()` возвращает все `id` из реестра (обязательное условие
prerender при adapter-static);
- `load()` возвращает запись реестра по `id`, неизвестный id → 404; - `load()` возвращает запись реестра по `id`, неизвестный id → 404;
- `+page.svelte` — тонкая обёртка над `ToolPage`. - `+page.svelte` — тонкая обёртка над `ToolPage`.
**Проверка:** `pnpm build` — в `build/tools/` лежат все 11 страниц; главная содержит ссылки на каждую. **Проверка:** `pnpm build` — в `build/tools/` лежат все 11 страниц; главная
содержит ссылки на каждую.
--- ---
## 7. Шаг 6 — Ошибки и минимальная полировка ## 7. Шаг 6 — Ошибки и минимальная полировка
- Обработка ошибок: неподдерживаемый формат, ошибка декода, некорректные параметры crop (выход за границы — клампы или понятная ошибка). - Обработка ошибок: неподдерживаемый формат, ошибка декода, некорректные
- Состояния: «перетащите файл», спиннер во время обработки, disabled-кнопки до загрузки. параметры crop (выход за границы — клампы или понятная ошибка).
- Состояния: «перетащите файл», спиннер во время обработки, disabled-кнопки до
загрузки.
- Тексты описаний для всех 11 инструментов (title/description уже в реестре). - Тексты описаний для всех 11 инструментов (title/description уже в реестре).
--- ---
@@ -161,6 +228,10 @@ cd web && pnpm build # статический экспорт должен
cd web && pnpm test # vitest: ядро (геометрия/цвет) + целостность реестра cd web && pnpm test # vitest: ядро (геометрия/цвет) + целостность реестра
``` ```
Ручной smoke-тест каждого из 11 инструментов: загрузка PNG → применить → скачать → открыть результат. Ручной smoke-тест каждого из 11 инструментов: загрузка PNG → применить → скачать
→ открыть результат.
**Checkpoint MVP:** любой из 11 инструментов доступен по прямому URL с главной страницы; PNG загружается drag&drop; параметры применяются; результат скачивается; всё работает офлайн со статического хостинга; ни одной серверной зависимости. **Checkpoint MVP:** любой из 11 инструментов доступен по прямому URL с главной
страницы; PNG загружается drag&drop; параметры применяются; результат
скачивается; всё работает офлайн со статического хостинга; ни одной серверной
зависимости.
+42 -43
View File
@@ -2,15 +2,14 @@
> **СТАТУС: РЕАЛИЗОВАН (2026-09-07) — устарел.** Скрипты > **СТАТУС: РЕАЛИЗОВАН (2026-09-07) — устарел.** Скрипты
> `web/scripts/audit-dom.mjs`, `audit-css.mjs`, `audit-cdp.mjs`, > `web/scripts/audit-dom.mjs`, `audit-css.mjs`, `audit-cdp.mjs`,
> `audit-cdp-responsive.mjs` и npm-скрипты `refs-dom-audit` / > `audit-cdp-responsive.mjs` и npm-скрипты `refs-dom-audit` / `refs-css-audit` /
> `refs-css-audit` / `refs-cdp-audit` на месте; старый текстовый `audit.mjs` > `refs-cdp-audit` на месте; старый текстовый `audit.mjs` удалён. Актуальная
> удалён. Актуальная доводка верности и статус пунктов F — в > доводка верности и статус пунктов F — в `plan-design-fix.md`.
> `plan-design-fix.md`.
> Статус: к выполнению. Ветка `feat/redesign`. > Статус: к выполнению. Ветка `feat/redesign`. Источник правды по пикселям:
> Источник правды по пикселям: `refs-html/*.html` (статический экспорт рефа). > `refs-html/*.html` (статический экспорт рефа). Актуальный рабочий аудит сейчас
> Актуальный рабочий аудит сейчас — `pnpm refs-audit` (`scripts/audit.mjs`), но он > — `pnpm refs-audit` (`scripts/audit.mjs`), но он **сломан** (см. «Почему
> **сломан** (см. «Почему текущий аудит не работает») и подлежит замене. > текущий аудит не работает») и подлежит замене.
## Почему текущий аудит не работает ## Почему текущий аудит не работает
@@ -37,13 +36,13 @@
Заводим их через `t()` — совпадёт с рефом (EN). Заводим их через `t()` — совпадёт с рефом (EN).
2. **Имена классов НЕ сводим к рефу.** Матчинг делаем по **структурному пути**, 2. **Имена классов НЕ сводим к рефу.** Матчинг делаем по **структурному пути**,
а не по классу. Компоненты `kit/` остаются идиоматичными, Stage 0 не а не по классу. Компоненты `kit/` остаются идиоматичными, Stage 0 не
откатываем. Требуется только эквивалентность *структуры* (теги, вложенность, откатываем. Требуется только эквивалентность _структуры_ (теги, вложенность,
порядок сиблингов, наличие узлов, текст). порядок сиблингов, наличие узлов, текст).
3. **Пиксельный дифф (Playwright + pixelmatch) пока НЕ делаем.** DOM+CSS-аудита 3. **Пиксельный дифф (Playwright + pixelmatch) пока НЕ делаем.** DOM+CSS-аудита
достаточно; добавим как сеть позже, если стили что-то упустят. достаточно; добавим как сеть позже, если стили что-то упустят.
4. **Порядок прогона маршрутов:** `demo``list-tools` → затем 2 инструмента 4. **Порядок прогона маршрутов:** `demo``list-tools` → затем 2 инструмента
(`tools/linear-gradient-png``gradient.html`, (`tools/linear-gradient-png``gradient.html`, `tools/remove-background-png`
`tools/remove-background-png``background-remover.html`). `background-remover.html`).
## Принципы ## Принципы
@@ -106,15 +105,15 @@ Svelte-атрибутов и комментов гидрации), чтобы п
- `moved` — тот же текст/тег, другой путь (перепутанный порядок/родитель); - `moved` — тот же текст/тег, другой путь (перепутанный порядок/родитель);
- `textMismatch` — нормализованный текст различается (в т.ч. RU vs EN). - `textMismatch` — нормализованный текст различается (в т.ч. RU vs EN).
Класс-мисматч — **только информационно**, не блокирует (классы свои по Класс-мисматч — **только информационно**, не блокирует (классы свои по решению
решению №2). Счётчики: `added / removed / moved / textMismatch`. №2). Счётчики: `added / removed / moved / textMismatch`.
### Рендеринг ### Рендеринг
- **наше:** dev-сервер на `5179` (как сейчас в `audit.mjs`). - **наше:** dev-сервер на `5179` (как сейчас в `audit.mjs`).
- **реф:** `pathToFileURL` к `refs-html/<ref>.html` (`file://`). Реф — - **реф:** `pathToFileURL` к `refs-html/<ref>.html` (`file://`). Реф —
самодостаточный (инлайн `<style>`), рендерится полностью. Шрифты для фазы A самодостаточный (инлайн `<style>`), рендерится полностью. Шрифты для фазы A не
не важны (сравниваем структуру/текст, не растеризацию). важны (сравниваем структуру/текст, не растеризацию).
- Переиспользуем из текущего `audit.mjs`: `discoverRoutes()`, `EXCLUDE` - Переиспользуем из текущего `audit.mjs`: `discoverRoutes()`, `EXCLUDE`
(`/preview`), `DYNAMIC` (`tools/[id]``gradient.html` / (`/preview`), `DYNAMIC` (`tools/[id]``gradient.html` /
`background-remover.html`), `refNameFor()`. `background-remover.html`), `refNameFor()`.
@@ -122,8 +121,8 @@ Svelte-атрибутов и комментов гидрации), чтобы п
### Шаги (на каждом маршруте) ### Шаги (на каждом маршруте)
1. Прогнать `pnpm refs-dom-audit`, получить дельты. 1. Прогнать `pnpm refs-dom-audit`, получить дельты.
2. Поправить структуру Svelte под реф (добавить/убрать/переставить узлы, 2. Поправить структуру Svelte под реф (добавить/убрать/переставить узлы, вынести
вынести тексты в `t()` с EN-базой). тексты в `t()` с EN-базой).
3. Перезапустить, пока `added/removed/moved/textMismatch` ≃ 0 (кроме `IGNORE`). 3. Перезапустить, пока `added/removed/moved/textMismatch` ≃ 0 (кроме `IGNORE`).
--- ---
@@ -135,17 +134,17 @@ Svelte-атрибутов и комментов гидрации), чтобы п
### Сравниваемые свойства (computed) ### Сравниваемые свойства (computed)
`borderRadius`, позиция/размер (`getBoundingClientRect`: x/y/w/h), `borderRadius`, позиция/размер (`getBoundingClientRect`: x/y/w/h), `padding`
`padding` (все стороны), `margin`, `color`, `backgroundColor`, (все стороны), `margin`, `color`, `backgroundColor`,
`borderTop/Right/Bottom/Left Width|Color`, `fontFamily`, `fontSize`, `borderTop/Right/Bottom/Left Width|Color`, `fontFamily`, `fontSize`,
`fontWeight`, `letterSpacing`, `lineHeight`, `gap`, `display`, `boxShadow`. `fontWeight`, `letterSpacing`, `lineHeight`, `gap`, `display`, `boxShadow`.
### Дифф ### Дифф
Для каждого состыкованного по пути элемента — список свойств с дельтой Для каждого состыкованного по пути элемента — список свойств с дельтой (наше
(наше значение → реф). Элементы, не совпавшие по пути (остаток из фазы A), значение → реф). Элементы, не совпавшие по пути (остаток из фазы A), идут в
идут в `unmatched`. Отчёт: элемент → свойство → наше/реф. Правится одной `unmatched`. Отчёт: элемент → свойство → наше/реф. Правится одной строкой CSS,
строкой CSS, без брутфорса. без брутфорса.
### Шаги ### Шаги
@@ -167,17 +166,17 @@ Svelte-атрибутов и комментов гидрации), чтобы п
(шаги 15 плана list-tools). То есть фаза A/B для list-tools стартует после (шаги 15 плана list-tools). То есть фаза A/B для list-tools стартует после
landing этих шагов. landing этих шагов.
3. **`tools/linear-gradient-png`** (↔ `gradient.html`) и 3. **`tools/linear-gradient-png`** (↔ `gradient.html`) и
**`tools/remove-background-png`** (↔ `background-remover.html`) — **последние**. **`tools/remove-background-png`** (↔ `background-remover.html`) —
Известные расхождения: наши инструменты могут иметь **другие наборы **последние**. Известные расхождения: наши инструменты могут иметь **другие
параметров / UI**, чем конкретные рефовые тулзы. Ожидаются структурные и наборы параметров / UI**, чем конкретные рефовые тулзы. Ожидаются структурные
стилевые дельты, которые не сводятся к «подровнять паддинг». Для них — и стилевые дельты, которые не сводятся к «подровнять паддинг». Для них —
триаж после прогона: триаж после прогона:
- реальный разрыв верности → фиксим; - реальный разрыв верности → фиксим;
- принципиальное отличие (параметр, которого нет в реф-тулзе; иной контрол) - принципиальное отличие (параметр, которого нет в реф-тулзе; иной контрол)
либо аккуратно приводим наш UI параметра к рефу, либо **документируем либо аккуратно приводим наш UI параметра к рефу, либо **документируем как
как допустимое расхождение / заносим в allowlist** конфига аудита допустимое расхождение / заносим в allowlist** конфига аудита (селектор +
(селектор + причина). «Скипы/фиксы» для инструментов оформляем явно, чтобы причина). «Скипы/фиксы» для инструментов оформляем явно, чтобы аудит не
аудит не шумел на осознанных отличиях. шумел на осознанных отличиях.
--- ---
@@ -185,25 +184,25 @@ Svelte-атрибутов и комментов гидрации), чтобы п
- `scripts/audit-dom.mjs` — фаза A (нормализация + структурный дифф). - `scripts/audit-dom.mjs` — фаза A (нормализация + структурный дифф).
- `scripts/audit-css.mjs` — фаза B (матчинг по пути + дифф стилей). - `scripts/audit-css.mjs` — фаза B (матчинг по пути + дифф стилей).
- `package.json`: `refs-dom-audit`, `refs-css-audit` (опционально - `package.json`: `refs-dom-audit`, `refs-css-audit` (опционально `refs-audit`,
`refs-audit`, прогоняющий обе фазы). прогоняющий обе фазы).
- Переиспользуем из старого `audit.mjs`: `discoverRoutes`, `EXCLUDE`, - Переиспользуем из старого `audit.mjs`: `discoverRoutes`, `EXCLUDE`, `DYNAMIC`,
`DYNAMIC`, `refNameFor`, `waitFor` (fast-fail на 4xx/5xx). `refNameFor`, `waitFor` (fast-fail на 4xx/5xx).
- Отчёты: `web/audit/dom-report.{md,json}` и `web/audit/css-report.{md,json}` - Отчёты: `web/audit/dom-report.{md,json}` и `web/audit/css-report.{md,json}`
(старый `audit-report.*` заменяем). (старый `audit-report.*` заменяем).
## Верификация (после каждого коммита) ## Верификация (после каждого коммита)
- `pnpm lint`, `pnpm exec svelte-check --tsconfig ./tsconfig.json`, - `pnpm lint`, `pnpm exec svelte-check --tsconfig ./tsconfig.json`, `pnpm build`
`pnpm build` — без ошибок. — без ошибок.
- `pnpm refs-dom-audit` (фаза A) / `pnpm refs-css-audit` (фаза B) по - `pnpm refs-dom-audit` (фаза A) / `pnpm refs-css-audit` (фаза B) по затронутому
затронутому маршруту: счётчики расхождений не растут без причины. маршруту: счётчики расхождений не растут без причины.
## Риски / заметки ## Риски / заметки
- **Стабильность матчинга:** нужна аккуратная нормализация (сортировка - **Стабильность матчинга:** нужна аккуратная нормализация (сортировка классов,
классов, игнор svelte-атрибутов/`data-*`, игнор whitespace-текстовых узлов, игнор svelte-атрибутов/`data-*`, игнор whitespace-текстовых узлов, игнор
игнор внутренностей SVG-иконок). Подстраивается по первым прогонам demo. внутренностей SVG-иконок). Подстраивается по первым прогонам demo.
- **Оболочка** (TopBar/footer/язык) сейчас отличается по дизайну — сводим к рефу - **Оболочка** (TopBar/footer/язык) сейчас отличается по дизайну — сводим к рефу
в рамках полного соответствия (без IGNORE); финальная унификация компонентов — в рамках полного соответствия (без IGNORE); финальная унификация компонентов —
отдельным рефакторингом позже. отдельным рефакторингом позже.
+173 -176
View File
@@ -5,56 +5,54 @@
> `schema.layout`, изоляция веток и вся миграция инструментов (121/125, > `schema.layout`, изоляция веток и вся миграция инструментов (121/125,
> остальные 4 — отложены решениями) выполнены. Ключевые архитектурные решения > остальные 4 — отложены решениями) выполнены. Ключевые архитектурные решения
> (параллельная сборка, правило копий, изоляция old↔preview) закреплены в > (параллельная сборка, правило копий, изоляция old↔preview) закреплены в
> `AGENTS.md`. > `AGENTS.md`. **Незакрытое из этого плана:** шаг 37 — физический переезд старых
> **Незакрытое из этого плана:** шаг 37 — физический переезд старых файлов > файлов в папки `old/` (перенесён в `backlog.md`), и удаление старого кода — по
> в папки `old/` (перенесён в `backlog.md`), и удаление старого кода — > `plan-redesign.md` (C17C21). Отложенные инструменты — `watermark-image-png`
> по `plan-redesign.md` (C17C21). Отложенные инструменты — > (overlay), `png-info`, `reduce-to-size` — в `backlog.md`. Ниже — полное
> `watermark-image-png` (overlay), `png-info`, `reduce-to-size` — в `backlog.md`. > состояние на момент закрытия.
> Ниже — полное состояние на момент закрытия.
> Статус: **в реализации.** Фаза 0 (фундамент) ✔, Фаза 1 (рабочий инструмент > Статус: **в реализации.** Фаза 0 (фундамент) ✔, Фаза 1 (рабочий инструмент в
> в preview) ✔, Фаза 2 (простые инструменты без составных типов) — переведены > preview) ✔, Фаза 2 (простые инструменты без составных типов) — переведены все
> все одиночные инструменты (18 шт). Фаза 3 — **составной тип `dimension` > одиночные инструменты (18 шт). Фаза 3 — **составной тип `dimension` полностью
> полностью переведён** (12 инструментов: create-empty, single-color, > переведён** (12 инструментов: create-empty, single-color, random-noise,
> random-noise, linear-gradient, color-spectrum, random-colors, draw-grid, > linear-gradient, color-spectrum, random-colors, draw-grid, placeholder,
> placeholder, fit-on-background, change-canvas-size, resize, crop); **составной > fit-on-background, change-canvas-size, resize, crop); **составной тип
> тип `color-pair` полностью переведён** (4 инструмента: blend-two, > `color-pair` полностью переведён** (4 инструмента: blend-two, step-colors,
> step-colors, linear-gradient, two-colors); **составной тип `offset` > linear-gradient, two-colors); **составной тип `offset` полностью переведён**
> полностью переведён** (4 инструмента: circle-mask, square-mask, > (4 инструмента: circle-mask, square-mask, star-mask, wavy-mask); **составной
> star-mask, wavy-mask); **составной тип `position9` — переведены > тип `position9` — переведены add-text-png и date-stamp-png** (водяной
> add-text-png и date-stamp-png** (водяной знак-картинка — отдельный шаг: > знак-картинка — отдельный шаг: требует overlay-механику в новом превью);
> требует overlay-механику в новом превью); **составной тип `font-style` > **составной тип `font-style` полностью переведён** (text-to-png, add-text,
> полностью переведён** (text-to-png, add-text, date-stamp); **составной тип > date-stamp); **составной тип `plate` полностью переведён** (add-text,
> `plate` полностью переведён** (add-text, date-stamp); **составной тип > date-stamp); **составной тип `gradient` полностью переведён**
> `gradient` полностью переведён** (linear-gradient): вместо горизонтального/ > (linear-gradient): вместо горизонтального/ вертикального select — **угол
> вертикального select — **угол 0..360°** (новый виджет `AngleControl`: > 0..360°** (новый виджет `AngleControl`: slider + кнопки-пресеты
> slider + кнопки-пресеты 0°/90°/180°/270°, переиспользуемый компонент); > 0°/90°/180°/270°, переиспользуемый компонент); preview научен применять
> preview > **генераторы** (`executeGenerate`, кнопка Generate) и рендерить все kinds
> научен применять **генераторы** (`executeGenerate`, кнопка Generate) и > схемы (slider/number/color/select/checkbox/dimension/
> рендерить все kinds схемы (slider/number/color/select/checkbox/dimension/
> color-pair/offset/position9/font-style/plate/gradient). **Пер-инструмент > color-pair/offset/position9/font-style/plate/gradient). **Пер-инструмент
> раскладка `schema.layout` реализована** (шаг 32): тип `ToolSchemaLayout` > раскладка `schema.layout` реализована** (шаг 32): тип `ToolSchemaLayout`
> {groups: [{title?, cols?, fields}]}, рендер групп в `SchemaFields.svelte` > {groups: [{title?, cols?, fields}]}, рендер групп в `SchemaFields.svelte`
> (заголовок группы + сетка колонок, неупомянутые поля — в общей группе), > (заголовок группы + сетка колонок, неупомянутые поля — в общей группе), пилот
> пилот — `add-text` (группы Text/Placement/Plate). **UI-макеты расставлены > — `add-text` (группы Text/Placement/Plate). **UI-макеты расставлены по
> по переведённым инструментам** (шаг 33): geometry (fit-on-background, > переведённым инструментам** (шаг 33): geometry (fit-on-background,
> change-canvas-size, resize, crop), маски alpha (Shape/Position), generate > change-canvas-size, resize, crop), маски alpha (Shape/Position), generate
> (create-empty, linear-gradient, color-spectrum, random-colors, draw-grid, > (create-empty, linear-gradient, color-spectrum, random-colors, draw-grid,
> step-colors, placeholder, text-to-png), text (add-text, date-stamp), > step-colors, placeholder, text-to-png), text (add-text, date-stamp), filters
> filters (randomize-pixels, add-noise) — везде, где канвас отделён от > (randomize-pixels, add-noise) — везде, где канвас отделён от параметров
> параметров эффекта либо фигура от позиции. > эффекта либо фигура от позиции. Следующее закрыто в
> Следующее закрыто в `archive/plan-migrate-remaining.md` (2026-09-07): > `archive/plan-migrate-remaining.md` (2026-09-07): перевод оставшихся
> перевод оставшихся инструментов выполнен (всего 121/125 в preview; 4 хвоста — > инструментов выполнен (всего 121/125 в preview; 4 хвоста —
> `watermark-image-png`, `png-info`, `reduce-to-size-png`, `compress-png` — > `watermark-image-png`, `png-info`, `reduce-to-size-png`, `compress-png` —
> отложены решениями в `backlog.md`). Незакрытые продолжения: переезд старых > отложены решениями в `backlog.md`). Незакрытые продолжения: переезд старых
> компонентов в папки `old/` (шаг 37 ниже), закрытие долгов и сбор фидбека — > компонентов в папки `old/` (шаг 37 ниже), закрытие долгов и сбор фидбека — см.
> см. также `plan-design-fix.md`. > также `plan-design-fix.md`.
> >
> Ключевые файлы нового registry: `web/src/lib/registry-new/{types,index,*}.ts` > Ключевые файлы нового registry: `web/src/lib/registry-new/{types,index,*}.ts`
> (по файлу на категорию: geometry/alpha/convert/analyze/filters/color/generate) > (по файлу на категорию: geometry/alpha/convert/analyze/filters/color/generate)
> >
> - `web/src/lib/preview/categories.ts`, > - `web/src/lib/preview/categories.ts`, `web/src/lib/registry-schema.ts` (kind
> `web/src/lib/registry-schema.ts` (kind `dimension`), `kit/fields/DimensionField.svelte`, > `dimension`), `kit/fields/DimensionField.svelte`,
> `preview/executor/index.ts` (`executeGenerate`), `SchemaFields.svelte` > `preview/executor/index.ts` (`executeGenerate`), `SchemaFields.svelte`
> (полный рендер kinds). Старый `web/src/lib/registry.ts` разбит по категориям > (полный рендер kinds). Старый `web/src/lib/registry.ts` разбит по категориям
> в `web/src/lib/registry/` (см. `registry.ts` — тонкий баррель). > в `web/src/lib/registry/` (см. `registry.ts` — тонкий баррель).
@@ -62,23 +60,23 @@
## Ключевая стратегия: параллельная сборка, старый UI не трогаем ## Ключевая стратегия: параллельная сборка, старый UI не трогаем
Старый UI (группа `(old)/`, `ParamForm.svelte` + старый pipeline) **продолжает Старый UI (группа `(old)/`, `ParamForm.svelte` + старый pipeline) **продолжает
работать как сейчас, без рефакторинга**. Новый `ToolSchema<P>` строится работать как сейчас, без рефакторинга**. Новый `ToolSchema<P>` строится рядом и
рядом и служит источником для **нового UI**. Инструменты переписываются под служит источником для **нового UI**. Инструменты переписываются под новый
новый registry по мере нужды — для нового UI. registry по мере нужды — для нового UI.
Это та же логика, что в `plan-redesign.md` применена к дизайну: параллельная Это та же логика, что в `plan-redesign.md` применена к дизайну: параллельная
сборка, изоляция от старого, затем новый становится основным и старый сборка, изоляция от старого, затем новый становится основным и старый удаляется
удаляется вместе со старым дизайном. вместе со старым дизайном.
**Правило копий (важно!):** если для нового UI/registry нужно внести изменения **Правило копий (важно!):** если для нового UI/registry нужно внести изменения в
в файл, который **прямо или косвенно** уже используется `(old)` веткой, — этот файл, который **прямо или косвенно** уже используется `(old)` веткой, — этот
файл **НЕ трогаем**. Вместо этого делаем **копию** в новом месте (например, файл **НЕ трогаем**. Вместо этого делаем **копию** в новом месте (например, в
в `lib/preview/`) и правим копию. Это приводит к дублированию, но `lib/preview/`) и правим копию. Это приводит к дублированию, но **гарантированно
**гарантированно не задевает старую ветку сайта**. Пример: категории — новый не задевает старую ветку сайта**. Пример: категории — новый
`lib/preview/categories.ts` (object as const) копирует и заменяет собой `lib/preview/categories.ts` (object as const) копирует и заменяет собой
`../categories` для нового кода, старый `categories.ts` обслуживает `(old)` и `../categories` для нового кода, старый `categories.ts` обслуживает `(old)` и
остаётся без изменений. После перехода (Фаза 5) копия становится основной, остаётся без изменений. После перехода (Фаза 5) копия становится основной, а
а исходник продолжает обслуживать `(old)` UI (он не удаляется — см. шаг 35). исходник продолжает обслуживать `(old)` UI (он не удаляется — см. шаг 35).
**Разделение схем: две независимые схемы.** **Разделение схем: две независимые схемы.**
@@ -98,14 +96,14 @@
Помимо двух схем, registry тоже разделён по UI (по факту миграции): Помимо двух схем, registry тоже разделён по UI (по факту миграции):
- **Старый registry** (`web/src/lib/registry.ts` + `registry/` + - **Старый registry** (`web/src/lib/registry.ts` + `registry/` +
`registry-helpers.ts`) — работает на старом UI, использует `ParamDef[]`. `registry-helpers.ts`) — работает на старом UI, использует `ParamDef[]`. **Не
**Не трогаем**; остаётся обслуживать `(old)/` маршруты (см. шаг 35). трогаем**; остаётся обслуживать `(old)/` маршруты (см. шаг 35).
- **Новый registry** (`web/src/lib/registry-new/`) — строится **с нуля «как надо»**: - **Новый registry** (`web/src/lib/registry-new/`) — строится **с нуля «как
`ToolEntry<P>` с обязательным `schema`, типизированный `run`, **без** `ParamDef[]` надо»**: `ToolEntry<P>` с обязательным `schema`, типизированный `run`, **без**
и **без** связи со старым. Импортирует core-функции (`expandCanvas`, `ParamDef[]` и **без** связи со старым. Импортирует core-функции
`strokeImage`, …) напрямую. Наполняется **по-инструментно** по мере миграции (`expandCanvas`, `strokeImage`, …) напрямую. Наполняется **по-инструментно**
(не разовым переводом всех 130). Preview показывает только переведённые по мере миграции (не разовым переводом всех 130). Preview показывает только
инструменты. переведённые инструменты.
Preview (`catalog.ts`, маршруты `preview/**`, `SchemaToolView`, `SchemaFields`) Preview (`catalog.ts`, маршруты `preview/**`, `SchemaToolView`, `SchemaFields`)
импортируют из `$lib/registry-new`. После удаления старого UI `registry-new/` импортируют из `$lib/registry-new`. После удаления старого UI `registry-new/`
@@ -122,12 +120,12 @@ Preview (`catalog.ts`, маршруты `preview/**`, `SchemaToolView`, `SchemaF
## Проблема (текущая) ## Проблема (текущая)
- `ParamDef` — плоский union из 6 примитивов (`number | slider | select | - `ParamDef` — плоский union из 6 примитивов
checkbox | color | text`). Параметры инструмента — отдельные записи в плоском (`number | slider | select | checkbox | color | text`). Параметры инструмента
массиве. — отдельные записи в плоском массиве.
- `run()`/`generate()` получают `Record<string, unknown>`; значения достаются - `run()`/`generate()` получают `Record<string, unknown>`; значения достаются
через `num(p, "fromColor")` — runtime string lookup **без type safety**, через `num(p, "fromColor")` — runtime string lookup **без type safety**, легко
легко напутать id. напутать id.
- Поля инструмента **слабо связаны** (например «ширина» и «цвет» рамки — два - Поля инструмента **слабо связаны** (например «ширина» и «цвет» рамки — два
независимых поля без общего контракта) → общий генерализованный рендер, нет независимых поля без общего контракта) → общий генерализованный рендер, нет
возможности сделать идеальный пер-инструмент UI. возможности сделать идеальный пер-инструмент UI.
@@ -139,16 +137,16 @@ checkbox | color | text`). Параметры инструмента — отд
1. **Полная типизация.** `interface Params` (runtime-тип) объявляется явно в 1. **Полная типизация.** `interface Params` (runtime-тип) объявляется явно в
каждом инструменте. `run`/`generate` типизируются прямо на него, а не на каждом инструменте. `run`/`generate` типизируются прямо на него, а не на
`Record<string, unknown>`. `Record<string, unknown>`.
2. **Отдельная типизированная схема для UI/дефолтов/валидации.** Два понятия 2. **Отдельная типизированная схема для UI/дефолтов/валидации.** Два понятия на
на инструмент: runtime-тип + схема. Схема проверяется компилятором на инструмент: runtime-тип + схема. Схема проверяется компилятором на
соответствие `Params` (чтобы не рассинхронизировались). соответствие `Params` (чтобы не рассинхронизировались).
3. **Общий рендер,** но с пер-инструмент layout: общие компоненты рисуют поля 3. **Общий рендер,** но с пер-инструмент layout: общие компоненты рисуют поля по
по схеме; схема инструмента задаёт layout и группировку. Без отдельного схеме; схема инструмента задаёт layout и группировку. Без отдельного
Svelte-компонента на каждый инструмент. Svelte-компонента на каждый инструмент.
4. **Составные типы** (`gradient`, `color-pair`, `dimension`, `offset`, 4. **Составные типы** (`gradient`, `color-pair`, `dimension`, `offset`,
`position9`, `font-style`, `plate`) остаются в плане — это способ выразить `position9`, `font-style`, `plate`) остаются в плане — это способ выразить в
в схеме связанную группу полей и переиспользовать её на нескольких схеме связанную группу полей и переиспользовать её на нескольких инструментах
инструментах и в UI. и в UI.
5. Рано или поздно `tool-views.ts` поглощается registry (preview/lede/layout 5. Рано или поздно `tool-views.ts` поглощается registry (preview/lede/layout
переезжают в meta инструмента) — но это отдельный шаг, см. ниже. переезжают в meta инструмента) — но это отдельный шаг, см. ниже.
6. **Старый UI не рефакторим.** Две независимые схемы: старый `ParamDef[]` 6. **Старый UI не рефакторим.** Две независимые схемы: старый `ParamDef[]`
@@ -157,8 +155,8 @@ checkbox | color | text`). Параметры инструмента — отд
## Целевая архитектура (на примере «добавить рамку») ## Целевая архитектура (на примере «добавить рамку»)
Цель: «добавить рамку: ширина и цвет» — **один интерфейс**, а не два Цель: «добавить рамку: ширина и цвет» — **один интерфейс**, а не два слабо
слабо связанных поля. связанных поля.
```ts ```ts
// 1. Runtime-тип — что получает run/generate // 1. Runtime-тип — что получает run/generate
@@ -232,8 +230,8 @@ function toolSchema<P>(fields: FieldsOf<P>): ToolSchema<P> { ... }
``` ```
Ключевая идея связки: **builder `field.x<T>()` связывает runtime-тип поля с его Ключевая идея связки: **builder `field.x<T>()` связывает runtime-тип поля с его
схемой на этапе компиляции**, а `toolSchema<P>` заставляет ключи схемы схемой на этапе компиляции**, а `toolSchema<P>` заставляет ключи схемы совпадать
совпадать с ключами `P`. Это и есть защита от рассинхрона. с ключами `P`. Это и есть защита от рассинхрона.
## Составные типы (для переиспользования и группировки) ## Составные типы (для переиспользования и группировки)
@@ -247,9 +245,9 @@ function toolSchema<P>(fields: FieldsOf<P>): ToolSchema<P> { ... }
| `font-style` | font + size + bold + color | 3-4 | | `font-style` | font + size + bold + color | 3-4 |
| `plate` | enabled + color + opacity | 2-3 | | `plate` | enabled + color + opacity | 2-3 |
Составной тип = группы полей, которые в интерфейсе `Params` могут разворачиваться Составной тип = группы полей, которые в интерфейсе `Params` могут
во вложенный объект или плоские ключи — решается на этапе реализации. разворачиваться во вложенный объект или плоские ключи — решается на этапе
Пример для `gradient`: реализации. Пример для `gradient`:
```ts ```ts
interface GradientParams { interface GradientParams {
@@ -293,27 +291,27 @@ add-text-png, date-stamp-png
## Пошаговый план перевода инструментов ## Пошаговый план перевода инструментов
> Каждый шаг — **атомарное, маленькое изменение** (в идеале 1 инструмент = > Каждый шаг — **атомарное, маленькое изменение** (в идеале 1 инструмент = 1
> 1 diff, укладывается в правило «коммиты < ~500 строк»). После каждого шага — > diff, укладывается в правило «коммиты < ~500 строк»). После каждого шага —
> ревью; параллельно можно делать UI нового инструмента. > ревью; параллельно можно делать UI нового инструмента.
> >
> **Принцип малых шагов:** перевод инструмента не меняет поведение старой > **Принцип малых шагов:** перевод инструмента не меняет поведение старой схемы
> схемы (`params` остаётся), не ломает старый UI. Инструмент получает ДОПОЛНИТЕЛЬНО > (`params` остаётся), не ломает старый UI. Инструмент получает ДОПОЛНИТЕЛЬНО
> новое поле `schema` + `interface Params`, старое `run`/`params` не трогаем. > новое поле `schema` + `interface Params`, старое `run`/`params` не трогаем.
> Это гарантирует, что старый UI продолжает работать без регресса. > Это гарантирует, что старый UI продолжает работать без регресса.
> >
> **Новый подход (решение пользователя):** сначала делаем **один полноценный > **Новый подход (решение пользователя):** сначала делаем **один полноценный
> рабочий инструмент в preview** — не эталон «на бумаге», а реально юзабельный, > рабочий инструмент в preview** — не эталон «на бумаге», а реально юзабельный,
> чтобы руками оценить, насколько это рабочее решение и какие возникнут сложности. > чтобы руками оценить, насколько это рабочее решение и какие возникнут
> Оцениваем, при необходимости правим инфраструктуру, и только потом переводим > сложности. Оцениваем, при необходимости правим инфраструктуру, и только потом
> остальные инструменты тем же проверенным паттерном. > переводим остальные инструменты тем же проверенным паттерном.
### Фаза 0 — фундамент ✔ (сделано) ### Фаза 0 — фундамент ✔ (сделано)
Typed field builders + `Field<T>` + `toolSchema<P>()` + `ToolSchema<P>` — Typed field builders + `Field<T>` + `toolSchema<P>()` + `ToolSchema<P>` — в
в `web/src/lib/registry-schema.ts`. `ToolEntry<P>` generic + опциональное `web/src/lib/registry-schema.ts`. `ToolEntry<P>` generic + опциональное поле
поле `schema`. Новые `defaultSchemaParams`/`sanitizeSchemaParams`. `schema`. Новые `defaultSchemaParams`/`sanitizeSchemaParams`. Старый
Старый `ParamDef[]`/pipeline не тронуты. Проверено: compile-time-защита `ParamDef[]`/pipeline не тронуты. Проверено: compile-time-защита
(лишний/неверный/отсутствующий ключ ловит TS), тесты, svelte-check без новых (лишний/неверный/отсутствующий ключ ловит TS), тесты, svelte-check без новых
ошибок. ошибок.
@@ -326,18 +324,18 @@ Typed field builders + `Field<T>` + `toolSchema<P>()` + `ToolSchema<P>` —
5. **Инструмент «добавить рамку» (`add-border-png`)** — сделать полностью 5. **Инструмент «добавить рамку» (`add-border-png`)** — сделать полностью
рабочим в preview: рабочим в preview:
- `interface AddBorderParams { thickness: number; color: string }` - `interface AddBorderParams { thickness: number; color: string }`
- `addBorderSchema = toolSchema<AddBorderParams>(...)` + `schema: ...` в entry - `addBorderSchema = toolSchema<AddBorderParams>(...)` + `schema: ...` в
(старый `params`/`run` остаются — старый UI не регрессит). entry (старый `params`/`run` остаются — старый UI не регрессит).
- **Новый рендер в preview** (`kit/ToolView``ParamControl` или новый - **Новый рендер в preview** (`kit/ToolView``ParamControl` или новый
компонент схемы): читает `ToolSchema<P>`, рисует поля рамки, применение компонент схемы): читает `ToolSchema<P>`, рисует поля рамки, применение
реально работает (end-to-end до картинки). реально работает (end-to-end до картинки).
- Дефолты/валидация/значения — из схемы (`defaultSchemaParams`, - Дефолты/валидация/значения — из схемы (`defaultSchemaParams`,
`sanitizeSchemaParams`). `sanitizeSchemaParams`).
- **Ревью-оценка:** удобно ли это, что сломалось/усложнилось, что поправить - **Ревью-оценка:** удобно ли это, что сломалось/усложнилось, что поправить в
в инфраструктуре до масштабирования. Принимается макет/решение. инфраструктуре до масштабирования. Принимается макет/решение.
Результат: один реально рабочий инструмент на новой системе = доказываем паттерн, Результат: один реально рабочий инструмент на новой системе = доказываем
прежде чем переводить остальных. паттерн, прежде чем переводить остальных.
### Фаза 2 — простые инструменты без составных типов (1 инструмент = 1 шаг) ### Фаза 2 — простые инструменты без составных типов (1 инструмент = 1 шаг)
@@ -345,8 +343,8 @@ Typed field builders + `Field<T>` + `toolSchema<P>()` + `ToolSchema<P>` —
составных типов. Каждый — отдельный маленький diff (~15-30 строк), тем самым составных типов. Каждый — отдельный маленький diff (~15-30 строк), тем самым
проверенным в Фазе 1 паттерном. **Переезжают в `registry-new`.** проверенным в Фазе 1 паттерном. **Переезжают в `registry-new`.**
Переведено: `add-border-png` (Фаза 1), `add-stroke-png`, `find-contour-png`, плюс Переведено: `add-border-png` (Фаза 1), `add-stroke-png`, `find-contour-png`,
весь блок простых инструментов ниже (пункты 8-22). Следующее — Фаза 3. плюс весь блок простых инструментов ниже (пункты 8-22). Следующее — Фаза 3.
6. ~~add-stroke-png (color + slider)~~ → переведён в `registry-new` 6. ~~add-stroke-png (color + slider)~~ → переведён в `registry-new`
7. ~~find-contour-png (color + slider)~~ → переведён в `registry-new` 7. ~~find-contour-png (color + slider)~~ → переведён в `registry-new`
@@ -369,8 +367,8 @@ Typed field builders + `Field<T>` + `toolSchema<P>()` + `ToolSchema<P>` —
> **Замечание (конвертеры):** старые `convert-png-to-jpg`/`convert-png-to-webp` > **Замечание (конвертеры):** старые `convert-png-to-jpg`/`convert-png-to-webp`
> несли `output`-метаданные (`mime`/`ext`/`qualityParamId`) для выбора формата > несли `output`-метаданные (`mime`/`ext`/`qualityParamId`) для выбора формата
> вывода. В новом `ToolEntry<P>` поля `output` пока нет, поэтому при переводе > вывода. В новом `ToolEntry<P>` поля `output` пока нет, поэтому при переводе
> эти метаданные не перенесены — preview пока отдаёт результат как PNG. > эти метаданные не перенесены — preview пока отдаёт результат как PNG. Механика
> Механика выбора формата/качества в новом UI — отдельный шаг (не блокирует Фазу 2). > выбора формата/качества в новом UI — отдельный шаг (не блокирует Фазу 2).
### Фаза 3 — инструменты с составными типами (по типу, затем по инструментам) ### Фаза 3 — инструменты с составными типами (по типу, затем по инструментам)
@@ -378,19 +376,19 @@ Typed field builders + `Field<T>` + `toolSchema<P>()` + `ToolSchema<P>` —
`single-color-png`, `random-noise-png`, `linear-gradient-png`, `single-color-png`, `random-noise-png`, `linear-gradient-png`,
`color-spectrum-png`, `random-colors-png`, `draw-grid-png`, `color-spectrum-png`, `random-colors-png`, `draw-grid-png`,
`placeholder-png` — генераторы в `registry-new/generate.ts`; `placeholder-png` — генераторы в `registry-new/generate.ts`;
`fit-on-background-png`, `change-canvas-size-png`, `resize-png`, `fit-on-background-png`, `change-canvas-size-png`, `resize-png`, `crop-png`
`crop-png` — в `registry-new/geometry.ts`). Составной тип во всех видах: — в `registry-new/geometry.ts`). Составной тип во всех видах: вложенный
вложенный объект `size: { width, height }` в Params + `field.dimension`, объект `size: { width, height }` в Params + `field.dimension`, виджет
виджет `kit/fields/DimensionField.svelte`. `kit/fields/DimensionField.svelte`. Preview: генераторы применяются через
Preview: генераторы применяются через `executeGenerate` (кнопка Generate), `executeGenerate` (кнопка Generate), `SchemaFields` рендерит все kinds схемы
`SchemaFields` рендерит все kinds схемы (slider/number/color/select/ (slider/number/color/select/ checkbox/dimension). Тесты: 592 passed.
checkbox/dimension). Тесты: 592 passed.
25. `color-pair`**все 4 инструмента переведены** ✔ (`blend-two-png`, 25. `color-pair`**все 4 инструмента переведены** ✔ (`blend-two-png`,
`step-colors-png`, `linear-gradient-png` — генераторы в `step-colors-png`, `linear-gradient-png` — генераторы в
`registry-new/generate.ts`, `two-colors-png` — run в `registry-new/color.ts`). `registry-new/generate.ts`, `two-colors-png` — run в
Составной тип во всех видах: вложенный объект `pair: { from, to }` + `registry-new/color.ts`). Составной тип во всех видах: вложенный объект
`field.colorPair`, виджет `kit/fields/schema/ColorPairControl.svelte`, `pair: { from, to }` + `field.colorPair`, виджет
kind `color-pair` в схеме (default/sanitize). Тесты: +4 (596 passed). `kit/fields/schema/ColorPairControl.svelte`, kind `color-pair` в схеме
(default/sanitize). Тесты: +4 (596 passed).
26. `offset`**все 4 инструмента переведены** ✔ (`circle-mask-png`, 26. `offset`**все 4 инструмента переведены** ✔ (`circle-mask-png`,
`square-mask-png`, `star-mask-png`, `wavy-mask-png` — run в `square-mask-png`, `star-mask-png`, `wavy-mask-png` — run в
`registry-new/alpha.ts`). Составной тип во всех видах: вложенный объект `registry-new/alpha.ts`). Составной тип во всех видах: вложенный объект
@@ -399,40 +397,39 @@ Typed field builders + `Field<T>` + `toolSchema<P>()` + `ToolSchema<P>` —
(default/sanitize). Тесты: +4 (600 passed). (default/sanitize). Тесты: +4 (600 passed).
27. `position9`**переведены `add-text-png` и `date-stamp-png`** ✔ (run в 27. `position9`**переведены `add-text-png` и `date-stamp-png`** ✔ (run в
`registry-new/text.ts`, оба `domOnly`). Составной тип во всех видах: `registry-new/text.ts`, оба `domOnly`). Составной тип во всех видах:
значение — строка `Position9`, kind `position9` в схеме (default/sanitize значение — строка `Position9`, kind `position9` в схеме (default/sanitize по
по `POSITION9_VALUES`), виджет 3×3 `kit/fields/schema/PositionControl.svelte`. `POSITION9_VALUES`), виджет 3×3 `kit/fields/schema/PositionControl.svelte`.
В `ToolEntry` добавлен флаг `domOnly` (превью-executor держит такие В `ToolEntry` добавлен флаг `domOnly` (превью-executor держит такие
инструменты вне worker). `watermark-image-png` — отдельный под-шаг: инструменты вне worker). `watermark-image-png` — отдельный под-шаг: ему
ему нужен overlay-source (`getOverlay`/store), которого в новом превью пока нужен overlay-source (`getOverlay`/store), которого в новом превью пока нет.
нет. Тесты: +2 (602 passed). `font-style` и `plate` на этих инструментах Тесты: +2 (602 passed). `font-style` и `plate` на этих инструментах сводятся
сводятся в шаги 28-29. в шаги 28-29.
28. `font-style` — **все 3 инструмента переведены** ✔ (`text-to-png` — 28. `font-style`**все 3 инструмента переведены** ✔ (`text-to-png` — генератор
генератор в `registry-new/generate.ts` (domOnly), `add-text-png`/ в `registry-new/generate.ts` (domOnly), `add-text-png`/ `date-stamp-png`
`date-stamp-png` — рефакторинг в `registry-new/text.ts`). Составной тип рефакторинг в `registry-new/text.ts`). Составной тип во всех видах:
во всех видах: вложенный объект `style: { font, size, bold, color }` + вложенный объект `style: { font, size, bold, color }` + `field.fontStyle`,
`field.fontStyle`, виджет `kit/fields/schema/FontStyleControl.svelte`, виджет `kit/fields/schema/FontStyleControl.svelte`, kind `font-style` в
kind `font-style` в схеме (default/sanitize, clamp размера к min/max). схеме (default/sanitize, clamp размера к min/max). Тесты: +1 (603 passed).
Тесты: +1 (603 passed).
29. `plate`**все 2 инструмента переведены** ✔ (`add-text-png`, 29. `plate`**все 2 инструмента переведены** ✔ (`add-text-png`,
`date-stamp-png` — рефакторинг в `registry-new/text.ts`). Составной тип `date-stamp-png` — рефакторинг в `registry-new/text.ts`). Составной тип во
во всех видах: вложенный объект `plate: { enabled, color, opacity }` + всех видах: вложенный объект `plate: { enabled, color, opacity }` +
`field.plate`, виджет `kit/fields/schema/PlateControl.svelte` (чекбокс + `field.plate`, виджет `kit/fields/schema/PlateControl.svelte` (чекбокс +
цвет + слайдер непрозрачности, деактивируется при выключенной плашке), цвет + слайдер непрозрачности, деактивируется при выключенной плашке), kind
kind `plate` в схеме (default/sanitize, clamp opacity к 0..100). `plate` в схеме (default/sanitize, clamp opacity к 0..100). Покрытие тестов
Покрытие тестов расширено (дефолты/sanitize plate в существующих тестах). расширено (дефолты/sanitize plate в существующих тестах).
30. `gradient` — **переведён** ✔ (linear-gradient-png). Направление градиента 30. `gradient`**переведён** ✔ (linear-gradient-png). Направление градиента
не горизонталь/вертикаль, а **произвольный угол**: новый переиспользуемый не горизонталь/вертикаль, а **произвольный угол**: новый переиспользуемый
виджет `kit/fields/schema/AngleControl.svelte` (слайдер 0..360° + кнопки виджет `kit/fields/schema/AngleControl.svelte` (слайдер 0..360° + кнопки
стандартных углов 0°/90°/180°/270° с активным состоянием). Составной тип стандартных углов 0°/90°/180°/270° с активным состоянием). Составной тип
`gradient: { from, to, angle }` во всех видах (`field.gradient`, kind `gradient: { from, to, angle }` во всех видах (`field.gradient`, kind
`gradient` в схеме — default/sanitize, clamp угла к 0..360, валидация `gradient` в схеме — default/sanitize, clamp угла к 0..360, валидация
цветов; виджет `kit/fields/schema/GradientControl.svelte` — пара цветов + цветов; виджет `kit/fields/schema/GradientControl.svelte` — пара цветов +
AngleControl). Рендер по углу — локальный `angleGradient` в AngleControl). Рендер по углу — локальный `angleGradient` в
`registry-new/generate.ts` (core-`gradientImage` не трогали: он умеет `registry-new/generate.ts` (core-`gradientImage` не трогали: он умеет только
только horizontal/vertical и используется старым UI). Семантика: 0° слева horizontal/vertical и используется старым UI). Семантика: 0° слева направо,
направо, 90° сверху вниз, рост угла по часовой (ось Y вниз); нормализация 90° сверху вниз, рост угла по часовой (ось Y вниз); нормализация по полному
по полному диапазону проекции на ось (180°/270° разворачивают градиент). диапазону проекции на ось (180°/270° разворачивают градиент). Тесты:
Тесты: дефолты, направление по углу (0°/90°/180°), sanitize/кламп. дефолты, направление по углу (0°/90°/180°), sanitize/кламп.
### Фаза 4 — масштаб UI на остальные инструменты ### Фаза 4 — масштаб UI на остальные инструменты
@@ -443,40 +440,41 @@ Typed field builders + `Field<T>` + `toolSchema<P>()` + `ToolSchema<P>` —
`ToolSchemaLayout.groups: { title?, cols?, fields[] }` в registry-schema; `ToolSchemaLayout.groups: { title?, cols?, fields[] }` в registry-schema;
`SchemaFields.svelte` рендерит группы (подпись + сетка `1..n` колонок, `SchemaFields.svelte` рендерит группы (подпись + сетка `1..n` колонок,
неупомянутые поля — в общей группе в конце, без заголовка). Пилот — неупомянутые поля — в общей группе в конце, без заголовка). Пилот —
`add-text-png` (Text: text+style; Placement: position+margin; Plate). `add-text-png` (Text: text+style; Placement: position+margin; Plate). Тесты:
Тесты: schema-layout в registry-schema.test.ts; полные запуски чисты. schema-layout в registry-schema.test.ts; полные запуски чисты.
33. Для каждого переведённого инструмента — UI-макет **расставлен** ✔. 33. Для каждого переведённого инструмента — UI-макет **расставлен** ✔.
Layout-группы заданы там, где группировка содержательна (канвас отдельно Layout-группы заданы там, где группировка содержательна (канвас отдельно от
от параметров эффекта, фигура отдельно от позиции; составные виджеты — параметров эффекта, фигура отдельно от позиции; составные виджеты — внутри
внутри групп целиком): групп целиком):
- geometry: fit-on-background (Canvas/Background), change-canvas-size - geometry: fit-on-background (Canvas/Background), change-canvas-size
(Canvas/Anchor), resize (Canvas/Scaling), crop (Offset/Crop area); (Canvas/Anchor), resize (Canvas/Scaling), crop (Offset/Crop area);
- alpha-маски: circle/square/star/wavy (Shape — с 2 колонками где плотно, - alpha-маски: circle/square/star/wavy (Shape — с 2 колонками где плотно,
Position — offset); Position — offset);
- generate: create-empty (Canvas/Fill), linear-gradient (Canvas/Colors), - generate: create-empty (Canvas/Fill), linear-gradient (Canvas/Colors),
color-spectrum (Canvas/Spectrum), random-colors (Canvas/Random), color-spectrum (Canvas/Spectrum), random-colors (Canvas/Random), draw-grid
draw-grid (Canvas/Grid), step-colors (Colors/Output), placeholder (Canvas/Grid), step-colors (Colors/Output), placeholder
(Canvas/Colors/Text), text-to-png (Text/Background/Padding); (Canvas/Colors/Text), text-to-png (Text/Background/Padding);
- text: add-text, date-stamp (Text/Placement/Plate); - text: add-text, date-stamp (Text/Placement/Plate);
- filters: randomize-pixels (Blocks), add-noise (Noise/Seed). - filters: randomize-pixels (Blocks), add-noise (Noise/Seed). Инструменты с
Инструменты с 1–3 простыми полями остались без layout (одна общая группа). 1–3 простыми полями остались без layout (одна общая группа).
### Фаза 5 — изоляция старого UI ### Фаза 5 — изоляция старого UI
34. Поглощение `tool-views.ts` (preview/lede/layout → meta инструмента) ✔. 34. Поглощение `tool-views.ts` (preview/lede/layout → meta инструмента) ✔.
Отдельного `tool-views.ts` в репо нет: preview сразу строился на Отдельного `tool-views.ts` в репо нет: preview сразу строился на
`registry-new`. Meta инструмента живёт в `ToolEntry` (`title`, `description`, `registry-new`. Meta инструмента живёт в `ToolEntry` (`title`,
`category`, `run`/`generate`), layout — в `schema.layout` (шаг 32), рендер — `description`, `category`, `run`/`generate`), layout — в `schema.layout`
`SchemaToolView`/`SchemaFields`/`SchemaPreview`. Дублирующей системы нет. (шаг 32), рендер — `SchemaToolView`/`SchemaFields`/`SchemaPreview`.
Дублирующей системы нет.
35. Старый UI **не удаляется**, а выносится в `(old)/`-маршруты и остаётся там 35. Старый UI **не удаляется**, а выносится в `(old)/`-маршруты и остаётся там
временно (посмотреть, как работает, сравнить с новым; старые тесты временно (посмотреть, как работает, сравнить с новым; старые тесты
продолжают проходить): продолжают проходить):
- маршруты `(old)/{+page,demo,list-tools,tools/[id]}` — старый дизайн, - маршруты `(old)/{+page,demo,list-tools,tools/[id]}` — старый дизайн, тянет
тянет `old.css` (не `design2.css`), старый header/footer; `old.css` (не `design2.css`), старый header/footer;
- старый `registry.ts`/`registry/` + `ParamDef[]` обслуживают только - старый `registry.ts`/`registry/` + `ParamDef[]` обслуживают только
`(old)/`-инструменты — не удаляются, не рефакторятся; `(old)/`-инструменты — не удаляются, не рефакторятся;
- удаление происходит позже, отдельным решением (когда новый UI покроет - удаление происходит позже, отдельным решением (когда новый UI покроет все
все инструменты и ревью завершено). инструменты и ревью завершено).
36. **Линтер-изоляция веток** (гарантия, что old и preview не смешиваются) ✔. 36. **Линтер-изоляция веток** (гарантия, что old и preview не смешиваются) ✔.
Кастомный ESLint-плагин `web/eslint-plugins/isolation/no-mixed-imports` Кастомный ESLint-плагин `web/eslint-plugins/isolation/no-mixed-imports`
Резолвит каждый импорт (и `$lib/...`, и относительные `./`/`../`) до Резолвит каждый импорт (и `$lib/...`, и относительные `./`/`../`) до
@@ -490,12 +488,12 @@ Typed field builders + `Field<T>` + `toolSchema<P>()` + `ToolSchema<P>` —
`lib/registry-schema.ts`, `lib/registry-schema.test.ts`, `lib/registry-schema.ts`, `lib/registry-schema.test.ts`,
`lib/components/kit/**`; `lib/components/kit/**`;
- общее (разрешено обоим): всё прочее — `core/`, `i18n/`, `theme`, - общее (разрешено обоим): всё прочее — `core/`, `i18n/`, `theme`,
`assets/`, корневой `lib` (`index.ts`, тесты). `assets/`, корневой `lib` (`index.ts`, тесты). Достигнутая полная изоляция
Достигнутая полная изоляция (одиночные пересечения устранены): (одиночные пересечения устранены):
- старые пилоты `registry/geometry.ts` (`add-border`) и `registry/alpha.ts` - старые пилоты `registry/geometry.ts` (`add-border`) и `registry/alpha.ts`
(`add-stroke`) получали `schema` из нового `registry-schema` — убрано; (`add-stroke`) получали `schema` из нового `registry-schema` — убрано; оба
оба инструмента работают в старом UI через `params: ParamDef[]`, инструмента работают в старом UI через `params: ParamDef[]`, в preview —
в preview — через свои schema-версии в `registry-new/`; через свои schema-версии в `registry-new/`;
- `registry.ts` больше не импортирует `ToolSchema` из `registry-schema`; - `registry.ts` больше не импортирует `ToolSchema` из `registry-schema`;
- preview `list-tools` тянул `TOOL_ICONS` из старого `lib/tools/tool-icons` - preview `list-tools` тянул `TOOL_ICONS` из старого `lib/tools/tool-icons`
→ создана копия `lib/preview/tool-icons.ts` (правило копий). → создана копия `lib/preview/tool-icons.ts` (правило копий).
@@ -507,10 +505,10 @@ Typed field builders + `Field<T>` + `toolSchema<P>()` + `ToolSchema<P>` —
### Как ревьюить каждый шаг ### Как ревьюить каждый шаг
- Diff **только один инструмент** (или фундамент без инструментов). - Diff **только один инструмент** (или фундамент без инструментов).
- Старый `params`/`run`/`defaultParams`/`sanitizeParams` — **не изменены** (кроме - Старый `params`/`run`/`defaultParams`/`sanitizeParams`**не изменены**
добаление `schema`, если это пилот). (кроме добаление `schema`, если это пилот).
- Старый UI и старые тесты продолжают работать — запустить - Старый UI и старые тесты продолжают работать — запустить `pnpm --dir web test`
`pnpm --dir web test` и `pnpm --dir web exec svelte-check`. и `pnpm --dir web exec svelte-check`.
- Порядок полей в `interface Params` и в `schema` совпадает; дефолты в сeme - Порядок полей в `interface Params` и в `schema` совпадает; дефолты в сeme
равны старым `default` из `params`. равны старым `default` из `params`.
- Пилот из Фазы 1 проверяется **руками в preview**: применяется рамка, - Пилот из Фазы 1 проверяется **руками в preview**: применяется рамка,
@@ -539,8 +537,8 @@ Typed field builders + `Field<T>` + `toolSchema<P>()` + `ToolSchema<P>` —
на `ParamDef[]` как раньше. на `ParamDef[]` как раньше.
- **Новый UI** (kit/`SchemaToolView` + `SchemaFields` + `SchemaPreview`, читает - **Новый UI** (kit/`SchemaToolView` + `SchemaFields` + `SchemaPreview`, читает
`ToolSchema<P>`) — сейчас рендерит поля по схеме (number/slider/color). `ToolSchema<P>`) — сейчас рендерит поля по схеме (number/slider/color).
- `tool-views.ts` отсутствует — preview/lede/layout уже живут в meta - `tool-views.ts` отсутствует — preview/lede/layout уже живут в meta инструмента
инструмента (`ToolEntry` + `schema.layout`), дублирования нет. (`ToolEntry` + `schema.layout`), дублирования нет.
## Оценка трудозатрат ## Оценка трудозатрат
@@ -557,12 +555,12 @@ Typed field builders + `Field<T>` + `toolSchema<P>()` + `ToolSchema<P>` —
| **Итого** | | **~22-32ч** (поэтапно) | | **Итого** | | **~22-32ч** (поэтапно) |
> Оценка выросла по сравнению с ранней версией плана, потому что принят путь > Оценка выросла по сравнению с ранней версией плана, потому что принят путь
> «явный interface Params + схема + общий рендер с пер-инструмент layout» — > «явный interface Params + схема + общий рендер с пер-инструмент layout» — это
> это полный рефакторинг pipeline, а не только добавление составных типов. > полный рефакторинг pipeline, а не только добавление составных типов.
> >
> Старый UI/`ParamDef[]`/старый pipeline в смету **не входят** — они не > Старый UI/`ParamDef[]`/старый pipeline в смету **не входят** — они не
> рефакторятся, а продолжают работать на `(old)/`-маршрутах до перехода > рефакторятся, а продолжают работать на `(old)/`-маршрутах до перехода (затем
> (затем убираются отдельным решением — см. шаг 35). > убираются отдельным решением — см. шаг 35).
## Порядок реализации (кратко) ## Порядок реализации (кратко)
@@ -571,20 +569,19 @@ Typed field builders + `Field<T>` + `toolSchema<P>()` + `ToolSchema<P>` —
1. **Фаза 0** — фундамент: builders + `toolSchema<P>` + `ToolEntry<P>.schema` + 1. **Фаза 0** — фундамент: builders + `toolSchema<P>` + `ToolEntry<P>.schema` +
новые default/sanitize. Ни одного инструмента не изменено. ✔ новые default/sanitize. Ни одного инструмента не изменено. ✔
2. **Фаза 1** — **один полноценный рабочий инструмент в preview** 2. **Фаза 1****один полноценный рабочий инструмент в preview** («добавить
(«добавить рамку»): schema + рендер + end-to-end применение. Ручная оценка рамку»): schema + рендер + end-to-end применение. Ручная оценка
работоспособности решения/сложностей. работоспособности решения/сложностей.
3. **Фаза 2** — простые/одиночные инструменты (проверенный паттерн Фазы 1), 3. **Фаза 2** — простые/одиночные инструменты (проверенный паттерн Фазы 1), 1
1 инструмент = 1 диф. инструмент = 1 диф.
4. **Фаза 3** — инструменты с составными типами (dimension → color-pair → 4. **Фаза 3** — инструменты с составными типами (dimension → color-pair → offset
offset → position9 → font-style → plate → gradient), пилот каждого типа → position9 → font-style → plate → gradient), пилот каждого типа отдельно.
отдельно.
5. **Фаза 4** — масштаб UI на остальные: составные виджеты, пер-инструмент 5. **Фаза 4** — масштаб UI на остальные: составные виджеты, пер-инструмент
layout (для каждого инструмента — UI-макет). layout (для каждого инструмента — UI-макет).
6. **Фаза 5** — изоляция: старый UI на `(old)/`-маршруты (не удаляется, 6. **Фаза 5** — изоляция: старый UI на `(old)/`-маршруты (не удаляется, остаётся
остаётся для ревью); preview на `registry-new` полностью; для ревью); preview на `registry-new` полностью; линтер-изоляция веток
линтер-изоляция веток (плагин `isolation`), затем перенос старых (плагин `isolation`), затем перенос старых компонентов/библиотек в папки
компонентов/библиотек в папки `old/`. `old/`.
## Зависимости ## Зависимости
+34 -36
View File
@@ -1,17 +1,17 @@
# План: доводка дизайна до верности (фикс по refs) # План: доводка дизайна до верности (фикс по refs)
> **СТАТУС: ЗАКРЫТ (2026-09-07).** Актуальный дизайн (`preview/*`) ушёл вперёд > **СТАТУС: ЗАКРЫТ (2026-09-07).** Актуальный дизайн (`preview/*`) ушёл вперёд
> от референсов `refs-html/` — сверка с ними больше не информативна > от референсов `refs-html/` — сверка с ними больше не информативна (новые
> (новые компоненты `SchemaToolView`/`SchemaPreview`/каталог не имеют > компоненты `SchemaToolView`/`SchemaPreview`/каталог не имеют рефовой копии).
> рефовой копии). Аудит против рефа прекращается; качество дизайна дальше > Аудит против рефа прекращается; качество дизайна дальше гоняется ревью руками
> гоняется ревью руками и специализированными проверками > и специализированными проверками (`lint:all` —
> (`lint:all` — stylelint/design-tokens/токен-аудит). Порядок удаления старого > stylelint/design-tokens/токен-аудит). Порядок удаления старого и перехода — по
> и перехода — по `plan-redesign.md` (C17C21). > `plan-redesign.md` (C17C21).
> Статус: к выполнению. Ветка `feat/redesign`. > Статус: к выполнению. Ветка `feat/redesign`. Источники:
> Источники: `docs/archive/demo-fidelity.md` (архивная копия старого отчёта по > `docs/archive/demo-fidelity.md` (архивная копия старого отчёта по верности,
> верности, неактуальна), `docs/plan-redesign.md` (общая стратегия, фазы 1–4 > неактуальна), `docs/plan-redesign.md` (общая стратегия, фазы 1–4 выполнены до
> выполнены до C16). Актуальный аудит верности — `pnpm refs-cdp-audit` / > C16). Актуальный аудит верности — `pnpm refs-cdp-audit` /
> `pnpm refs-css-audit` / `pnpm refs-dom-audit` → `web/audit/audit-report.md` > `pnpm refs-css-audit` / `pnpm refs-dom-audit` → `web/audit/audit-report.md`
> (текущий список скриптов — в `web/package.json`, см. `refs-*audit`). > (текущий список скриптов — в `web/package.json`, см. `refs-*audit`).
@@ -22,11 +22,10 @@
- `refs/` и `refs-html/` приведены к консистентному виду — текущая «правда по - `refs/` и `refs-html/` приведены к консистентному виду — текущая «правда по
пикселям». Механизм синхронизации ref→код не создаётся (см. §9 plan-redesign). пикселям». Механизм синхронизации ref→код не создаётся (см. §9 plan-redesign).
- Аудит верности автоматизирован: `pnpm refs-cdp-audit` (1440×900, - Аудит верности автоматизирован: `pnpm refs-cdp-audit` (1440×900,
`scripts/audit-cdp.mjs`) пишет `scripts/audit-cdp.mjs`) пишет `web/audit/audit-report.md` — light/dark
`web/audit/audit-report.md` — light/dark токены + поэлементный дифф стилей; токены + поэлементный дифф стилей; пиксель-дифф не используется. Палитры
пиксель-дифф не используется. Палитры токенов уже сверены с рефом (light токенов уже сверены с рефом (light совпадает, dark собран). Темы в `refs-html`
совпадает, dark собран). Темы в `refs-html` переключаются — скрипт инжектится переключаются — скрипт инжектится в `scripts/extract-static.mjs`.
в `scripts/extract-static.mjs`.
- Исходный отчёт `demo-fidelity.md` перенесён в `docs/archive/` как утративший - Исходный отчёт `demo-fidelity.md` перенесён в `docs/archive/` как утративший
актуальность; его данные актуализированы ниже (Этап 1). актуальность; его данные актуализированы ниже (Этап 1).
- Зафиксированные решения (подтверждены автором): - Зафиксированные решения (подтверждены автором):
@@ -38,13 +37,14 @@
- Никаких правок старого дизайна `(old)/` и старого `ui/`. - Никаких правок старого дизайна `(old)/` и старого `ui/`.
- Компоненты пишем в `kit/` (переименование в `ui/` — на C19 по plan-redesign). - Компоненты пишем в `kit/` (переименование в `ui/` — на C19 по plan-redesign).
- Коммиты атомарные (< ~500 строк), по 1–2 компонента; после каждого — - Коммиты атомарные (< ~500 строк), по 1–2 компонента; после каждого —
`svelte-check` + `lint` + `build` + `refs-cdp-audit` (diff% не должен расти без причины). `svelte-check` + `lint` + `build` + `refs-cdp-audit` (diff% не должен расти
без причины).
- Коммиты делает автор (агент не коммитит). - Коммиты делает автор (агент не коммитит).
## Этап 0 — Рефакторинг (SRP / дизайн-обёртки), без изменения визуала ## Этап 0 — Рефакторинг (SRP / дизайн-обёртки), без изменения визуала
Гигантские страницы (`demo` ~513, `tools/[id]` ~451 строк) дробим на Гигантские страницы (`demo` ~513, `tools/[id]` ~451 строк) дробим на компоненты
компоненты единой ответственности; убираем дублирующиеся inline-стили. единой ответственности; убираем дублирующиеся inline-стили.
Новые обёртки в `src/lib/components/kit/`: Новые обёртки в `src/lib/components/kit/`:
@@ -62,9 +62,8 @@
Декомпозиция страниц: Декомпозиция страниц:
- `tools/[id]`: вынести `ParamControl` (диспетчер полей по `ParamDef`), - `tools/[id]`: вынести `ParamControl` (диспетчер полей по `ParamDef`),
`SettingsPanel` (левая панель), `PreviewPanel` (правая панель) — `SettingsPanel` (левая панель), `PreviewPanel` (правая панель) — колоцированно
колоцированно в `routes/preview/tools/[id]/`. Страница сжимается до в `routes/preview/tools/[id]/`. Страница сжимается до состояния + глёв.
состояния + глёв.
- `demo`: переписать на те же обёртки + `WorkspaceHeader` (eyebrow + h1 + lede + - `demo`: переписать на те же обёртки + `WorkspaceHeader` (eyebrow + h1 + lede +
file-chip). file-chip).
@@ -81,10 +80,9 @@
Аудит: `pnpm refs-cdp-audit` (1440×900) пишет `web/audit/audit-report.md` Аудит: `pnpm refs-cdp-audit` (1440×900) пишет `web/audit/audit-report.md`
light/dark токены + поэлементный дифф стилей. Пиксель-дифф не используется. light/dark токены + поэлементный дифф стилей. Пиксель-дифф не используется.
Палитры токенов уже сверены с рефом: light совпадает (`--background`, Палитры токенов уже сверены с рефом: light совпадает (`--background`, `--panel`,
`--panel`, `--foreground`, `--muted`, `--line`, `--blue`), dark собран (16 `--foreground`, `--muted`, `--line`, `--blue`), dark собран (16 токенов). Темы в
токенов). Темы в `refs-html` переключаются (скрипт инжектится в `refs-html` переключаются (скрипт инжектится в `extract-static.mjs`).
`extract-static.mjs`).
Открытые / решённые пункты (исходник — `docs/archive/demo-fidelity.md`): Открытые / решённые пункты (исходник — `docs/archive/demo-fidelity.md`):
@@ -94,26 +92,26 @@ light/dark токены + поэлементный дифф стилей. Пик
рендерит ровно один `TopBar`; `demo` свой `.topbar` не рендерит. Подтвердить рендерит ровно один `TopBar`; `demo` свой `.topbar` не рендерит. Подтвердить
аудитом/визуально при Этапе 1. аудитом/визуально при Этапе 1.
- **F1** (решено — плоский фон): наш `--background` уже `#EEF1F4` (как реф); - **F1** (решено — плоский фон): наш `--background` уже `#EEF1F4` (как реф);
видимое отличие было в blueprint-сетке поверх фона. Фикс: убрать `BlueprintGrid` видимое отличие было в blueprint-сетке поверх фона. Фикс: убрать
из `AppShell`. `BlueprintGrid` из `AppShell`.
- **F2** (сделано C16): тип шага (`StepCard`) — приглушённый mono-текст, не - **F2** (сделано C16): тип шага (`StepCard`) — приглушённый mono-текст, не
синий бейдж; индекс шага — синий `#1769D2`. синий бейдж; индекс шага — синий `#1769D2`.
- **F3** (сделано C16): подпись превью-тайла (`PreviewTile`) — синяя `#1769D2`. - **F3** (сделано C16): подпись превью-тайла (`PreviewTile`) — синяя `#1769D2`.
- **F4** (решено — одна колонка): реализуется в `PreviewStack` (Этап 0), - **F4** (решено — одна колонка): реализуется в `PreviewStack` (Этап 0), колонка
колонка = `1fr`. = `1fr`.
- **F5** (открыт): вес `MonoLabel` 500 → 400 (как в рефе). - **F5** (открыт): вес `MonoLabel` 500 → 400 (как в рефе).
- **F6** (открыт): мета-подписи (MetaRow/MetaList, DIMENSIONS/FORMAT/SIZE) - **F6** (открыт): мета-подписи (MetaRow/MetaList, DIMENSIONS/FORMAT/SIZE) fs
fs 10px → 9px. 10px → 9px.
- **F7** (низкий): копирайт/стиль — у нас `+ Add tool` (реф `Add tool`); - **F7** (низкий): копирайт/стиль — у нас `+ Add tool` (реф `Add tool`); кнопка
кнопка `Download result` — сверить с рефом (синяя, ~478×42). Проверить `Download result` — сверить с рефом (синяя, ~478×42). Проверить визуально
визуально (замеры NOT FOUND — артефакт вложенного чипа размера). (замеры NOT FOUND — артефакт вложенного чипа размера).
- **F8** (низкий): `h1` line-height чуть выше рефа (150px vs 115px). - **F8** (низкий): `h1` line-height чуть выше рефа (150px vs 115px).
- **F9** (открыт): вертикальный ритм — подровнять паддинги секций под реф - **F9** (открыт): вертикальный ритм — подровнять паддинги секций под реф
(pipeline-head и пр.). (pipeline-head и пр.).
- **F10** (открыт): внутренние отступы preview-панели (паддинг панели). - **F10** (открыт): внутренние отступы preview-панели (паддинг панели).
После Этапа 1 — повторный `refs-cdp-audit`, зафиксировать остаточный diff (sub-pixel / После Этапа 1 — повторный `refs-cdp-audit`, зафиксировать остаточный diff
структурный) и обновить статус пунктов выше прямо в этом файле. (sub-pixel / структурный) и обновить статус пунктов выше прямо в этом файле.
## Этап 2 — plan-redesign.md (C17C21), ОТЛОЖЕНО ## Этап 2 — plan-redesign.md (C17C21), ОТЛОЖЕНО
+65 -34
View File
@@ -4,65 +4,96 @@
## 1. Принципы отбора ## 1. Принципы отбора
- Максимум переиспользования: готовые контракты записей (`run`, `preview`, формат вывода, info-режим) и существующие операции ядра. - Максимум переиспользования: готовые контракты записей (`run`, `preview`,
- Новые механизмы ядра — минимальные, чистые, покрытые тестами до подключения инструментов. формат вывода, info-режим) и существующие операции ядра.
- Canvas/DOM-зависимые утилиты (SVG-вход, надписи и водяные знаки, эффекты через фильтры контекста) — вне этого пакета, отдельная волна. - Новые механизмы ядра — минимальные, чистые, покрытые тестами до подключения
- Целевой объём волны: около 37 новых инструментов, каталог вырастает с 11 до ~48. инструментов.
- Canvas/DOM-зависимые утилиты (SVG-вход, надписи и водяные знаки, эффекты через
фильтры контекста) — вне этого пакета, отдельная волна.
- Целевой объём волны: около 37 новых инструментов, каталог вырастает с 11 до
~48.
## 2. Новые механизмы ## 2. Новые механизмы
- **Генераторы без входного файла**: у записи появляется признак «источник не нужен»; страница такого инструмента не показывает левую панель и зону перетаскивания — только параметры и результат. - **Генераторы без входного файла**: у записи появляется признак «источник не
- **Текстовый ввод и вывод**: часть утилит принимает вставленный текст вместо файла и выдаёт текстовый результат (панель с полем только для чтения, кнопки «копировать» и «скачать как текст»). Распространяется на base64, data-uri, hex. нужен»; страница такого инструмента не показывает левую панель и зону
- **BMP-энкодер в ядре**: 24-битный, чистая функция из пикселей в байты, с тестами на маленьких матрицах (заголовок, порядок каналов BGR, паддинг строки). перетаскивания — только параметры и результат.
- **Текстовый ввод и вывод**: часть утилит принимает вставленный текст вместо
файла и выдаёт текстовый результат (панель с полем только для чтения, кнопки
«копировать» и «скачать как текст»). Распространяется на base64, data-uri,
hex.
- **BMP-энкодер в ядре**: 24-битный, чистая функция из пикселей в байты, с
тестами на маленьких матрицах (заголовок, порядок каналов BGR, паддинг
строки).
## 3. Состав пакета ## 3. Состав пакета
**Конвертация — 6** **Конвертация — 6** `jpg-to-png`, `webp-to-png`, `gif-to-png` (первый кадр),
`jpg-to-png`, `webp-to-png`, `gif-to-png` (первый кадр), `bmp-to-png`, `ico-to-png` — всё через уже умеющий декодировать ввод, сохранение в PNG; плюс `png-to-bmp` на новом энкодере. `bmp-to-png`, `ico-to-png` — всё через уже умеющий декодировать ввод, сохранение
в PNG; плюс `png-to-bmp` на новом энкодере.
**Текстовые представления — 6** **Текстовые представления — 6** `png-to-base64`, `base64-to-png`,
`png-to-base64`, `base64-to-png`, `png-to-data-uri`, `data-uri-to-png`, `png-to-hex`, `hex-to-png`. `png-to-data-uri`, `data-uri-to-png`, `png-to-hex`, `hex-to-png`.
**Цвет — 9** **Цвет — 9** `change-opacity` (множитель прозрачности), `sepia`, `change-hue`
`change-opacity` (множитель прозрачности), `sepia`, `change-hue` (сдвиг тона), `extract-channel` (выбор R/G/B в оттенках серого), `swap-channels` (перестановка парой), `threshold-black-white` (порог яркости), `posterize` (число уровней), `two-colors` (два цвета + порог), `invert-alpha`. (сдвиг тона), `extract-channel` (выбор R/G/B в оттенках серого), `swap-channels`
(перестановка парой), `threshold-black-white` (порог яркости), `posterize`
(число уровней), `two-colors` (два цвета + порог), `invert-alpha`.
**Прозрачность и холст — 7** **Прозрачность и холст — 7** `remove-alpha-channel` (подложка белым),
`remove-alpha-channel` (подложка белым), `set-alpha-channel` (константная прозрачность), `extract-alpha-mask` (альфа в чёрно-белую маску), `add-padding` (отступ + цвет/прозрачность), `add-border` (толщина + цвет), `round-corners` (радиус), `fit-on-background` (вписать на полотно цвета с полями). `set-alpha-channel` (константная прозрачность), `extract-alpha-mask` (альфа в
чёрно-белую маску), `add-padding` (отступ + цвет/прозрачность), `add-border`
(толщина + цвет), `round-corners` (радиус), `fit-on-background` (вписать на
полотно цвета с полями).
**Композиция — 2** **Композиция — 2** `tile` (повтор по сетке), `center-by-alpha` (центрирование по
`tile` (повтор по сетке), `center-by-alpha` (центрирование по непрозрачному содержимому). непрозрачному содержимому).
**Генераторы — 4** **Генераторы — 4** `create-empty` (размер + цвет или прозрачность),
`create-empty` (размер + цвет или прозрачность), `single-color`, `random-noise` (с фиксированным зерном для воспроизводимости), `linear-gradient` (два цвета, направление). `single-color`, `random-noise` (с фиксированным зерном для воспроизводимости),
`linear-gradient` (два цвета, направление).
**Анализ — 3** **Анализ — 3** `is-grayscale`, `is-transparent`, `orientation`
`is-grayscale`, `is-transparent`, `orientation` (портрет/ландшафт/квадрат) — текстовые сводки в info-режиме. (портрет/ландшафт/квадрат) — текстовые сводки в info-режиме.
## 4. Новые операции ядра (все — чистые функции с тестами) ## 4. Новые операции ядра (все — чистые функции с тестами)
- Холст: расширение полотна (общая база для padding/border/fit), тайлинг, центр по bounding-box альфы, скругление углов маской радиуса. - Холст: расширение полотна (общая база для padding/border/fit), тайлинг, центр
- Цвет: сепия, сдвиг тона через HSL, множитель альфы, извлечение/перестановка каналов, пороги (яркость и двухцветный), постеризация уровней, инверсия альфы. по bounding-box альфы, скругление углов маской радиуса.
- Цвет: сепия, сдвиг тона через HSL, множитель альфы, извлечение/перестановка
каналов, пороги (яркость и двухцветный), постеризация уровней, инверсия альфы.
- Генерация: заливка цветом, шум с зерном, линейный градиент. - Генерация: заливка цветом, шум с зерном, линейный градиент.
- Форматы: BMP-энкодер; кодеки текстовых представлений (base64/data-uri/hex в обе стороны). - Форматы: BMP-энкодер; кодеки текстовых представлений (base64/data-uri/hex в
обе стороны).
- Анализ: проверка монохромности, полной непрозрачности, ориентации. - Анализ: проверка монохромности, полной непрозрачности, ориентации.
## 5. Этапы и проверки ## 5. Этапы и проверки
- **A. Механизмы** — генераторный режим страницы, текстовый ввод/вывод, BMP-энкодер. Проверка: `test/check/build`. - **A. Механизмы** — генераторный режим страницы, текстовый ввод/вывод,
- **B. Конвертация и текстовые представления** — 12 инструментов. Проверка: ручной прогон каждой конвертации туда-обратно. BMP-энкодер. Проверка: `test/check/build`.
- **B. Конвертация и текстовые представления** — 12 инструментов. Проверка:
ручной прогон каждой конвертации туда-обратно.
- **C. Цвет** — 9 инструментов. Проверка: юнит-тесты операций + визуально. - **C. Цвет** — 9 инструментов. Проверка: юнит-тесты операций + визуально.
- **D. Прозрачность, холст, композиция** — 9 инструментов. Проверка: юнит-тесты + визуально. - **D. Прозрачность, холст, композиция** — 9 инструментов. Проверка:
- **E. Генераторы и анализ** — 7 инструментов. Проверка: детерминизм шума (одно зерно — одно изображение), своды анализа. юнит-тесты + визуально.
- **F. Итог** — полный прогон, смоук всех новых инструментов «настроить → скачать», регресс старых одиннадцати. - **E. Генераторы и анализ** — 7 инструментов. Проверка: детерминизм шума (одно
зерно — одно изображение), своды анализа.
- **F. Итог** — полный прогон, смоук всех новых инструментов «настроить →
скачать», регресс старых одиннадцати.
## 6. Критерии готовности ## 6. Критерии готовности
- Каталог содержит ~48 рабочих инструментов, каждый доступен страницей с формы из реестра. - Каталог содержит ~48 рабочих инструментов, каждый доступен страницей с формы
- Все новые операции ядра покрыты тестами; обратимые конвертации проверены круговым прогоном (например PNG → base64 → PNG даёт те же пиксели). из реестра.
- Генераторы работают без загрузки файла; текстовые представления копируются и скачиваются. - Все новые операции ядра покрыты тестами; обратимые конвертации проверены
круговым прогоном (например PNG → base64 → PNG даёт те же пиксели).
- Генераторы работают без загрузки файла; текстовые представления копируются и
скачиваются.
- `test/check/build` зелёные. - `test/check/build` зелёные.
## 7. Что сознательно не делаем ## 7. Что сознательно не делаем
- SVG-вход, рисование текста и водяных знаков, эффекты через фильтры контекста, GIF/APNG-энкодинг, пакетная обработка — последующие волны. - SVG-вход, рисование текста и водяных знаков, эффекты через фильтры контекста,
- Семейства logo/icon/signature/stamp как отдельные страницы — они будут конфигурациями тех же операций после этой волны. GIF/APNG-энкодинг, пакетная обработка — последующие волны.
- Семейства logo/icon/signature/stamp как отдельные страницы — они будут
конфигурациями тех же операций после этой волны.
+47 -17
View File
@@ -4,41 +4,69 @@
## 1. Что получается ## 1. Что получается
Одиннадцать новых инструментов: свободные трансформации формы (наклон, поворот на произвольный угол, зум, сдвиг), светокоррекция (гамма, автоконтраст, температура, тонирование), виньетка, эффект «артефактов сжатия» и вход SVG. Каталог вырастает с 58 до ~69. Одиннадцать новых инструментов: свободные трансформации формы (наклон, поворот
на произвольный угол, зум, сдвиг), светокоррекция (гамма, автоконтраст,
температура, тонирование), виньетка, эффект «артефактов сжатия» и вход SVG.
Каталог вырастает с 58 до ~69.
Текстовые инструменты и водяные знаки в волну не входят — им нужен рендер текста на канвасе, это отдельная четвёртая волна. Текстовые инструменты и водяные знаки в волну не входят — им нужен рендер текста
на канвасе, это отдельная четвёртая волна.
## 2. Новое ядро ## 2. Новое ядро
- **Аффинные трансформации** (`core/affine.ts`): единая функция обратного отображения — для каждого пикселя результата вычисляется исходная координата матрицей и семплируется билинейно (общий семплер, вынесенный из `resize`). Поверх неё: наклон по осям, поворот на произвольный угол (с расширением холста под новые габариты), зум к центру, сдвиг. - **Аффинные трансформации** (`core/affine.ts`): единая функция обратного
- **Свет и цвет** (`core/color.ts` дополнения): гамма-коррекция через LUT, автоконтраст (растяжка по гистограмме яркости с отсечкой хвостов), температура (множители тёплых/холодных каналов), тонирование (умножение RGB на цвет с сохранением яркостного баланса). отображения — для каждого пикселя результата вычисляется исходная координата
- **Виньетка** (`core/effects.ts`): радиальное затемнение от центра, сила слайдером; чистая математика без canvas. матрицей и семплируется билинейно (общий семплер, вынесенный из `resize`).
- **IO-расширения** (`core/io.ts`): декодирование SVG из текста/файла через blob-URL и `Image`; раундтрип «закодировать JPEG с качеством Q → декодировать обратно» для эффекта артефактов сжатия. Поверх неё: наклон по осям, поворот на произвольный угол (с расширением холста
под новые габариты), зум к центру, сдвиг.
- **Свет и цвет** (`core/color.ts` дополнения): гамма-коррекция через LUT,
автоконтраст (растяжка по гистограмме яркости с отсечкой хвостов), температура
(множители тёплых/холодных каналов), тонирование (умножение RGB на цвет с
сохранением яркостного баланса).
- **Виньетка** (`core/effects.ts`): радиальное затемнение от центра, сила
слайдером; чистая математика без canvas.
- **IO-расширения** (`core/io.ts`): декодирование SVG из текста/файла через
blob-URL и `Image`; раундтрип «закодировать JPEG с качеством Q → декодировать
обратно» для эффекта артефактов сжатия.
## 3. Инструменты — 11 ## 3. Инструменты — 11
- **Геометрия:** `skew-png` (углы X/Y), `rotate-free-png` (любой угол, холст расширяется), `zoom-png` (масштаб ≥100 % к центру), `shift-png` (сдвиг со смещением содержимого). - **Геометрия:** `skew-png` (углы X/Y), `rotate-free-png` (любой угол, холст
- **Фильтры:** `vignette-png` (сила затемнения), `jpeg-artifacts-png` (имитация пережатия). расширяется), `zoom-png` (масштаб ≥100 % к центру), `shift-png` (сдвиг со
- **Цвет:** `gamma-png` (0.13.0), `auto-contrast-png` (без параметров), `temperature-png` (−100 тёплый…+100 холодный или наоборот — подпись честная), `tint-png` (цвет + сила). смещением содержимого).
- **Фильтры:** `vignette-png` (сила затемнения), `jpeg-artifacts-png` (имитация
пережатия).
- **Цвет:** `gamma-png` (0.13.0), `auto-contrast-png` (без параметров),
`temperature-png` (−100 тёплый…+100 холодный или наоборот — подпись честная),
`tint-png` (цвет + сила).
- **Конвертация:** `svg-to-png` (текст SVG слева, как base64-режим). - **Конвертация:** `svg-to-png` (текст SVG слева, как base64-режим).
## 4. Этапы и проверки ## 4. Этапы и проверки
- **A. Ядро** — семплер, аффинный модуль, свет/цвет, эффекты + точные тесты: поворот квадрата на 90° совпадает с существующим `rotate90`, зум ×2 удваивает размеры и центр остаётся центром, гамма 1 — идентичность, автоконтраст растягивает известный диапазон, виньетка симметрична и не трогает центр при нулевой силе. Проверка: `pnpm test/check`. - **A. Ядро** — семплер, аффинный модуль, свет/цвет, эффекты + точные тесты:
- **B. IO** — SVG-декод и JPEG-раундтрип. Проверка: сборка; живой прогон на этапе D. поворот квадрата на 90° совпадает с существующим `rotate90`, зум ×2 удваивает
- **C. Реестр** — одиннадцать записей, иконки, тесты целостности. Проверка: `test/build`, страницы в экспорте. размеры и центр остаётся центром, гамма 1 — идентичность, автоконтраст
растягивает известный диапазон, виньетка симметрична и не трогает центр при
нулевой силе. Проверка: `pnpm test/check`.
- **B. IO** — SVG-декод и JPEG-раундтрип. Проверка: сборка; живой прогон на
этапе D.
- **C. Реестр** — одиннадцать записей, иконки, тесты целостности. Проверка:
`test/build`, страницы в экспорте.
- **D. Итог** — полный прогон, смоук §7. - **D. Итог** — полный прогон, смоук §7.
## 5. Смоук-чеклист ## 5. Смоук-чеклист
1. Наклон X/Y по отдельности и вместе — форма искажается предсказуемо, без чёрных полос при малых углах. 1. Наклон X/Y по отдельности и вместе — форма искажается предсказуемо, без
чёрных полос при малых углах.
2. Поворот на 30° — холст вырос, углы прозрачны, содержимое повернуто. 2. Поворот на 30° — холст вырос, углы прозрачны, содержимое повернуто.
3. Зум 200 % — содержимое увеличено к центру, холст прежнего размера, края обрезаны. 3. Зум 200 % — содержимое увеличено к центру, холст прежнего размера, края
обрезаны.
4. Сдвиг уводит содержимое за край, противоположный край заполняется фоном. 4. Сдвиг уводит содержимое за край, противоположный край заполняется фоном.
5. Гамма 0.5 темнее, 2.0 светлее, 1.0 — без изменений. 5. Гамма 0.5 темнее, 2.0 светлее, 1.0 — без изменений.
6. Автоконтраст на тусклом фото вытягивает диапазон. 6. Автоконтраст на тусклом фото вытягивает диапазон.
7. Температура ±50 заметно теплеет/холодеет. 7. Температура ±50 заметно теплеет/холодеет.
8. Тонирование красным даёт равномерный оттенок, белые области окрашиваются сильнее всего. 8. Тонирование красным даёт равномерный оттенок, белые области окрашиваются
сильнее всего.
9. Виньетка затемняет только края, центр не трогает. 9. Виньетка затемняет только края, центр не трогает.
10. Артефакты: качество 10 даёт видимые квадраты JPEG. 10. Артефакты: качество 10 даёт видимые квадраты JPEG.
11. SVG вставленный слева декодируется в результат справа; битый SVG — баннер. 11. SVG вставленный слева декодируется в результат справа; битый SVG — баннер.
@@ -46,8 +74,10 @@
## 6. Критерии готовности ## 6. Критерии готовности
- Одиннадцать инструментов работают и покрыты юнит-тестами ядра (кроме IO-дом-части). - Одиннадцать инструментов работают и покрыты юнит-тестами ядра (кроме
- Аффинные операции опираются на общий билинейный семплер; дублирования сэмплинга нет. IO-дом-части).
- Аффинные операции опираются на общий билинейный семплер; дублирования
сэмплинга нет.
- Каталог ~69 инструментов; `test/check/build` зелёные, смоук §5 пройден. - Каталог ~69 инструментов; `test/check/build` зелёные, смоук §5 пройден.
## 7. Что сознательно не делаем ## 7. Что сознательно не делаем
+48 -27
View File
@@ -1,35 +1,42 @@
# План: закрытие EASY/MEDIUM-пробелов из сравнения с onlinepngtools # План: закрытие EASY/MEDIUM-пробелов из сравнения с onlinepngtools
> **СТАТУС: ВЫПОЛНЕН (2026-09-07) — устарел.** Все волны W1–W9 закрыты. > **СТАТУС: ВЫПОЛНЕН (2026-09-07) — устарел.** Все волны W1–W9 закрыты. Идеи из
> Идеи из раздела «Вне очереди» (region-инструменты, мультифайловый вывод, > раздела «Вне очереди» (region-инструменты, мультифайловый вывод, анимационные,
> анимационные, HARD-серия) перенесены в `backlog.md`. > HARD-серия) перенесены в `backlog.md`.
> Статус: черновик на ревью > Статус: черновик на ревью
## Принципы ## Принципы
- Волны собираются вокруг общего математического ядра: одна волна = один движок + N инструментов на нём. - Волны собираются вокруг общего математического ядра: одна волна = один
- Каждая волна оформляется отдельным планом при старте; здесь — очередь и состав. движок + N инструментов на нём.
- Порядок: сначала кластеры, дающие много инструментов малой кровью, затем одиночные эффекты, MEDIUM-пакеты в конце. - Каждая волна оформляется отдельным планом при старте; здесь — очередь и
- Полный список параметров каждого инструмента живёт в карте инструментов (tools-map.md); здесь только состав и ядро. состав.
- Порядок: сначала кластеры, дающие много инструментов малой кровью, затем
одиночные эффекты, MEDIUM-пакеты в конце.
- Полный список параметров каждого инструмента живёт в карте инструментов
(tools-map.md); здесь только состав и ядро.
## Очередь волн ## Очередь волн
### W1. Палитры — ВЫПОЛНЕНА (11 инструментов) ### W1. Палитры — ВЫПОЛНЕНА (11 инструментов)
Ядро: RGB↔HSL + гармонии цветового круга. Вывод: свотч-полотно (генератор). Ядро: RGB↔HSL + гармонии цветового круга. Вывод: свотч-полотно (генератор).
Состав: color-wheel, complementary, monochromatic, analogous, triadic, tetradic, similar-shades, sort-colors. Состав: color-wheel, complementary, monochromatic, analogous, triadic, tetradic,
Плюс утилиты смешения тем же ядром: mix-colors, average-color, blend-two, step-between (+4, итого 12). similar-shades, sort-colors. Плюс утилиты смешения тем же ядром: mix-colors,
average-color, blend-two, step-between (+4, итого 12).
### W2. Каналы и пространства — ВЫПОЛНЕНА (6 инструментов) ### W2. Каналы и пространства — ВЫПОЛНЕНА (6 инструментов)
Ядро: матрицы преобразования RGB→(HSL/HSV/HSI/CMYK/YCbCr/LAB) + визуализация выбранного компонента серым или окрашенно. Ядро: матрицы преобразования RGB→(HSL/HSV/HSI/CMYK/YCbCr/LAB) + визуализация
Общий select «компонент» + select «режим отображения». выбранного компонента серым или окрашенно. Общий select «компонент» + select
«режим отображения».
### W3. Маски по свойствам пикселей — ВЫПОЛНЕНА (7 инструментов) ### W3. Маски по свойствам пикселей — ВЫПОЛНЕНА (7 инструментов)
Ядро: предикат над пикселем → бинарная маска (с инверсией и подсветкой цветом). Ядро: предикат над пикселем → бинарная маска (с инверсией и подсветкой цветом).
Состав: show-transparent, show-grayscale, show-color, light-mask, dark-mask, unique-color-mask, extract-by-color. Состав: show-transparent, show-grayscale, show-color, light-mask, dark-mask,
unique-color-mask, extract-by-color.
### W4. Фигурные маски — ВЫПОЛНЕНА (4 инструмента) ### W4. Фигурные маски — ВЫПОЛНЕНА (4 инструмента)
@@ -39,38 +46,52 @@
### W5. Геометрия-добивки — ВЫПОЛНЕНА (5 инструментов) ### W5. Геометрия-добивки — ВЫПОЛНЕНА (5 инструментов)
Ядро: bbox по альфе (trim) переиспользуется тремя инструментами источника. Ядро: bbox по альфе (trim) переиспользуется тремя инструментами источника.
Состав: trim-empty-space (закрывает remove border/space), change-canvas-size, change-aspect-ratio, landscape↔portrait, symmetric-copy. Состав: trim-empty-space (закрывает remove border/space), change-canvas-size,
change-aspect-ratio, landscape↔portrait, symmetric-copy.
### W6. Эффекты лёгкие — ВЫПОЛНЕНА (6 инструментов; color-blocks покрыт pixelate) ### W6. Эффекты лёгкие — ВЫПОЛНЕНА (6 инструментов; color-blocks покрыт pixelate)
Состав: pixelate, color-blocks, randomize-pixels (seed), add-noise, feather-edges, clean-edges, silhouette. Состав: pixelate, color-blocks, randomize-pixels (seed), add-noise,
Shadow/glow — сюда же, если потянет этап: оба = размытая альфа + смещение + цвет (ядро blur уже есть). feather-edges, clean-edges, silhouette. Shadow/glow — сюда же, если потянет
этап: оба = размытая альфа + смещение + цвет (ядро blur уже есть).
### W7. Конвертеры и генераторы — ВЫПОЛНЕНА (11 инструментов) ### W7. Конвертеры и генераторы — ВЫПОЛНЕНА (11 инструментов)
PNG↔bytes, PNG↔rgb-values (текстовые результаты), verify-is-png (анализ сигнатуры), PNG↔bytes, PNG↔rgb-values (текстовые результаты), verify-is-png (анализ
text-to-png (domText без входной картинки), emoji-to-png, placeholder-png, сигнатуры), text-to-png (domText без входной картинки), emoji-to-png,
color-spectrum, colorful-random (seed), draw-grid. placeholder-png, color-spectrum, colorful-random (seed), draw-grid.
multi-color-gradient — если успеем новый тип параметра «список цветов», иначе перенос. multi-color-gradient — если успеем новый тип параметра «список цветов», иначе
перенос.
### W8. Цветовые MEDIUM — ВЫПОЛНЕНА (4 инструмента; median-cut + FloydSteinberg/Bayer, custom-palette через text-параметр) ### W8. Цветовые MEDIUM — ВЫПОЛНЕНА (4 инструмента; median-cut + FloydSteinberg/Bayer, custom-palette через text-параметр)
Ядро: квантование (median-cut или k-means). Ядро: квантование (median-cut или k-means). Состав: quantize (k),
Состав: quantize (k), decrease-color-count (=quantize с пресетами), custom-palette (маппинг на список цветов — нужен тип параметра «список»), dithering (FloydSteinberg/Bayer поверх квантования). decrease-color-count (=quantize с пресетами), custom-palette (маппинг на список
цветов — нужен тип параметра «список»), dithering (FloydSteinberg/Bayer поверх
квантования).
### W9. Сжатие — ВЫПОЛНЕНА (3 инструмента; optimize/change-quality осознанно не выделены — покрыты пресетами compress и W8) ### W9. Сжатие — ВЫПОЛНЕНА (3 инструмента; optimize/change-quality осознанно не выделены — покрыты пресетами compress и W8)
Без wasm честное управление размером PNG ограничено: реальный рычаг — квантование (W8) + обрезка метаданных + итеративный подбор под целевой KB. Без wasm честное управление размером PNG ограничено: реальный рычаг —
Состав: compress (пресеты усилий), reduce-to-size (целевой KB, бинарный поиск по k), optimize (re-encode), change-quality/low-quality — переосмыслить как пресеты W8; jpeg-artifacts уже покрывает «испортить». квантование (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 выделения области на превью; отдельное решение. - Region-инструменты
- Мультифайловый вывод (split-parts, gif-frames, separate-colors) — ждут механизм «результат = набор файлов». (censor/erase/pixelate-area/blur-area/sharpen-area/reverse-area) — ждут UI
- Анимационные (slow-reveal/fade/scrolling) — выход не PNG; отдельное решение о формате. выделения области на превью; отдельное решение.
- Мультифайловый вывод (split-parts, gif-frames, separate-colors) — ждут
механизм «результат = набор файлов».
- Анимационные (slow-reveal/fade/scrolling) — выход не PNG; отдельное решение о
формате.
- HARD: glitch, barcode, signature-extract, handwritten-digital. - HARD: glitch, barcode, signature-extract, handwritten-digital.
- Нишевые серии logo/icon/stamp/signature — не копируем. - Нишевые серии logo/icon/stamp/signature — не копируем.
## Оценка масштаба ## Оценка масштаба
W1W7 дают **~47 новых инструментов**, почти все EASY. W8 добавляет ядро квантования (открывает custom-palette/dithering и улучшает W9). Суммарно каталог достигает ~120+ при текущих 71. W1W7 дают **~47 новых инструментов**, почти все EASY. W8 добавляет ядро
квантования (открывает custom-palette/dithering и улучшает W9). Суммарно каталог
достигает ~120+ при текущих 71.
+37 -14
View File
@@ -4,43 +4,65 @@
## 1. Что получается ## 1. Что получается
Выдача поиска на главной превращается из списка строк в сетку карточек: слева иконка инструмента, справа название и описание. Инструменты получают два новых поля реестра — `popularity` (приоритет в выдаче) и `icon` (идентификатор иконки). Иконки переиспользуются в каталоге `/list-tools` и в легендах шагов цепочки. Поиск работает по-прежнему: нечёткий скоринг, клавиатура ↑↓ Enter Esc. Выдача поиска на главной превращается из списка строк в сетку карточек: слева
иконка инструмента, справа название и описание. Инструменты получают два новых
поля реестра — `popularity` (приоритет в выдаче) и `icon` (идентификатор
иконки). Иконки переиспользуются в каталоге `/list-tools` и в легендах шагов
цепочки. Поиск работает по-прежнему: нечёткий скоринг, клавиатура ↑↓ Enter Esc.
## 2. Данные реестра ## 2. Данные реестра
- **`popularity?: number`** (0–100, по умолчанию 50) — приоритет внутри равного скоринга запроса. Значения проставляются вручную осознанно: базовые операции (обрезка, размер, удаление фона, конвертации JPG/PNG) — 80–95; цветовые и геометрия — 40–70; узкие и генераторы — 10–30. - **`popularity?: number`** (0–100, по умолчанию 50) — приоритет внутри равного
- **`icon?: string`** — имя иконки из набора Lucide. Явный словарь-маппинг для всех инструментов в одном месте; отсутствие записи даёт нейтральную заглушку (Wrench). скоринга запроса. Значения проставляются вручную осознанно: базовые операции
- Тест целостности расширяется: популярность в диапазоне, иконка — непустая строка из разрешённого набора. (обрезка, размер, удаление фона, конвертации JPG/PNG) — 80–95; цветовые и
геометрия — 40–70; узкие и генераторы — 10–30.
- **`icon?: string`** — имя иконки из набора Lucide. Явный словарь-маппинг для
всех инструментов в одном месте; отсутствие записи даёт нейтральную заглушку
(Wrench).
- Тест целостности расширяется: популярность в диапазоне, иконка — непустая
строка из разрешённого набора.
## 3. Иконки ## 3. Иконки
Зависимость `lucide-svelte` (MIT) — один пакет, иконки рендерятся компонентами, дерево не раздувается за счёт выборочного импорта. Зависимость `lucide-svelte` (MIT) — один пакет, иконки рендерятся компонентами,
дерево не раздувается за счёт выборочного импорта.
Маппинг ведётся одним объектом `tool-icons.ts` рядом с реестром: 51 запись вида `'crop-png': Crop`. Подбор по смыслу: обрезка — Crop, размер — Scaling, поворот — RotateCw, отражение — FlipHorizontal2, оттенок — Palette, размытие — Droplets, резкость — Focus, удаление фона — Scissors, анализ — Info и так далее; для семейств конвертаций — FileInput/FileOutput варианты. Маппинг ведётся одним объектом `tool-icons.ts` рядом с реестром: 51 запись вида
`'crop-png': Crop`. Подбор по смыслу: обрезка — Crop, размер — Scaling, поворот
— RotateCw, отражение — FlipHorizontal2, оттенок — Palette, размытие — Droplets,
резкость — Focus, удаление фона — Scissors, анализ — Info и так далее; для
семейств конвертаций — FileInput/FileOutput варианты.
Иконка появляется в трёх местах: карточка выдачи, карточка каталога `/list-tools`, легенда шага цепочки («Шаг N: [icon] Название»). Иконка появляется в трёх местах: карточка выдачи, карточка каталога
`/list-tools`, легенда шага цепочки («Шаг N: [icon] Название»).
## 4. Карточки выдачи ## 4. Карточки выдачи
- Сетка: одна колонка на узком экране, две — начиная со среднего героя. - Сетка: одна колонка на узком экране, две — начиная со среднего героя.
- Композиция карточки: квадрат иконки слева, справа название (полужирное) и однострочное описание с обрезкой. - Композиция карточки: квадрат иконки слева, справа название (полужирное) и
однострочное описание с обрезкой.
- Состояния hover и активной с клавиатуры — тот же акцент подсветки, что сейчас. - Состояния hover и активной с клавиатуры — тот же акцент подсветки, что сейчас.
- Сортировка выдачи: скоринг запроса ↓, затем популярность ↓, затем название. - Сортировка выдачи: скоринг запроса ↓, затем популярность ↓, затем название.
## 5. Этапы и проверки ## 5. Этапы и проверки
- **A. Реестр** — поля `popularity` и `icon`, словарь иконок на все 51, расширение тестов целостности. Проверка: `pnpm test/check`. - **A. Реестр** — поля `popularity` и `icon`, словарь иконок на все 51,
- **B. Карточки поиска** — сетка выдачи с иконками и учётом популярности, клавиатура без изменений. Проверка: визуально, `check/build`. расширение тестов целостности. Проверка: `pnpm test/check`.
- **C. Распространение** — иконки в каталоге `/list-tools` и легендах звеньев. Проверка: визуально, `build`. - **B. Карточки поиска** — сетка выдачи с иконками и учётом популярности,
клавиатура без изменений. Проверка: визуально, `check/build`.
- **C. Распространение** — иконки в каталоге `/list-tools` и легендах звеньев.
Проверка: визуально, `build`.
- **D. Итог** — полный прогон и смоук §8. - **D. Итог** — полный прогон и смоук §8.
## 6. Смоук-чеклист ## 6. Смоук-чеклист
1. Пустой запрос на герое показывает популярные первыми после первого символа. 1. Пустой запрос на герое показывает популярные первыми после первого символа.
2. Запрос «обрез» поднимает карточку обрезки выше прочих совпадений. 2. Запрос «обрез» поднимает карточку обрезки выше прочих совпадений.
3. У каждой карточки есть иконка; у большинства — разные, соответствующие смыслу. 3. У каждой карточки есть иконка; у большинства — разные, соответствующие
смыслу.
4. Наведение и стрелки подсвечивают карточку; Enter открывает инструмент. 4. Наведение и стрелки подсвечивают карточку; Enter открывает инструмент.
5. Открытый инструмент работает как раньше: параметры в колонках, результат, скачивание. 5. Открытый инструмент работает как раньше: параметры в колонках, результат,
скачивание.
6. Цепочка: легенда каждого шага показывает иконку выбранного инструмента. 6. Цепочка: легенда каждого шага показывает иконку выбранного инструмента.
7. `/list-tools`: карточки категорий с иконками, ссылки работают. 7. `/list-tools`: карточки категорий с иконками, ссылки работают.
8. Регресс: старые сценарии (загрузка файла, цепочка, скачивание) не изменились. 8. Регресс: старые сценарии (загрузка файла, цепочка, скачивание) не изменились.
@@ -54,7 +76,8 @@
## 8. Что сознательно не делаем ## 8. Что сознательно не делаем
- Автоматический расчёт популярности из статистики использования — значения ручные. - Автоматический расчёт популярности из статистики использования — значения
ручные.
- Собственный набор SVG-иконок — берём готовый открытый. - Собственный набор SVG-иконок — берём готовый открытый.
- Переводы названий и поиск на других языках — отдельная i18n-волна. - Переводы названий и поиск на других языках — отдельная i18n-волна.
- Изображения-превью в карточках поиска — только иконка и текст. - Изображения-превью в карточках поиска — только иконка и текст.
+76 -22
View File
@@ -1,52 +1,106 @@
# План: i18n — русский и английский # План: i18n — русский и английский
> **СТАТУС: ВЫПОЛНЕН 25.08.2026.** Базовая локаль — английский (реестр), русский — перевод в словаре; строки инструментов в реестре переведены на английский в рамках этапа C. > **СТАТУС: ВЫПОЛНЕН 25.08.2026.** Базовая локаль — английский (реестр), русский
> — перевод в словаре; строки инструментов в реестре переведены на английский в
> рамках этапа C.
## 1. Что получается ## 1. Что получается
Сайт работает на двух языках: русский (базовый) и английский. Переключатель в шапке, выбор сохраняется в localStorage, тег `html lang` обновляется на клиенте. Переведено всё: каркас интерфейса, карточки инструментов, цепочки, поиск, тексты ошибок. Поиск находит инструменты по запросу на любом из двух языков независимо от активной локали. Сайт работает на двух языках: русский (базовый) и английский. Переключатель в
шапке, выбор сохраняется в localStorage, тег `html lang` обновляется на клиенте.
Переведено всё: каркас интерфейса, карточки инструментов, цепочки, поиск, тексты
ошибок. Поиск находит инструменты по запросу на любом из двух языков независимо
от активной локали.
Каталог и поведение не меняются: ни новых инструментов, ни изменений логики существующих. Каталог и поведение не меняются: ни новых инструментов, ни изменений логики
существующих.
## 2. Решения ## 2. Решения
- Без сегментов языка в URL. Статика пререндерится на русском; переключение на клиенте, выбор живёт в localStorage. hreflang/canonical и вторая копия страниц для поисковиков — не в этой волне. - Без сегментов языка в URL. Статика пререндерится на русском; переключение на
- Ядро не знает о локалях. Ошибки в core выбрасываются стабильными кодами (например ERR_BAD_HEX); перевод кода в человекочитаемый текст происходит в слое исполнителя/UI. клиенте, выбор живёт в localStorage. hreflang/canonical и вторая копия страниц
- Английский — базовая локаль и фолбэк: строки инструментов в реестре записаны по-английски. Переводы живут в секции tools словарей по ключу id инструмента (title, description, подписи параметров, подписи опций select, тексты-результаты анализаторов); сейчас перевод — русский в ru.ts. Доступ из компонентов только через хелперы toolTitle/toolDescription/paramLabel/optionLabel. Так каждая строка имеет единственный источник: en — реестр, переводы — словари. для поисковиков — не в этой волне.
- Ядро не знает о локалях. Ошибки в core выбрасываются стабильными кодами
(например ERR_BAD_HEX); перевод кода в человекочитаемый текст происходит в
слое исполнителя/UI.
- Английский — базовая локаль и фолбэк: строки инструментов в реестре записаны
по-английски. Переводы живут в секции tools словарей по ключу id инструмента
(title, description, подписи параметров, подписи опций select,
тексты-результаты анализаторов); сейчас перевод — русский в ru.ts. Доступ из
компонентов только через хелперы
toolTitle/toolDescription/paramLabel/optionLabel. Так каждая строка имеет
единственный источник: en — реестр, переводы — словари.
## 3. Структура i18n-модуля ## 3. Структура i18n-модуля
- web/src/lib/i18n/locale.svelte.ts — состояние активной локали (runes), персист в localStorage (ключ locale, guard на SSR), обновление document.documentElement.lang. - web/src/lib/i18n/locale.svelte.ts — состояние активной локали (runes), персист
- web/src/lib/i18n/t.ts — функция перевода с фолбэком на базовый язык и интерполяцией переменных вида {n}. в localStorage (ключ locale, guard на SSR), обновление
- web/src/lib/i18n/ru.ts и en.ts — словари одного типа Dict; тип экспортируется из ru.ts, en.ts обязан ему соответствовать — пропущенные ключи ловятся на компиляции. document.documentElement.lang.
- Секции словаря: header, home, catalog, toolPage, chain, sourceCard, resultCard, paramsCard, textInput, textResult, download, infoPanel, dropZone, search, ui (aria кнопок слайдера и пипетки), categories, errors, tools. - 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. Этапы и проверки ## 4. Этапы и проверки
Каждый этап заканчивается зелёными test/check/build и ревью. Каждый этап заканчивается зелёными test/check/build и ревью.
- A. Инфраструктура — модуль состояния, функция перевода с фолбэком и интерполяцией, словари-скелеты со всеми секциями (заполнены header и categories как пилот), переключатель RU/EN в шапке, реактивный lang. Тесты: фолбэк на ru при отсутствии ключа в en, интерполяция, персист локали. - A. Инфраструктура — модуль состояния, функция перевода с фолбэком и
- B. Каркас интерфейса — перенос всех хардкод-строк компонентов и маршрутов в словари: layout (шапка, футер), главная (герой, «вернуть последний», title), каталог, карточки источника/результата/параметров, текстовый вход и результат, «Показать маску», DropZone/DropOverlay, кнопка скачивания (busyText), InfoPanel плюс форматирование чисел через тег локали вместо жёсткого ru-RU, ToolPage (легенды, «Шаг n», ошибки шага), ChainToolBlock, placeholder и пустой результат поиска, страницы инструмента (title, 404). Обёрточные сообщения исполнителя («Ошибка исполнения в воркере») — тоже здесь. интерполяцией, словари-скелеты со всеми секциями (заполнены header и
- C. Контент реестра — переводы 69 инструментов (title/description/labels/options) в tools-секции словаря en; компоненты получают строки через хелперы toolTitle/toolDescription/paramLabel/optionLabel с фолбэком на реестр. Тест целостности: у каждого инструмента есть непустые перевод title/description, полный набор param-ключей и подписи всех опций select; лишние ключи словарей отсутствуют. categories как пилот), переключатель RU/EN в шапке, реактивный lang. Тесты:
- D. Поиск по любому языку — нормализация запроса и полей: нижний регистр, ё→е, снятие диакритики через NFD; скоринг против активной и базовой локали сразу, лучший балл побеждает; сортировка совпадений через Intl.Collator активной локали. Тесты нормализации: ё/е, диакритика, английский запрос при русской локали и наоборот. фолбэк на ru при отсутствии ключа в en, интерполяция, персист локали.
- E. Ошибки на кодах — около 40 мест throw в core, registry, pipeline заменяются на коды; исполнитель переводит код в текст активной локали, неизвестные сообщения показываются как есть. Пять тестов, матчащих русские подстроки ошибок (executor, pipeline, registry, io, alpha), переходят на коды — это упрощает ассерты. Русские describe/it в тестах не трогаем. - 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, архивация плана. - F. Итог — полный прогон, смоук чеклист §6, архивация плана.
## 5. Смоук-чеклист ## 5. Смоук-чеклист
1. Переключение RU/EN в шапке мгновенно меняет язык без перезагрузки; после перезагрузки выбор сохранён. 1. Переключение RU/EN в шапке мгновенно меняет язык без перезагрузки; после
перезагрузки выбор сохранён.
2. Тег html lang соответствует активной локали. 2. Тег html lang соответствует активной локали.
3. Главная, каталог, страница инструмента, цепочка из трёх звеньев — везде переведено, без смеси языков. 3. Главная, каталог, страница инструмента, цепочка из трёх звеньев — везде
4. Форма параметров: подписи полей и опции select переведены у произвольных пяти инструментов из разных категорий. переведено, без смеси языков.
5. Битый вход (некорректный HEX в тонировании, нулевой размер) показывает ошибку на активном языке. 4. Форма параметров: подписи полей и опции select переведены у произвольных пяти
6. Поиск: «повер» находит повороты при русской локали; «rotate» находит их же; при английской локали наоборот. Диакритика и ё не мешают совпадению. инструментов из разных категорий.
5. Битый вход (некорректный HEX в тонировании, нулевой размер) показывает ошибку
на активном языке.
6. Поиск: «повер» находит повороты при русской локали; «rotate» находит их же;
при английской локали наоборот. Диакритика и ё не мешают совпадению.
7. Числа в панели информации отформатированы по локали. 7. Числа в панели информации отформатированы по локали.
8. Скачивание, пипетка, маски, слайдеры работают одинаково на обеих локалях — функциональность не задета. 8. Скачивание, пипетка, маски, слайдеры работают одинаково на обеих локалях —
функциональность не задета.
## 6. Критерии готовности ## 6. Критерии готовности
- Оба словаря типобезопасны и полны; тест целостности секции tools зелёный. - Оба словаря типобезопасны и полны; тест целостности секции tools зелёный.
- Ни одного пользовательского русского строки вне словарей (кроме тестовых describe/it). - Ни одного пользовательского русского строки вне словарей (кроме тестовых
describe/it).
- test/check/build зелёные, смоук пройден. - test/check/build зелёные, смоук пройден.
## 7. Что сознательно не делаем ## 7. Что сознательно не делаем
+46 -17
View File
@@ -9,14 +9,26 @@
## 1. Что получается ## 1. Что получается
Альтернативная раскладка этапа инструмента: три колонки «Исходник | Параметры | Результат» на широких экранах, вертикальный стек на узких. Цель — отдать вариант дизайнеру как отправную точку. Откат к текущей раскладке — замена одной строки в `ToolPage`. Альтернативная раскладка этапа инструмента: три колонки «Исходник | Параметры |
Результат» на широких экранах, вертикальный стек на узких. Цель — отдать вариант
дизайнеру как отправную точку. Откат к текущей раскладке — замена одной строки в
`ToolPage`.
## 2. Решения ## 2. Решения
- Два взаимозаменяемых компонента этапа с одинаковым набором пропсов; `ToolPage` рендерит один тег. Попробовать новое — поменять компонент в этом теге; вернуть старое — поменять обратно. Никаких флагов и конфигов. - Два взаимозаменяемых компонента этапа с одинаковым набором пропсов; `ToolPage`
- Общая типизация пропсов в одном файле рядом с компонентами; сами карточки (`SourceCard`, `ResultCard`, `TextInputCard`, `ParamsCard`) переиспользуются как есть — вся логика исполнения, маски, пипетки и автозапуска остаётся в `ToolPage` и приходит сверху. рендерит один тег. Попробовать новое — поменять компонент в этом теге; вернуть
- Chain-звенья используют ту же панельную систему «Вход | Параметры | Результат» — общие классы `.pane` / `.pane-legend` / `.pane-params` вынесены в app.css, вертикальные поля в узкой колонке заданы один раз глобально. старое — поменять обратно. Никаких флагов и конфигов.
- Один порог отзывчивости: три колонки от ~75rem, ниже — стек «исходник → параметры → результат». Промежуточные перестроения не придумываем до фидбека дизайнера. - Общая типизация пропсов в одном файле рядом с компонентами; сами карточки
(`SourceCard`, `ResultCard`, `TextInputCard`, `ParamsCard`) переиспользуются
как есть — вся логика исполнения, маски, пипетки и автозапуска остаётся в
`ToolPage` и приходит сверху.
- Chain-звенья используют ту же панельную систему «Вход | Параметры | Результат»
— общие классы `.pane` / `.pane-legend` / `.pane-params` вынесены в app.css,
вертикальные поля в узкой колонке заданы один раз глобально.
- Один порог отзывчивости: три колонки от ~75rem, ниже — стек «исходник →
параметры → результат». Промежуточные перестроения не придумываем до фидбека
дизайнера.
## 3. Компоненты ## 3. Компоненты
@@ -27,28 +39,45 @@ src/lib/components/stage/
ToolStageInline.svelte # новая сетка [source | params | result] ToolStageInline.svelte # новая сетка [source | params | result]
``` ```
- `ToolStageClassic` — вырезка существующего шаблона со стилями; после извлечения страница должна быть пиксель-в-пиксель прежней. - `ToolStageClassic` — вырезка существующего шаблона со стилями; после
- `ToolStageInline` — grid-колонки `minmax(0, 1fr) | фикс-ширина параметров | minmax(0, 1fr)`; легенда «Параметры» над средней колонкой в том же стиле edge-legend. извлечения страница должна быть пиксель-в-пиксель прежней.
- Модификаторы состояний через классы на корне компонента: `no-params` (инструменты без параметров → две колонки), `no-source` (генераторы → параметры и результат), текстовый вход до декодирования занимает левую ячейку как сейчас. - `ToolStageInline` — grid-колонки
`minmax(0, 1fr) | фикс-ширина параметров | minmax(0, 1fr)`; легенда
«Параметры» над средней колонкой в том же стиле edge-legend.
- Модификаторы состояний через классы на корне компонента: `no-params`
(инструменты без параметров → две колонки), `no-source` (генераторы →
параметры и результат), текстовый вход до декодирования занимает левую ячейку
как сейчас.
## 4. Этапы и проверки ## 4. Этапы и проверки
Каждый этап заканчивается зелёными test/check/build и ревью. Каждый этап заканчивается зелёными test/check/build и ревью.
- **A. Извлечение классики** — перенос разметки и стилей этапа из `ToolPage` в `ToolStageClassic` без изменений поведения; пропсы собираются в одном объекте. Критерий: страница неотличима, diff по логике `ToolPage` нулевой. - **A. Извлечение классики** — перенос разметки и стилей этапа из `ToolPage` в
- **B. Инлайн-раскладка**`ToolStageInline`, переключение в `ToolPage` на новый компонент, прогон смоука §5. Ревью в браузере; решение дизайнера — потом, код уже позволяет жить в обоих вариантах. `ToolStageClassic` без изменений поведения; пропсы собираются в одном объекте.
Критерий: страница неотличима, diff по логике `ToolPage` нулевой.
- **B. Инлайн-раскладка**`ToolStageInline`, переключение в `ToolPage` на
новый компонент, прогон смоука §5. Ревью в браузере; решение дизайнера —
потом, код уже позволяет жить в обоих вариантах.
## 5. Смоук ## 5. Смоук
1. Замена компонента этапа — одна строка; обе версии собираются без предупреждений. 1. Замена компонента этапа — одна строка; обе версии собираются без
2. Инструмент без параметров (например Автоконтраст): две колонки, пустой середины нет. предупреждений.
2. Инструмент без параметров (например Автоконтраст): две колонки, пустой
середины нет.
3. Генератор без исходника: параметры и результат в две колонки. 3. Генератор без исходника: параметры и результат в две колонки.
4. Текстовый инструмент до декодирования: вход слева, параметры в центре, пустое состояние результата справа. 4. Текстовый инструмент до декодирования: вход слева, параметры в центре, пустое
5. Маска, пипетка, слайдеры в средней колонке; изменение параметра перезапускает инструмент как раньше. состояние результата справа.
6. Узкий экран: стек без горизонтального скролла, порядок исходник → параметры → результат. 5. Маска, пипетка, слайдеры в средней колонке; изменение параметра перезапускает
инструмент как раньше.
6. Узкий экран: стек без горизонтального скролла, порядок исходник → параметры →
результат.
7. Легенды присутствуют у всех трёх зон в обеих раскладках. 7. Легенды присутствуют у всех трёх зон в обеих раскладках.
## 6. Что сознательно не делаем ## 6. Что сознательно не делаем
- Редизайн визуального языка (цвета, типографика, формы) — материал для дизайнера, не для этого плана. - Редизайн визуального языка (цвета, типографика, формы) — материал для
- Новая логика: исполнение, автосохранение, пресеты — раскладка ничего не знает про исполнение. дизайнера, не для этого плана.
- Новая логика: исполнение, автосохранение, пресеты — раскладка ничего не знает
про исполнение.
+32 -31
View File
@@ -1,9 +1,9 @@
# Plan: `/preview/list-tools` → design2 (catalog) # Plan: `/preview/list-tools` → design2 (catalog)
> **СТАТУС: ВЫПОЛНЕН (2026-09-07) — устарел.** Каталог > **СТАТУС: ВЫПОЛНЕН (2026-09-07) — устарел.** Каталог `/preview/list-tools`
> `/preview/list-tools` переписан на `CatalogHeader` / `CatalogToolbar` / > переписан на `CatalogHeader` / `CatalogToolbar` / `CatalogGroup` / `ToolCard`
> `CatalogGroup` / `ToolCard` (kit), поиск и фильтры функциональны. > (kit), поиск и фильтры функциональны. Доводка верности против рефа остаётся в
> Доводка верности против рефа остаётся в рамках `plan-design-fix.md`. > рамках `plan-design-fix.md`.
Ветка `feat/redesign`. Цель — привести каталог инструментов к рефу Ветка `feat/redesign`. Цель — привести каталог инструментов к рефу
`refs-html/list-tools.html`. Сейчас страница вообще не на design2: по `refs-html/list-tools.html`. Сейчас страница вообще не на design2: по
@@ -16,19 +16,19 @@
`ToolCard` (kit). Нет поиска, фильтров, счётчика, навигация по категориям `ToolCard` (kit). Нет поиска, фильтров, счётчика, навигация по категориям
сделана через `Panel`-блоки, а не через `.catalog-groups`. сделана через `Panel`-блоки, а не через `.catalog-groups`.
- Реф (`list-tools.html`) строит: - Реф (`list-tools.html`) строит:
- `.catalog-page``.catalog-head` (`.eyebrow`, `h1` «Tool catalog», - `.catalog-page``.catalog-head` (`.eyebrow`, `h1` «Tool catalog», `p`
`p` lede, `.catalog-total` «NN TOOLS AVAILABLE» с большим числом), lede, `.catalog-total` «NN TOOLS AVAILABLE» с большим числом),
- `.catalog-toolbar` (`.catalog-search` + `.catalog-filters` с кнопками), - `.catalog-toolbar` (`.catalog-search` + `.catalog-filters` с кнопками),
- `.catalog-groups` (grid `repeat(2,minmax(0,1fr))`, gap `56px 28px`) → - `.catalog-groups` (grid `repeat(2,minmax(0,1fr))`, gap `56px 28px`) →
`.catalog-group` (`.group-title` «CAT · NN TOOLS») → `.tool-cards` `.catalog-group` (`.group-title` «CAT · NN TOOLS») → `.tool-cards`
`.tool-card`. `.tool-card`.
- `.tool-card` (реф) — `display:grid`, колонки `42px minmax(0,1fr) 24px 18px`, - `.tool-card` (реф) — `display:grid`, колонки `42px minmax(0,1fr) 24px 18px`,
`min-height:106px`, `gap:14px`, `padding:16px`: `.tool-icon` (42×42 `min-height:106px`, `gap:14px`, `padding:16px`: `.tool-icon` (42×42 синий
синий размытый квадрат), `.tool-copy` (`strong` 13px mono + `span` 12px mono размытый квадрат), `.tool-copy` (`strong` 13px mono + `span` 12px mono muted),
muted), `.tool-index` (10px mono, сверху), `.tool-arrow` (появляется при hover). `.tool-index` (10px mono, сверху), `.tool-arrow` (появляется при hover).
- Токены `design2.css` уже совпадают с рефом (`--background #eef1f4`, - Токены `design2.css` уже совпадают с рефом (`--background #eef1f4`,
`--panel #f8fafb`, `--foreground #17212b`, `--muted #6d7883`, `--line #cbd3da`, `--panel #f8fafb`, `--foreground #17212b`, `--muted #6d7883`,
`--blue #1769d2`, `--radius 4px`) — править токены не нужно. `--line #cbd3da`, `--blue #1769d2`, `--radius 4px`) — править токены не нужно.
## Шаги ## Шаги
@@ -37,20 +37,21 @@
- grid-колонки `42px minmax(0,1fr) 24px 18px`, `align-items:center`, - grid-колонки `42px minmax(0,1fr) 24px 18px`, `align-items:center`,
`gap:14px`, `min-height:106px`, `padding:16px`, `border:1px solid line`, `gap:14px`, `min-height:106px`, `padding:16px`, `border:1px solid line`,
`background:panel`, `border-radius:radius`. `background:panel`, `border-radius:radius`.
- `.tool-icon`: 42×42 `place-items:center`, `background:color-mix(in srgb, - `.tool-icon`: 42×42 `place-items:center`,
var(--blue) 12%, transparent)`, `color:var(--blue)` (реф использует `background:color-mix(in srgb, var(--blue) 12%, transparent)`,
`--panel-strong`; вместо нового токена берём этот `color-mix`). `color:var(--blue)` (реф использует `--panel-strong`; вместо нового токена
берём этот `color-mix`).
- `.tool-copy strong`: `600 13px var(--font-mono)`; `span`: `12px/1.5 mono` - `.tool-copy strong`: `600 13px var(--font-mono)`; `span`: `12px/1.5 mono`
`muted`. `muted`.
- `.tool-index`: `10px mono muted`, `align-self:start`. - `.tool-index`: `10px mono muted`, `align-self:start`.
- `.tool-arrow`: `color:var(--blue)`, `opacity:0``1` + `translate(2px,-2px)` - `.tool-arrow`: `color:var(--blue)`, `opacity:0``1` +
на `.tool-card:hover`. `translate(2px,-2px)` на `.tool-card:hover`.
- DOM-порядок как в рефе: `icon, copy, index, arrow`. - DOM-порядок как в рефе: `icon, copy, index, arrow`.
- `href` через `resolve` из `$app/paths` (убрать `svelte-ignore`). - `href` через `resolve` из `$app/paths` (убрать `svelte-ignore`).
2. **`kit/CatalogHeader.svelte`**: `.eyebrow` «EASY-PNG-TOOLS / CATALOG», 2. **`kit/CatalogHeader.svelte`**: `.eyebrow` «EASY-PNG-TOOLS / CATALOG», `h1`
`h1` «Tool catalog», lede; `.catalog-total` с динамическим `TOOLS.length` «Tool catalog», lede; `.catalog-total` с динамическим `TOOLS.length` (реф
(реф показывает `32`). показывает `32`).
3. **`kit/CatalogToolbar.svelte`**: `.catalog-search` (иконка + `<input>`) и 3. **`kit/CatalogToolbar.svelte`**: `.catalog-search` (иконка + `<input>`) и
`.catalog-filters` (кнопки по категориям). Состояние `query` / `category` `.catalog-filters` (кнопки по категориям). Состояние `query` / `category`
@@ -68,8 +69,8 @@
- `<CatalogToolbar bind:query bind:category />`; - `<CatalogToolbar bind:query bind:category />`;
- `.catalog-groups` из отфильтрованных групп (по `CATEGORIES`, пересчитав - `.catalog-groups` из отфильтрованных групп (по `CATEGORIES`, пересчитав
`tools` с учётом `query`/`category`); `tools` с учётом `query`/`category`);
- внутри `<CatalogGroup>``<ToolCard>` на каждый tool (`href= - внутри `<CatalogGroup>``<ToolCard>` на каждый tool
"/preview/tools/{id}"`, `icon` из `tool-icons`, `index` по порядку, (`href= "/preview/tools/{id}"`, `icon` из `tool-icons`, `index` по порядку,
`title`/`description` из registry); `title`/`description` из registry);
- убрать `Panel`/`PanelHeading` из этого маршрута. - убрать `Panel`/`PanelHeading` из этого маршрута.
@@ -77,9 +78,9 @@
TRANSPARENCY / COLOR / GEOMETRY / FILTERS / ANALYZE). Наши `CATEGORIES` TRANSPARENCY / COLOR / GEOMETRY / FILTERS / ANALYZE). Наши `CATEGORIES`
(`convert/alpha/color/geometry/filters/text/analyze/generate`) — сопоставить (`convert/alpha/color/geometry/filters/text/analyze/generate`) — сопоставить
(`alpha`→TRANSPARENCY и т.п.); точное сопоставление уточнить по рефу и (`alpha`→TRANSPARENCY и т.п.); точное сопоставление уточнить по рефу и
`i18n/ru.ts`. Фильтры = ALL + те же label'ы. Тексты заголовков/lede — по `i18n/ru.ts`. Фильтры = ALL + те же label'ы. Тексты заголовков/lede — по рефу
рефу (англ. «Tool catalog» / «Focused utilities for working with PNG…»), (англ. «Tool catalog» / «Focused utilities for working with PNG…»), либо
либо оставить RU-вариант, если это сознательное отклонение (зафиксировать). оставить RU-вариант, если это сознательное отклонение (зафиксировать).
7. **Адаптив**: перенести правила рефа (`@media max-width:800px`) — 7. **Адаптив**: перенести правила рефа (`@media max-width:800px`) —
`.catalog-groups` → 1fr, `.tool-card``38px minmax(0,1fr) 18px`, скрыть `.catalog-groups` → 1fr, `.tool-card``38px minmax(0,1fr) 18px`, скрыть
@@ -87,16 +88,16 @@
## Верификация ## Верификация
- `pnpm lint`, `pnpm exec svelte-check --tsconfig ./tsconfig.json`, - `pnpm lint`, `pnpm exec svelte-check --tsconfig ./tsconfig.json`, `pnpm build`
`pnpm build` — без ошибок. — без ошибок.
- `pnpm refs-audit` (теперь мульти-маршрутный) → глянуть секцию - `pnpm refs-audit` (теперь мульти-маршрутный) → глянуть секцию
`/preview/list-tools`: `onlyOurs`/`onlyRef` должны упасть с `257/54` `/preview/list-tools`: `onlyOurs`/`onlyRef` должны упасть с `257/54` примерно
примерно до уровня demo (`18/11`). Остаточный diff (если будет) зафиксировать до уровня demo (`18/11`). Остаточный diff (если будет) зафиксировать в этом
в этом файле. файле.
## Примечания ## Примечания
- `ToolCard` правится на месте — влияет на `preview/+page` и `preview/kit` - `ToolCard` правится на месте — влияет на `preview/+page` и `preview/kit` (там
(там тоже карточки каталога, унификация уместна). тоже карточки каталога, унификация уместна).
- Старый дизайн (`routes/(old)/**`, `components/search/**`) не трогать. - Старый дизайн (`routes/(old)/**`, `components/search/**`) не трогать.
- Коммиты делает автор (агент не коммитит). - Коммиты делает автор (агент не коммитит).
+55 -24
View File
@@ -4,15 +4,25 @@
## 1. Зачем ## 1. Зачем
- Первые операции настоящего алгоритмического уровня: свёртки и заливка связной области. - Первые операции настоящего алгоритмического уровня: свёртки и заливка связной
- Витринная фича каталога — удаление однотонного фона без нейросетей: порог по цвету + заливка от краёв изображения + сглаживание границы маски. области.
- Эти операции реально блокируют UI-поток на больших картинках — повод включить давно подготовленный путь: исполнитель `executeStep` переезжает в Web Worker, страницы не меняются вовсе. - Витринная фича каталога — удаление однотонного фона без нейросетей: порог по
цвету + заливка от краёв изображения + сглаживание границы маски.
- Эти операции реально блокируют UI-поток на больших картинках — повод включить
давно подготовленный путь: исполнитель `executeStep` переезжает в Web Worker,
страницы не меняются вовсе.
## 2. Инструменты — 3 ## 2. Инструменты — 3
- **Удалить фон PNG** (`remove-background-png`, категория «Прозрачность») — параметры: цвет фона, допуск похожести %, режим «только внешние области» (заливка от краёв вместо глобального совпадения), сглаживание границы (проходы мажоритарного фильтра маски). Даёт превью маски через существующий контракт `preview`. - **Удалить фон PNG** (`remove-background-png`, категория «Прозрачность») —
- **Размытие PNG** (`blur-png`, новая категория «Фильтры») — гауссово приближение тремя проходами разделяемого бокса; радиус слайдером 1–32 px. параметры: цвет фона, допуск похожести %, режим «только внешние области»
- **Резкость PNG** (`sharpen-png`, «Фильтры») — свёртка ядром резкости с регулируемой силой (смесь исходного и обработанного), сила слайдером 0100 %. (заливка от краёв вместо глобального совпадения), сглаживание границы (проходы
мажоритарного фильтра маски). Даёт превью маски через существующий контракт
`preview`.
- **Размытие PNG** (`blur-png`, новая категория «Фильтры») — гауссово
приближение тремя проходами разделяемого бокса; радиус слайдером 1–32 px.
- **Резкость PNG** (`sharpen-png`, «Фильтры») — свёртка ядром резкости с
регулируемой силой (смесь исходного и обработанного), сила слайдером 0–100 %.
## 3. Новое ядро (чистые функции, детерминированные, покрытые тестами) ## 3. Новое ядро (чистые функции, детерминированные, покрытые тестами)
@@ -21,46 +31,67 @@
- разделяемый бокс-блюр и сборка гауссова приближения из трёх проходов; - разделяемый бокс-блюр и сборка гауссова приближения из трёх проходов;
- ядро резкости и смешивание с оригиналом по силе. - ядро резкости и смешивание с оригиналом по силе.
- `core/background.ts` - `core/background.ts`
- построение маски удаления по цветовому расстоянию (та же метрика, что у удаления цвета); - построение маски удаления по цветовому расстоянию (та же метрика, что у
удаления цвета);
- заливка связной области от всех граничных пикселей (очередь, без рекурсии); - заливка связной области от всех граничных пикселей (очередь, без рекурсии);
- мажоритарное сглаживание маски заданным числом проходов; - мажоритарное сглаживание маски заданным числом проходов;
- сборка результата: альфа 0 у удалённых пикселей; плюс функция маски для превью. - сборка результата: альфа 0 у удалённых пикселей; плюс функция маски для
превью.
## 4. Web Worker ## 4. Web Worker
- Точка исполнения одна — `tools/executor.ts`; меняется только её внутренность. - Точка исполнения одна — `tools/executor.ts`; меняется только её внутренность.
- Воркер-файл импортирует реестр и исполняет операцию по `toolId`; картинка приходит копией (структурный клон), результат возвращается передаваемым буфером. - Воркер-файл импортирует реестр и исполняет операцию по `toolId`; картинка
- Ленивая инициализация воркера при первом вызове; если воркер недоступен (старый браузер/тестовая среда) — автоматический откат на прямое исполнение в главном потоке. Обе ветви обязаны давать побайтово одинаковый результат. приходит копией (структурный клон), результат возвращается передаваемым
- Генераторы и текстовые представления остаются в главном потоке — они мгновенные. буфером.
- Ленивая инициализация воркера при первом вызове; если воркер недоступен
(старый браузер/тестовая среда) — автоматический откат на прямое исполнение в
главном потоке. Обе ветви обязаны давать побайтово одинаковый результат.
- Генераторы и текстовые представления остаются в главном потоке — они
мгновенные.
- Пипетка и лупа работают с уже готовыми картинками и не зависят от переноса. - Пипетка и лупа работают с уже готовыми картинками и не зависят от переноса.
## 5. Этапы и проверки ## 5. Этапы и проверки
- **A. Ядро** — свёртки и фон с тестами на малых матрицах (точные ожидания руками). Проверка: `pnpm test/check`. - **A. Ядро** — свёртки и фон с тестами на малых матрицах (точные ожидания
- **B. Реестр** — категория «Фильтры», три записи, превью маски. Проверка: тесты целостности, визуально. руками). Проверка: `pnpm test/check`.
- **C. Web Worker** — перенос исполнителя, отккат-ветвь. Проверка: результаты до/после переключения идентичны байт-в-байт на фиксированной картинке и параметрах; `test/build` зелёные. - **B. Реестр** — категория «Фильтры», три записи, превью маски. Проверка: тесты
целостности, визуально.
- **C. Web Worker** — перенос исполнителя, отккат-ветвь. Проверка: результаты
до/после переключения идентичны байт-в-байт на фиксированной картинке и
параметрах; `test/build` зелёные.
- **D. Итог** — полный прогон и смоук §8. - **D. Итог** — полный прогон и смоук §8.
## 6. Смоук-чеклист ## 6. Смоук-чеклист
1. Размытие: радиус 1 и 32 — плавно усиливается, прозрачные края не темнеют. 1. Размытие: радиус 1 и 32 — плавно усиливается, прозрачные края не темнеют.
2. Резкость: сила 0 не меняет картинку; сила 100 заметно подчёркивает края без цветового мусора. 2. Резкость: сила 0 не меняет картинку; сила 100 заметно подчёркивает края без
цветового мусора.
3. Удаление фона на логотипе с однотонным фоном: фон исчез, содержимое цело. 3. Удаление фона на логотипе с однотонным фоном: фон исчез, содержимое цело.
4. Режим «только внешние области»: дырка внутри объекта того же цвета остаётся непрозрачной. 4. Режим «только внешние области»: дырка внутри объекта того же цвета остаётся
5. Сглаживание 0/2/4 прохода — граница от жёсткой к мягкой, результат воспроизводим. непрозрачной.
6. Превью маски показывает белым удаляемую область; скачивание всегда отдаёт результат. 5. Сглаживание 0/2/4 прохода — граница от жёсткой к мягкой, результат
7. Во время размытия большой картинки интерфейс остаётся отзывчивым (чип «Пересчёт…», скролл живой). воспроизводим.
8. Цепочка: размытие → резкость → удаление фона работает; каждый блок скачивает свой промежуток. 6. Превью маски показывает белым удаляемую область; скачивание всегда отдаёт
результат.
7. Во время размытия большой картинки интерфейс остаётся отзывчивым (чип
«Пересчёт…», скролл живой).
8. Цепочка: размытие → резкость → удаление фона работает; каждый блок скачивает
свой промежуток.
9. Регресс старых инструментов и главной-поиска без изменений поведения. 9. Регресс старых инструментов и главной-поиска без изменений поведения.
## 7. Критерии готовности ## 7. Критерии готовности
- Три новых инструмента проходят смоук §6; операции ядра покрыты точными юнит-тестами. - Три новых инструмента проходят смоук §6; операции ядра покрыты точными
- Исполнение изображений идёт через воркер там, где он доступен, с гарантированно одинаковым результатом обеих ветвей. юнит-тестами.
- Исполнение изображений идёт через воркер там, где он доступен, с
гарантированно одинаковым результатом обеих ветвей.
- `test/check/build` зелёные. - `test/check/build` зелёные.
## 8. Что сознательно не делаем ## 8. Что сознательно не делаем
- Многопроходное гауссово ядро «по-настоящему» (O(r²)) — трёхпроходного боксового приближения достаточно визуально и оно быстрее. - Многопроходное гауссово ядро «по-настоящему» (O(r²)) — трёхпроходного
боксового приближения достаточно визуально и оно быстрее.
- Хромакей по нескольким цветам, ИИ-сегментация — следующие волны. - Хромакей по нескольким цветам, ИИ-сегментация — следующие волны.
- Отмена выполняющегося прогона воркера — устаревшие результаты отбрасываются токеном, сама работа завершается фоном. - Отмена выполняющегося прогона воркера — устаревшие результаты отбрасываются
токеном, сама работа завершается фоном.
+43 -19
View File
@@ -4,17 +4,26 @@
## 1. Зачем ## 1. Зачем
Одна пара функций — расширение и сужение непрозрачных областей — открывает целое семейство пользовательских операций: обводка логотипов и иконок, контуры, утолщение и утончение подписей, удаление мелкого мусора и закрытие дырок. Всё это частые запросы в наборах PNG-утилит, а ядро получается компактным и легко покрывается тестами. Одна пара функций — расширение и сужение непрозрачных областей — открывает целое
семейство пользовательских операций: обводка логотипов и иконок, контуры,
утолщение и утончение подписей, удаление мелкого мусора и закрытие дырок. Всё
это частые запросы в наборах PNG-утилит, а ядро получается компактным и легко
покрывается тестами.
## 2. Инструменты — 7 (все в категории «Прозрачность») ## 2. Инструменты — 7 (все в категории «Прозрачность»)
- **Обвести PNG** (`add-stroke-png`) — кольцо выбранного цвета вокруг непрозрачного содержимого; параметры: цвет, толщина 1–10 px. - **Обвести PNG** (`add-stroke-png`) — кольцо выбранного цвета вокруг
- **Найти контур PNG** (`find-contour-png`) — линия по краю непрозрачных областей; параметр: толщина линии. непрозрачного содержимого; параметры: цвет, толщина 110 px.
- **Найти контур PNG** (`find-contour-png`) — линия по краю непрозрачных
областей; параметр: толщина линии.
- **Утолщить PNG** (`make-thicker-png`) — расширение содержимого на N пикселей. - **Утолщить PNG** (`make-thicker-png`) — расширение содержимого на N пикселей.
- **Утончить PNG** (`make-thinner-png`) — сужение содержимого на N пикселей. - **Утончить PNG** (`make-thinner-png`) — сужение содержимого на N пикселей.
- **Жёсткие края PNG** (`harden-alpha-png`) — бинаризация альфы по порогу: полупрозрачность уходит, остаётся два состояния. - **Жёсткие края PNG** (`harden-alpha-png`) — бинаризация альфы по порогу:
- **Убрать мусор PNG** (`despeckle-alpha-png`) — открытие: удаляет одиночные полупрозрачные пиксели и мелкие крапинки. полупрозрачность уходит, остаётся два состояния.
- **Закрыть дыры PNG** (`close-holes-png`) — закрытие: заполняет одиночные прозрачные точки внутри объекта. - **Убрать мусор PNG** (`despeckle-alpha-png`) — открытие: удаляет одиночные
полупрозрачные пиксели и мелкие крапинки.
- **Закрыть дыры PNG** (`close-holes-png`) — закрытие: заполняет одиночные
прозрачные точки внутри объекта.
Все операции применяются к альфа-каналу; RGB пикселей сохраняется. Все операции применяются к альфа-каналу; RGB пикселей сохраняется.
@@ -23,49 +32,64 @@
Чистые функции над бинарной маской и над альфа-каналом: Чистые функции над бинарной маской и над альфа-каналом:
- `buildAlphaMask(img)` — 1 там, где альфа > 0. - `buildAlphaMask(img)` — 1 там, где альфа > 0.
- `dilateMask(mask, w, h, radius, shape)` — расширение; `shape`: квадрат или диск (для обводок диск даёт ровную круглую линию). - `dilateMask(mask, w, h, radius, shape)` — расширение; `shape`: квадрат или
диск (для обводок диск даёт ровную круглую линию).
- `erodeMask(...)` — зеркальная операция. - `erodeMask(...)` — зеркальная операция.
- `dilateImage / erodeImage(img, radiusPx)` — применяют маскирование к альфе изображения, RGB сохраняется. - `dilateImage / erodeImage(img, radiusPx)` — применяют маскирование к альфе
изображения, RGB сохраняется.
- Сборщики: - Сборщики:
- обводка: `dilate(mask) mask` → новое кольцо заливается цветом; - обводка: `dilate(mask) mask` → новое кольцо заливается цветом;
- контур: `mask erode(mask)` → линия; - контур: `mask erode(mask)` → линия;
- открытие: `erode → dilate`; закрытие: `dilate → erode`. - открытие: `erode → dilate`; закрытие: `dilate → erode`.
- `hardenAlpha(img, thresholdPercent)` — отдельно от морфологии, но той же волной. - `hardenAlpha(img, thresholdPercent)` — отдельно от морфологии, но той же
волной.
Структурирующий элемент при радиусе r — диск радиуса r (расстояние до центра ≤ r); квадрат не используем, чтобы углы обводок не были угловатыми. Структурирующий элемент при радиусе r — диск радиуса r (расстояние до центра ≤
r); квадрат не используем, чтобы углы обводок не были угловатыми.
## 4. Web Worker ## 4. Web Worker
Ничего не требуется: новые операции попадают в реестр, исполнитель автоматически исполняет их в воркере. Тесты ядра идут мимо воркера (чистые функции). Ничего не требуется: новые операции попадают в реестр, исполнитель автоматически
исполняет их в воркере. Тесты ядра идут мимо воркера (чистые функции).
## 5. Этапы и проверки ## 5. Этапы и проверки
- **A. Ядро**`morphology.ts` + точные тесты на матрицах 5×5: расширение диска, сужение, открытие убирает одиночный пиксель, закрытие закрывает одиночную дыру, кольцо обводки нужного цвета и толщины. Проверка: `pnpm test/check`. - **A. Ядро**`morphology.ts` + точные тесты на матрицах 5×5: расширение
- **B. Реестр** — семь записей, тест целостности обновляется (51 → 58). Проверка: `test/build`, визуально. диска, сужение, открытие убирает одиночный пиксель, закрытие закрывает
одиночную дыру, кольцо обводки нужного цвета и толщины. Проверка:
`pnpm test/check`.
- **B. Реестр** — семь записей, тест целостности обновляется (51 → 58).
Проверка: `test/build`, визуально.
- **C. Итог** — полный прогон, смоук §7. - **C. Итог** — полный прогон, смоук §7.
## 6. Критерии готовности ## 6. Критерии готовности
- Семь новых инструментов работают на прозрачных PNG: обводка, контур, толще/тоньше, жёсткие края, убрать мусор, закрыть дыры. - Семь новых инструментов работают на прозрачных PNG: обводка, контур,
толще/тоньше, жёсткие края, убрать мусор, закрыть дыры.
- Все операции детерминированы и покрыты юнит-тестами с ручными ожиданиями. - Все операции детерминированы и покрыты юнит-тестами с ручными ожиданиями.
- Исполнение автоматически идёт в воркере. - Исполнение автоматически идёт в воркере.
- `test/check/build` зелёные, смоук §7 пройден. - `test/check/build` зелёные, смоук §7 пройден.
## 7. Смоук-чеклист ## 7. Смоук-чеклист
1. Обводка толщиной 3 вокруг круглого стикера — ровное кольцо выбранного цвета, без угловатости. 1. Обводка толщиной 3 вокруг круглого стикера — ровное кольцо выбранного цвета,
2. Толщина 1 — тонкая аккуратная линия; 10 — толстая, форма содержимого не искажается сверх роста. без угловатости.
2. Толщина 1 — тонкая аккуратная линия; 10 — толстая, форма содержимого не
искажается сверх роста.
3. Контур показывает только край содержимого линией. 3. Контур показывает только край содержимого линией.
4. Толще/тоньше на тонкой надписи — штрихи растут/сжимаются без разрывов при умеренных значениях. 4. Толще/тоньше на тонкой надписи — штрихи растут/сжимаются без разрывов при
умеренных значениях.
5. Жёсткие края: полупрозрачность исчезает, силуэт сохраняется. 5. Жёсткие края: полупрозрачность исчезает, силуэт сохраняется.
6. Убрать мусор убирает одиночные точки-крапинки, не трогая крупные области. 6. Убрать мусор убирает одиночные точки-крапинки, не трогая крупные области.
7. Закрыть дыры заполняет одиночные прозрачные точки внутри логотипа. 7. Закрыть дыры заполняет одиночные прозрачные точки внутри логотипа.
8. Цепочка: обводка после удаления фона работает; каждое звено скачивает свой промежуток. 8. Цепочка: обводка после удаления фона работает; каждое звено скачивает свой
промежуток.
9. Регресс остальных инструментов без изменений. 9. Регресс остальных инструментов без изменений.
## 8. Что сознательно не делаем ## 8. Что сознательно не делаем
- Морфология по яркости для непрозрачных сканов — волна фокусируется на альфе прозрачных PNG. - Морфология по яркости для непрозрачных сканов — волна фокусируется на альфе
прозрачных PNG.
- Выбор структурирующего элемента пользователем — всегда диск. - Выбор структурирующего элемента пользователем — всегда диск.
- Feather/свечение краёв — отдельная будущая задача поверх размытия. - Feather/свечение краёв — отдельная будущая задача поверх размытия.
- Произвольные SVG-маски и градиентные обводки. - Произвольные SVG-маски и градиентные обводки.
+44 -45
View File
@@ -11,10 +11,9 @@
> Архивная копия, ниже — состояние на момент закрытия. > Архивная копия, ниже — состояние на момент закрытия.
> Статус: **переведено 121 из 125; остальные 4 — отложены/закрыты решениями.** > Статус: **переведено 121 из 125; остальные 4 — отложены/закрыты решениями.**
> Текущее > Текущее состояние: **121 инструмент переведён в `registry-new`**, старый UI
> состояние: **121 инструмент переведён в `registry-new`**, старый UI уже живёт > уже живёт на `(old)/`-маршрутах, линтер-изоляция old↔preview на месте (плагин
> на `(old)/`-маршрутах, линтер-изоляция old↔preview на месте > `isolation/no-mixed-imports`). Здесь — что сделать, чтобы:
> (плагин `isolation/no-mixed-imports`). Здесь — что сделать, чтобы:
> >
> 1. в preview был **весь** каталог инструментов (плюс закрыты известные долги); > 1. в preview был **весь** каталог инструментов (плюс закрыты известные долги);
> 2. физически перенести старую ветку в папки `old/` (шаг 37 основного плана); > 2. физически перенести старую ветку в папки `old/` (шаг 37 основного плана);
@@ -31,44 +30,44 @@
Не переведены только: `png-info` (отложен, хвост-фича с exif), `compress-png` Не переведены только: `png-info` (отложен, хвост-фича с exif), `compress-png`
и `reduce-to-size-png` (закрыто решениями, см. ниже), `watermark-image-png` и `reduce-to-size-png` (закрыто решениями, см. ниже), `watermark-image-png`
(overlay — единственный реальный остаток). (overlay — единственный реальный остаток).
- Preview (`/preview`) показывает переведённые; старый UI работает - Preview (`/preview`) показывает переведённые; старый UI работает на
на `/`-маршрутах (группа `(old)/`): `+page`, `demo`, `list-tools`, `/`-маршрутах (группа `(old)/`): `+page`, `demo`, `list-tools`, `tools/[id]`.
`tools/[id]`. Переведено всё кроме четырёх хвостов: color 18, filters 3, Переведено всё кроме четырёх хвостов: color 18, filters 3, geometry 13, alpha
geometry 13, alpha 13 (+watermark-image в остатке), generate 10 (mix/sort), 13 (+watermark-image в остатке), generate 10 (mix/sort), analyze 12 (6 масок +
analyze 12 (6 масок + verify + 5 вердиктов), convert 14 (jpg/webp/bmp + verify + 5 вердиктов), convert 14 (jpg/webp/bmp + 5 png→texт + 6 text→png),
5 png→texт + 6 text→png), text `watermark-tile-png`. text `watermark-tile-png`.
- Плагин `isolation/no-mixed-imports` (`web/eslint-plugins/isolation/`) - Плагин `isolation/no-mixed-imports` (`web/eslint-plugins/isolation/`)
полностью разделяет ветки: старый `lib/registry.ts|/registry/**`, полностью разделяет ветки: старый `lib/registry.ts|/registry/**`,
`lib/registry-helpers.ts`, `lib/categories.ts`, `lib/tools/**`, `lib/registry-helpers.ts`, `lib/categories.ts`, `lib/tools/**`,
`lib/components/**`(кроме `kit/`) vs новый `routes/preview/**`, `lib/components/**`(кроме `kit/`) vs новый `routes/preview/**`,
`lib/registry-new/**`, `lib/preview/**`, `lib/registry-schema.ts`, `lib/registry-new/**`, `lib/preview/**`, `lib/registry-schema.ts`,
`lib/components/kit/**`. Общее: `core/`, `i18n/`, `theme`, `assets/`, `lib/components/kit/**`. Общее: `core/`, `i18n/`, `theme`, `assets/`, корневой
корневой `lib`. `lib`.
- Правило копий (из основного плана): файл, используемый `(old)`, не трогаем — - Правило копий (из основного плана): файл, используемый `(old)`, не трогаем —
делаем копию в `lib/preview/` (пример: `lib/preview/tool-icons.ts`). делаем копию в `lib/preview/` (пример: `lib/preview/tool-icons.ts`).
- Известные долги (предсуществующие, не создавались нами): ошибка - Известные долги (предсуществующие, не создавались нами): ошибка
`ToolCard.svelte:19:36` (svelte-check), фейл `palette.test.ts` (ENOENT `ToolCard.svelte:19:36` (svelte-check), фейл `palette.test.ts` (ENOENT
`src/app.css` — из-за него `pnpm test` красный при 607 passed), ~335 `src/app.css` — из-за него `pnpm test` красный при 607 passed), ~335
design-tokens ошибок `lint:all` в старых kit-компонентах, a11y-warning в design-tokens ошибок `lint:all` в старых kit-компонентах, a11y-warning в
`ColorField.svelte`, пустой ruleset в `Toggle.svelte`, неиспользуемые токены `ColorField.svelte`, пустой ruleset в `Toggle.svelte`, неиспользуемые токены в
в `preview.css`. `preview.css`.
## Скоуп перевода: переведено всё переводимое (отложено 4 хвоста) ## Скоуп перевода: переведено всё переводимое (отложено 4 хвоста)
Каталог дожат до предела текущих решений: переведено 121 из 125. Оставшиеся Каталог дожат до предела текущих решений: переведено 121 из 125. Оставшиеся 4
4 инструмента **отложены осознанно** (не делаем сейчас): инструмента **отложены осознанно** (не делаем сейчас):
- **`watermark-image-png` (alpha)** — **отложен**. Нужна overlay-механика - **`watermark-image-png` (alpha)** — **отложен**. Нужна overlay-механика
(второе изображение-знак через `getOverlay`/store + параметры (второе изображение-знак через `getOverlay`/store + параметры
scale/opacity/position/margin/aspect); решается отдельно, вместе с вопросом, scale/opacity/position/margin/aspect); решается отдельно, вместе с вопросом,
как второй источник изображения вписывается в schema-driven preview. как второй источник изображения вписывается в schema-driven preview.
- **`png-info`** — **отложен осознанно**: будет серьёзно дорабатываться - **`png-info`** — **отложен осознанно**: будет серьёзно дорабатываться
отдельным райзом (exif-теги, редактирование, структурированный вывод), отдельным райзом (exif-теги, редактирование, структурированный вывод), для
для него нужен свой отдельный случай в UI, не «ещё один text/verdict». него нужен свой отдельный случай в UI, не «ещё один text/verdict».
- **`compress-png`** — **закрыт как дубликат** `decrease-color-count-png`: - **`compress-png`** — **закрыт как дубликат** `decrease-color-count-png`:
пресеты сжатия 192/96/44 добавлены прямо в схему `decrease-color-count`. пресеты сжатия 192/96/44 добавлены прямо в схему `decrease-color-count`.
- **`reduce-to-size-png`** — **отложен** до достройки кнопки Download - **`reduce-to-size-png`** — **отложен** до достройки кнопки Download (backlog
(backlog №7, «экспорт с лимитом размера»). №7, «экспорт с лимитом размера»).
### Переведено в этом проходе (text-механика) ### Переведено в этом проходе (text-механика)
@@ -77,10 +76,10 @@
qualityParamId) для download. qualityParamId) для download.
- **Executor:** `executeFromText` / `executeToText` / `executeTextToText` - **Executor:** `executeFromText` / `executeToText` / `executeTextToText`
(прямые, без worker), экспорт в `preview/executor/index.ts`. (прямые, без worker), экспорт в `preview/executor/index.ts`.
- **UI:** `SchemaTextSource` (textarea + Render), `SchemaTextResult` - **UI:** `SchemaTextSource` (textarea + Render), `SchemaTextResult` (кол-блок с
(кол-блок с Copy/Download .txt или бейдж-вердикт по префиксу Yes/No), Copy/Download .txt или бейдж-вердикт по префиксу Yes/No), интеграция в
интеграция в `SchemaPreview`/`SchemaToolView` (режимы file/text/generate, `SchemaPreview`/`SchemaToolView` (режимы file/text/generate, рендер
рендер image/text/verdict, скрытие image-download для text-результата). image/text/verdict, скрытие image-download для text-результата).
- **Инструменты:** convert 5×png→text (base64/data-uri/hex/bytes/rgb-values), - **Инструменты:** convert 5×png→text (base64/data-uri/hex/bytes/rgb-values),
6×text→png (base64/data-uri/hex/bytes/rgb-values/svg, +width), analyze 6×text→png (base64/data-uri/hex/bytes/rgb-values/svg, +width), analyze
verify-is-png (text→text) и 5 вердиктов (grayscale/file-size/transparent/ verify-is-png (text→text) и 5 вердиктов (grayscale/file-size/transparent/
@@ -92,9 +91,9 @@
## Известные долги перед переездом (завести tasks и закрыть) ## Известные долги перед переездом (завести tasks и закрыть)
По AGENTS.md техдолг «чинится только по заведённым tasks, не игнорируется»: По AGENTS.md техдолг «чинится только по заведённым tasks, не игнорируется»:
`lint:all` (~335 design-tokens в kit), неиспользуемые токены preview.css, `lint:all` (~335 design-tokens в kit), неиспользуемые токены preview.css, a11y
a11y `ColorField`, пустой ruleset `Toggle`. Плюс два, мешающих зелёной `ColorField`, пустой ruleset `Toggle`. Плюс два, мешающих зелёной проверке
проверке до/после переезда: svelte-check error `ToolCard.svelte:19:36` и фейл до/после переезда: svelte-check error `ToolCard.svelte:19:36` и фейл
`palette.test.ts` (ENOENT `src/app.css`). После их закрытия целевое состояние `palette.test.ts` (ENOENT `src/app.css`). После их закрытия целевое состояние
проверок перед переездом: `pnpm --dir web exec svelte-check` → 0 errors, проверок перед переездом: `pnpm --dir web exec svelte-check` → 0 errors,
`pnpm --dir web test` → зелёный, `pnpm --dir web lint:all` → только задокумент. `pnpm --dir web test` → зелёный, `pnpm --dir web lint:all` → только задокумент.
@@ -102,9 +101,9 @@ a11y `ColorField`, пустой ruleset `Toggle`. Плюс два, мешающ
## Шаги перевода остатка (порядок) ## Шаги перевода остатка (порядок)
1. **Простые пачки****выполнено**: color (18) → filters (3) → geometry (13) 1. **Простые пачки****выполнено**: color (18) → filters (3) → geometry (13)
alpha (13/14, кроме watermark-image) → generate (10, включая mix/sort alpha (13/14, кроме watermark-image) → generate (10, включая mix/sort через
через kind `colors`) → analyze-маски (6) → text `watermark-tile-png`. kind `colors`) → analyze-маски (6) → text `watermark-tile-png`.
2. **Text-механика****выполнено**: типы `input`/`result` + output-формат, 2. **Text-механика****выполнено**: типы `input`/`result` + output-формат,
executor `executeFromText`/`toText`/`textToText`, UI executor `executeFromText`/`toText`/`textToText`, UI
`SchemaTextSource`/`SchemaTextResult` + интеграция в preview, перевод `SchemaTextSource`/`SchemaTextResult` + интеграция в preview, перевод
@@ -113,8 +112,8 @@ a11y `ColorField`, пустой ruleset `Toggle`. Плюс два, мешающ
3. **Шаг про overlay не делается**`watermark-image-png` **отложен решением** 3. **Шаг про overlay не делается**`watermark-image-png` **отложен решением**
(см. «Скоуп перевода» выше), отдельным райзом вместе с вопросом второго (см. «Скоуп перевода» выше), отдельным райзом вместе с вопросом второго
источника изображения в schema-driven preview. источника изображения в schema-driven preview.
4. После категории — `schema.layout` для сгруппированных инструментов 4. После категории — `schema.layout` для сгруппированных инструментов (шаг 32-33
(шаг 32-33 паттерн) и ручная проверка в `/preview`. паттерн) и ручная проверка в `/preview`.
## Переезд в `old/` (шаг 37 основного плана) ## Переезд в `old/` (шаг 37 основного плана)
@@ -130,19 +129,19 @@ a11y `ColorField`, пустой ruleset `Toggle`. Плюс два, мешающ
`lib/registry/**…` заменить на `lib/old/**` (+ `lib/old/components/**` и `lib/registry/**…` заменить на `lib/old/**` (+ `lib/old/components/**` и
т.п.), «новое» и «общее» не меняются. Проверить, что правило по-прежнему т.п.), «новое» и «общее» не меняются. Проверить, что правило по-прежнему
разделяет (тест фолс-позитивов: временный импорт old↔new ловится). разделяет (тест фолс-позитивов: временный импорт old↔new ловится).
3. **Импорты внутри старого кода** — относительные пути остаются рабочими 3. **Импорты внутри старого кода** — относительные пути остаются рабочими (папки
(папки двигаются целиком); поправить только то, что ссылалось на `$lib/...` двигаются целиком); поправить только то, что ссылалось на `$lib/...` в другом
в другом стиле. Старые тесты (`registry.test.ts` и др.) переезжают вместе с стиле. Старые тесты (`registry.test.ts` и др.) переезжают вместе с
исходниками и продолжают проходить. исходниками и продолжают проходить.
4. **Гейты переезда:** `svelte-check` 0 errors, `pnpm --dir web test` зелёный, 4. **Гейты переезда:** `svelte-check` 0 errors, `pnpm --dir web test` зелёный,
`prettier --check .` чистый, `lint:all` = задокументированный остаток, `prettier --check .` чистый, `lint:all` = задокументированный остаток, обе
обе ветки открываются (старая `/`, новая `/preview`). ветки открываются (старая `/`, новая `/preview`).
## Проверка и сбор отзывов после переезда ## Проверка и сбор отзывов после переезда
1. **Регресс-чеклист по каталогу** — пройти все инструменты в `/preview`: 1. **Регресс-чеклист по каталогу** — пройти все инструменты в `/preview`:
дефолты совпадают со старыми, валидация/клампы поведения не меняют, дефолты совпадают со старыми, валидация/клампы поведения не меняют, результат
результат эквивалентен старому UI (сверить на одинаковых входах). эквивалентен старому UI (сверить на одинаковых входах).
2. **Тяжёлые кейсы руками:** генераторы, маски, font-style/plate/gradient на 2. **Тяжёлые кейсы руками:** генераторы, маски, font-style/plate/gradient на
всем наборе, text-source, output-формат. (`watermark-image` — позже, после всем наборе, text-source, output-формат. (`watermark-image` — позже, после
overlay-райза.) overlay-райза.)
@@ -150,16 +149,16 @@ a11y `ColorField`, пустой ruleset `Toggle`. Плюс два, мешающ
зафиксировать в этом документе по мере находок (или ссылкой на tasks). зафиксировать в этом документе по мере находок (или ссылкой на tasks).
4. Сверка каталога с `tools-map.md` (покрытие/дедупликация 4. Сверка каталога с `tools-map.md` (покрытие/дедупликация
`convert-png-to-jpg`/`webp`), при необходимости — обновить cards/мета. `convert-png-to-jpg`/`webp`), при необходимости — обновить cards/мета.
5. **Решение об удалении старого** — после завершения ревью и фидбека, 5. **Решение об удалении старого** — после завершения ревью и фидбека, отдельным
отдельным шагом: тогда чистятся `lib/old/**`, `routes/(old)/**`, шагом: тогда чистятся `lib/old/**`, `routes/(old)/**`, `old.css`, `exports`,
`old.css`, `exports`, изоляционный плагин (glob-ы больше не нужны) и изоляционный плагин (glob-ы больше не нужны) и открываются «правило
открываются «правило копий»-дубли. копий»-дубли.
## Как ревьюить каждый шаг (гайды) ## Как ревьюить каждый шаг (гайды)
- Диф на **один инструмент** (или фундамент без инструментов) < ~500 строк. - Диф на **один инструмент** (или фундамент без инструментов) < ~500 строк.
- Старый `params`/`run`/`defaultParams`/`sanitizeParams` не изменены; - Старый `params`/`run`/`defaultParams`/`sanitizeParams` не изменены; в
в `registry-new` копия логики + `schema` (дефолты равны старым). `registry-new` копия логики + `schema` (дефолты равны старым).
- Старый UI и старые тесты продолжают работать: `pnpm --dir web test`, - Старый UI и старые тесты продолжают работать: `pnpm --dir web test`,
`pnpm --dir web exec svelte-check`. `pnpm --dir web exec svelte-check`.
- Новый инструмент проверяется **руками в `/preview`** end-to-end (применение + - Новый инструмент проверяется **руками в `/preview`** end-to-end (применение +
+60 -25
View File
@@ -4,36 +4,63 @@
## 1. Целевое поведение ## 1. Целевое поведение
1. «Процентные» параметры (яркость, контраст, качество JPEG/WebP, порог похожести) — слайдеры с числовым значением рядом; точные координаты и размеры (resize/crop) остаются полями ввода. 1. «Процентные» параметры (яркость, контраст, качество JPEG/WebP, порог
2. У цветовых параметров появляется кнопка-переключатель «Пипетка»: активна → клик по превью исходника снимает цвет пикселя и подставляет его в поле цвета; после выбора режим гаснет. похожести) — слайдеры с числовым значением рядом; точные координаты и размеры
3. В режиме пипетки возле курсора видна лупа: увеличенный блок пикселей вокруг точки с перекрестием и подписью hex текущего пикселя; клик выбирает центр перекрестия. (resize/crop) остаются полями ввода.
4. Активная пипетка видна на превью перекрестием курсора; повторный клик по кнопке отменяет режим без выбора. 2. У цветовых параметров появляется кнопка-переключатель «Пипетка»: активна →
5. Всё работает поверх текущего процесса: авто-применение с задержкой, токен запусков, маски — без изменений. клик по превью исходника снимает цвет пикселя и подставляет его в поле цвета;
после выбора режим гаснет.
3. В режиме пипетки возле курсора видна лупа: увеличенный блок пикселей вокруг
точки с перекрестием и подписью hex текущего пикселя; клик выбирает центр
перекрестия.
4. Активная пипетка видна на превью перекрестием курсора; повторный клик по
кнопке отменяет режим без выбора.
5. Всё работает поверх текущего процесса: авто-применение с задержкой, токен
запусков, маски — без изменений.
## 2. Слайдеры ## 2. Слайдеры
- В реестре новый тип параметра `'slider'` с обязательными `min`/`max`; валидируется как число (дефолт в диапазоне, минимум меньше максимума). - В реестре новый тип параметра `'slider'` с обязательными `min`/`max`;
- Форма маппит его на готовый `SliderField` из `ui/`; санитизация параметров обрабатывает slider так же, как number. валидируется как число (дефолт в диапазоне, минимум меньше максимума).
- Переводятся: яркость и контраст, качество JPEG, качество WebP, порог похожести. Остальные инструменты — без изменений. - Форма маппит его на готовый `SliderField` из `ui/`; санитизация параметров
обрабатывает slider так же, как number.
- Переводятся: яркость и контраст, качество JPEG, качество WebP, порог
похожести. Остальные инструменты — без изменений.
## 3. Пипетка и лупа ## 3. Пипетка и лупа
- В ядро добавляется чистая функция `rgbToHex(r, g, b)` → строка вида `#rrggbb`, с юнит-тестами; чтение пикселя — из уже отрисованного canvas превью исходника через `getImageData(x, y, 1, 1)`. - В ядро добавляется чистая функция `rgbToHex(r, g, b)` → строка вида `#rrggbb`,
- `Preview.svelte` получает опциональный режим пипетки: пропсы «активна» и колбэк «цвет выбран»; курсор перекрестие, клик вычисляет координату пикселя с учётом разницы CSS-размера canvas и его внутренней ширины. с юнит-тестами; чтение пикселя — из уже отрисованного canvas превью исходника
- **Лупа** — компонент `PipetteLoupe.svelte`: канвас около 110×110 px у курсора (позиция fixed, `pointer-events: none`); рисует блок 15×15 пикселей из исходника с увеличением ×8 без сглаживания, перекрестие в центре, подпись hex текущего пикселя. Координаты блока клампятся у краёв изображения; лупа скрыта вне превью и вне режима пипетки. Тач-устройства: прямой тап выбирает пиксель без лупы. через `getImageData(x, y, 1, 1)`.
- Проводка: кнопка у каждого цветового поля в карточке параметров → состояние «активное цветовое поле» живёт в оркестраторе → выбор цвета записывает hex в это поле и выключает режим. - `Preview.svelte` получает опциональный режим пипетки: пропсы «активна» и
- Пипетка работает только по левой панели (исходнику): удаляемый цвет и подложка берутся с оригинала. колбэк «цвет выбран»; курсор перекрестие, клик вычисляет координату пикселя с
учётом разницы CSS-размера canvas и его внутренней ширины.
- **Лупа** — компонент `PipetteLoupe.svelte`: канвас около 110×110 px у курсора
(позиция fixed, `pointer-events: none`); рисует блок 15×15 пикселей из
исходника с увеличением ×8 без сглаживания, перекрестие в центре, подпись hex
текущего пикселя. Координаты блока клампятся у краёв изображения; лупа скрыта
вне превью и вне режима пипетки. Тач-устройства: прямой тап выбирает пиксель
без лупы.
- Проводка: кнопка у каждого цветового поля в карточке параметров → состояние
«активное цветовое поле» живёт в оркестраторе → выбор цвета записывает hex в
это поле и выключает режим.
- Пипетка работает только по левой панели (исходнику): удаляемый цвет и подложка
берутся с оригинала.
## 4. Этапы и проверки ## 4. Этапы и проверки
- **A. Слайдеры** — тип в реестре, перевод четырёх инструментов, тесты реестра и санитизации, тест `rgbToHex`. Проверка: `pnpm test/check`, визуально слайдеры. - **A. Слайдеры** — тип в реестре, перевод четырёх инструментов, тесты реестра и
- **B. Пипетка** — режим в Preview, проводка через карточки до полей цвета. Проверка: смоук §6 пп. 5–7. санитизации, тест `rgbToHex`. Проверка: `pnpm test/check`, визуально слайдеры.
- **B2. Лупа**компонент лупы поверх режима пипетки. Проверка: смоук §6 пп. 8–10. - **B. Пипетка**режим в Preview, проводка через карточки до полей цвета.
Проверка: смоук §6 пп. 5–7.
- **B2. Лупа** — компонент лупы поверх режима пипетки. Проверка: смоук §6 пп.
810.
- **C. Итог** — полный прогон `test/check/build`. - **C. Итог** — полный прогон `test/check/build`.
## 5. Изменения по файлам ## 5. Изменения по файлам
- `registry.ts` — тип `'slider'` в `ParamDef`, перевод четырёх записей, ветка в `sanitizeParams`. - `registry.ts` — тип `'slider'` в `ParamDef`, перевод четырёх записей, ветка в
`sanitizeParams`.
- `ParamForm.svelte` — ветка slider → `SliderField`. - `ParamForm.svelte` — ветка slider → `SliderField`.
- `core/color.ts``rgbToHex`. - `core/color.ts``rgbToHex`.
- `Preview.svelte` — режим пипетки (курсор, клик, колбэк). - `Preview.svelte` — режим пипетки (курсор, клик, колбэк).
@@ -44,21 +71,29 @@
## 6. Смоук-чеклист ## 6. Смоук-чеклист
1. Яркость/контраст — слайдеры со значением справа, результат обновляется сам при перетаскивании. 1. Яркость/контраст — слайдеры со значением справа, результат обновляется сам
при перетаскивании.
2. Качество JPEG/WebP — слайдер от 1 до 100. 2. Качество JPEG/WebP — слайдер от 1 до 100.
3. Порог похожести — слайдер от 0 до 100. 3. Порог похожести — слайдер от 0 до 100.
4. Resize и crop остались числовыми полями. 4. Resize и crop остались числовыми полями.
5. Удаление цвета: «Пипетка» → клик по красной области исходника → поле цвета получил hex этого пикселя, режим погас, результат пересчитался. 5. Удаление цвета: «Пипетка» → клик по красной области исходника → поле цвета
6. Повторный клик по кнопке или выбор без подтверждения — режим снят, цвет не изменился. получил hex этого пикселя, режим погас, результат пересчитался.
6. Повторный клик по кнопке или выбор без подтверждения — режим снят, цвет не
изменился.
7. Подложка JPEG: пипетка проставляет цвет подложки тем же способом. 7. Подложка JPEG: пипетка проставляет цвет подложки тем же способом.
8. В режиме пипетки лупа следует за курсором, перекрестие показывает пиксель под ним, подпись hex совпадает с выбранным при клике. 8. В режиме пипетки лупа следует за курсором, перекрестие показывает пиксель под
9. У края картинки блок лупи корректно клампится — за границей тянется чёрный/прозрачный фон, выбор по-прежнему точен. ним, подпись hex совпадает с выбранным при клике.
10. Курсор ушёл с превью или режим выключен — лупа скрыта; клики по кнопкам под превью не перехватываются лупой. 9. У края картинки блок лупи корректно клампится — за границей тянется
11. Слайдеры и пипетка дружат с быстрыми правками: одна финальная версия результата, чип «Пересчёт…» на месте. чёрный/прозрачный фон, выбор по-прежнему точен.
10. Курсор ушёл с превью или режим выключен — лупа скрыта; клики по кнопкам под
превью не перехватываются лупой.
11. Слайдеры и пипетка дружат с быстрыми правками: одна финальная версия
результата, чип «Пересчёт…» на месте.
## 7. Критерии готовности ## 7. Критерии готовности
- Все процентные параметры — слайдеры; числовые поля остались только там, где нужен точный ввод. - Все процентные параметры — слайдеры; числовые поля остались только там, где
нужен точный ввод.
- Цвет любого color-параметра можно взять кликом с исходника. - Цвет любого color-параметра можно взять кликом с исходника.
- `test/check/build` зелёные, чеклист §6 пройден. - `test/check/build` зелёные, чеклист §6 пройден.
+48 -45
View File
@@ -1,23 +1,26 @@
# План: тестирование preview перед промоушеном в главную # План: тестирование preview перед промоушеном в главную
> Статус: **реализация завершена**. Playwright-набор в `web/e2e/` > Статус: **реализация завершена**. Playwright-набор в `web/e2e/` (108 тестов:
> (108 тестов: 104 прошли, 4 `test.fixme` — известные баги №1–4 ниже), > 104 прошли, 4 `test.fixme` — известные баги №1–4 ниже), ручной чек-лист —
> ручной чек-лист — `docs/checklist-manual-testing.md`. План ветки preview > `docs/checklist-manual-testing.md`. План ветки preview
> (`web/src/routes/preview/**`, регистр `lib/registry-new/`, 121 инструмент) > (`web/src/routes/preview/**`, регистр `lib/registry-new/`, 121 инструмент)
> перед шагом C17 из `docs/plan-redesign.md` (переезд preview → реальные > перед шагом C17 из `docs/plan-redesign.md` (переезд preview → реальные
> маршруты). > маршруты).
> >
> Охват: **документация плана** — реализация тестов (Playwright-раннер, > Охват: **документация плана** — реализация тестов (Playwright-раннер, сами
> сами тесты) делается отдельными задачами после ревью этого документа. > тесты) делается отдельными задачами после ревью этого документа.
> >
> Приоритет: **отсутствие падений/ошибок** > пиксельная корректность. > Приоритет: **отсутствие падений/ошибок** > пиксельная корректность. Дизайн
> Дизайн доделываем по ходу, визуальная сверка — ручная и вторичная. > доделываем по ходу, визуальная сверка — ручная и вторичная.
> >
> Команды для проверки в процессе: > Команды для проверки в процессе:
> >
> - `pnpm --dir web test` — существующий Vitest (42 файла, 121 инструмент частично покрыт юнит-тестами). > - `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 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 lint:all` — ESLint + stylelint + токен-аудит.
> - `pnpm --dir web build` — статическая сборка (adapter-static). > - `pnpm --dir web build` — статическая сборка (adapter-static).
@@ -44,21 +47,21 @@ GEOMETRY / FILTERS / TEXT / ANALYZE / GENERATE. Режимы инструмен
- **text-output** (`result: "text"`, file): `toText` → текстовый результат; - **text-output** (`result: "text"`, file): `toText` → текстовый результат;
- **generate**: без входа, `executeGenerate` → картинка. - **generate**: без входа, `executeGenerate` → картинка.
Исполнение: `preview/executor` — worker с fallback на main-thread Исполнение: `preview/executor` — worker с fallback на main-thread (domOnly / нет
(domOnly / нет Worker / ошибка worker). Auto-run с debounce 200 мс. Worker / ошибка worker). Auto-run с debounce 200 мс.
## 1. Функциональные блоки для проверки ## 1. Функциональные блоки для проверки
### A. Инфраструктура и навигация ### A. Инфраструктура и навигация
1. Все 4 маршрута + 404 открываются без console errors, uncaught exceptions 1. Все 4 маршрута + 404 открываются без console errors, uncaught exceptions и
и failed requests. failed requests.
2. Поиск на `/preview` — фильтрация по названию/описанию, пустой результат → 2. Поиск на `/preview` — фильтрация по названию/описанию, пустой результат →
аккуратное empty-state. аккуратное empty-state.
3. Каталог `/preview/list-tools` — 8 групп, счётчик 121, фильтр категории, 3. Каталог `/preview/list-tools` — 8 групп, счётчик 121, фильтр категории,
иконки не падают. иконки не падают.
4. Тема light/dark и язык RU/EN переключаются на превью-страницах без 4. Тема light/dark и язык RU/EN переключаются на превью-страницах без регресса
регресса рендера. рендера.
5. 404 на несуществующий id инструмента → EmptyState, без краша. 5. 404 на несуществующий id инструмента → EmptyState, без краша.
6. Prerender: `pnpm --dir web build` проходит; все 121 страницы 6. Prerender: `pnpm --dir web build` проходит; все 121 страницы
`/preview/tools/[id]` генерируются (проверка `entries()`/`getTool`). `/preview/tools/[id]` генерируются (проверка `entries()`/`getTool`).
@@ -66,7 +69,8 @@ GEOMETRY / FILTERS / TEXT / ANALYZE / GENERATE. Режимы инструмен
### B. Конвейер инструмента (file-инструменты) ### B. Конвейер инструмента (file-инструменты)
7. Upload → авто-run → результатный тайл появился, поле ошибки пустое. 7. Upload → авто-run → результатный тайл появился, поле ошибки пустое.
8. Download — событие download, имя `<toolId>.<ext>`, MIME соответствует `output`. 8. Download — событие download, имя `<toolId>.<ext>`, MIME соответствует
`output`.
9. Reset — возвращает дефолты, сбрасывает результат и ошибку. 9. Reset — возвращает дефолты, сбрасывает результат и ошибку.
10. Смена параметра → авто-перезапуск (debounce 200 мс) без «залипания» 10. Смена параметра → авто-перезапуск (debounce 200 мс) без «залипания»
running-стейта. running-стейта.
@@ -75,8 +79,8 @@ GEOMETRY / FILTERS / TEXT / ANALYZE / GENERATE. Режимы инструмен
### C. Текстовые инструменты и вердикты ### C. Текстовые инструменты и вердикты
12. Text-source: ввод → текстовый результат или картинка; copy/download .txt. 12. Text-source: ввод → текстовый результат или картинка; copy/download .txt.
13. Вердикты (analyze): корректный вердикт на PNG с альфой, на PNG без альфы, 13. Вердикты (analyze): корректный вердикт на PNG с альфой, на PNG без альфы, на
на не-PNG входе → без падений. не-PNG входе → без падений.
14. Пустой text-source → run пропускается без ошибки. 14. Пустой text-source → run пропускается без ошибки.
### D. Генераторы ### D. Генераторы
@@ -103,11 +107,11 @@ GEOMETRY / FILTERS / TEXT / ANALYZE / GENERATE. Режимы инструмен
### ✅ Playwright — детерминированные сквозные потоки и «нет падений» ### ✅ Playwright — детерминированные сквозные потоки и «нет падений»
- Все маршруты + 404: открытие, отсутствие console errors / uncaught - Все маршруты + 404: открытие, отсутствие console errors / uncaught exceptions
exceptions / failed requests (слушатели `page.on('console'/'pageerror')`). / failed requests (слушатели `page.on('console'/'pageerror')`).
- Поиск и каталог: фильтрация, счётчики, переходы. - Поиск и каталог: фильтрация, счётчики, переходы.
- Upload (`setInputFiles` с фикстурой PNG) → авто-результат → Download - Upload (`setInputFiles` с фикстурой PNG) → авто-результат → Download (ожидание
(ожидание `download` события, проверка имени/расширения/suggestedFilename). `download` события, проверка имени/расширения/suggestedFilename).
- Reset / смена параметра → перегенерация, running не «залипает». - Reset / смена параметра → перегенерация, running не «залипает».
- Тема light/dark и язык RU/EN. - Тема light/dark и язык RU/EN.
- Текстовые инструменты: ввод → текстовый результат → копия/скачивание. - Текстовые инструменты: ввод → текстовый результат → копия/скачивание.
@@ -129,12 +133,12 @@ GEOMETRY / FILTERS / TEXT / ANALYZE / GENERATE. Режимы инструмен
- **Краевые PNG:** большие (мегапиксели), 1×1, анимация, палитровые, - **Краевые PNG:** большие (мегапиксели), 1×1, анимация, палитровые,
чекерборд/полупрозрачность, повреждённые файлы → нет зависаний, понятные чекерборд/полупрозрачность, повреждённые файлы → нет зависаний, понятные
сообщения об ошибке. сообщения об ошибке.
- **Края параметров:** экстремальные значения, NaN/пустые поля, инвалидные - **Края параметров:** экстремальные значения, NaN/пустые поля, инвалидные цвета
цвета в плашках/градиентах → корректная санитизация (silent repair). в плашках/градиентах → корректная санитизация (silent repair).
- **Производительность/отзывчивость:** задержка авто-реранов на слайдерах, - **Производительность/отзывчивость:** задержка авто-реранов на слайдерах, нет
нет «залипания» UI, скролл при длинных настройках. «залипания» UI, скролл при длинных настройках.
- **Визуальный слой дизайна** (доделываем по ходу): сетки, выравнивание, - **Визуальный слой дизайна** (доделываем по ходу): сетки, выравнивание, отступы
отступы на брейкпоинтах mobile/tablet/desktop (640/800/1100). на брейкпоинтах mobile/tablet/desktop (640/800/1100).
- **Буфер обмена** (copy) в реальном браузере. - **Буфер обмена** (copy) в реальном браузере.
- **Drag-n-drop** файла на dropzone (UX-ощущения). - **Drag-n-drop** файла на dropzone (UX-ощущения).
- **Разные браузеры:** Firefox/Safari-нюансы (webkit в Playwright опционален). - **Разные браузеры:** Firefox/Safari-нюансы (webkit в Playwright опционален).
@@ -144,8 +148,8 @@ GEOMETRY / FILTERS / TEXT / ANALYZE / GENERATE. Режимы инструмен
## 4. Критерии приёмки (доступ в главную) ## 4. Критерии приёмки (доступ в главную)
1. Все 4 маршрута + 404 открываются без console errors и исключений. 1. Все 4 маршрута + 404 открываются без console errors и исключений.
2. Репрезентативный прогон по 8 категориям: ни один инструмент не падает и 2. Репрезентативный прогон по 8 категориям: ни один инструмент не падает и не
не показывает поле ошибки. показывает поле ошибки.
3. Upload → результат → download завершается для file-инструментов; 3. Upload → результат → download завершается для file-инструментов;
текст/вердикты/генераторы дают корректный output-тип. текст/вердикты/генераторы дают корректный output-тип.
4. `pnpm --dir web test`, `pnpm --dir web lint:all`, `pnpm --dir web build` 4. `pnpm --dir web test`, `pnpm --dir web lint:all`, `pnpm --dir web build`
@@ -156,17 +160,17 @@ GEOMETRY / FILTERS / TEXT / ANALYZE / GENERATE. Режимы инструмен
- **~60–70%** «нет падений/ошибок» закрывается Playwright детерминированно - **~60–70%** «нет падений/ошибок» закрывается Playwright детерминированно
(маршруты, сквозные потоки, отсутствие ошибок). (маршруты, сквозные потоки, отсутствие ошибок).
- **~30–40%** — руками: краевые PNG/параметры, реальные пользовательские - **~30–40%** — руками: краевые PNG/параметры, реальные пользовательские файлы,
файлы, буфер обмена, кросс-браузеры, визуальный/производительный слой, буфер обмена, кросс-браузеры, визуальный/производительный слой, субъективная
субъективная корректность. корректность.
## 6. Шаги реализации (после ревью) ## 6. Шаги реализации (после ревью)
1. ✅ Playwright-раннер: `web/playwright.config.ts` (webServer на `pnpm build + 1. ✅ Playwright-раннер: `web/playwright.config.ts` (webServer на
`scripts/serve-static.mjs` `--port 4173`), папка `web/e2e/`, npm-скрипт `pnpm build + `scripts/serve-static.mjs` `--port
`test:e2e`. 4173`), папка `web/e2e/`, npm-скрипт `test:e2e`.
2. ✅ Фикстуры PNG (генерируются в рантайме, `web/e2e/helpers/fixtures.ts`): 2. ✅ Фикстуры PNG (генерируются в рантайме, `web/e2e/helpers/fixtures.ts`): с
с альфой, без альфы, 1×1, большой, повреждённый, ландшафт. альфой, без альфы, 1×1, большой, повреждённый, ландшафт.
3. ✅ E2E по блокам A–D и секции 3 (авто-часть): `navigation.spec.ts`, 3. ✅ E2E по блокам A–D и секции 3 (авто-часть): `navigation.spec.ts`,
`catalog.spec.ts`, `pipeline.spec.ts`, `text-and-verdicts.spec.ts`, `catalog.spec.ts`, `pipeline.spec.ts`, `text-and-verdicts.spec.ts`,
`tools-smoke.spec.ts` (матрица ~71 инструмента), `generators.spec.ts`, `tools-smoke.spec.ts` (матрица ~71 инструмента), `generators.spec.ts`,
@@ -174,14 +178,13 @@ GEOMETRY / FILTERS / TEXT / ANALYZE / GENERATE. Режимы инструмен
4. ✅ Ручной чек-лист из секции 3 (ручная часть) — 4. ✅ Ручной чек-лист из секции 3 (ручная часть) —
`docs/checklist-manual-testing.md` (разделы A–G, включая известные баги). `docs/checklist-manual-testing.md` (разделы A–G, включая известные баги).
5. ✅ Найденные баги (зафиксированы как `test.fixme` + чек-лист §G): 5. ✅ Найденные баги (зафиксированы как `test.fixme` + чек-лист §G):
1. генераторы (21/121) не имеют кнопки «Generate» и полей схемы — 1. генераторы (21/121) не имеют кнопки «Generate» и полей схемы — результат
результат через UI недостижим; через UI недостижим;
2. `resize-png` с дефолтом 0×0 → `errors.resizeSize` при любом входе; 2. `resize-png` с дефолтом 0×0 → `errors.resizeSize` при любом входе;
3. `crop-png` с дефолтом 0×0 → `errors.cropSize` при любом входе; 3. `crop-png` с дефолтом 0×0 → `errors.cropSize` при любом входе;
4. ключи ошибок показываются сырыми (`errors.*`) вместо переведённого 4. ключи ошибок показываются сырыми (`errors.*`) вместо переведённого текста
текста из `en.ts`/`ru.ts`. из `en.ts`/`ru.ts`. Исправления — отдельными атомарными задачами по
Исправления — отдельными атомарными задачами по конвенции AGENTS.md конвенции AGENTS.md (после правок: `lint:all`, `test:e2e`).
(после правок: `lint:all`, `test:e2e`).
## 7. Итоговое состояние на момент сдачи ## 7. Итоговое состояние на момент сдачи
+55 -18
View File
@@ -4,41 +4,78 @@
## 1. Что получается ## 1. Что получается
Четыре инструмента новой категории «Текст»: надпись на изображении, дата-штамп, защитная плитка (повторяющийся полупрозрачный текст под углом), водяной знак картинкой. Каталог вырастает с 69 до 73. Это первые операции, рисующие через canvas 2D — класс perceptual по роадмапу: точные пиксельные тесты на них не пишутся, юнит-тестами покрывается чистая математика раскладки. Четыре инструмента новой категории «Текст»: надпись на изображении, дата-штамп,
защитная плитка (повторяющийся полупрозрачный текст под углом), водяной знак
картинкой. Каталог вырастает с 69 до 73. Это первые операции, рисующие через
canvas 2D — класс perceptual по роадмапу: точные пиксельные тесты на них не
пишутся, юнит-тестами покрывается чистая математика раскладки.
## 2. Решения ## 2. Решения
- Вся геометрия — чистые функции без DOM: якорь из 9-позиционной сетки → координаты, перенос текста по ширине с инъекцией измерителя, раскладка плитки с углом и шагом. Canvas-вызовы — тонкий слой рядом (`core/textdraw.ts`), в юнит-тесты не попадает. - Вся геометрия — чистые функции без DOM: якорь из 9-позиционной сетки →
- Один движок рендера текста на все три текстовых инструмента: шрифт из системного набора (sans/serif/mono), размер, цвет, жирность, прозрачность, поворот. Различия инструментов — только в пресетах параметров и количестве слоёв (одна надпись vs плитка). координаты, перенос текста по ширине с инъекцией измерителя, раскладка плитки
- Дата-штамп — надпись с автотекстом: строка формата (`YYYY-MM-DD hh:mm`) разворачивается в текущую дату в момент запуска; свой мини-форматтер без зависимостей. с углом и шагом. Canvas-вызовы — тонкий слой рядом (`core/textdraw.ts`), в
- Водяной знак картинкой — первый двухисточниковый инструмент: вторая картинка грузится отдельной зоной в этапе, живёт только в состоянии страницы и сознательно не сохраняется ни в workspace-pipeline, ни в экспорте JSON; после восстановления цепочки знак нужно выбрать заново. Ограничение фиксируем в описании инструмента. юнит-тесты не попадает.
- Новая категория `text` встаёт после «Фильтров»; названия в обоих словарях, порядок в CATEGORIES один. - Один движок рендера текста на все три текстовых инструмента: шрифт из
системного набора (sans/serif/mono), размер, цвет, жирность, прозрачность,
поворот. Различия инструментов — только в пресетах параметров и количестве
слоёв (одна надпись vs плитка).
- Дата-штамп — надпись с автотекстом: строка формата (`YYYY-MM-DD hh:mm`)
разворачивается в текущую дату в момент запуска; свой мини-форматтер без
зависимостей.
- Водяной знак картинкой — первый двухисточниковый инструмент: вторая картинка
грузится отдельной зоной в этапе, живёт только в состоянии страницы и
сознательно не сохраняется ни в workspace-pipeline, ни в экспорте JSON; после
восстановления цепочки знак нужно выбрать заново. Ограничение фиксируем в
описании инструмента.
- Новая категория `text` встаёт после «Фильтров»; названия в обоих словарях,
порядок в CATEGORIES один.
## 3. Инструменты ## 3. Инструменты
- `add-text-png` — параметры: текст (строка), размер (слайдер 8–200), цвет, шрифт (select), жирность (чекбокс), позиция (select 3×3: углы/стороны/центр), отступ (слайдер). Опциональная подложка: цвет + прозрачность плашки за текстом. - `add-text-png` — параметры: текст (строка), размер (слайдер 8–200), цвет,
- `date-stamp-png` — тот же набор минус текст, плюс формат даты; дефолт — нижний правый угол. шрифт (select), жирность (чекбокс), позиция (select 3×3: углы/стороны/центр),
- `watermark-tile-png` — текст, размер, цвет, прозрачность (0100), угол (-90…90), шаг по X/Y; плитка заполняет весь холст. отступ (слайдер). Опциональная подложка: цвет + прозрачность плашки за
- `watermark-image-png` — второй источник (загрузка PNG с альфой), масштаб знака (% от ширины холста), прозрачность, позиция 3×3, отступ. текстом.
- `date-stamp-png` — тот же набор минус текст, плюс формат даты; дефолт — нижний
правый угол.
- `watermark-tile-png` — текст, размер, цвет, прозрачность (0–100), угол
(-90…90), шаг по X/Y; плитка заполняет весь холст.
- `watermark-image-png` — второй источник (загрузка PNG с альфой), масштаб знака
(% от ширины холста), прозрачность, позиция 3×3, отступ.
## 4. Этапы и проверки ## 4. Этапы и проверки
Каждый этап заканчивается зелёными test/check/build и ревью. Каждый этап заканчивается зелёными test/check/build и ревью.
- **A. Фундамент** — категория `text` (categories + словари + порядок), `core/textdraw.ts`: anchor9, wrap, tileLayout с юнит-тестами на математике (координаты углов/центра, переносы слов, плотность плитки), тонкий canvas-модуль `domText.ts` (без тестов). Реестр пока пуст в категории. - **A. Фундамент** — категория `text` (categories + словари + порядок),
- **B. Надпись и дата**`add-text-png`, `date-stamp-png` на общем движке; иконки, переводы, записи реестра. Смоук: обе локали, тёмная тема (плашка читаема), длинный текст переносится и не вылезает за край. `core/textdraw.ts`: anchor9, wrap, tileLayout с юнит-тестами на математике
- **C. Плитка**`watermark-tile-png`; смоук: отрицательные и нулевой угол, прозрачность 0/50/100, мелкий шаг не подвешивает страницу на больших холстах (ограничение количества плиток капом). (координаты углов/центра, переносы слов, плотность плитки), тонкий
- **D. Знак картинкой** — двухисточниковый UX: вторая drop-зона в этапе (компонент рядом с SourceCard, виден только у этого инструмента), масштаб/позиция/отступ; ограничение про сохранение — в описании и подсказке. Этап режется без ущерба остальным, если решим отложить. canvas-модуль `domText.ts` (без тестов). Реестр пока пуст в категории.
- **B. Надпись и дата**`add-text-png`, `date-stamp-png` на общем движке;
иконки, переводы, записи реестра. Смоук: обе локали, тёмная тема (плашка
читаема), длинный текст переносится и не вылезает за край.
- **C. Плитка**`watermark-tile-png`; смоук: отрицательные и нулевой угол,
прозрачность 0/50/100, мелкий шаг не подвешивает страницу на больших холстах
(ограничение количества плиток капом).
- **D. Знак картинкой** — двухисточниковый UX: вторая drop-зона в этапе
(компонент рядом с SourceCard, виден только у этого инструмента),
масштаб/позиция/отступ; ограничение про сохранение — в описании и подсказке.
Этап режется без ущерба остальным, если решим отложить.
- **E. Итог** — полный прогон, смоук §6, архивация плана. - **E. Итог** — полный прогон, смоук §6, архивация плана.
## 5. Критерии готовности ## 5. Критерии готовности
- 73 инструмента; вся математика раскладки покрыта юнит-тестами; canvas-слой изолирован в одном модуле. - 73 инструмента; вся математика раскладки покрыта юнит-тестами; canvas-слой
- test/check/build зелёные; новые инструменты работают в цепочке как обычные шаги (кроме источника знака — см. ограничение). изолирован в одном модуле.
- test/check/build зелёные; новые инструменты работают в цепочке как обычные
шаги (кроме источника знака — см. ограничение).
## 6. Что сознательно не делаем ## 6. Что сознательно не делаем
- Выбор произвольных пользовательских шрифтов (файлами) и веб-шрифтов — только системный набор. - Выбор произвольных пользовательских шрифтов (файлами) и веб-шрифтов — только
- Кернинг/линии/многострочное редактирование с абзацами — один абзац с автопереносом. системный набор.
- Кернинг/линии/многострочное редактирование с абзацами — один абзац с
автопереносом.
- Сохранение картинки-знака в пайплайне и её передача между сессиями. - Сохранение картинки-знака в пайплайне и её передача между сессиями.
- Текст по контуру, градиентные заливки текста. - Текст по контуру, градиентные заливки текста.
+25 -10
View File
@@ -4,30 +4,44 @@
## 1. Что получается ## 1. Что получается
Переключатель light/dark в шапке рядом с языковым. Выбор в localStorage (`theme`), первый визит — по `prefers-color-scheme`. Тёмная палитра — переопределение токенов селектором `[data-theme='dark']`; компоненты не правятся. Переключатель light/dark в шапке рядом с языковым. Выбор в localStorage
(`theme`), первый визит — по `prefers-color-scheme`. Тёмная палитра —
переопределение токенов селектором `[data-theme='dark']`; компоненты не
правятся.
## 2. Решения ## 2. Решения
- Два состояния, без «авто»: ключ хранит только явный выбор, отсутствие ключа = системная тема. - Два состояния, без «авто»: ключ хранит только явный выбор, отсутствие ключа =
- Анти-вспышка: инлайн-скрипт в `app.html` до гидрации ставит `data-theme` из localStorage или `matchMedia`; пререндеренная разметка остаётся светлой. системная тема.
- `color-scheme: dark` на корне — нативные контролы (input[type=color], range, select, скроллбары) темнеют без правок. - Анти-вспышка: инлайн-скрипт в `app.html` до гидрации ставит `data-theme` из
- Модуль `theme.svelte.ts` по образцу locale: состояние, персист, применение атрибута. Стартовую установку делает скрипт из `app.html`, модуль синхронизирует runes-состояние после гидрации. localStorage или `matchMedia`; пререндеренная разметка остаётся светлой.
- `color-scheme: dark` на корне — нативные контролы (input[type=color], range,
select, скроллбары) темнеют без правок.
- Модуль `theme.svelte.ts` по образцу locale: состояние, персист, применение
атрибута. Стартовую установку делает скрипт из `app.html`, модуль
синхронизирует runes-состояние после гидрации.
## 3. Палитра ## 3. Палитра
- Переопределяем: `--bg`, `--surface`, `--border`, `--text`, `--text-muted`. - Переопределяем: `--bg`, `--surface`, `--border`, `--text`, `--text-muted`.
- Акцент `#2563eb` не меняется; `--accent-hover` — светлее для тёмного фона. - Акцент `#2563eb` не меняется; `--accent-hover` — светлее для тёмного фона.
- `--danger` осветлить для читаемости. - `--danger` осветлить для читаемости.
- `--check-a`/`--check-b` шахматки — приглушённые серые: паттерн прозрачности остаётся узнаваемым, но не слепит. - `--check-a`/`--check-b` шахматки — приглушённые серые: паттерн прозрачности
остаётся узнаваемым, но не слепит.
- `--shadow-card` — почти чёрная с низкой альфой вместо синей. - `--shadow-card` — почти чёрная с низкой альфой вместо синей.
- Все `color-mix(… var(--surface))` пересчитаются сами — проверить визуально легенды, recalc-бейдж, drag-оверлеи. - Все `color-mix(… var(--surface))` пересчитаются сами — проверить визуально
легенды, recalc-бейдж, drag-оверлеи.
## 4. Этапы и проверки ## 4. Этапы и проверки
Каждый этап заканчивается зелёными test/check/build и ревью. Каждый этап заканчивается зелёными test/check/build и ревью.
- **A. Инфраструктура**`theme.svelte.ts`, скрипт в `app.html`, переключатель в шапке, черновая тёмная палитра. Тесты: персист выбора, игнор мусора в localStorage, применение атрибута (стаб document). - **A. Инфраструктура**`theme.svelte.ts`, скрипт в `app.html`, переключатель
- **B. Палитра** — выверка значений: герой, каталог, страница инструмента, цепочка, баннер ошибки, маска, текстовые инструменты; фокус-ринги и hover; обе локали. в шапке, черновая тёмная палитра. Тесты: персист выбора, игнор мусора в
localStorage, применение атрибута (стаб document).
- **B. Палитра** — выверка значений: герой, каталог, страница инструмента,
цепочка, баннер ошибки, маска, текстовые инструменты; фокус-ринги и hover; обе
локали.
## 5. Смоук ## 5. Смоук
@@ -43,4 +57,5 @@ Grep: ни одного цвета вне токенов в компонента
## 7. Не делаем ## 7. Не делаем
Три позиции в переключателе; пользовательские цветовые схемы; автосмену по времени суток; отдельную тему для превью изображений. Три позиции в переключателе; пользовательские цветовые схемы; автосмену по
времени суток; отдельную тему для превью изображений.
+56 -24
View File
@@ -4,57 +4,87 @@
## 1. Зачем ## 1. Зачем
- Страница инструмента (`ToolPage.svelte`) разрослась: состояние процесса, загрузка файлов, вставка из буфера и разметка трёх зон живут в одном файле. - Страница инструмента (`ToolPage.svelte`) разрослась: состояние процесса,
- Назревают два требования к процессу: параметры применяются автоматически без кнопки; у части утилит на экране показывается одно — визуализация фильтра/маски, а скачивается (и в будущем передаётся следующему инструменту) другое — результат. загрузка файлов, вставка из буфера и разметка трёх зон живут в одном файле.
- Назревают два требования к процессу: параметры применяются автоматически без
кнопки; у части утилит на экране показывается одно — визуализация
фильтра/маски, а скачивается (и в будущем передаётся следующему инструменту)
другое — результат.
## 2. Контракт «результат и маска» в реестре ## 2. Контракт «результат и маска» в реестре
- `run(img, params)` — как сейчас: возвращает итоговое изображение. Его скачивают и будут передавать дальше по цепочке. - `run(img, params)` — как сейчас: возвращает итоговое изображение. Его
- Запись реестра получает опциональное поле `preview?(img, params)` — возвращает картинку для показа на экране (маску/фильтр). скачивают и будут передавать дальше по цепочке.
- Если у записи есть `preview`, в карточке результата появляется переключатель «Показать маску». Включён — на превью маска, выключен (по умолчанию) — результат. Скачивание всегда отдаёт результат `run`, независимо от положения переключателя. - Запись реестра получает опциональное поле `preview?(img, params)` — возвращает
- Первая реализация — удаление цвета: в ядро добавляется чистая функция `colorMask(img, hex, tolerance)` — чёрно-белая маска, где удаляемые пиксели белые; с юнит-тестами; `preview` записи вызывает её. картинку для показа на экране (маску/фильтр).
- Если у записи есть `preview`, в карточке результата появляется переключатель
«Показать маску». Включён — на превью маска, выключен (по умолчанию) —
результат. Скачивание всегда отдаёт результат `run`, независимо от положения
переключателя.
- Первая реализация — удаление цвета: в ядро добавляется чистая функция
`colorMask(img, hex, tolerance)` — чёрно-белая маска, где удаляемые пиксели
белые; с юнит-тестами; `preview` записи вызывает её.
## 3. Декомпозиция страницы ## 3. Декомпозиция страницы
Новая папка `components/tool/`; карточки — чисто отображательные, состояние живёт только в оркестраторе: Новая папка `components/tool/`; карточки — чисто отображательные, состояние
живёт только в оркестраторе:
- **`SourceCard.svelte`** — левая половина сцены: пусто → зона перетаскивания; загружено → превью исходника под оверлеем замены с кнопкой «Заменить изображение». - **`SourceCard.svelte`** — левая половина сцены: пусто → зона перетаскивания;
- **`ResultCard.svelte`** — правая половина: пустые состояния, превью результата или сводка, чип «Пересчёт…», кнопка скачивания, при наличии `preview` — переключатель маски. загружено → превью исходника под оверлеем замены с кнопкой «Заменить
- **`ParamsCard.svelte`** — форма параметров и подпись «Изменения применяются автоматически»; для инструментов без параметров не выводится. изображение».
- **`ToolPage.svelte`** остаётся оркестратором: состояние (исходник, результат, сводка, статус, ошибка, значения), декодирование файлов, вставка из буфера, сброс, токен запусков и эффект с задержкой. Ожидаемо худеет примерно вдвое. - **`ResultCard.svelte`** — правая половина: пустые состояния, превью результата
или сводка, чип «Пересчёт…», кнопка скачивания, при наличии `preview`
переключатель маски.
- **`ParamsCard.svelte`** — форма параметров и подпись «Изменения применяются
автоматически»; для инструментов без параметров не выводится.
- **`ToolPage.svelte`** остаётся оркестратором: состояние (исходник, результат,
сводка, статус, ошибка, значения), декодирование файлов, вставка из буфера,
сброс, токен запусков и эффект с задержкой. Ожидаемо худеет примерно вдвое.
Карточки переиспользуются будущим многошаговым режимом. Карточки переиспользуются будущим многошаговым режимом.
## 4. Реактивные параметры (в этом же проходе) ## 4. Реактивные параметры (в этом же проходе)
- Кнопки «Применить» больше нет ни у одного инструмента. - Кнопки «Применить» больше нет ни у одного инструмента.
- Изменение параметра запускает вычисление через 300 мс после последнего изменения; каждое новое изменение перезапускает таймер. - Изменение параметра запускает вычисление через 300 мс после последнего
- Токен запусков: если во время вычисления параметры поменялись, устаревший ответ отбрасывается. изменения; каждое новое изменение перезапускает таймер.
- Загрузка и замена файла считают сразу и подавляют ровно один авто-запуск эффекта. - Токен запусков: если во время вычисления параметры поменялись, устаревший
ответ отбрасывается.
- Загрузка и замена файла считают сразу и подавляют ровно один авто-запуск
эффекта.
- Ошибка пересчёта — баннер сверху, прежний результат не сбрасывается. - Ошибка пересчёта — баннер сверху, прежний результат не сбрасывается.
## 5. Этапы и проверки ## 5. Этапы и проверки
- **A. Декомпозиция** — перенос разметки в три карточки один к одному, поведение не меняется. Проверка: `check/build`, визуально идентично. - **A. Декомпозиция** — перенос разметки в три карточки один к одному, поведение
- **B. Реактивность** — токен, задержка, чип пересчёта, минус кнопка. Проверка: смоук §6 пп. 1–6. не меняется. Проверка: `check/build`, визуально идентично.
- **C. Маска** — типы реестра, `colorMask` с тестами, переключатель в карточке результата, пример на удалении цвета. Проверка: тесты + смоук §6 пп. 7–9. - **B. Реактивность** — токен, задержка, чип пересчёта, минус кнопка. Проверка:
смоук §6 пп. 16.
- **C. Маска** — типы реестра, `colorMask` с тестами, переключатель в карточке
результата, пример на удалении цвета. Проверка: тесты + смоук §6 пп. 7–9.
- **D. Итог** — полный прогон `test/check/build`. - **D. Итог** — полный прогон `test/check/build`.
## 6. Смоук-чеклист ## 6. Смоук-чеклист
1. Загрузка файла → результат появляется сразу. 1. Загрузка файла → результат появляется сразу.
2. Изменить параметр → около 0.3 с и результат обновился сам. 2. Изменить параметр → около 0.3 с и результат обновился сам.
3. Быстро изменить яркость несколько раз → одна финальная версия, промежуточные не мелькают. 3. Быстро изменить яркость несколько раз → одна финальная версия, промежуточные
не мелькают.
4. Во время пересчёта чип виден, предыдущий результат не исчезает. 4. Во время пересчёта чип виден, предыдущий результат не исчезает.
5. Инструмент без параметров и информационная страница ведут себя как раньше. 5. Инструмент без параметров и информационная страница ведут себя как раньше.
6. Некорректные параметры (обрезка вне картинки) → баннер ошибки, прежний результат цел. 6. Некорректные параметры (обрезка вне картинки) → баннер ошибки, прежний
7. Переключатель «Показать маску» у удаления цвета: включён — белым по чёрному видно, что удалится; выключен — обычный полупрозрачный результат. результат цел.
8. С включённой маской скачивание всё равно сохраняет прозрачный результат, а не маску. 7. Переключатель «Показать маску» у удаления цвета: включён — белым по чёрному
видно, что удалится; выключен — обычный полупрозрачный результат.
8. С включённой маской скачивание всё равно сохраняет прозрачный результат, а не
маску.
9. Замена исходника возвращает переключатель маски в выключенное состояние. 9. Замена исходника возвращает переключатель маски в выключенное состояние.
## 7. Критерии готовности ## 7. Критерии готовности
- `ToolPage` содержит только оркестрацию состояния и эффектов; вся разметка панелей — в `components/tool/*`. - `ToolPage` содержит только оркестрацию состояния и эффектов; вся разметка
панелей — в `components/tool/*`.
- Кнопки «Применить» нет ни на одной странице. - Кнопки «Применить» нет ни на одной странице.
- Скачивание всегда соответствует `run`; экран может показывать `preview`. - Скачивание всегда соответствует `run`; экран может показывать `preview`.
- `test/check/build` зелёные, смоук §6 пройден. - `test/check/build` зелёные, смоук §6 пройден.
@@ -62,5 +92,7 @@
## 8. Что сознательно не делаем ## 8. Что сознательно не делаем
- Вынос вычислений в веб-воркер; настраиваемая задержка — фиксированные 300 мс. - Вынос вычислений в веб-воркер; настраиваемая задержка — фиксированные 300 мс.
- Редактор масок (кисть, слои, история) — только автогенерация из параметров инструмента. - Редактор масок (кисть, слои, история) — только автогенерация из параметров
- Передача результата между инструментами — придёт вместе с многошаговым режимом позже. инструмента.
- Передача результата между инструментами — придёт вместе с многошаговым режимом
позже.
+74 -30
View File
@@ -4,48 +4,89 @@
> >
> **Что сделано по этапам:** > **Что сделано по этапам:**
> >
> - A — общая MIME-валидация `isSupportedImage` + `unsupportedImageMessage` в `core/io.ts`; `DropZone` переведён на них ✔ > - A — общая MIME-валидация `isSupportedImage` + `unsupportedImageMessage` в
> - B — постоянная двухпанельная раскладка: одна карточка-«сцена» с равными половинами «Исходник | Результат», кнопки внутри своих половин, параметры — отдельной карточкой ниже; контент сайта растянут на всю ширину окна > `core/io.ts`; `DropZone` переведён на них
> - CCtrl+V: `<svelte:window onpaste>`, поиск image-item в буфере, общая валидация; текстовые вставки не затрагиваются ✔ > - Bпостоянная двухпанельная раскладка: одна карточка-«сцена» с равными
> - D — drag-замена: компонент `DropOverlay` с вуалью-подсказкой и счётчиком `dragenter/dragleave` против мерцания; тот же счётчик в `DropZone` > половинами «Исходник | Результат», кнопки внутри своих половин, параметры —
> - E — верификация: тесты 85/85, svelte-check 0 ошибок, статический билд с 11 страницами; ручной смоук-чеклист §5 пройден > отдельной карточкой ниже; контент сайта растянут на всю ширину окна
> - 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-события ловит контент под курсором); раскладка доработана по ревью — одна карточка равных половин вместо двух независимых колонок, сайт без ограничения ширины. > **Отступления от исходного плана в ходе реализации:** вместо пропа `overlay` у
> `DropZone` сделан отдельный компонент-обёртка `DropOverlay` (drag-события
> ловит контент под курсором); раскладка доработана по ревью — одна карточка
> равных половин вместо двух независимых колонок, сайт без ограничения ширины.
## 1. Целевое поведение ## 1. Целевое поведение
1. **Постоянная двухпанельная раскладка** страницы инструмента — отдельного состояния «страница загрузки» больше нет: 1. **Постоянная двухпанельная раскладка** страницы инструмента — отдельного
- **Слева — исходник**, панель есть всегда. Не загружено → пустое дроп-поле (drag&drop или клик). Загружено → превью исходника. состояния «страница загрузки» больше нет:
- **Справа — результат**: до загрузки — `EmptyState` «Результат появится здесь»; во время обработки — `EmptyState` «Обработка…»; после — превью результата (для info-инструмента — `InfoPanel`). - **Слева — исходник**, панель есть всегда. Не загружено → пустое дроп-поле
2. **Ctrl+V** — картинка из буфера обмена вставляется как исходник с любого места страницы. (drag&drop или клик). Загружено → превью исходника.
3. **Замена перетаскиванием** — новый файл, брошенный на левую панель с загруженной картинкой, заменяет исходник (с оверлеем-подсказкой во время перетаскивания). - **Справа — результат**: до загрузки — `EmptyState` «Результат появится
4. **Кнопка «Заменить»** под превью исходника (переносится туда текущая «Загрузить другое изображение»). здесь»; во время обработки — `EmptyState` «Обработка…»; после — превью
результата (для info-инструмента — `InfoPanel`).
2. **Ctrl+V** — картинка из буфера обмена вставляется как исходник с любого
места страницы.
3. **Замена перетаскиванием** — новый файл, брошенный на левую панель с
загруженной картинкой, заменяет исходник (с оверлеем-подсказкой во время
перетаскивания).
4. **Кнопка «Заменить»** под превью исходника (переносится туда текущая
«Загрузить другое изображение»).
На мобильной ширине панели складываются в столбец: исходник сверху, результат снизу. На мобильной ширине панели складываются в столбец: исходник сверху, результат
снизу.
## 2. Изменения по файлам ## 2. Изменения по файлам
Новые компоненты не вводим — `EmptyState`, `Preview`, `DropZone` покрывают всё. Меняем три файла: Новые компоненты не вводим — `EmptyState`, `Preview`, `DropZone` покрывают всё.
Меняем три файла:
- **`core/io.ts`** — экспорт `isSupportedImage(file): boolean`: общая MIME-проверка на базе `ACCEPTED_IMAGE_TYPES`. - **`core/io.ts`** — экспорт `isSupportedImage(file): boolean`: общая
- **`DropZone.svelte`** — переходит на `isSupportedImage`; новый опциональный проп `overlay` — режим «невидимая зона приёма поверх контента» для замены перетаскиванием. MIME-проверка на базе `ACCEPTED_IMAGE_TYPES`.
- **`ToolPage.svelte`** — постоянная сетка из двух панелей; `<svelte:window onpaste>`; drag-оверлей левой панели; кнопки замены/сброса переезжают в левую панель; правая панель — три состояния через `EmptyState`. - **`DropZone.svelte`** — переходит на `isSupportedImage`; новый опциональный
проп `overlay` — режим «невидимая зона приёма поверх контента» для замены
перетаскиванием.
- **`ToolPage.svelte`** — постоянная сетка из двух панелей;
`<svelte:window onpaste>`; drag-оверлей левой панели; кнопки замены/сброса
переезжают в левую панель; правая панель — три состояния через `EmptyState`.
## 3. Этапы и проверки ## 3. Этапы и проверки
- **A. Общая валидация**`isSupportedImage` в `io.ts`, `DropZone` переведён на неё. Проверка: `pnpm check`. - **A. Общая валидация**`isSupportedImage` в `io.ts`, `DropZone` переведён на
- **B. Раскладка** — две постоянные колонки в ToolPage; правая панель через EmptyState; кнопки уходят в левую панель; обёртка `{#if !source}` на всю страницу убирается. Проверка: визуальный осмотр всех инструментов, `check/build`. неё. Проверка: `pnpm check`.
- **C. Ctrl+V**`onpaste` на окне → поиск image-item в `clipboardData` → тот же `handleFile`; не-изображения игнорируются молча. Проверка: ручной смоук со скриншотом из буфера. - **B. Раскладка** — две постоянные колонки в ToolPage; правая панель через
- **D. Drag-замена** — оверлей DropZone(overlay) при dragover на левой панели; drop → `handleFile` нового файла. Проверка: ручной смоук «бросить файл на загруженный исходник». EmptyState; кнопки уходят в левую панель; обёртка `{#if !source}` на всю
- **E. Итог** — полный ручной смоук-чеклист (§5). Проверка: `test/check/build` зелёные. страницу убирается. Проверка: визуальный осмотр всех инструментов,
`check/build`.
- **C. Ctrl+V**`onpaste` на окне → поиск image-item в `clipboardData` → тот
же `handleFile`; не-изображения игнорируются молча. Проверка: ручной смоук со
скриншотом из буфера.
- **D. Drag-замена** — оверлей DropZone(overlay) при dragover на левой панели;
drop → `handleFile` нового файла. Проверка: ручной смоук «бросить файл на
загруженный исходник».
- **E. Итог** — полный ручной смоук-чеклист (§5). Проверка: `test/check/build`
зелёные.
## 4. Нюансы и решения ## 4. Нюансы и решения
- **Мерцание dragleave** — классическая проблема оверлеев: используем счётчик `dragenter/dragleave`, оверлей скрывается только когда счётчик обнулился и произошёл `drop`/`dragend`. - **Мерцание dragleave** — классическая проблема оверлеев: используем счётчик
- **Paste при фокусе в поле параметров** — обрабатываем всегда: если в буфере именно изображение, пользователь очевидно хочет заменить исходник; текстовые вставки обработчик не трогает. `dragenter/dragleave`, оверлей скрывается только когда счётчик обнулился и
- **Форматы буфера** — скриншоты приходят как PNG, часть приложений отдаёт JPEG/WebP; все проходят общую `isSupportedImage`. GIF/BMP из буфера редки, но тоже допустимы. произошёл `drop`/`dragend`.
- **Info-инструменты** — ведут себя так же: слева исходник, справа после загрузки появляется `InfoPanel`. - **Paste при фокусе в поле параметров** — обрабатываем всегда: если в буфере
- **Состояние ошибки** — баннер остаётся сверху страницы; неудачная замена не сбрасывает старый исходник. именно изображение, пользователь очевидно хочет заменить исходник; текстовые
вставки обработчик не трогает.
- **Форматы буфера** — скриншоты приходят как PNG, часть приложений отдаёт
JPEG/WebP; все проходят общую `isSupportedImage`. GIF/BMP из буфера редки, но
тоже допустимы.
- **Info-инструменты** — ведут себя так же: слева исходник, справа после
загрузки появляется `InfoPanel`.
- **Состояние ошибки** — баннер остаётся сверху страницы; неудачная замена не
сбрасывает старый исходник.
## 5. Чеклист ручного смоука ## 5. Чеклист ручного смоука
@@ -54,7 +95,8 @@
3. Ctrl+V со скриншотом → исходник заменился, результат пересчитался. 3. Ctrl+V со скриншотом → исходник заменился, результат пересчитался.
4. Drag файла на загруженную левую панель → оверлей виден, после drop — замена. 4. Drag файла на загруженную левую панель → оверлей виден, после drop — замена.
5. Отмена перетаскивания (Esc / мимо) → оверлей скрылся, исходник не изменился. 5. Отмена перетаскивания (Esc / мимо) → оверлей скрылся, исходник не изменился.
6. Бросок неподдерживаемого файла (например .txt) → баннер ошибки, старый исходник цел. 6. Бросок неподдерживаемого файла (например .txt) → баннер ошибки, старый
исходник цел.
7. Кнопка «Заменить» открывает выбор файла. 7. Кнопка «Заменить» открывает выбор файла.
8. png-info: работает та же схема, справа сводка. 8. png-info: работает та же схема, справа сводка.
9. Мобильная ширина: панели в столбец, порядок исходник → результат. 9. Мобильная ширина: панели в столбец, порядок исходник → результат.
@@ -62,7 +104,8 @@
## 6. Критерии готовности ## 6. Критерии готовности
- На странице инструмента нет состояния «только дропзона на всю страницу» — раскладка двухпанельная всегда. - На странице инструмента нет состояния «только дропзона на всю страницу» —
раскладка двухпанельная всегда.
- Загрузка работает тремя способами: клик, drag&drop (включая замену), Ctrl+V. - Загрузка работает тремя способами: клик, drag&drop (включая замену), Ctrl+V.
- Ошибочные файлы и отмена перетаскивания не портят текущий исходник. - Ошибочные файлы и отмена перетаскивания не портят текущий исходник.
- `test/check/build` зелёные; чеклист §5 пройден. - `test/check/build` зелёные; чеклист §5 пройден.
@@ -71,4 +114,5 @@
- Множественная загрузка / пакетная обработка — вне скоупа этого плана. - Множественная загрузка / пакетная обработка — вне скоупа этого плана.
- Камера/скриншот-инструменты захвата — только буфер обмена. - Камера/скриншот-инструменты захвата — только буфер обмена.
- Прогресс-бары декодирования — файлы читаются быстро, достаточно индикации «Обработка…». - Прогресс-бары декодирования — файлы читаются быстро, достаточно индикации
«Обработка…».
+92 -37
View File
@@ -1,75 +1,130 @@
# План: главная страница как рабочая область с цепочкой # План: главная страница как рабочая область с цепочкой
> **СТАТУС: ВЫПОЛНЕН 2026-08-23.** Заменяет идею отдельной страницы рабочего пространства. > **СТАТУС: ВЫПОЛНЕН 2026-08-23.** Заменяет идею отдельной страницы рабочего
> пространства.
## 1. Модель ## 1. Модель
1. **Главная страница — это рабочая область.** Без выбранного инструмента на ней большой поиск: начните вводить название — список отфильтровывается; выбор разворачивает инструмент прямо здесь, без перехода. 1. **Главная страница — это рабочая область.** Без выбранного инструмента на ней
2. Развернутый инструмент выглядит как привычная страница: слева исходник, справа результат со своей кнопкой скачивания, параметры — в отдельной карточке под сценой. большой поиск: начните вводить название — список отфильтровывается; выбор
3. Под результатом рядом с кнопкой скачивания — кнопка **«⛓ Chain with…»**: добавляет пустую секцию, внутри которой такой же поиск; выбор заполняет секцию следующим инструментом. Так цепочка растёт сколько угодно. разворачивает инструмент прямо здесь, без перехода.
4. Каждая заполненная секция цепи — полноценная: свои параметры, своё превью, своя кнопка скачивания промежуточного результата. Крестик убирает секцию; кнопка «✂ Remove chain» снимает всю цепь разом (без подтверждения). 2. Развернутый инструмент выглядит как привычная страница: слева исходник,
5. Чекбокс «Показать маску» живёт в карточке параметров базового инструмента и влияет только на его превью: в цепь всегда передаётся сам результат. справа результат со своей кнопкой скачивания, параметры — в отдельной
6. Сетка-каталог инструментов переезжает на отдельную страницу `/list-tools` — для SEO и визуального поиска. Основная работа происходит на главной; в шапке появляются ссылки на обе. карточке под сценой.
7. Прямые адреса `/tools/<id>` сохраняются: тот же развернутый инструмент для глубоких ссылок. 3. Под результатом рядом с кнопкой скачивания — кнопка **«⛓ Chain with…»**:
8. Цепочка переживает перезагрузку (автосохранение в localStorage). Экспорт/импорт файлом отложен до появления CLI/wasm-части. добавляет пустую секцию, внутри которой такой же поиск; выбор заполняет
секцию следующим инструментом. Так цепочка растёт сколько угодно.
4. Каждая заполненная секция цепи — полноценная: свои параметры, своё превью,
своя кнопка скачивания промежуточного результата. Крестик убирает секцию;
кнопка «✂ Remove chain» снимает всю цепь разом (без подтверждения).
5. Чекбокс «Показать маску» живёт в карточке параметров базового инструмента и
влияет только на его превью: в цепь всегда передаётся сам результат.
6. Сетка-каталог инструментов переезжает на отдельную страницу `/list-tools`
для SEO и визуального поиска. Основная работа происходит на главной; в шапке
появляются ссылки на обе.
7. Прямые адреса `/tools/<id>` сохраняются: тот же развернутый инструмент для
глубоких ссылок.
8. Цепочка переживает перезагрузку (автосохранение в localStorage).
Экспорт/импорт файлом отложен до появления CLI/wasm-части.
## 2. Архитектура ## 2. Архитектура
- Страница инструмента уже собрана как самодостаточный компонент, принимающий запись реестра, — он и становится встраиваемым блоком. Маршруты `/tools/[id]` и главная используют один и тот же компонент: первая — по адресу из URL, вторая — по выбору из поиска. - Страница инструмента уже собрана как самодостаточный компонент, принимающий
- Состояние главной: выбран ли инструмент; если да — рендерится блок инструмента, целиком владеющий своим исходником, параметрами, результатом и цепочкой. запись реестра, — он и становится встраиваемым блоком. Маршруты `/tools/[id]`
и главная используют один и тот же компонент: первая — по адресу из URL,
вторая — по выбору из поиска.
- Состояние главной: выбран ли инструмент; если да — рендерится блок
инструмента, целиком владеющий своим исходником, параметрами, результатом и
цепочкой.
- Смена инструмента на главной (через повторный поиск) сбрасывает блок целиком. - Смена инструмента на главной (через повторный поиск) сбрасывает блок целиком.
- Цепочка хранится массивом шагов; у пустого звена инструмент ещё не выбран — такие звенья не сохраняются в localStorage и не участвуют в прогоне. - Цепочка хранится массивом шагов; у пустого звена инструмент ещё не выбран —
- Исполнение — последовательная свёртка: выход каждого звена — вход следующего; результаты всех звеньев доступны их секциям. Общий авто-раннер даёт токен отмены и задержку ~300 мс. такие звенья не сохраняются в localStorage и не участвуют в прогоне.
- Исполнение — последовательная свёртка: выход каждого звена — вход следующего;
результаты всех звеньев доступны их секциям. Общий авто-раннер даёт токен
отмены и задержку ~300 мс.
## 3. Переиспользование и доработки существующего ## 3. Переиспользование и доработки существующего
Готовое берётся как есть: `SourceCard`, `Preview`, `ParamForm`, `DownloadButton`, `EmptyState`, модуль пайплайна, авто-раннер, `PNG_OUTPUT`. Готовое берётся как есть: `SourceCard`, `Preview`, `ParamForm`,
`DownloadButton`, `EmptyState`, модуль пайплайна, авто-раннер, `PNG_OUTPUT`.
Доработки: Доработки:
- **Блок страницы инструмента** — расширяется секцией цепочки: список заполненных звеньев, каждое со своими параметрами, превью и скачиванием, плюс пустые звенья с поиском. Кнопки «⛓» и «✂ Remove chain» — рядом со скачиванием базового результата. - **Блок страницы инструмента** — расширяется секцией цепочки: список
- **`ParamsCard`** — опциональный блок «Показать маску» (флаг preview у инструмента + двусторонний флаг). заполненных звеньев, каждое со своими параметрами, превью и скачиванием, плюс
- **Карточка результата** — упрощается: получает готовую картинку, маска уходит в карточку параметров. пустые звенья с поиском. Кнопки «⛓» и «✂ Remove chain» — рядом со скачиванием
- **Существующая главная-каталог** — переезжает на маршрут `/list-tools` почти без изменений. базового результата.
- **`ParamsCard`** — опциональный блок «Показать маску» (флаг preview у
инструмента + двусторонний флаг).
- **Карточка результата** — упрощается: получает готовую картинку, маска уходит
в карточку параметров.
- **Существующая главная-каталог** — переезжает на маршрут `/list-tools` почти
без изменений.
Удаляется: черновой маршрут рабочего пространства и его компоненты списка/селектора — вместо них новые, под вертикальную модель. Удаляется: черновой маршрут рабочего пространства и его компоненты
списка/селектора — вместо них новые, под вертикальную модель.
## 4. Новые компоненты ## 4. Новые компоненты
- **`ToolSearch.svelte`** — поиск с нечётким совпадением: поле ввода, отфильтрованный список (регистронезависимая подпоследовательность по названию и описанию, приоритет совпадению с начала), клавиатура ↑↓ Enter Esc; два размера — герой на главной и компактный внутри пустого звена. Предлагает только инструменты-трансформеры. - **`ToolSearch.svelte`** — поиск с нечётким совпадением: поле ввода,
- **`ChainStep.svelte`** — секция одного звена: пустое звено показывает компактный поиск; заполненное — заголовок с номером и крестиком, форму параметров, превью результата, кнопку скачивания. отфильтрованный список (регистронезависимая подпоследовательность по названию
- **`ChainBar.svelte`** — строка под результатом базового инструмента: «⛓ Chain with…» и «✂ Remove chain» (видна только при непустой цепи). и описанию, приоритет совпадению с начала), клавиатура ↑↓ Enter Esc; два
размера — герой на главной и компактный внутри пустого звена. Предлагает
только инструменты-трансформеры.
- **`ChainStep.svelte`** — секция одного звена: пустое звено показывает
компактный поиск; заполненное — заголовок с номером и крестиком, форму
параметров, превью результата, кнопку скачивания.
- **`ChainBar.svelte`** — строка под результатом базового инструмента: «⛓ Chain
with…» и «✂ Remove chain» (видна только при непустой цепи).
## 5. Этапы и проверки ## 5. Этапы и проверки
- **A. Поиск и главная**`ToolSearch`, герой поиска на главной, встраивание блока инструмента, перенос каталога на `/list-tools`, ссылки в шапке. Проверка: `check/build`, визуально. - **A. Поиск и главная**`ToolSearch`, герой поиска на главной, встраивание
- **B. Цепочка** — состояние цепи в блоке инструмента, пустые звенья с поиском, прогон, промежуточные превью и скачивания, ошибки звеньев. Проверка: смоук §8 пп. 1–6. блока инструмента, перенос каталога на `/list-tools`, ссылки в шапке.
- **C. Маска и упрощение** — блок маски в `ParamsCard`, упрощение карточки результата. Проверка: смоук §8 п. 7. Проверка: `check/build`, визуально.
- **D. Хранение и итог** — автосохранение и восстановление цепочки, удаление чернового маршрута рабочего пространства, полный регресс каталога, `test/check/build`. - **B. Цепочка** — состояние цепи в блоке инструмента, пустые звенья с поиском,
прогон, промежуточные превью и скачивания, ошибки звеньев. Проверка: смоук §8
пп. 16.
- **C. Маска и упрощение** — блок маски в `ParamsCard`, упрощение карточки
результата. Проверка: смоук §8 п. 7.
- **D. Хранение и итог** — автосохранение и восстановление цепочки, удаление
чернового маршрута рабочего пространства, полный регресс каталога,
`test/check/build`.
## 6. Смоук-чеклист ## 6. Смоук-чеклист
1. На главной — большое поле поиска; ввод фильтрует список по мере набора; Enter выбирает подсвеченный. 1. На главной — большое поле поиска; ввод фильтрует список по мере набора; Enter
2. Выбранный инструмент разворачивается на той же странице: исходник, параметры, результат. выбирает подсвеченный.
3. «⛓ Chain with…» добавляет пустую секцию с компактным поиском; выбор заполняет её. 2. Выбранный инструмент разворачивается на той же странице: исходник, параметры,
результат.
3. «⛓ Chain with…» добавляет пустую секцию с компактным поиском; выбор заполняет
её.
4. У каждой секции своё превью и своё скачивание промежуточного результата. 4. У каждой секции своё превью и своё скачивание промежуточного результата.
5. Крестик убирает секцию; «✂ Remove chain» снимает все звенья разом. 5. Крестик убирает секцию; «✂ Remove chain» снимает все звенья разом.
6. Ошибка звена — баннер «Шаг N (имя): …»; последние успешные результаты секций не исчезают. 6. Ошибка звена — баннер «Шаг N (имя): …»; последние успешные результаты секций
7. «Показать маску» переключает превью базового инструмента; в цепь идёт результат, скачивание базового отдаёт результат, не маску. не исчезают.
8. Перезагрузка восстанавливает цепочку заполненных звеньев; пустые звенья при этом исчезают. 7. «Показать маску» переключает превью базового инструмента; в цепь идёт
результат, скачивание базового отдаёт результат, не маску.
8. Перезагрузка восстанавливает цепочку заполненных звеньев; пустые звенья при
этом исчезают.
9. Быстрые правки подряд — один финальный пересчёт всей вертикали. 9. Быстрые правки подряд — один финальный пересчёт всей вертикали.
10. `/list-tools` открывает старый каталог по категориям; прямые адреса `/tools/<id>` работают. 10. `/list-tools` открывает старый каталог по категориям; прямые адреса
`/tools/<id>` работают.
## 7. Критерии готовности ## 7. Критерии готовности
- Работа начинается с главной: поиск → инструмент → цепочка звеньями, каждый со своим превью и скачиванием. - Работа начинается с главной: поиск → инструмент → цепочка звеньями, каждый со
- Отдельной страницы рабочего пространства нет; каталог доступен на `/list-tools`; прямые ссылки инструментов работают. своим превью и скачиванием.
- Отдельной страницы рабочего пространства нет; каталог доступен на
`/list-tools`; прямые ссылки инструментов работают.
- Пустые звенья не попадают в сохранение и не ломают прогон. - Пустые звенья не попадают в сохранение и не ломают прогон.
- `test/check/build` зелёные, смоук §6 пройден. - `test/check/build` зелёные, смоук §6 пройден.
## 8. Что сознательно не делаем ## 8. Что сознательно не делаем
- Ветвления и прицепывание от середины цепи — новое звено всегда добавляется в конец. - Ветвления и прицепывание от середины цепи — новое звено всегда добавляется в
конец.
- Пипетка и маски внутри звеньев цепи — они остаются фичей базового инструмента. - Пипетка и маски внутри звеньев цепи — они остаются фичей базового инструмента.
- Экспорт и импорт цепочки файлом — отложено до появления CLI/wasm-части. - Экспорт и импорт цепочки файлом — отложено до появления CLI/wasm-части.
- Несколько параллельных инструментов на главной одновременно — один активный блок за раз. - Несколько параллельных инструментов на главной одновременно — один активный
блок за раз.
+10 -10
View File
@@ -4,8 +4,8 @@
> (`web/e2e/`, `pnpm --dir web test:e2e`). Автоматизированная часть закрывает > (`web/e2e/`, `pnpm --dir web test:e2e`). Автоматизированная часть закрывает
> «нет падений/ошибок»; здесь — то, что руками, и что Playwright не покрывает. > «нет падений/ошибок»; здесь — то, что руками, и что Playwright не покрывает.
> >
> Перед прогоном: `pnpm --dir web build`, поднять локально `web/` (dev или > Перед прогоном: `pnpm --dir web build`, поднять локально `web/` (dev или build
> build`node scripts/serve-static.mjs`). > → `node scripts/serve-static.mjs`).
> >
> Критерий приёмки большинства пунктов: **нет зависит, нет «залипания», > Критерий приёмки большинства пунктов: **нет зависит, нет «залипания»,
> сообщения об ошибках человекочитаемые**. > сообщения об ошибках человекочитаемые**.
@@ -25,8 +25,8 @@
## B. Края параметров ## B. Края параметров
- [ ] Экстремальные значения слайдеров (0 и max) в blur/sharpen/pixelate — - [ ] Экстремальные значения слайдеров (0 и max) в blur/sharpen/pixelate — не
не «залипает» running. «залипает» running.
- [ ] Пустые/NaN поля (напр. width/height) — санитизация (silent repair), нет - [ ] Пустые/NaN поля (напр. width/height) — санитизация (silent repair), нет
красного alert-текста в виде `errors.*`. красного alert-текста в виде `errors.*`.
- [ ] Невалидные цвета в плашках/градиентах (мисс-спелл `#xyz`, короткие hex, - [ ] Невалидные цвета в плашках/градиентах (мисс-спелл `#xyz`, короткие hex,
@@ -37,17 +37,17 @@
## C. Буфер обмена и drag-n-drop ## C. Буфер обмена и drag-n-drop
- [ ] В реальном браузере кнопка copy на текстовом результате (png-to-base64 - [ ] В реальном браузере кнопка copy на текстовом результате (png-to-base64 и
и т.п.) кладёт текст в буфер (в Playwright не тестируем из-за permissions). т.п.) кладёт текст в буфер (в Playwright не тестируем из-за permissions).
- [ ] Drag-n-drop файла на dropzone страницы инструмента (не только - [ ] Drag-n-drop файла на dropzone страницы инструмента (не только файл-диалог)
файл-диалог) — UX-ощущения и корректность. — UX-ощущения и корректность.
## D. Производительность и отзывчивость ## D. Производительность и отзывчивость
- [ ] Долгие авто-рераны на больших картинках (jpeg-artifacts, dithering, - [ ] Долгие авто-рераны на больших картинках (jpeg-artifacts, dithering,
quantize на 4К) — UI не фризит, канселяция/дебаунс срабатывает. quantize на 4К) — UI не фризит, канселяция/дебаунс срабатывает.
- [ ] Скролл страницы при длинных настройках (например, text-инструменты) — - [ ] Скролл страницы при длинных настройках (например, text-инструменты) — нет
нет залипаний, панели не прыгают. залипаний, панели не прыгают.
- [ ] Вкладка не «съедает» память при 10+ повторах blur/sharpen на мегапикселе - [ ] Вкладка не «съедает» память при 10+ повторах blur/sharpen на мегапикселе
(утечек blob-URL/bitmap быть не должно). (утечек blob-URL/bitmap быть не должно).
+195 -181
View File
@@ -1,40 +1,41 @@
# План: переезд на новый дизайн (refs) — параллельная сборка # План: переезд на новый дизайн (refs) — параллельная сборка
> Статус: план к выполнению. Шаги 5–6 (C17–C21) остаются **следующим этапом**. > Статус: план к выполнению. Шаги 5–6 (C17–C21) остаются **следующим этапом**.
> Они больше не привязаны к «верности против рефа»: `docs/archive/plan-design-fix.md` > Они больше не привязаны к «верности против рефа»:
> закрыт (2026-09-07, дизайн ушёл вперёд от `refs-html/`), аудит против рефа > `docs/archive/plan-design-fix.md` закрыт (2026-09-07, дизайн ушёл вперёд от
> не информативен и прекращён. Дальнейшее качество — ручное ревью + `lint:all`. > `refs-html/`), аудит против рефа не информативен и прекращён. Дальнейшее
> Шаг 37 (переезд старых файлов в папки `old/`) вынесен в `backlog.md`. > качество — ручное ревью + `lint:all`. Шаг 37 (переезд старых файлов в папки
> `old/`) вынесен в `backlog.md`.
> >
> Источники: `refs/` — Next/React-референс (правда по пикселям и интерактиву), > Источники: `refs/` — Next/React-референс (правда по пикселям и интерактиву),
> `refs-html/` — статические HTML-снимки для быстрого просмотра в браузере. > `refs-html/` — статические HTML-снимки для быстрого просмотра в браузере.
> >
> **Стратегия изменена.** Вместо in-place рестайла (старый план A–F, правка > **Стратегия изменена.** Вместо in-place рестайла (старый план A–F, правка
> общих компонентов на месте с алиасами токенов) — **параллельная сборка**: > общих компонентов на месте с алиасами токенов) — **параллельная сборка**:
> новый дизайн строится отдельно, изолируется от старого, проверяется > новый дизайн строится отдельно, изолируется от старого, проверяется целиком,
> целиком, затем становится основным и старый удаляется. Старое приложение > затем становится основным и старый удаляется. Старое приложение остаётся
> остаётся рабочим и шippable на всём протяжении перехода. > рабочим и шippable на всём протяжении перехода.
## 0. Почему меняем стратегию ## 0. Почему меняем стратегию
- **Старый план (A–F):** правим `ui/*` и страницы на месте, держим старые - **Старый план (A–F):** правим `ui/*` и страницы на месте, держим старые токены
токены как алиасы, чтобы ничего не сломалось до этапа F. Риск: общие как алиасы, чтобы ничего не сломалось до этапа F. Риск: общие компоненты
компоненты редактируются вживую → регресс старого UI во время работы, редактируются вживую → регресс старого UI во время работы, новый дизайн нельзя
новый дизайн нельзя принять изолированно. принять изолированно.
- **Новый подход (5 фаз):** (1) строим новый ui-kit, (2) строим новые - **Новый подход (5 фаз):** (1) строим новый ui-kit, (2) строим новые страницы
страницы рядом, не трогая старые, (3) проверяем новый дизайн, (4) делаем рядом, не трогая старые, (3) проверяем новый дизайн, (4) делаем его основным,
его основным, (5) удаляем старый. Ниже фазы 1–5 — это требуемые шаги. (5) удаляем старый. Ниже фазы 1–5 — это требуемые шаги.
- **Главный технический риск и его решение.** Сейчас `web/src/app.css` - **Главный технический риск и его решение.** Сейчас `web/src/app.css`
импортируется в **корневом** `web/src/routes/+layout.svelte:2` и потому импортируется в **корневом** `web/src/routes/+layout.svelte:2` и потому
грузится на ВСЕХ маршрутах, включая новые. Решение — **не префиксовать грузится на ВСЕХ маршрутах, включая новые. Решение — **не префиксовать новый
новый CSS (никакого `.ds2`), а разнести глобальные стили по layout'ам без CSS (никакого `.ds2`), а разнести глобальные стили по layout'ам без
наследования**: (а) убрать `import '../app.css'` из корневого layout; наследования**: (а) убрать `import '../app.css'` из корневого layout; (б)
(б) старые маршруты обернуть группой `(old)/` с собственным layout, старые маршруты обернуть группой `(old)/` с собственным layout, импортирующим
импортирующим `app.css`; (в) новые маршруты (`preview/*`) — собственный `app.css`; (в) новые маршруты (`preview/*`) — собственный layout,
layout, импортирующий `design2.css`. SvelteKit делает code-splitting CSS импортирующий `design2.css`. SvelteKit делает code-splitting CSS по маршрутам,
по маршрутам, а стили компонентов скоупятся сами — поэтому глобальные а стили компонентов скоупятся сами — поэтому глобальные стили каждого дизайна
стили каждого дизайна живут только в своей ветке, и префикс-обёртка живут только в своей ветке, и префикс-обёртка `.ds2` не нужна. Старые `ui/` и
`.ds2` не нужна. Старые `ui/` и страницы не трогаются до фазы 5. страницы не трогаются до фазы 5.
## 1. Источники и их оценка ## 1. Источники и их оценка
@@ -57,10 +58,11 @@
`panel-heading`, `setting-group`, `settings-footer`, `settings-panel`, `panel-heading`, `setting-group`, `settings-footer`, `settings-panel`,
`segmented`, `status`, `step-card` (`step-index`/`step-heading`/`step-type`/ `segmented`, `status`, `step-card` (`step-index`/`step-heading`/`step-type`/
`step-body`/`drag`), `color-field` (`swatch`/`native-color`), `step-body`/`drag`), `color-field` (`swatch`/`native-color`),
`preview-toolbar`/`checker` (`tile-canvas`/`remover-canvas`/`comparison-grid`), `preview-toolbar`/`checker`
`result-meta`/`meta-row`, `tool-card`/`image-card` (`tool-icon`/`tool-index`/ (`tile-canvas`/`remover-canvas`/`comparison-grid`), `result-meta`/`meta-row`,
`tool-copy`), `code-block`, `icon-btn`, `download-btn`, `reset-btn`, `lede`, `tool-card`/`image-card` (`tool-icon`/`tool-index`/ `tool-copy`),
`auto-note`, `label`, `toggle`/`toggle-group`, `direction-grid`, `gradient-bar`. `code-block`, `icon-btn`, `download-btn`, `reset-btn`, `lede`, `auto-note`,
`label`, `toggle`/`toggle-group`, `direction-grid`, `gradient-bar`.
- **Дыры в refs-html:** нет состояний empty/error/loading, нет глобальных - **Дыры в refs-html:** нет состояний empty/error/loading, нет глобальных
модалок/тултипов, i18n не представлен (все лейблы — английские брендовые). модалок/тултипов, i18n не представлен (все лейблы — английские брендовые).
Решения — см. §9. Решения — см. §9.
@@ -68,9 +70,9 @@
## 2. Токены (фундамент нового дизайна) ## 2. Токены (фундамент нового дизайна)
Живут в `src/lib/styles/design2.css` как обычный `:root { … }` (плюс Живут в `src/lib/styles/design2.css` как обычный `:root { … }` (плюс
`[data-theme='dark'] { … }`). Файл грузится **только** layout'ом новой `[data-theme='dark'] { … }`). Файл грузится **только** layout'ом новой ветки
ветки (§5), поэтому глобально не конфликтует со старым `app.css`. Маппинг (§5), поэтому глобально не конфликтует со старым `app.css`. Маппинг старых
старых токенов `web/src/app.css` → новые: токенов `web/src/app.css` → новые:
| Старый | Новый | Light | Dark | | Старый | Новый | Light | Dark |
| -------------- | -------------- | --------- | ------------------------ | | -------------- | -------------- | --------- | ------------------------ |
@@ -90,45 +92,45 @@
- Радиусы: один `--radius: 4px` (мягкость старой темы уходит). - Радиусы: один `--radius: 4px` (мягкость старой темы уходит).
- Шрифты: `IBM Plex Sans` + `IBM Plex Mono`, self-host через `@fontsource` - Шрифты: `IBM Plex Sans` + `IBM Plex Mono`, self-host через `@fontsource`
(offline-first, CDN нельзя). Mono — рабочий шрифт для лейблов/метаданных/ (offline-first, CDN нельзя). Mono — рабочий шрифт для лейблов/метаданных/
цифр; Sans — заголовки и текст. Задаются на `.app-shell` (корень новой цифр; Sans — заголовки и текст. Задаются на `.app-shell` (корень новой ветки),
ветки), чтобы не зависеть от старых глобальных правил `body`. чтобы не зависеть от старых глобальных правил `body`.
- Фон-сетка (32px blueprint-grid) и шахматка канвасов — утилиты `.page-grid` - Фон-сетка (32px blueprint-grid) и шахматка канвасов — утилиты `.page-grid` и
и `.checker-canvas` внутри `.ds2`, не токены. `.checker-canvas` внутри `.ds2`, не токены.
- Тёмная тема — на `[data-theme='dark']` (инлайн-скрипт уже есть в - Тёмная тема — на `[data-theme='dark']` (инлайн-скрипт уже есть в `app.html`);
`app.html`); класс `.dark-mode` из рефа не переносим, используем наш класс `.dark-mode` из рефа не переносим, используем наш механизм. На
механизм. На переходный период **никаких алиасов старых токенов** (старые переходный период **никаких алиасов старых токенов** (старые страницы сами
страницы сами живут на своём `app.css`). живут на своём `app.css`).
## 3. Базовые примитивы дизайн-системы (все — компоненты) ## 3. Базовые примитивы дизайн-системы (все — компоненты)
Правило фазы 1: **любой повторяющийся паттерн — компонент**, даже «просто Правило фазы 1: **любой повторяющийся паттерн — компонент**, даже «просто div с
div с двумя стилями». Никакой копипасты классов между страницами. Каждый двумя стилями». Никакой копипасты классов между страницами. Каждый примитив ниже
примитив ниже — отдельный `.svelte` в `src/lib/components/kit/`. — отдельный `.svelte` в `src/lib/components/kit/`.
- `MonoLabel` — микро-лейбл: mono 10px, uppercase, letter-spacing .1em, - `MonoLabel` — микро-лейбл: mono 10px, uppercase, letter-spacing .1em, muted;
muted; слот-модификатор accent (синие eyebrow/version). слот-модификатор accent (синие eyebrow/version).
- `StatusDot` / `StatusLine` — точка `--success` + подпись - `StatusDot` / `StatusLine` — точка `--success` + подпись («AUTO PIPELINE»,
(«AUTO PIPELINE», «LIVE PREVIEW»). «LIVE PREVIEW»).
- `Panel` / `PanelHeading` / `SettingGroup` / `SettingsFooter` — панель с - `Panel` / `PanelHeading` / `SettingGroup` / `SettingsFooter` — панель с
1px-бордером, составные части с разделителями. 1px-бордером, составные части с разделителями.
- `CheckerCanvas` — рабочая поверхность превью: тёмная подложка + шахматка. - `CheckerCanvas` — рабочая поверхность превью: тёмная подложка + шахматка.
- `MetaRow` / `MetaList` — пары «CAPTION значение». - `MetaRow` / `MetaList` — пары «CAPTION значение».
- `Segmented` — сегмент-переключатель (язык, тип градиента, пресеты). - `Segmented` — сегмент-переключатель (язык, тип градиента, пресеты).
- `Badge` — тег типа шага (BACKGROUND/TRANSFORM/STYLE), чип «✓ AUTO». - `Badge` — тег типа шага (BACKGROUND/TRANSFORM/STYLE), чип «✓ AUTO».
- `StepCard` — нумерованная карточка этапа: индекс `01`, drag-handle, - `StepCard` — нумерованная карточка этапа: индекс `01`, drag-handle, Badge
Badge типа, заголовок, кнопка удаления; слот для параметров. типа, заголовок, кнопка удаления; слот для параметров.
- `ToolCard` — карточка каталога (icon, index, title, copy, arrow). - `ToolCard` — карточка каталога (icon, index, title, copy, arrow).
- `ImageCard` / `PreviewTile` — label сверху + canvas-превью. - `ImageCard` / `PreviewTile` — label сверху + canvas-превью.
- `AppShell` — корневой контейнер `.app-shell` + `.page-grid` (обёртка - `AppShell` — корневой контейнер `.app-shell` + `.page-grid` (обёртка новой
новой ветки маршрутов); задаёт шрифты и фон нового дизайна. ветки маршрутов); задаёт шрифты и фон нового дизайна.
- `BlueprintGrid` — фоновая сетка (используется в `AppShell`). - `BlueprintGrid` — фоновая сетка (используется в `AppShell`).
- `IconButton`, `Icon` (обёртка `lucide-svelte`, 1617px, muted). - `IconButton`, `Icon` (обёртка `lucide-svelte`, 1617px, muted).
## 4. Минимальный UI-kit (`src/lib/components/kit/`) ## 4. Минимальный UI-kit (`src/lib/components/kit/`)
Контролы, поверх примитивов §3. API проектируем сразу под новый дизайн Контролы, поверх примитивов §3. API проектируем сразу под новый дизайн (обратной
(обратной совместимости со старым `ui/` не нужно — старые страницы его совместимости со старым `ui/` не нужно — старые страницы его продолжают
продолжают использовать). использовать).
| Компонент | Назначение | | Компонент | Назначение |
| ------------------------- | ----------------------------------------------------------------- | | ------------------------- | ----------------------------------------------------------------- |
@@ -156,33 +158,33 @@ div с двумя стилями». Никакой копипасты класс
- **Корневой layout `src/routes/+layout.svelte`** перестаёт импортировать - **Корневой layout `src/routes/+layout.svelte`** перестаёт импортировать
`../app.css`. В нём остаётся только общая логика (init темы/locale) и `../app.css`. В нём остаётся только общая логика (init темы/locale) и
нейтральный `<div>{children}</div>` без глобальных стилей дизайна. нейтральный `<div>{children}</div>` без глобальных стилей дизайна.
- **Старая ветка `src/routes/(old)/+layout.svelte`** импортирует `app.css` - **Старая ветка `src/routes/(old)/+layout.svelte`** импортирует `app.css` и
и содержит текущую шапку/подвал (разметку из нынешнего корневого layout). содержит текущую шапку/подвал (разметку из нынешнего корневого layout). Все
Все старые страницы переносятся внутрь `(old)/` (группа в скобках не старые страницы переносятся внутрь `(old)/` (группа в скобках не меняет URL):
меняет URL): `(old)/+page.svelte`, `(old)/demo/...`, `(old)/+page.svelte`, `(old)/demo/...`, `(old)/list-tools/...`,
`(old)/list-tools/...`, `(old)/tools/[id]/...`. `(old)/tools/[id]/...`.
- **Новая ветка `src/routes/preview/+layout.svelte`** импортирует - **Новая ветка `src/routes/preview/+layout.svelte`** импортирует `design2.css`
`design2.css` и рендерит `<AppShell>`. Сюда же — новые страницы (§6). и рендерит `<AppShell>`. Сюда же — новые страницы (§6).
- Благодаря code-splitting CSS в SvelteKit: на старых маршрутах грузится - Благодаря code-splitting CSS в SvelteKit: на старых маршрутах грузится только
только `app.css`, на `preview/*` — только `design2.css`. Скоуп стилей `app.css`, на `preview/*` — только `design2.css`. Скоуп стилей
Svelte-компонентов добавляет второй уровень изоляции. Никаких `.ds2`. Svelte-компонентов добавляет второй уровень изоляции. Никаких `.ds2`.
- Переключатель для ручной проверки: временный флаг `previewDesign` в - Переключатель для ручной проверки: временный флаг `previewDesign` в
`localStorage` + кнопка в старой шапке («Preview v2»), ведущая на `localStorage` + кнопка в старой шапке («Preview v2»), ведущая на `/preview`.
`/preview`. На фазе 4 флаг убираем. На фазе 4 флаг убираем.
> Почему не `(new)`-группа с теми же путями вместо `preview/*`: две группы > Почему не `(new)`-группа с теми же путями вместо `preview/*`: две группы не
> не могут одновременно определять один и тот же URL (`(old)/+page.svelte` > могут одновременно определять один и тот же URL (`(old)/+page.svelte` и
> и `(new)/+page.svelte` оба → `/` = конфликт маршрутов). Поэтому новая > `(new)/+page.svelte` оба → `/` = конфликт маршрутов). Поэтому новая ветка
> ветка живёт под префиксом `preview/`, а на фазе 4 файлы переносятся на > живёт под префиксом `preview/`, а на фазе 4 файлы переносятся на реальные пути
> реальные пути и группа `(old)` удаляется. > и группа `(old)` удаляется.
> >
> Это совпадает с рекомендованным в документации SvelteKit паттерном: > Это совпадает с рекомендованным в документации SvelteKit паттерном: «put your
> «put your entire app inside one or more groups except the routes that > entire app inside one or more groups except the routes that should not inherit
> should not inherit the common layouts» — старое целиком в `(old)`, новое > the common layouts» — старое целиком в `(old)`, новое снаружи. Синтаксис
> снаружи. Синтаксис `@`-breakout (`+layout@.svelte`, `+page@.svelte`) здесь > `@`-breakout (`+layout@.svelte`, `+page@.svelte`) здесь не требуется:
> не требуется: «выломаться» можно лишь до корня, а нам нужно, чтобы именно > «выломаться» можно лишь до корня, а нам нужно, чтобы именно у корня НЕ было
> у корня НЕ было `app.css`; если позже захотим держать новые маршруты внутри > `app.css`; если позже захотим держать новые маршруты внутри группы, `@`
> группы, `@` пригодится для сброса родительского layout. > пригодится для сброса родительского layout.
## 6. Маршрутизация параллельных страниц ## 6. Маршрутизация параллельных страниц
@@ -202,55 +204,50 @@ src/routes/preview/list-tools/+page.svelte
src/routes/preview/tools/[id]/+page.svelte src/routes/preview/tools/[id]/+page.svelte
``` ```
- Каждая новая страница импортирует **только** `kit/*`. Старые страницы - Каждая новая страница импортирует **только** `kit/*`. Старые страницы не
не изменяются (только перемещены в `(old)/`). изменяются (только перемещены в `(old)/`).
- Фаза 4: содержимое `preview/*` переносится на реальные маршруты - Фаза 4: содержимое `preview/*` переносится на реальные маршруты (move-файлы),
(move-файлы), группа `(old)/` удаляется, `design2.css` подключается в группа `(old)/` удаляется, `design2.css` подключается в корневом
корневом `+layout.svelte` (как новый основной слой), временный флаг/кнопка `+layout.svelte` (как новый основной слой), временный флаг/кнопка убираются.
убираются.
## 7. Фазы выполнения (1–5 по запросу) ## 7. Фазы выполнения (1–5 по запросу)
Каждая фаза заканчивается зелёными `test`/`check`/`build` и ревью в браузере Каждая фаза заканчивается зелёными `test`/`check`/`build` и ревью в браузере
(обе темы) против `refs-html/`. (обе темы) против `refs-html/`.
- **Фаза 1 — Фундамент + UI-kit.** Токены §2, `design2.css` по §5, все - **Фаза 1 — Фундамент + UI-kit.** Токены §2, `design2.css` по §5, все примитивы
примитивы §3 и контролы §4 как отдельные компоненты. Критерий: §3 и контролы §4 как отдельные компоненты. Критерий: демо-страница kit'а
демо-страница kit'а (`/preview` или временная) показывает все состояния (`/preview` или временная) показывает все состояния (light/dark,
(light/dark, hover/disabled, все варианты Button/Segmented/Badge); hover/disabled, все варианты Button/Segmented/Badge); unit-тесты форм не
unit-тесты форм не падают; **старые страницы визуально не изменились** падают; **старые страницы визуально не изменились** (изоляция работает).
(изоляция работает).
- **Фаза 2 — Новые страницы.** Под `preview/*` (§6): воркспейс, demo, - **Фаза 2 — Новые страницы.** Под `preview/*` (§6): воркспейс, demo,
list-tools, tool-page (двухпанельная раскладка из рефа: settings-panel + list-tools, tool-page (двухпанельная раскладка из рефа: settings-panel +
preview-panel, step-cards для chain). Критерий: разметка соответствует preview-panel, step-cards для chain). Критерий: разметка соответствует
`refs-html/index.html`, `gradient.html`, `background-remover.html`, `refs-html/index.html`, `gradient.html`, `background-remover.html`,
`list-tools.html`; интерактив (слайдеры/тоглы/загрузка/автозапуск) работает `list-tools.html`; интерактив (слайдеры/тоглы/загрузка/автозапуск) работает в
в новом стиле; старые маршруты продолжают работать как раньше. новом стиле; старые маршруты продолжают работать как раньше.
- **Фазa 3 — Проверка нового дизайна.** Смоук §8 (обе темы, узкий экран, - **Фазa 3 — Проверка нового дизайна.** Смоук §8 (обе темы, узкий экран, 800px),
800px), скриншоты против `refs-html/`, прогон старого тест-сьюта для скриншоты против `refs-html/`, прогон старого тест-сьюта для доказательства
доказательства отсутствия регресса, RU/EN-локализация новых лейблов. отсутствия регресса, RU/EN-локализация новых лейблов. Критерий: визуальное
Критерий: визуальное совпадение с рефом, зелёные тесты, записанные совпадение с рефом, зелёные тесты, записанные скриншоты приняты.
скриншоты приняты.
- **Фаза 4 — Сделать новый дизайн основным.** Перенос `preview/*` - **Фаза 4 — Сделать новый дизайн основным.** Перенос `preview/*` реальные
реальные маршруты; `design2.css` подключается в корневом `+layout.svelte`; маршруты; `design2.css` подключается в корневом `+layout.svelte`; удаление
удаление preview-группы и временного флага/кнопки. Старые страницы теперь preview-группы и временного флага/кнопки. Старые страницы теперь не
не используются (мёртвый код), но физически ещё в репо. используются (мёртвый код), но физически ещё в репо. Критерий: реальные
Критерий: реальные маршруты отдают новый дизайн, тесты зелёные, билд без маршруты отдают новый дизайн, тесты зелёные, билд без предупреждений.
предупреждений.
- **Фаза 5 — Удалить старый дизайн.** Удаление старых страниц, старого - **Фаза 5 — Удалить старый дизайн.** Удаление старых страниц, старого `ui/`,
`ui/`, старого `app.css` (и его токенов), неиспользуемых классов; старого `app.css` (и его токенов), неиспользуемых классов; переименование
переименование `kit/``ui/` (финальное имя) либо оставление `kit/` как `kit/``ui/` (финальное имя) либо оставление `kit/` как основного — решается
основного — решается на фазе 1. Финальный проход по брейкпоинтам на фазе 1. Финальный проход по брейкпоинтам 1200/1100/800/480. Критерий:
1200/1100/800/480. Критерий: `grep` по старым токенам/классам пуст, `grep` по старым токенам/классам пуст, build чистый, старый дизайн нигде не
build чистый, старый дизайн нигде не упоминается. упоминается.
Оценка объёма: Фаза 1 — фундамент+kit (~1 день), Фаза 2 — страницы Оценка объёма: Фаза 1 — фундамент+kit (~1 день), Фаза 2 — страницы (~12 дня),
(~12 дня), Фаза 3 — проверка (~полдня), Фаза 4–5 — переключение и чистка Фаза 3 — проверка (~полдня), Фаза 4–5 — переключение и чистка (~полдня).
(~полдня).
## 8. Смоук (после фазы 3, контроль на фазе 4) ## 8. Смоук (после фазы 3, контроль на фазе 4)
@@ -262,35 +259,34 @@ src/routes/preview/tools/[id]/+page.svelte
шахматке. шахматке.
4. Chain из 3+ инструментов: step-cards рендерятся, удаление/сброс работают. 4. Chain из 3+ инструментов: step-cards рендерятся, удаление/сброс работают.
5. Узкий экран 800px: настройки над превью, sticky выключается как в рефе. 5. Узкий экран 800px: настройки над превью, sticky выключается как в рефе.
6. RU/EN: новые лейблы локализованы; mono-лейблы рефа — либо ключи i18n, 6. RU/EN: новые лейблы локализованы; mono-лейблы рефа — либо ключи i18n, либо
либо осознанно английские брендовые элементы (см. §9). осознанно английские брендовые элементы (см. §9).
## 9. Открытые вопросы ## 9. Открытые вопросы
- **Имя финального каталога компонентов:** оставить `kit/` как основное имя - **Имя финального каталога компонентов:** оставить `kit/` как основное имя или
или переименовать в `ui/` на фазе 5 (удалив старое `ui/`). Рекомендую переименовать в `ui/` на фазе 5 (удалив старое `ui/`). Рекомендую сразу писать
сразу писать в `kit/`, на фазе 5 переименовать в `ui/`. Решить на фазе 1. в `kit/`, на фазе 5 переименовать в `ui/`. Решить на фазе 1.
- **Изоляция через layout:** `app.css` вынесен из корневого layout в - **Изоляция через layout:** `app.css` вынесен из корневого layout в
`(old)/+layout.svelte`; новый `design2.css` — только в `preview/+layout`. `(old)/+layout.svelte`; новый `design2.css` — только в `preview/+layout`.
Проверить на фазе 1, что на `/preview/*` не попадают стили старого Проверить на фазе 1, что на `/preview/*` не попадают стили старого дизайна
дизайна (DevTools → вкладка Styles, нет правил из `app.css`). (DevTools → вкладка Styles, нет правил из `app.css`).
- **Переключатель для проверки:** рекомендую `localStorage`-флаг + - **Переключатель для проверки:** рекомендую `localStorage`-флаг + кнопка
кнопка «Preview v2» в старой шапке + группа `preview/*`. Убрать на фазе 4. «Preview v2» в старой шапке + группа `preview/*`. Убрать на фазе 4.
- **Mono-лейблы рефа на английском** (`PROCESSING PIPELINE`): завести i18n- - **Mono-лейблы рефа на английском** (`PROCESSING PIPELINE`): завести i18n-
ключи, но допустить часть английских брендовых («приборная панель»). ключи, но допустить часть английских брендовых («приборная панель»).
- **Версия в шапке/футере:** из `package.json` через `define` или - **Версия в шапке/футере:** из `package.json` через `define` или захардкодить —
захардкодить — мелочь, решить при сборке topbar. мелочь, решить при сборке topbar.
- **`refs/` и `refs-html/` — одноразовый источник, НЕ поддерживаемый.** - **`refs/` и `refs-html/` — одноразовый источник, НЕ поддерживаемый.** После
После внедрения нового дизайна все правки дизайна делаются сразу в внедрения нового дизайна все правки дизайна делаются сразу в продакшен-коде
продакшен-коде (`design2.css` + `kit/`), а не в `refs/`. Механизм (`design2.css` + `kit/`), а не в `refs/`. Механизм синхронизации «ref → код»
синхронизации «ref → код» не нужен и не создаётся. `refs/` и `refs-html/` не нужен и не создаётся. `refs/` и `refs-html/` можно удалить из репо на фазе
можно удалить из репо на фазе 5 (по желанию), они не являются частью 5 (по желанию), они не являются частью рантайма.
рантайма.
## 10. План исполнения (мелкие коммиты) ## 10. План исполнения (мелкие коммиты)
Ветка `feat/redesign`. Каждый шаг — отдельный коммит, цель < ~500 строк. Ветка `feat/redesign`. Каждый шаг — отдельный коммит, цель < ~500 строк. Перед
Перед стартом: `test`/`build` зелёные на базе (якорь регрессии). стартом: `test`/`build` зелёные на базе (якорь регрессии).
### Шаг 0. Изоляция layout (без дизайна) ### Шаг 0. Изоляция layout (без дизайна)
@@ -299,13 +295,15 @@ src/routes/preview/tools/[id]/+page.svelte
`<div>{children}</div>`. `<div>{children}</div>`.
- [x] **[C2]** Создать `routes/(old)/+layout.svelte` (импорт `app.css` + старая - [x] **[C2]** Создать `routes/(old)/+layout.svelte` (импорт `app.css` + старая
шапка/подвал) и перенести старые страницы (`+page`, `demo`, `list-tools`, шапка/подвал) и перенести старые страницы (`+page`, `demo`, `list-tools`,
`tools/[id]`) в `(old)/`. URL не меняются. Проверка: старый сайт идентичен. `tools/[id]`) в `(old)/`. URL не меняются. Проверка: старый сайт
_(файлы — move, объём строк минимален)_ идентичен. _(файлы — move, объём строк минимален)_
### Шаг 1. Фундамент нового дизайна ### Шаг 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). - [x] **[C3]** Создать `src/lib/styles/design2.css` (токены `:root` +
подключить шрифты в `design2.css` (self-host, offline-first). `[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`)_ - [x] **[C4]** _(слит в C3: шрифты нельзя подключить до создания `design2.css`)_
`[data-theme='dark']` (таблица §2). Без утилитарных классов. `[data-theme='dark']` (таблица §2). Без утилитарных классов.
- [x] **[C5]** Примитивы batch 1: `AppShell`, `BlueprintGrid`, `CheckerCanvas`, - [x] **[C5]** Примитивы batch 1: `AppShell`, `BlueprintGrid`, `CheckerCanvas`,
@@ -320,87 +318,103 @@ src/routes/preview/tools/[id]/+page.svelte
### Шаг 2. Витрина kit (проверка компонентов) ### Шаг 2. Витрина kit (проверка компонентов)
- [x] **[C8]** `preview/+layout.svelte` (импорт `design2.css` + `<AppShell>` + переключатель темы) и - [x] **[C8]** `preview/+layout.svelte` (импорт `design2.css` + `<AppShell>` +
временная `preview/kit` страница со всеми состояниями (light/dark, переключатель темы) и временная `preview/kit` страница со всеми
hover/disabled, варианты Button/Segmented/Badge и пр.) состояниями (light/dark, hover/disabled, варианты Button/Segmented/Badge и
пр.)
### Шаг 3. Новые страницы ### Шаг 3. Новые страницы
- [x] **[C9]** `/preview` — воркспейс (eyebrow, h1, lede, поиск, «открыть последний»). - [x] **[C9]** `/preview` — воркспейс (eyebrow, h1, lede, поиск, «открыть
Поиск фильтрует `TOOLS` из `$lib/registry`; карточки результата пока последний»). Поиск фильтрует `TOOLS` из `$lib/registry`; карточки
ведут на `#` (реальные `/preview/tools/[id]` подключим на C12, иначе результата пока ведут на `#` (реальные `/preview/tools/[id]` подключим на
prerender падает на 404). C12, иначе prerender падает на 404).
- [x] **[C10]** `/preview/list-tools` — каталог карточек (группировка по - [x] **[C10]** `/preview/list-tools` — каталог карточек (группировка по
`CATEGORIES`, `ToolCard`). Ссылки пока `#` (реальные `/preview/tools/[id]` `CATEGORIES`, `ToolCard`). Ссылки пока `#` (реальные `/preview/tools/[id]`
подключим на C12). подключим на C12).
- [x] **[C11]** `/preview/demo` — зеркало воркспейса (4 шага + панель превью), статичный приёмочный стенд. - [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] **[C12]** `/preview/tools/[id]`data-driven settings-panel +
- [x] **[C14]** `/preview/tools/[id]` — исполняемая цепочка: `StepCard`-шаги (параметры inline) + «Add step» (chainable), живой прогон пайплайна на исходнике, превью каждого шага + итог + Download. 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. Переключатель и проверка ### Шаг 4. Переключатель и проверка
- [x] **[C15]** В старой шапке ссылка «Preview v2» → `/preview`, ставит `localStorage['easy-png-tools:preview-v2']='1'`. - [x] **[C15]** В старой шапке ссылка «Preview v2» → `/preview`, ставит
- [x] **[C16]** База под дизайн: рабочий переключатель тем (persist в localStorage, применяется на `<html>`; проверено — фон `#eef1f4``#11171d`), `TopBar` приведён к виду `demo.html` (бренд + синий хлебный крош `/* страница */`, безрамочные иконки, язык — active = `--foreground`/`--background`), страницы `/preview/*` на всю ширину (убран `max-width`). Точечное выравнивание к рефу отложено: реф-дизайн неконсистентен. `localStorage['easy-png-tools:preview-v2']='1'`.
(доказательство отсутствия регресса). Фиксы по результатам — отдельными - [x] **[C16]** База под дизайн: рабочий переключатель тем (persist в
мелкими коммитами. localStorage, применяется на `<html>`; проверено — фон
`#eef1f4``#11171d`), `TopBar` приведён к виду `demo.html` (бренд + синий
хлебный крош `/* страница */`, безрамочные иконки, язык — active =
`--foreground`/`--background`), страницы `/preview/*` на всю ширину (убран
`max-width`). Точечное выравнивание к рефу отложено: реф-дизайн
неконсистентен. (доказательство отсутствия регресса). Фиксы по результатам
— отдельными мелкими коммитами.
### Шаг 5. Сделать новый дизайн основным — СЛЕДУЮЩИЙ ЭТАП ### Шаг 5. Сделать новый дизайн основным — СЛЕДУЮЩИЙ ЭТАП
> Раньше планировалось после «верности к рефу» (Этапы 0–1 > Раньше планировалось после «верности к рефу» (Этапы 0–1
> `docs/archive/plan-design-fix.md`) — план закрыт, гейт снят (2026-09-07). > `docs/archive/plan-design-fix.md`) — план закрыт, гейт снят (2026-09-07).
> Перед стартом C17 выполнить переезд старых файлов в папки `old/` > Перед стартом C17 выполнить переезд старых файлов в папки `old/` (backlog:
> (backlog: «Переезд старых файлов в `old/`», шаг 37 закрытого > «Переезд старых файлов в `old/`», шаг 37 закрытого
> `docs/archive/plan-composite-params.md`). Пока не трогаем `preview/*` и `(old)/`. > `docs/archive/plan-composite-params.md`). Пока не трогаем `preview/*` и
> `(old)/`.
- [ ] **[C17]** Перенести `preview/*` → реальные маршруты (move), удалить - [ ] **[C17]** Перенести `preview/*` → реальные маршруты (move), удалить
`(old)/`, корневой `+layout.svelte` импортирует `design2.css` + новый shell. `(old)/`, корневой `+layout.svelte` импортирует `design2.css` + новый
Убрать флаг/кнопку. **И обновить `newCode` в `web/eslint.config.js`:** заменить shell. Убрать флаг/кнопку. **И обновить `newCode` в
`src/routes/preview/**` на `src/routes/**` (после переезда все маршруты — `web/eslint.config.js`:** заменить `src/routes/preview/**` на
новый дизайн), компонентный glob `src/lib/components/kit/**` пока оставить. `src/routes/**` (после переезда все маршруты — новый дизайн), компонентный
glob `src/lib/components/kit/**` пока оставить.
- [ ] **[C18]** Правки импортов/редиректов после переноса, проверка билда. - [ ] **[C18]** Правки импортов/редиректов после переноса, проверка билда.
### Шаг 6. Удаление старого ### Шаг 6. Удаление старого
- [ ] **[C19]** Удалить старое `ui/`, прочие старые компоненты, старый `app.css`, - [ ] **[C19]** Удалить старое `ui/`, прочие старые компоненты, старый
неиспользуемые классы. Переименовать `kit/``ui/` (если решено на C1 — `app.css`, неиспользуемые классы. Переименовать `kit/``ui/` (если
тогда в `eslint.config.js` обновить и компонентный glob). решено на C1 — тогда в `eslint.config.js` обновить и компонентный glob).
**И раскрыть ESLint `recommended` на весь код** — убрать scoped-блок **И раскрыть ESLint `recommended` на весь код** — убрать scoped-блок
`newCode` в `web/eslint.config.js` (см. AGENTS.md), прогнать `lint` и `newCode` в `web/eslint.config.js` (см. AGENTS.md), прогнать `lint` и
починить всплывшие ошибки в оставшемся коде. починить всплывшие ошибки в оставшемся коде. _Примечание: доменные модули
_Примечание: доменные модули (`lib/core`, `lib/registry`, тесты и т.п.) — (`lib/core`, `lib/registry`, тесты и т.п.) — это не «старый дизайн», у них
это не «старый дизайн», у них свой линт-долг; расширение `recommended` на свой линт-долг; расширение `recommended` на них может выдать много ошибок.
них может выдать много ошибок. Решить на C19: либо чиним сразу, либо Решить на C19: либо чиним сразу, либо расширяем scoped только на
расширяем scoped только на `src/routes/**` + `src/lib/components/**`._ `src/routes/**` + `src/lib/components/**`._
- [ ] **[C20]** Финальный проход по брейкпоинтам 1200/1100/800/480; build без - [ ] **[C20]** Финальный проход по брейкпоинтам 1200/1100/800/480; build без
предупреждений; `grep` по старым токенам/классам пуст. предупреждений; `grep` по старым токенам/классам пуст.
- [ ] **[C21]** _(опционально)_ Удалить `refs/` и `refs-html/` из репо — они - [ ] **[C21]** _(опционально)_ Удалить `refs/` и `refs-html/` из репо — они
больше не источник правды. больше не источник правды.
> Если какой-то коммит тянет за собой > 500 строк (например, C6/C7), > Если какой-то коммит тянет за собой > 500 строк (например, C6/C7), разбивать
> разбивать на под-коммиты по 2–4 компонента. Тесты/билд прогонять после > на под-коммиты по 2–4 компонента. Тесты/билд прогонять после каждого коммита,
> каждого коммита, чтобы регрессия локализовалась одним шагом назад. > чтобы регрессия локализовалась одним шагом назад.
> >
> **Атомарность и точки «поломки» между коммитами.** > **Атомарность и точки «поломки» между коммитами.**
> >
> - C1+C2 уже сделаны вместе (связаны: нейтральный root без `(old)`-layout > - C1+C2 уже сделаны вместе (связаны: нейтральный root без `(old)`-layout
> ломает старый сайт). Аналогично связанных пар больше нет. > ломает старый сайт). Аналогично связанных пар больше нет.
> - C3 и C4 слиты: шрифты подключаются внутри `design2.css`, поэтому файл > - C3 и C4 слиты: шрифты подключаются внутри `design2.css`, поэтому файл должен
> должен существовать в том же коммите. > существовать в том же коммите.
> - Коммиты **C3→C16 не трогают старый сайт** `(old)/` — они только > - Коммиты **C3→C16 не трогают старый сайт** `(old)/` — они только добавляют
> добавляют новые файлы/маршруты, сломать старый сайт на них нельзя. > новые файлы/маршруты, сломать старый сайт на них нельзя.
> - **C17 — единственный атомарный переключатель**: перенос `preview/*` на > - **C17 — единственный атомарный переключатель**: перенос `preview/*` на
> реальные пути + удаление `(old)/` + смена root-layout должны быть в > реальные пути + удаление `(old)/` + смена root-layout должны быть в ОДНОМ
> ОДНОМ коммите (иначе конфликт маршрутов или пропажа страниц). Не дробить. > коммите (иначе конфликт маршрутов или пропажа страниц). Не дробить.
> - **C19 должен идти строго после C17** (старый `app.css`/`ui/` удаляем > - **C19 должен идти строго после C17** (старый `app.css`/`ui/` удаляем только
> только когда old-сайт уже выключен). > когда old-сайт уже выключен).
> - Очерёдность зависимостей: `design2.css` (C3) и kit-компоненты (C5–C7) > - Очерёдность зависимостей: `design2.css` (C3) и kit-компоненты (C5–C7) должны
> должны существовать до страниц, которые их импортируют (C8–C14). > существовать до страниц, которые их импортируют (C8–C14).
## Бэклог полировки (после C9–C21, отдельным проходом) ## Бэклог полировки (после C9–C21, отдельным проходом)
Конкретные расхождения витрины `preview/kit` с `refs-html/`. Править Конкретные расхождения витрины `preview/kit` с `refs-html/`. Править системно в
системно в конце, а не по ходу сборки базы. конце, а не по ходу сборки базы.
- **Toggle** в дизайне прямоугольный, у нас — закруглённый. Сделать - **Toggle** в дизайне прямоугольный, у нас — закруглённый. Сделать
прямоугольным (или уточнить радиус у свича отдельно от общих токенов). прямоугольным (или уточнить радиус у свича отдельно от общих токенов).
@@ -408,11 +422,11 @@ src/routes/preview/tools/[id]/+page.svelte
ненулевой `--radius` для всех бордеров. Проверить токен `--radius` против ненулевой `--radius` для всех бордеров. Проверить токен `--radius` против
референсов; возможно он должен быть 0 (или близко), а скругления оставить референсов; возможно он должен быть 0 (или близко), а скругления оставить
только там, где они реально есть в дизайне. только там, где они реально есть в дизайне.
- **DownloadButton**: нет отступа между текстом и размером файла - **DownloadButton**: нет отступа между текстом и размером файла (`size`-чип).
(`size`-чип). Добавить `gap`/отступ внутри кнопки. Добавить `gap`/отступ внутри кнопки.
- **Поля ввода (controls)**: выглядят без паддингов (возможно артефакт самой - **Поля ввода (controls)**: выглядят без паддингов (возможно артефакт самой
витрины, но визуально плохо). Проверить паддинги в `TextField`/`SelectField`/ витрины, но визуально плохо). Проверить паддинги в `TextField`/`SelectField`/
`ColorField`/`SliderField` против референса и поправить. `ColorField`/`SliderField` против референса и поправить.
- **Segmented**: на витрине последняя кнопка растянута и нечитаемо, что это - **Segmented**: на витрине последняя кнопка растянута и нечитаемо, что это
сегмент. Проверить, как `Segmented` выглядит в реальных раскладках сегмент. Проверить, как `Segmented` выглядит в реальных раскладках (C9C14);
(C9C14); возможно нужен фиксированный/авто размер сегментов или перенос. возможно нужен фиксированный/авто размер сегментов или перенос.
+32 -32
View File
@@ -1,12 +1,11 @@
# План: закрытие техдолга перед переездом `old/` (C17) # План: закрытие техдолга перед переездом `old/` (C17)
> Статус: план к выполнению. Ветка `feat/redesign`. > Статус: план к выполнению. Ветка `feat/redesign`. Входит в гейт перед
> Входит в гейт перед переездом старых файлов в `old/` (backlog №14; см. также > переездом старых файлов в `old/` (backlog №14; см. также
> `archive/plan-migrate-remaining.md` — техдолг «чинить по заведённым задачам, > `archive/plan-migrate-remaining.md` — техдолг «чинить по заведённым задачам,
> не игнорировать правилами»). > не игнорировать правилами»). Целевое состояние по завершении: `svelte-check`
> Целевое состояние по завершении: `svelte-check` → 0 errors, `pnpm test` > 0 errors, `pnpm test` → зелёный, `lint:all` → без ошибок (допустим только
> зелёный, `lint:all` → без ошибок (допустим только задокументированный остаток), > задокументированный остаток), дизайн-токены → единый словарь `preview.css`.
> дизайн-токены → единый словарь `preview.css`.
## Состав техдолга (на 2026-09-07) ## Состав техдолга (на 2026-09-07)
@@ -15,9 +14,9 @@
svelte-check, висит на каждом прогоне. svelte-check, висит на каждом прогоне.
- **фейл теста**`src/lib/palette.test.ts`: `ENOENT src/app.css` (файл - **фейл теста**`src/lib/palette.test.ts`: `ENOENT src/app.css` (файл
разъехался на `old.css` + `design2.css` при редизайне). Тест проверял разъехался на `old.css` + `design2.css` при редизайне). Тест проверял
контрасты палитры по токенам старого `app.css` (`text/surface/bg/link/ контрасты палитры по токенам старого `app.css`
accent/danger/border`). При гейтах `pnpm test` красный, хотя 607 тестов (`text/surface/bg/link/ accent/danger/border`). При гейтах `pnpm test`
проходят. красный, хотя 607 тестов проходят.
- **design-tokens в `kit/**`** — ~335 ошибок `lint:all` от локального плагина - **design-tokens в `kit/**`** — ~335 ошибок `lint:all` от локального плагина
`eslint-plugins/design-tokens` (захардкоженные цвета/размеры/длительности/ `eslint-plugins/design-tokens` (захардкоженные цвета/размеры/длительности/
z-index/`color-mix()` в `<style>`-блоках svelte), плюс stylelint по CSS. z-index/`color-mix()` в `<style>`-блоках svelte), плюс stylelint по CSS.
@@ -43,46 +42,47 @@
### Этап 2 — миграция `kit/**` на design-токены (main объём) ### Этап 2 — миграция `kit/**` на design-токены (main объём)
Цель — `lint:all` без ошибок design-tokens (все примитивы/`color-mix()` Цель — `lint:all` без ошибок design-tokens (все примитивы/`color-mix()` уходят в
уходят в токены `preview.css`). токены `preview.css`).
**Вариант А (основной): авто-фикс одноразовым скриптом.** **Вариант А (основной): авто-фикс одноразовым скриптом.** Писать скрипт ТОЛЬКО
Писать скрипт ТОЛЬКО если он гарантированно ничего не испортит. Гарантии: если он гарантированно ничего не испортит. Гарантии:
- Скрипт — одноразовый (`web/scripts/fix-design-tokens.mjs`), после прогона - Скрипт — одноразовый (`web/scripts/fix-design-tokens.mjs`), после прогона
удаляется либо помечается неактуальным. удаляется либо помечается неактуальным.
- Строгая карта замен: примитив → токен **1:1, только если значение точно - Строгая карта замен: примитив → токен **1:1, только если значение точно
совпадает с уже существующим токеном словаря** `preview.css` совпадает с уже существующим токеном словаря** `preview.css` (цвет →
(цвет → `--color-*`/`--brand-*`, радиус → `--radius-*`, размер → `--color-*`/`--brand-*`, радиус → `--radius-*`, размер →
`--space-*`/`--size-*`, длительность → `--duration-*`, z-index → `--z-*`). `--space-*`/`--size-*`, длительность → `--duration-*`, z-index → `--z-*`).
Никаких новых токенов скрипт не генерирует. Никаких новых токенов скрипт не генерирует.
- Замены — только примитивы, не содержащие `var()`/`calc()`/`color-mix()`; - Замены — только примитивы, не содержащие `var()`/`calc()`/`color-mix()`;
исключения по правилам плагина (0, 0px, 1px, проценты, unitless) не исключения по правилам плагина (0, 0px, 1px, проценты, unitless) не трогаются.
трогаются. `color-mix()` → выносится в `preview.css` как производный `color-mix()` → выносится в `preview.css` как производный `--color-*` токен
`--color-*` токен (может требовать ручного решения имени). (может требовать ручного решения имени).
- Скрипт обрабатывает только `src/lib/components/kit/**` и `src/preview.css` - Скрипт обрабатывает только `src/lib/components/kit/**` и `src/preview.css`
(scoped-пути плагина) и только `<style>`-блоки/`.css` — логику/разметку не (scoped-пути плагина) и только `<style>`-блоки/`.css` — логику/разметку не
меняет, только строки стилей. меняет, только строки стилей.
- После прогона: обязательный дифф-ревью (все изменения — замена значений на - После прогона: обязательный дифф-ревью (все изменения — замена значений на
`var(--x)`, ничего удалено/переставлено), затем полный `lint:all` + `var(--x)`, ничего удалено/переставлено), затем полный `lint:all` +
`svelte-check` + `pnpm test`. Если хоть что-то вне «замена примитива на `svelte-check` + `pnpm test`. Если хоть что-то вне «замена примитива на токен»
токен» — скрипт считается проваленным, откатываем и переходим на вариант Б. — скрипт считается проваленным, откатываем и переходим на вариант Б.
**Вариант Б (страховка): руками, пакетами по компонентам.** **Вариант Б (страховка): руками, пакетами по компонентам.** Если скрипт не
Если скрипт не удался — чистим/заводим токены вручную, пакетами по компонентам удался — чистим/заводим токены вручную, пакетами по компонентам (12 на задачу,
(12 на задачу, диф < ~500 строк). Отсутствующие токены (например диф < ~500 строк). Отсутствующие токены (например `--space-2xl`/`--space-panel`,
`--space-2xl`/`--space-panel`, уже дважды обходились в SchemaTextSource) уже дважды обходились в SchemaTextSource) добавляем в словарь `preview.css`
добавляем в словарь `preview.css` осознанно. осознанно.
Гейт этапа: `pnpm --dir web lint:all` — только допустимый остаток (неиспользуемые Гейт этапа: `pnpm --dir web lint:all` — только допустимый остаток
токены — варнинг), плюс регресс-прогон тестов и svelte-check. (неиспользуемые токены — варнинг), плюс регресс-прогон тестов и svelte-check.
### Этап 3 — чистота словаря `preview.css` ### Этап 3 — чистота словаря `preview.css`
- Разобрать варнинги неиспользуемых токенов (check-tokens): либо использовать, - Разобрать варнинги неиспользуемых токенов (check-tokens): либо использовать,
либо удалить из словаря. либо удалить из словаря.
- После этапа 2 — повторный аудит токенов (`pnpm --dir web exec node - После этапа 2 — повторный аудит токенов
scripts/check-tokens.mjs`): parity light/dark, hct-only, без мёртвых токенов. (`pnpm --dir web exec node scripts/check-tokens.mjs`): parity light/dark,
hct-only, без мёртвых токенов.
### Этап 4 — финальные гейты (гейт переезда `old/`) ### Этап 4 — финальные гейты (гейт переезда `old/`)
@@ -92,12 +92,12 @@
4. `pnpm --dir web lint:all` → 0 (или только задокументированный остаток) 4. `pnpm --dir web lint:all` → 0 (или только задокументированный остаток)
5. Обе ветки открываются: старый UI `/`, новый `/preview` 5. Обе ветки открываются: старый UI `/`, новый `/preview`
После зеленых гейтов — закрытие задач и переход к `backlog` №14 После зеленых гейтов — закрытие задач и переход к `backlog` №14 (переезд старых
(переезд старых файлов в `old/`). файлов в `old/`).
## Порядок и коммиты ## Порядок и коммиты
- Коммиты атомарные, < ~500 строк (AGENTS). По завершении каждой мини-задачи — - Коммиты атомарные, < ~500 строк (AGENTS). По завершении каждой мини-задачи —
ревью (агент не коммитит). ревью (агент не коммитит).
- Техдолг чинится по заведённым задачам, правила линтеров не отключаются и не - Техдолг чинится по заведённым задачам, правила линтеров не отключаются и не
игнорируются. игнорируются.
+83 -37
View File
@@ -1,23 +1,39 @@
# План разработки: easy-png-tools # План разработки: easy-png-tools
> **Статус (2026-09-07):** Фаза 1 (полноценный TS-сайт) в основном выполнена — > **Статус (2026-09-07):** Фаза 1 (полноценный TS-сайт) в основном выполнена —
> сайт живёт, каталог переведён в типизированный `registry-new` (121/125), > сайт живёт, каталог переведён в типизированный `registry-new` (121/125), идёт
> идёт редизайн на `preview/*` по `plan-redesign.md` (параллельная ветка, > редизайн на `preview/*` по `plan-redesign.md` (параллельная ветка, старый UI
> старый UI на `(old)/`). Фазы 28 (эталоны, Rust/wasm, CLI, harness) — > на `(old)/`). Фазы 28 (эталоны, Rust/wasm, CLI, harness) — будущие, разделам
> будущие, разделам ниже не запущены. > ниже не запущены.
## 0. Решения ## 0. Решения
- **Набор PNG-утилит, всё в браузере** (SvelteKit + `adapter-static`), данные не покидают машину. - **Набор PNG-утилит, всё в браузере** (SvelteKit + `adapter-static`), данные не
- **Первый и главный приоритет — полностью рабочий TS-сайт**: максимум простых инструментов уровня EASY, UX/UI, пайплайны (несколько инструментов подряд), описания инструментов. Сайтом можно пользоваться уже после этой фазы. покидают машину.
- **Никакого wasm/Rust на этом этапе.** TS-реализация на canvas + `ImageData` — эталон алгоритмов, из которого позже генерируются эталонные файлы для проверки Rust-порта. Rust-ядро, WASM и CLI — отдельные фазы позже, когда сайт живёт. - **Первый и главный приоритет — полностью рабочий TS-сайт**: максимум простых
- **Пайплайн = JSON-список шагов** `{tool, params}`. Формат закладывается сейчас и потом станет общим для браузера, wasm и CLI. инструментов уровня EASY, UX/UI, пайплайны (несколько инструментов подряд),
- **Реестр инструментов** (id, title, description, схема параметров, `run`) — источник истины: из него генерируются страницы, формы, пайплайны. Позже — общий формат для Rust-парсера. описания инструментов. Сайтом можно пользоваться уже после этой фазы.
- Валидация будущего Rust-порта — эталонные файлы, сгенерированные из TS-реализации. Сравнение: **бит-в-бит** для чистых пиксельных операций, **perceptual diff** для canvas-зависимых. - **Никакого wasm/Rust на этом этапе.** TS-реализация на canvas + `ImageData`
- **Единая точка исполнения инструментов.** Страницы вызывают операции только через `web/src/lib/tools/executor.ts::executeStep` — нигде напрямую `tool.run`. Контракт уже асинхронный (`Promise<PixelImage>`), поэтому: эталон алгоритмов, из которого позже генерируются эталонные файлы для проверки
- когда появятся тяжёлые MEDIUM-операции (свёртки, квантование), внутренности `executeStep` переезжают в Web Worker — воркер импортирует тот же чистый core как TS-фолбэк, алгоритмы не дублируются, вызывающий код не меняется; Rust-порта. Rust-ядро, WASM и CLI — отдельные фазы позже, когда сайт живёт.
- в WASM-фазе тот же воркер принимает wasm-модуль и диспетчеризует по `toolId` — свитч A/B из фазы 5 сводится к замене реализации внутри исполнителя; - **Пайплайн = JSON-список шагов** `{tool, params}`. Формат закладывается сейчас
- до появления MEDIUM-операций воркер не вводится: EASY-операции быстры и не блокируют UI. и потом станет общим для браузера, 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) ## 1. Структура репозитория (этап TS)
@@ -39,15 +55,28 @@ easy-png-tools/
### Фаза 1 — Полноценный TS-сайт (единственный приоритет) ### Фаза 1 — Полноценный TS-сайт (единственный приоритет)
**Цель: сайт, которым можно пользоваться.** Без wasm, без Rust, без clamp-семантики (в TS это делает `Uint8ClampedArray` сам). **Цель: сайт, которым можно пользоваться.** Без wasm, без Rust, без
clamp-семантики (в TS это делает `Uint8ClampedArray` сам).
1.1 **Каркас и UX/UI.** SvelteKit + static adapter, дизайн-система (цвета, типографика, компоненты), общий лейаут, шапка с навигацией по категориям. 1.1 **Каркас и UX/UI.** SvelteKit + static adapter, дизайн-система (цвета,
типографика, компоненты), общий лейаут, шапка с навигацией по категориям.
1.2 **Ядро.** Тип `Image` (обёртка над `ImageData`), загрузка/декодирование файла, кодирование и скачивание, библиотека операций (color/alpha/geometry/format/text/analyze). Всё без DOM-зависимостей внутри `core`. 1.2 **Ядро.** Тип `Image` (обёртка над `ImageData`), загрузка/декодирование
файла, кодирование и скачивание, библиотека операций
(color/alpha/geometry/format/text/analyze). Всё без DOM-зависимостей внутри
`core`.
1.3 **Реестр инструментов.** Одна запись = `{ id, title, description, category, params: ParamDef[], run }`. Универсальный рендерер: страница инструмента и форма параметров строятся из записи реестра автоматически; маршруты-утилиты генерируются из реестра на билде. Новый инструмент = новая запись + функция `run`. 1.3 **Реестр инструментов.** Одна запись =
`{ id, title, description, category, params: ParamDef[], run }`. Универсальный
рендерер: страница инструмента и форма параметров строятся из записи реестра
автоматически; маршруты-утилиты генерируются из реестра на билде. Новый
инструмент = новая запись + функция `run`.
1.4 **Пайплайн-workspace** (`/workspace`): загрузил изображение → список шагов (инструмент + его параметры) → последовательное применение с превью каждого шага → скачивание финального результата. Добавление/удаление/перестановка шагов. Сохранение пайплайнов в localStorage, экспорт/импорт JSON. Общие компоненты: DropZone, Preview, ParamForm, Download, PipelineSteps. 1.4 **Пайплайн-workspace** (`/workspace`): загрузил изображение → список шагов
(инструмент + его параметры) → последовательное применение с превью каждого шага
→ скачивание финального результата. Добавление/удаление/перестановка шагов.
Сохранение пайплайнов в localStorage, экспорт/импорт JSON. Общие компоненты:
DropZone, Preview, ParamForm, Download, PipelineSteps.
1.5 **Массовая реализация EASY-инструментов** по категориям: 1.5 **Массовая реализация EASY-инструментов** по категориям:
@@ -56,29 +85,36 @@ easy-png-tools/
- цвет: замена, тон, каналы, оттенки серого, инверсии; - цвет: замена, тон, каналы, оттенки серого, инверсии;
- геометрия и холст: resize, crop, rotate, flip, border, padding, background; - геометрия и холст: resize, crop, rotate, flip, border, padding, background;
- текст и простые эффекты: watermark, add-text, рамки, шум, pixelate; - текст и простые эффекты: watermark, add-text, рамки, шум, pixelate;
- анализ: размеры, палитра, проверки, просмотр. - анализ: размеры, палитра, проверки, просмотр. Приоритет — ширина, не глубина:
Приоритет — ширина, не глубина: как можно больше простых инструментов. как можно больше простых инструментов.
1.6 **Полировка.** Описания и подсказки для всех инструментов, состояния загрузки/ошибок, доступность, пустые состояния. 1.6 **Полировка.** Описания и подсказки для всех инструментов, состояния
загрузки/ошибок, доступность, пустые состояния.
**Checkpoint:** загрузил PNG → применил пайплайн из N шагов → сохранил и вернул пайплайн → скачал результат. Все инструменты 1.5 работают на сайте. Ни одного wasm. **Checkpoint:** загрузил PNG → применил пайплайн из N шагов → сохранил и вернул
пайплайн → скачал результат. Все инструменты 1.5 работают на сайте. Ни одного
wasm.
### Фаза 2 — Тестовые PNG и генератор эталонов ### Фаза 2 — Тестовые PNG и генератор эталонов
- Фикстурные входы: градиенты/паттерны/шум + пара «настоящих» PNG. - Фикстурные входы: градиенты/паттерны/шум + пара «настоящих» PNG.
- Правило: сравниваем **распакованные пиксели**, не байты файла. - Правило: сравниваем **распакованные пиксели**, не байты файла.
- **Checkpoint:** в `tests/reference/<op>/` лежат `input-*.png` + `expected-*.png`. - **Checkpoint:** в `tests/reference/<op>/` лежат `input-*.png` +
`expected-*.png`.
### Фаза 3 — Playwright-тесты ### Фаза 3 — Playwright-тесты
- `tools/gen-references.ts`: грузит страницу, гоняет TS-эталон, сохраняет `expected-*.png`. - `tools/gen-references.ts`: грузит страницу, гоняет TS-эталон, сохраняет
`expected-*.png`.
- Регресс: изменение TS-ядра ловится тестами. - Регресс: изменение TS-ядра ловится тестами.
- **Checkpoint:** регресс-тесты падают при изменении выхода эталона. - **Checkpoint:** регресс-тесты падают при изменении выхода эталона.
### Фаза 4 — Rust-ядро ### Фаза 4 — Rust-ядро
- Порт 1:1 чистых операций (первым делом те, что покрыты эталонными файлами) с `Image` ↔ TS `ImageData`. - Порт 1:1 чистых операций (первым делом те, что покрыты эталонными файлами) с
- `clamp_to_u8` с семантикой `ToUint8Clamp` (round-half-to-even, ≥255 → 255) + юнит-тесты на краях. `Image` ↔ TS `ImageData`.
- `clamp_to_u8` с семантикой `ToUint8Clamp` (round-half-to-even, ≥255 → 255) +
юнит-тесты на краях.
- `cargo test` читает эталонные файлы напрямую, **без браузера**. - `cargo test` читает эталонные файлы напрямую, **без браузера**.
- **Checkpoint:** `cargo test` зелёный на всех эталонных файлах. - **Checkpoint:** `cargo test` зелёный на всех эталонных файлах.
@@ -97,14 +133,18 @@ easy-png-tools/
### Фаза 7 — Сквозная проверка идентичности ### Фаза 7 — Сквозная проверка идентичности
- Матрица: **эталонные файлы (из TS) ↔ Rust-native ↔ Rust-wasm**, одна команда `harness compare`. - Матрица: **эталонные файлы (из TS) ↔ Rust-native ↔ Rust-wasm**, одна команда
`harness compare`.
- **Checkpoint:** сходимость по всем покрытым операциям во всех трёх рантаймах. - **Checkpoint:** сходимость по всем покрытым операциям во всех трёх рантаймах.
### Фаза 8 — Масштабирование и продвинутое ### Фаза 8 — Масштабирование и продвинутое
- Порт оставшихся EASY → MEDIUM (свёртки, морфология, квантование; кодеки через крейт `image`). - Порт оставшихся EASY → MEDIUM (свёртки, морфология, квантование; кодеки через
- Canvas-зависимые операции (текст, градиенты, штампы, `ctx.filter`): остаются в TS с perceptual-классом либо реимплементация в Rust. крейт `image`).
- batch-страница: дроп папки → wasm по всем файлам → zip. localStorage-пресеты (тот же JSON, что у CLI). - Canvas-зависимые операции (текст, градиенты, штампы, `ctx.filter`): остаются в
TS с perceptual-классом либо реимплементация в Rust.
- batch-страница: дроп папки → wasm по всем файлам → zip. localStorage-пресеты
(тот же JSON, что у CLI).
- HARD: compress/optimize через oxipng/zopfli (в Rust — нативно). - HARD: compress/optimize через oxipng/zopfli (в Rust — нативно).
## 3. Правила сравнения (для фаз 4–7) ## 3. Правила сравнения (для фаз 4–7)
@@ -120,12 +160,18 @@ easy-png-tools/
## 4. Риски и контрмеры ## 4. Риски и контрмеры
- **Реестр разрастается, страницы дублируются** — один универсальный рендерер страниц/форм из записей реестра; инструмент = данные + `run`. - **Реестр разрастается, страницы дублируются** — один универсальный рендерер
- **Семантика пайплайна** — зафиксировать: каждый шаг применяется к результату предыдущего; единый тип `Image` на всём пути; схема параметров на шаге. страниц/форм из записей реестра; инструмент = данные + `run`.
- **Canvas-операции не бит-в-бит** — класс perceptual закладывается на Фазе 2–3, а не в конце. - **Семантика пайплайна** — зафиксировать: каждый шаг применяется к результату
- **WASM + Vite** — спайк на Фазе 5 до массовой интеграции (риск отложен сознательно: сайт уже живёт на TS). предыдущего; единый тип `Image` на всём пути; схема параметров на шаге.
- **Расползание реестра при будущем Rust-порте** — схема параметров в JSON, её можно парсить и из Rust. - **Canvas-операции не бит-в-бит** — класс perceptual закладывается на Фазе 2–3,
- **Разные декодеры PNG** — фикстурные PNG генерировать самим, чтобы не тащить чужие артефакты. а не в конце.
- **WASM + Vite** — спайк на Фазе 5 до массовой интеграции (риск отложен
сознательно: сайт уже живёт на TS).
- **Расползание реестра при будущем Rust-порте** — схема параметров в JSON, её
можно парсить и из Rust.
- **Разные декодеры PNG** — фикстурные PNG генерировать самим, чтобы не тащить
чужие артефакты.
## 5. Команды верификации ## 5. Команды верификации
+77
View File
@@ -0,0 +1,77 @@
# Хранить данные для сортировки по популярности инструментов
Идея выложить на 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 млн) запросов на чтение в сутки.
- Для пет-проекта этого хватит навсегда, даже если вы будете делать запросы
напрямую с фронтенда без кеширования.
+48 -22
View File
@@ -1,7 +1,9 @@
# Карта инструментов: реализовано · добавить · идеи # Карта инструментов: реализовано · добавить · идеи
> Живой документ для сверки с каталогом. Формат записи: `id / название — параметры через запятую`. > Живой документ для сверки с каталогом. Формат записи:
> Источник сравнения: onlinepngtools.com (311 уникальных; 107 из нихнишевые клоны серий Logo*/Icon*/Stamp*/Signature* поверх обычных операций). > `id / названиепараметры через запятую`. Источник сравнения:
> onlinepngtools.com (311 уникальных; 107 из них — нишевые клоны серий
> Logo*/Icon*/Stamp*/Signature* поверх обычных операций).
--- ---
@@ -21,8 +23,10 @@
- png-to-base64 / base64-to-png — строка - png-to-base64 / base64-to-png — строка
- png-to-data-uri / data-uri-to-png — строка - png-to-data-uri / data-uri-to-png — строка
- png-to-hex — rrggbbaa по строкам / hex-to-png — tokens + width - png-to-hex — rrggbbaa по строкам / hex-to-png — tokens + width
- png-to-bytes — десятичные RGBA-байты по строкам / bytes-to-png — tokens + width - png-to-bytes — десятичные RGBA-байты по строкам / bytes-to-png — tokens +
- png-to-rgb-values — rgba(r,g,b,a) по пикселям / rgb-values-to-png — числа + width width
- png-to-rgb-values — rgba(r,g,b,a) по пикселям / rgb-values-to-png — числа +
width
- svg-to-png — width результата - svg-to-png — width результата
- verify-is-png — текстовый источник (base64/data-uri), вердикт по сигнатуре - verify-is-png — текстовый источник (base64/data-uri), вердикт по сигнатуре
@@ -41,7 +45,8 @@
- harden-alpha-png — threshold - harden-alpha-png — threshold
- feather-edges-png — radius (размытие только альфы) - feather-edges-png — radius (размытие только альфы)
- clean-edges-png — radius (defringe: RGB от ближайшего непрозрачного) - clean-edges-png — radius (defringe: RGB от ближайшего непрозрачного)
- watermark-image-png — вторая картинка-знак (загружается на странице), scale, opacity, position, margin - watermark-image-png — вторая картинка-знак (загружается на странице), scale,
opacity, position, margin
- despeckle-alpha-png / close-holes-png — radius - despeckle-alpha-png / close-holes-png — radius
- center-by-alpha-png — без параметров - center-by-alpha-png — без параметров
- round-corners-png — radius - round-corners-png — radius
@@ -52,7 +57,8 @@
### Цвет ### Цвет
- grayscale-png / invert-colors-png / sepia-png / auto-contrast-png — без параметров - grayscale-png / invert-colors-png / sepia-png / auto-contrast-png — без
параметров
- adjust-brightness-contrast-png — brightness, contrast - adjust-brightness-contrast-png — brightness, contrast
- change-png-hue — degrees - change-png-hue — degrees
- extract-channel-png — channel (r/g/b) - extract-channel-png — channel (r/g/b)
@@ -73,7 +79,8 @@
### Разложение каналов ### Разложение каналов
- png-to-hsl / png-to-hsv / png-to-hsi — component (h/s/l и т.п.), display (gray | space-as-rgb) - 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-cmyk — component (c/m/y/k), display
- png-to-ycbcr — component (y/cb/cr), display - png-to-ycbcr — component (y/cb/cr), display
- png-to-lab — component (l/a/b), display - png-to-lab — component (l/a/b), display
@@ -117,12 +124,15 @@
### Маски по свойствам пикселей ### Маски по свойствам пикселей
- show-transparent-png — color, opacity (подсветка прозрачных/полупрозрачных) - show-transparent-png — color, opacity (подсветка прозрачных/полупрозрачных)
- show-grayscale-pixels-png — tolerance, mode (binary/highlight), highlightColor, highlightOpacity - show-grayscale-pixels-png — tolerance, mode (binary/highlight),
highlightColor, highlightOpacity
- show-color-pixels-png — tolerance, mode, highlightColor, highlightOpacity - show-color-pixels-png — tolerance, mode, highlightColor, highlightOpacity
- light-pixel-mask-png — threshold, mode, highlightColor, highlightOpacity - light-pixel-mask-png — threshold, mode, highlightColor, highlightOpacity
- dark-pixel-mask-png — threshold, mode, highlightColor, highlightOpacity - dark-pixel-mask-png — threshold, mode, highlightColor, highlightOpacity
- unique-color-mask-png — rarity (макс. повторов), mode, highlightColor, highlightOpacity - unique-color-mask-png — rarity (макс. повторов), mode, highlightColor,
- extract-color-from-png — color, tolerance (оставить близкие, остальное прозрачным) highlightOpacity
- extract-color-from-png — color, tolerance (оставить близкие, остальное
прозрачным)
### Генерация ### Генерация
@@ -130,7 +140,8 @@
- single-color-png — width, height, color - single-color-png — width, height, color
- random-noise-png — width, height, seed - random-noise-png — width, height, seed
- linear-gradient-png — width, height, fromColor, toColor, direction - linear-gradient-png — width, height, fromColor, toColor, direction
- text-to-png — text, fontSize, font, bold, color, transparentBg, backgroundColor, padding - text-to-png — text, fontSize, font, bold, color, transparentBg,
backgroundColor, padding
- emoji-to-png — emoji, size - emoji-to-png — emoji, size
- placeholder-png — width, height, backgroundColor, color, showText - placeholder-png — width, height, backgroundColor, color, showText
- color-spectrum-png — width, height, direction, saturation, lightness - color-spectrum-png — width, height, direction, saturation, lightness
@@ -139,13 +150,17 @@
### Текст ### Текст
- add-text-png — text, fontSize, color, font, bold, position (3×3), margin, plate, plateColor, plateOpacity - add-text-png — text, fontSize, color, font, bold, position (3×3), margin,
- date-stamp-png — format, fontSize, color, font, bold, position, margin, plate, plateColor, plateOpacity plate, plateColor, plateOpacity
- watermark-tile-png — text, fontSize, color, opacity, angle, stepX, stepY, font, bold - 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 при клике (у нас — статичный свотч-полотно) - color-wheel-generator — size, кольца/сектора, показ hex при клике (у нас —
статичный свотч-полотно)
- complementary-palette — baseColor - complementary-palette — baseColor
- monochromatic-palette — baseColor, count - monochromatic-palette — baseColor, count
- analogous-palette — baseColor, spread - analogous-palette — baseColor, spread
@@ -154,7 +169,8 @@
- similar-shades-palette — baseColor, count, range - similar-shades-palette — baseColor, count, range
- sort-colors — источник (палитра-картинка или список), порядок (hsl/luma) - sort-colors — источник (палитра-картинка или список), порядок (hsl/luma)
- mix-colors — colors[], веса? - mix-colors — colors[], веса?
- average-color — colors[]; blend-two — a, b, steps; step-between — a, b, steps (три частных случая одного движка) - average-color — colors[]; blend-two — a, b, steps; step-between — a, b, steps
(три частных случая одного движка)
--- ---
@@ -193,7 +209,8 @@
### Прочее единичное ### Прочее единичное
- pick-a-color — пипетка уже есть в превью; отдельная страница не планируется (покрыто) - pick-a-color — пипетка уже есть в превью; отдельная страница не планируется
(покрыто)
- preview/test на цветных фонах — фича превью, не инструмент (решить позже) - preview/test на цветных фонах — фича превью, не инструмент (решить позже)
- extract-barcode — HARD, вне планов - extract-barcode — HARD, вне планов
@@ -201,8 +218,17 @@
## 3. Идеи на рассмотрение (нужна архитектура или спорная ценность) ## 3. Идеи на рассмотрение (нужна архитектура или спорная ценность)
- **Region-инструменты** — требуется UI выделения прямоугольника/ластика на превью: censor-region, erase-region, pixelate-area, blur-area, sharpen-area, reverse-colors-area. Один раз делаем selection-компонент — получаем сразу шесть инструментов. - **Region-инструменты** — требуется UI выделения прямоугольника/ластика на
- **Мультифайловый вывод** — сейчас инструмент отдаёт одну картинку: split-into-parts, gif-to-frames, separate-colors, multiply-grid-as-files. Нужен механизм «результат = набор файлов» (zip?). превью: censor-region, erase-region, pixelate-area, blur-area, sharpen-area,
- **Анимационные эффекты** — slow-reveal, fade-in/out, disappearing, scrolling: это видео/GIF на выходе, а не PNG. Отдельное решение о формате результата. reverse-colors-area. Один раз делаем selection-компонент — получаем сразу
- **HARD-хвост** — glitch-art, extract-signature, handwritten→digital, extract-barcode. шесть инструментов.
- **Нишевые серии** (logo/icon/stamp/signature — 107 клонов у источника) — сознательно не копируем: это обычные операции над конкретным контентом, у нас они доступны через базовые инструменты + цепочки. - **Мультифайловый вывод** — сейчас инструмент отдаёт одну картинку:
split-into-parts, gif-to-frames, separate-colors, multiply-grid-as-files.
Нужен механизм «результат = набор файлов» (zip?).
- **Анимационные эффекты** — slow-reveal, fade-in/out, disappearing, scrolling:
это видео/GIF на выходе, а не PNG. Отдельное решение о формате результата.
- **HARD-хвост** — glitch-art, extract-signature, handwritten→digital,
extract-barcode.
- **Нишевые серии** (logo/icon/stamp/signature — 107 клонов у источника) —
сознательно не копируем: это обычные операции над конкретным контентом, у нас
они доступны через базовые инструменты + цепочки.
+7 -2
View File
@@ -7,7 +7,9 @@
"test": "echo \"Error: no test specified\" && exit 1", "test": "echo \"Error: no test specified\" && exit 1",
"dev": "pnpm --dir web dev", "dev": "pnpm --dir web dev",
"build": "pnpm --dir web build", "build": "pnpm --dir web build",
"format": "pnpm --dir web format", "format": "pnpm format:docs && pnpm --dir web format",
"format:docs": "prettier --write docs --log-level warn",
"check:docs": "prettier --check docs",
"html": "pnpm --dir refs build && node scripts/extract-static.mjs refs/.next/server/app refs-html", "html": "pnpm --dir refs build && node scripts/extract-static.mjs refs/.next/server/app refs-html",
"cdp-audit": "pnpm --dir web refs-cdp-audit:large" "cdp-audit": "pnpm --dir web refs-cdp-audit:large"
}, },
@@ -21,5 +23,8 @@
"onFail": "download" "onFail": "download"
} }
}, },
"type": "module" "type": "module",
"devDependencies": {
"prettier": "^3.9.6"
}
} }
+16 -1
View File
@@ -205,4 +205,19 @@ settings:
importers: 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: {}