mirror of
https://github.com/Ku6epXBOCTuK/easy-png-tools.git
synced 2026-09-14 13:36:36 +00:00
style: format docs markdown
This commit is contained in:
+208
-74
@@ -1,9 +1,13 @@
|
||||
# Анализ: набор PNG-утилит для браузера
|
||||
|
||||
Проект: **easy-png-tools** — набор PNG-утилит, работающий полностью в браузере. Вдохновлён идеей онлайн-сервисов «все PNG-операции в одном месте»: никаких загрузок на сервер, всё считается локально.
|
||||
Стек: **SvelteKit + @sveltejs/adapter-static** (статический экспорт, никакого сервера). Все данные обрабатываются на клиенте, ничего не уходит в сеть.
|
||||
Проект: **easy-png-tools** — набор PNG-утилит, работающий полностью в браузере.
|
||||
Вдохновлён идеей онлайн-сервисов «все PNG-операции в одном месте»: никаких
|
||||
загрузок на сервер, всё считается локально. Стек: **SvelteKit +
|
||||
@sveltejs/adapter-static** (статический экспорт, никакого сервера). Все данные
|
||||
обрабатываются на клиенте, ничего не уходит в сеть.
|
||||
|
||||
Источник идеи: типовой перечень утилит, который встречается в онлайн-наборах PNG-инструментов (~296 операций).
|
||||
Источник идеи: типовой перечень утилит, который встречается в онлайн-наборах
|
||||
PNG-инструментов (~296 операций).
|
||||
|
||||
---
|
||||
|
||||
@@ -21,63 +25,121 @@
|
||||
| Тяжёлая математика без блокировки UI | Web Workers, SharedArrayBuffer, WASM (zlib, квантование, ML) |
|
||||
| Экономия памяти при больших изображениях | `ImageBitmap`, работа тайлами, downscale до декорирования |
|
||||
|
||||
**Вывод для архитектуры:** ~90% утилит — это одна и та же связка «декодировать → прогнать через пиксельную функцию → закодировать». Разумно сделать общий ядро (`core/pipeline.ts` + набор операций над `ImageData`) и тонькие страницы-обёртки в SvelteKit. Каждая утилита = отдельный маршрут со своим UI (drag&drop слева, превью, параметры, скачивание справа).
|
||||
**Вывод для архитектуры:** ~90% утилит — это одна и та же связка «декодировать →
|
||||
прогнать через пиксельную функцию → закодировать». Разумно сделать общий ядро
|
||||
(`core/pipeline.ts` + набор операций над `ImageData`) и тонькие страницы-обёртки
|
||||
в SvelteKit. Каждая утилита = отдельный маршрут со своим UI (drag&drop слева,
|
||||
превью, параметры, скачивание справа).
|
||||
|
||||
---
|
||||
|
||||
## 2. Уровень EASY — canvas + ImageData, простые попиксельные операции и композиция
|
||||
|
||||
Реализация: загрузка в canvas → `getImageData()` → цикл по пикселям / перекомпоновка / отрисовка на выходной canvas → `toBlob('image/png')`. Без сложных алгоритмов, каждая утилита — часы работы.
|
||||
Реализация: загрузка в canvas → `getImageData()` → цикл по пикселям /
|
||||
перекомпоновка / отрисовка на выходной canvas → `toBlob('image/png')`. Без
|
||||
сложных алгоритмов, каждая утилита — часы работы.
|
||||
|
||||
### 2.1 Конвертация форматов и текстовые представления
|
||||
|
||||
- **convert-png-to-jpg / convert-jpg-to-png / convert-webp-to-png / convert-png-to-webp / convert-svg-to-png** — рисование на canvas + `toBlob()` нужного типа
|
||||
- **convert-bmp-to-png / convert-ico-to-png / convert-gif-to-png** (первый кадр) — нативное декодирование браузером
|
||||
- **convert-png-to-jpg / convert-jpg-to-png / convert-webp-to-png /
|
||||
convert-png-to-webp / convert-svg-to-png** — рисование на canvas + `toBlob()`
|
||||
нужного типа
|
||||
- **convert-bmp-to-png / convert-ico-to-png / convert-gif-to-png** (первый кадр)
|
||||
— нативное декодирование браузером
|
||||
- **convert-png-to-bmp** — BMP-формат простой, ~30 строк энкодера
|
||||
- **convert-png-to-base64 / convert-base64-to-png / data-uri (в обе стороны)** — `FileReader.readAsDataURL` + строка
|
||||
- **convert-png-to-bytes / convert-bytes-to-png / convert-png-to-hex / convert-hex-to-png / convert-png-to-rgb-values / convert-rgb-values-to-png** — чтение/запись `ImageData` в текст
|
||||
- **convert-png-to-base64 / convert-base64-to-png / data-uri (в обе стороны)** —
|
||||
`FileReader.readAsDataURL` + строка
|
||||
- **convert-png-to-bytes / convert-bytes-to-png / convert-png-to-hex /
|
||||
convert-hex-to-png / convert-png-to-rgb-values / convert-rgb-values-to-png** —
|
||||
чтение/запись `ImageData` в текст
|
||||
- **convert-png-to-gif** (один кадр) — `gifenc`, тривиально
|
||||
|
||||
### 2.2 Прозрачность и альфа-канал
|
||||
|
||||
- **create-transparent-png, remove-color-from-png(простая версия), transparent-logo-maker, remove-logo/icon-background (по цвету)** — замена цвета на прозрачность по порогу близости
|
||||
- **remove-alpha-channel-from-png, fill-alpha-channel-in-png, replace-alpha-channel-in-png, extract-alpha-channel-from-png, invert-png-alpha-channel** — манипуляции только с A-компонентой
|
||||
- **generate-png-alpha-mask, show-transparent-png-areas, fill-transparent-png-pixels, convert-semi-transparent-png-to-opaque** — пороги и заливки по альфе
|
||||
- **create-transparent-png, remove-color-from-png(простая версия),
|
||||
transparent-logo-maker, remove-logo/icon-background (по цвету)** — замена
|
||||
цвета на прозрачность по порогу близости
|
||||
- **remove-alpha-channel-from-png, fill-alpha-channel-in-png,
|
||||
replace-alpha-channel-in-png, extract-alpha-channel-from-png,
|
||||
invert-png-alpha-channel** — манипуляции только с A-компонентой
|
||||
- **generate-png-alpha-mask, show-transparent-png-areas,
|
||||
fill-transparent-png-pixels, convert-semi-transparent-png-to-opaque** — пороги
|
||||
и заливки по альфе
|
||||
- **harden-png-edges** — бинаризация альфы по порогу
|
||||
|
||||
### 2.3 Цвет: замена, тон, каналы, цветовые пространства
|
||||
|
||||
- **change-png-color, change-png-color-tone, change-png-opacity, swap-png-colors, change-png-brightness, change-png-contrast, convert-png-to-grayscale, add-sepia-tone-to-png, change-png-hue, invert-png-colors, reverse-png-colors, convert-png-to-two-colors, convert-png-to-black-and-white, create-monochrome-png, generate-single-color-png** — попиксельные арифметики/преобразования
|
||||
- **extract-png-color-channels, swap-rgba-color-channels, split-png-into-rgb-components** — перестановка/выделение RGBA-каналов
|
||||
- **convert-png-to-hsl/hsv/hsi/cmyk/ycbcr/lab-colors** — известные формулы конвертации (Lab — чуть больше математики, но детерминировано)
|
||||
- **generate-\*-color-palette (complementary/monochromatic/analogous/triadic/tetradic), generate-similar-color-shades, color-wheel-generator, mix-multiple-colors, calculate-average-color, simulate-paint-colors, blend-two-colors, step-between-two-colors, convert-color-names-to-png** — генераторы палитр/смешение без работы с изображением
|
||||
- **posterize-png, create-color-spectrum-png, create-multi-color-gradient-png, generate-png-gradient-palette** — простые градиенты/квантование уровней
|
||||
- **change-png-color, change-png-color-tone, change-png-opacity,
|
||||
swap-png-colors, change-png-brightness, change-png-contrast,
|
||||
convert-png-to-grayscale, add-sepia-tone-to-png, change-png-hue,
|
||||
invert-png-colors, reverse-png-colors, convert-png-to-two-colors,
|
||||
convert-png-to-black-and-white, create-monochrome-png,
|
||||
generate-single-color-png** — попиксельные арифметики/преобразования
|
||||
- **extract-png-color-channels, swap-rgba-color-channels,
|
||||
split-png-into-rgb-components** — перестановка/выделение RGBA-каналов
|
||||
- **convert-png-to-hsl/hsv/hsi/cmyk/ycbcr/lab-colors** — известные формулы
|
||||
конвертации (Lab — чуть больше математики, но детерминировано)
|
||||
- **generate-\*-color-palette
|
||||
(complementary/monochromatic/analogous/triadic/tetradic),
|
||||
generate-similar-color-shades, color-wheel-generator, mix-multiple-colors,
|
||||
calculate-average-color, simulate-paint-colors, blend-two-colors,
|
||||
step-between-two-colors, convert-color-names-to-png** — генераторы
|
||||
палитр/смешение без работы с изображением
|
||||
- **posterize-png, create-color-spectrum-png, create-multi-color-gradient-png,
|
||||
generate-png-gradient-palette** — простые градиенты/квантование уровней
|
||||
|
||||
### 2.4 Геометрия, композиция, холст
|
||||
|
||||
- **resize-png, zoom-png, crop-png, cut-png, rotate-png, skew-png, shift-png, change-png-canvas-size, change-png-aspect-ratio, fit-png-in-rectangle, convert-landscape/portrait-png-to-portrait/landscape-png** — операции через `CanvasRenderingContext2D` (`drawImage`, `rotate`, `setTransform`)
|
||||
- **flip-png-horizontally/vertically, mirror-png, reverse-png** — отражение/поворот на 180°
|
||||
- **center-png, center-logo, center-icon** — центровка по bounding box альфа-канала
|
||||
- **duplicate-png, clone-png, multiply-png, create-png-tile, create-symmetric-png, split-png-into-parts** — тайлинг/копирование
|
||||
- **create-round-png, create-square-png, create-star-shaped-png, create-wavy-shaped-png, round-png-corners** — маски-«штампы» на альфе
|
||||
- **add-png-border, add-padding-to-png, add-space-around-png, remove-padding-from-png, remove-space-around-png, remove-png-padding** — расширение/усадка холста
|
||||
- **add-png-background, add-signature/logo/icon/stamp-background** — подложка под прозрачность
|
||||
- **resize-png, zoom-png, crop-png, cut-png, rotate-png, skew-png, shift-png,
|
||||
change-png-canvas-size, change-png-aspect-ratio, fit-png-in-rectangle,
|
||||
convert-landscape/portrait-png-to-portrait/landscape-png** — операции через
|
||||
`CanvasRenderingContext2D` (`drawImage`, `rotate`, `setTransform`)
|
||||
- **flip-png-horizontally/vertically, mirror-png, reverse-png** —
|
||||
отражение/поворот на 180°
|
||||
- **center-png, center-logo, center-icon** — центровка по bounding box
|
||||
альфа-канала
|
||||
- **duplicate-png, clone-png, multiply-png, create-png-tile,
|
||||
create-symmetric-png, split-png-into-parts** — тайлинг/копирование
|
||||
- **create-round-png, create-square-png, create-star-shaped-png,
|
||||
create-wavy-shaped-png, round-png-corners** — маски-«штампы» на альфе
|
||||
- **add-png-border, add-padding-to-png, add-space-around-png,
|
||||
remove-padding-from-png, remove-space-around-png, remove-png-padding** —
|
||||
расширение/усадка холста
|
||||
- **add-png-background, add-signature/logo/icon/stamp-background** — подложка
|
||||
под прозрачность
|
||||
|
||||
### 2.5 Текст, рамки, простые эффекты
|
||||
|
||||
- **add-text-to-png, convert-text-to-png, create-emoji-png, add-png-watermark, wrap-png-in-polaroid, create-logo-from-text, create-icon-from-text/emoji, create-custom-stamp, create-\*-stamp (round/rect/green/red/number/date-time/monogram/from-text/emoji), create-placeholder-png, create-empty-png, create-custom-png, generate-colorful-png, generate-random-png, generate-white-noise-png, generate-1x1-png, draw-png-grid** — рисование примитивов/случайных данных
|
||||
- **add-noise-to-png, create-glitch-png, pixelate-png, censor-png, erase-part-of-png, randomize-png-pixels** — шум/порча байтов/блоки/кисть
|
||||
- **add-text-to-png, convert-text-to-png, create-emoji-png, add-png-watermark,
|
||||
wrap-png-in-polaroid, create-logo-from-text, create-icon-from-text/emoji,
|
||||
create-custom-stamp, create-\*-stamp
|
||||
(round/rect/green/red/number/date-time/monogram/from-text/emoji),
|
||||
create-placeholder-png, create-empty-png, create-custom-png,
|
||||
generate-colorful-png, generate-random-png, generate-white-noise-png,
|
||||
generate-1x1-png, draw-png-grid** — рисование примитивов/случайных данных
|
||||
- **add-noise-to-png, create-glitch-png, pixelate-png, censor-png,
|
||||
erase-part-of-png, randomize-png-pixels** — шум/порча байтов/блоки/кисть
|
||||
- **add-shadow-to-png** — смещение + лёгкое размытие + композиция
|
||||
- **add-artifacts-to-png** — простой вариант: JPEG-раундтрип через canvas
|
||||
|
||||
### 2.6 Анализ и проверки
|
||||
|
||||
- **analyze-png, find-png-file-size, find-png-dimensions, check-if-png-is-landscape-or-portrait, find-png-color-count, check-if-png-is-grayscale, check-if-png-is-transparent, verify-if-image-is-png, pick-png-color, pick-logo/icon-color, png-viewer, test-png, preview-png-on-colorful-background, test-png-on-multiple-backgrounds** — чтение заголовков/ImageData/гистограмм, «пипетка» по клику
|
||||
- **analyze-png, find-png-file-size, find-png-dimensions,
|
||||
check-if-png-is-landscape-or-portrait, find-png-color-count,
|
||||
check-if-png-is-grayscale, check-if-png-is-transparent,
|
||||
verify-if-image-is-png, pick-png-color, pick-logo/icon-color, png-viewer,
|
||||
test-png, preview-png-on-colorful-background,
|
||||
test-png-on-multiple-backgrounds** — чтение заголовков/ImageData/гистограмм,
|
||||
«пипетка» по клику
|
||||
- **sort-colors** — сортировка списка палитры (не изображения)
|
||||
|
||||
### 2.7 Семейства signature/logo/icon/stamp — простые вариации
|
||||
|
||||
Почти все «-maker/-color/-resize/-crop/-rotate/-bg/-space/-padding/-grayscale/-single-color/-white/-black/-semi-transparent/-shadow» для signature/logo/icon/stamp — это переиспользование операций из разделов выше с одним цветом/боксом. Отдельно перечислены только те, что требуют алгоритмов (см. MEDIUM/HARD).
|
||||
Почти все
|
||||
«-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.**
|
||||
|
||||
@@ -85,54 +147,96 @@
|
||||
|
||||
## 3. Уровень MEDIUM — нужен настоящий алгоритм (свёртки, морфология, квантование, анимация)
|
||||
|
||||
Реализация: честные алгоритмы на `ImageData`, часто в Web Worker, при необходимости — обвязка вокруг готовых JS-библиотек.
|
||||
Реализация: честные алгоритмы на `ImageData`, часто в Web Worker, при
|
||||
необходимости — обвязка вокруг готовых JS-библиотек.
|
||||
|
||||
### 3.1 Фильтры и свёртки
|
||||
|
||||
- **blur-png, sharpen-png** — box/gaussian blur, ядро свёртки (упрощённо можно `ctx.filter`, но качественно — своя свёртка)
|
||||
- **refine-png-edges, feather-png-edges, clean-png-edges** — работа с полупрозрачными краями: размытие альфы, де-контуринг, suppression полупрозрачности
|
||||
- **blur-png, sharpen-png** — box/gaussian blur, ядро свёртки (упрощённо можно
|
||||
`ctx.filter`, но качественно — своя свёртка)
|
||||
- **refine-png-edges, feather-png-edges, clean-png-edges** — работа с
|
||||
полупрозрачными краями: размытие альфы, де-контуринг, suppression
|
||||
полупрозрачности
|
||||
- **add-glow-effect-to-png** — расширение альфы + размытие + цветное наложение
|
||||
|
||||
### 3.2 Морфология и контуры (операции над альфа-маской)
|
||||
|
||||
- **add-stroke-to-png, add-outline-to-png, remove-stroke-from-png, remove-outline-from-png, find-png-outline** — dilation/erosion по альфе, извлечение контура
|
||||
- **make-signature-thicker / make-signature-thinner, make-icon-thicker / make-icon-thinner** — dilation/erosion
|
||||
- **convert-outlined-icon-to-filled-icon** — flood-fill замкнутых областей; **convert-filled-icon-to-outlined-icon** — эрозия
|
||||
- **remove-png-border, remove-border-around-icon, remove-stamp-border** — детект однотонной/декоративной рамки и вырезание
|
||||
- **add-stroke-to-png, add-outline-to-png, remove-stroke-from-png,
|
||||
remove-outline-from-png, find-png-outline** — dilation/erosion по альфе,
|
||||
извлечение контура
|
||||
- **make-signature-thicker / make-signature-thinner, make-icon-thicker /
|
||||
make-icon-thinner** — dilation/erosion
|
||||
- **convert-outlined-icon-to-filled-icon** — flood-fill замкнутых областей;
|
||||
**convert-filled-icon-to-outlined-icon** — эрозия
|
||||
- **remove-png-border, remove-border-around-icon, remove-stamp-border** — детект
|
||||
однотонной/декоративной рамки и вырезание
|
||||
|
||||
### 3.3 Квантование и цветовая кластеризация
|
||||
|
||||
- **quantize-png, decrease-png-color-count, set-custom-png-color-palette, add-dithering-to-png** — median cut / octree / Wu + Floyd–Steinberg; готовые JS-библиотеки (image-q и т.п.)
|
||||
- **change-png-quality, create-low-quality-png** — симуляция потери качества через квантование + JPEG-раундтрип
|
||||
- **convert-png-to-color-blocks** — мультипликативный блок-эффект/квантование по блокам
|
||||
- **extract-png-color-palette, find-logo-color-scheme, extract-icon-colors** — частотная гистограмма + выбор топ-N / кластеризация
|
||||
- **create-unique/popular/multi-color/outlier-color-mask** — статистика распределения цветов и выделение по маске
|
||||
- **quantize-png, decrease-png-color-count, set-custom-png-color-palette,
|
||||
add-dithering-to-png** — median cut / octree / Wu + Floyd–Steinberg; готовые
|
||||
JS-библиотеки (image-q и т.п.)
|
||||
- **change-png-quality, create-low-quality-png** — симуляция потери качества
|
||||
через квантование + JPEG-раундтрип
|
||||
- **convert-png-to-color-blocks** — мультипликативный блок-эффект/квантование по
|
||||
блокам
|
||||
- **extract-png-color-palette, find-logo-color-scheme, extract-icon-colors** —
|
||||
частотная гистограмма + выбор топ-N / кластеризация
|
||||
- **create-unique/popular/multi-color/outlier-color-mask** — статистика
|
||||
распределения цветов и выделение по маске
|
||||
|
||||
### 3.4 Выделение объектов по цвету / фону
|
||||
|
||||
- **remove-png-chroma-key** — порог по цвету + деспилл (убрать зелёную окантовку)
|
||||
- **remove-color-from-png (качественная версия), extract-color-from-png, separate-png-colors, separate-logo-colors, separate-icon-colors** — сегментация по близости цвета
|
||||
- **convert-png-to-silhouette, convert-png-to-stencil** — порог + заливка силуэта
|
||||
- **extract-stamp-from-image (по цвету печати)** — хромакей/частотный фильтр по цвету чернил
|
||||
- **remove-png-background (вариант без ИИ)** — проверенный цветовой подход, знакомый по популярным онлайн-наборам PNG-утилит. Работает хорошо на однотонном/градиентном фоне и в ряде случаев удобнее ИИ. Опции и алгоритм:
|
||||
- **Background Color** — выбор удаляемого цвета: пипеткой по изображению или hex/RGB
|
||||
- **Percentage (похожесть цвета)** — slider; 0% = убрать только точный цвет, >0% = убрать соседние оттенки в окрестности цвета (в цветовом пространстве)
|
||||
- **Delete Outer Areas (галочка)** — режим «только внешние пиксели»: удаляются только области, связанные с краями изображения (flood-fill/region-growing от границ). Выключена = удалять по всему холсту. Именно эта опция позволяет убрать фон, не задевая «пятна» того же цвета внутри объекта
|
||||
- **Smooth Edge Line + Radius** — постобработка: полупрозрачная полоса толщиной N px по границе удаления (сгладить переход)
|
||||
- **Preview Deleted Background** — двухцветный предпросмотр маски (что удаляется/что остаётся)
|
||||
- Реализация: выбор цвета → порог по цветовому расстоянию → (опц.) flood-fill от краёв → сглаживание краёв → маска поверх превью. Всё на `ImageData`, без ML.
|
||||
- **remove-png-chroma-key** — порог по цвету + деспилл (убрать зелёную
|
||||
окантовку)
|
||||
- **remove-color-from-png (качественная версия), extract-color-from-png,
|
||||
separate-png-colors, separate-logo-colors, separate-icon-colors** —
|
||||
сегментация по близости цвета
|
||||
- **convert-png-to-silhouette, convert-png-to-stencil** — порог + заливка
|
||||
силуэта
|
||||
- **extract-stamp-from-image (по цвету печати)** — хромакей/частотный фильтр по
|
||||
цвету чернил
|
||||
- **remove-png-background (вариант без ИИ)** — проверенный цветовой подход,
|
||||
знакомый по популярным онлайн-наборам PNG-утилит. Работает хорошо на
|
||||
однотонном/градиентном фоне и в ряде случаев удобнее ИИ. Опции и алгоритм:
|
||||
- **Background Color** — выбор удаляемого цвета: пипеткой по изображению или
|
||||
hex/RGB
|
||||
- **Percentage (похожесть цвета)** — slider; 0% = убрать только точный
|
||||
цвет, >0% = убрать соседние оттенки в окрестности цвета (в цветовом
|
||||
пространстве)
|
||||
- **Delete Outer Areas (галочка)** — режим «только внешние пиксели»: удаляются
|
||||
только области, связанные с краями изображения (flood-fill/region-growing от
|
||||
границ). Выключена = удалять по всему холсту. Именно эта опция позволяет
|
||||
убрать фон, не задевая «пятна» того же цвета внутри объекта
|
||||
- **Smooth Edge Line + Radius** — постобработка: полупрозрачная полоса
|
||||
толщиной N px по границе удаления (сгладить переход)
|
||||
- **Preview Deleted Background** — двухцветный предпросмотр маски (что
|
||||
удаляется/что остаётся)
|
||||
- Реализация: выбор цвета → порог по цветовому расстоянию → (опц.) flood-fill
|
||||
от краёв → сглаживание краёв → маска поверх превью. Всё на `ImageData`, без
|
||||
ML.
|
||||
|
||||
### 3.5 Геометрические искажения и пиксель-сортировка
|
||||
|
||||
- **add-whirl-to-png** — свирл/вихрь: обратное отображение координат
|
||||
- **sort-png-pixels** — pixel sorting (сортировка строк/столбцов по яркости/цвету)
|
||||
- **upscale-png / downscale-png** — качественный ресемплинг (Lanczos, mipmap) поверх базового canvas-resize
|
||||
- **remove-signature-background, transparent-png-signature-maker, clean-up-signature-edges, improve-low-quality-signature, clean-logo-edges, clean-icon-edges, fix-low-quality-stamp, convert-rubber-stamp-to-digital-stamp** — комбинация «цвет→прозрачность + де-шум + морфология краёв»
|
||||
- **convert-image/png/jpg/icon/logo/signature-to-stamp** — стилизация в двухцветную «печать» (порог + зернистость + края)
|
||||
- **sort-png-pixels** — pixel sorting (сортировка строк/столбцов по
|
||||
яркости/цвету)
|
||||
- **upscale-png / downscale-png** — качественный ресемплинг (Lanczos, mipmap)
|
||||
поверх базового canvas-resize
|
||||
- **remove-signature-background, transparent-png-signature-maker,
|
||||
clean-up-signature-edges, improve-low-quality-signature, clean-logo-edges,
|
||||
clean-icon-edges, fix-low-quality-stamp,
|
||||
convert-rubber-stamp-to-digital-stamp** — комбинация «цвет→прозрачность +
|
||||
де-шум + морфология краёв»
|
||||
- **convert-image/png/jpg/icon/logo/signature-to-stamp** — стилизация в
|
||||
двухцветную «печать» (порог + зернистость + края)
|
||||
|
||||
### 3.6 Анимации и GIF-энкодинг
|
||||
|
||||
- **convert-png-to-gif, slowly-reveal-png, create-disappearing-png, fade-in-png, fade-out-png, create-scrolling-png, create-png-sequence-from-gif** — декомпозиция кадров GIF / генерация последовательности кадров + `gifenc`/`UPNG` (APNG)
|
||||
- **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.**
|
||||
|
||||
@@ -140,35 +244,65 @@
|
||||
|
||||
## 4. Уровень HARD — WASM, ML, длительная обработка, тонкая подгонка алгоритма
|
||||
|
||||
Реализация: WASM-модули, ML-модели в браузере, длительная обработка с прогресс-барами.
|
||||
Реализация: WASM-модули, ML-модели в браузере, длительная обработка с
|
||||
прогресс-барами.
|
||||
|
||||
- **compress-png / reduce-png-file-size / optimize-png** — сжатие на уровне формата: нужен качественный PNG-энкодер с контролем zlib-фильтров/битовой глубины и оптимизаторы. Решение: **pngquant / oxipng / zopfli (WASM)** + подгонка под «качество→размер» (это главный кандидат на HARD — именно здесь тюнинг бесконечен)
|
||||
- **remove-png-background (вариант с ИИ)** — опциональное улучшение к MEDIUM-версии: ML-сегментация (Rembg/DeepLab в WASM или ONNX) для произвольных фото, где цветовой матчинг бессилен. Начинать можно без него — не-ИИ версия уже даёт рабочий инструмент
|
||||
- **extract-signature-from-image** — детект подписи на скане/фото с произвольным фоном: де-шум, контраст, ML-детекция
|
||||
- **convert-handwritten-signature-to-digital-signature** — очистка и «оцифровка» почерка (ML, денойзинг, выравнивание)
|
||||
- **extract-barcode-from-image** — декодирование штрих-кодов/QR: готовая библиотека **zxing-wasm**, сопряжение с камерой/фото
|
||||
- **compress-png / reduce-png-file-size / optimize-png** — сжатие на уровне
|
||||
формата: нужен качественный PNG-энкодер с контролем zlib-фильтров/битовой
|
||||
глубины и оптимизаторы. Решение: **pngquant / oxipng / zopfli (WASM)** +
|
||||
подгонка под «качество→размер» (это главный кандидат на HARD — именно здесь
|
||||
тюнинг бесконечен)
|
||||
- **remove-png-background (вариант с ИИ)** — опциональное улучшение к
|
||||
MEDIUM-версии: ML-сегментация (Rembg/DeepLab в WASM или ONNX) для произвольных
|
||||
фото, где цветовой матчинг бессилен. Начинать можно без него — не-ИИ версия
|
||||
уже даёт рабочий инструмент
|
||||
- **extract-signature-from-image** — детект подписи на скане/фото с произвольным
|
||||
фоном: де-шум, контраст, ML-детекция
|
||||
- **convert-handwritten-signature-to-digital-signature** — очистка и «оцифровка»
|
||||
почерка (ML, денойзинг, выравнивание)
|
||||
- **extract-barcode-from-image** — декодирование штрих-кодов/QR: готовая
|
||||
библиотека **zxing-wasm**, сопряжение с камерой/фото
|
||||
|
||||
> Итого **6 утилит уровня HARD** (все — опциональные улучшения/надстройки). Всё остальное (≈290) — EASY/MEDIUM.
|
||||
> Итого **6 утилит уровня HARD** (все — опциональные улучшения/надстройки). Всё
|
||||
> остальное (≈290) — EASY/MEDIUM.
|
||||
|
||||
---
|
||||
|
||||
## 5. Итоговая сводка
|
||||
|
||||
**EASY — ~185 утилит.** canvas + ImageData.
|
||||
Примеры: crop, resize, rotate, конвертации, альфа-канал, палитры, текст, рамки, анализ.
|
||||
**EASY — ~185 утилит.** canvas + ImageData. Примеры: crop, resize, rotate,
|
||||
конвертации, альфа-канал, палитры, текст, рамки, анализ.
|
||||
|
||||
**MEDIUM — ~71 утилита.** Алгоритмы, Web Workers, gifenc.
|
||||
Примеры: blur, sharpen, морфология, квантование, дизеринг, pixel sorting, анимации GIF, remove-background (без ИИ).
|
||||
**MEDIUM — ~71 утилита.** Алгоритмы, Web Workers, gifenc. Примеры: blur,
|
||||
sharpen, морфология, квантование, дизеринг, pixel sorting, анимации GIF,
|
||||
remove-background (без ИИ).
|
||||
|
||||
**HARD — 6 утилит.** WASM (pngquant/oxipng/zxing), ML (ONNX/Rembg).
|
||||
Примеры: compress/optimize, remove-background (ИИ), extract-signature, barcode.
|
||||
**HARD — 6 утилит.** WASM (pngquant/oxipng/zxing), ML (ONNX/Rembg). Примеры:
|
||||
compress/optimize, remove-background (ИИ), extract-signature, barcode.
|
||||
|
||||
---
|
||||
|
||||
## 6. Рекомендации по реализации
|
||||
|
||||
1. **Единое ядро обработки** — `src/lib/core/`: загрузка файла → `ImageData`, библиотека операций (`color.ts`, `alpha.ts`, `geometry.ts`, `filters.ts`, `morphology.ts`, `quantize.ts`, `channels.ts`), экспорт через `toBlob`. Каждая утилита — тонкая SvelteKit-страница + общие компоненты (DropZone, Preview, Download).
|
||||
2. **Порядок внедрения:** MVP = ~40 топовых EASY (crop, resize, rotate, convert, alpha, grayscale, brightness, палитры, watermark, add-text, add-background, pick-color, analyze). Затем остальные EASY → MEDIUM пакетами по темам (фильтры, морфология, квантование, анимации). `remove-png-background` (без ИИ) — отличная «витринная» MEDIUM-фича ранних этапов: не требует ML и переиспользует `remove-color` + flood-fill + сглаживание краёв. HARD — в последнюю очередь.
|
||||
3. **Производительность:** вся пиксельная работа и тяжёлые алгоритмы — в Web Worker; большие изображения декодировать сразу в downscale для превью; прогресс-бары для MEDIUM/HARD.
|
||||
4. **SEO/структура:** SvelteKit + static adapter — каждая утилита отдельный маршрут (`/crop-png`, `/rotate-png`), что даёт удобные адреса и повторяет распространённую структуру наборов PNG-инструментов.
|
||||
5. **Семейства logo/icon/stamp/signature** — переиспользуют общие операции; в коде это конфиг-опции той же страницы (например `/resize-png` и `/resize-icon` = один компонент с разными подписями/дефолтами). Не плодить дублирующий код.
|
||||
1. **Единое ядро обработки** — `src/lib/core/`: загрузка файла → `ImageData`,
|
||||
библиотека операций (`color.ts`, `alpha.ts`, `geometry.ts`, `filters.ts`,
|
||||
`morphology.ts`, `quantize.ts`, `channels.ts`), экспорт через `toBlob`.
|
||||
Каждая утилита — тонкая SvelteKit-страница + общие компоненты (DropZone,
|
||||
Preview, Download).
|
||||
2. **Порядок внедрения:** MVP = ~40 топовых EASY (crop, resize, rotate, convert,
|
||||
alpha, grayscale, brightness, палитры, watermark, add-text, add-background,
|
||||
pick-color, analyze). Затем остальные EASY → MEDIUM пакетами по темам
|
||||
(фильтры, морфология, квантование, анимации). `remove-png-background` (без
|
||||
ИИ) — отличная «витринная» MEDIUM-фича ранних этапов: не требует ML и
|
||||
переиспользует `remove-color` + flood-fill + сглаживание краёв. HARD — в
|
||||
последнюю очередь.
|
||||
3. **Производительность:** вся пиксельная работа и тяжёлые алгоритмы — в Web
|
||||
Worker; большие изображения декодировать сразу в downscale для превью;
|
||||
прогресс-бары для MEDIUM/HARD.
|
||||
4. **SEO/структура:** SvelteKit + static adapter — каждая утилита отдельный
|
||||
маршрут (`/crop-png`, `/rotate-png`), что даёт удобные адреса и повторяет
|
||||
распространённую структуру наборов PNG-инструментов.
|
||||
5. **Семейства logo/icon/stamp/signature** — переиспользуют общие операции; в
|
||||
коде это конфиг-опции той же страницы (например `/resize-png` и
|
||||
`/resize-icon` = один компонент с разными подписями/дефолтами). Не плодить
|
||||
дублирующий код.
|
||||
|
||||
@@ -1,103 +1,118 @@
|
||||
# Demo fidelity: /preview/demo vs refs-html/demo.html
|
||||
|
||||
> ⚠️ **УТРАТИЛ АКТУАЛЬНОСТЬ — архивная копия.** Актуальный план фикса верности и
|
||||
> текущий статус пунктов B/F — в `docs/plan-design-fix.md` (Этап 1). Аудит верности
|
||||
> теперь автоматизирован (`pnpm audit` → `web/audit/audit-report.md`; пиксель-дифф не
|
||||
> используется); темы в `refs-html` переключаются (скрипт инжектится в
|
||||
> `scripts/extract-static.mjs`). Ниже — исходный текст на момент написания.
|
||||
> текущий статус пунктов B/F — в `docs/plan-design-fix.md` (Этап 1). Аудит
|
||||
> верности теперь автоматизирован (`pnpm audit` → `web/audit/audit-report.md`;
|
||||
> пиксель-дифф не используется); темы в `refs-html` переключаются (скрипт
|
||||
> инжектится в `scripts/extract-static.mjs`). Ниже — исходный текст на момент
|
||||
> написания.
|
||||
|
||||
Метод: Playwright, viewport 1440×900, скриншоты обеих страниц + замер вычисленных
|
||||
стилей ключевых элементов + пиксель-дифф (без чтения картинки моделью).
|
||||
Пиксель-дифф: **24.9%** (наш скролл 1731px, реф 1579px). Зоны различий размазаны
|
||||
по всей высоте → править надо глобально, не точечно.
|
||||
Метод: Playwright, viewport 1440×900, скриншоты обеих страниц + замер
|
||||
вычисленных стилей ключевых элементов + пиксель-дифф (без чтения картинки
|
||||
моделью). Пиксель-дифф: **24.9%** (наш скролл 1731px, реф 1579px). Зоны различий
|
||||
размазаны по всей высоте → править надо глобально, не точечно.
|
||||
|
||||
Теперь процесс автоматизирован: `pnpm audit` (Playwright) пишет
|
||||
`web/audit/audit-report.md` — light/dark токены + поэлементный дифф стилей по тексту.
|
||||
Пиксель-дифф убран (неудобен для работы). Фиксы можно делать в любой момент и
|
||||
перепроверять аудитом.
|
||||
`web/audit/audit-report.md` — light/dark токены + поэлементный дифф стилей по
|
||||
тексту. Пиксель-дифф убран (неудобен для работы). Фиксы можно делать в любой
|
||||
момент и перепроверять аудитом.
|
||||
|
||||
## Баги (объективно, правим в любом случае)
|
||||
|
||||
- **B1. Кнопки/инпуты рендерятся шрифтом Arial, а не IBM Plex Sans.**
|
||||
Доказательство: пробы `Reset pipeline` → `fontFamily: Arial`. Причина: у `button/input`
|
||||
не задан `font-family: inherit`. Фикс: добавить в reset `design2.css`
|
||||
`button, input, select, textarea { font-family: inherit; }`. Влияет на все страницы. (P1)
|
||||
- **B2. Двойной топбар.** Страница демо рендерит свой `.topbar` (brand / AUTO PIPELINE /
|
||||
help / theme / lang), который перекрывается глобальным `fixed` Header из
|
||||
`preview/+layout.svelte` (в нём только theme-toggle). В рефе — один топбар.
|
||||
Контент демо-топбара (brand, статус, язык) визуально спрятан за фикс-хедером.
|
||||
Фикс: убрать кастомный `.topbar` из демо и сделать хедер лейаута реф-стайл топбаром
|
||||
(brand + version + status + help + theme + lang) — улучшит и C9/C10. (P1)
|
||||
Доказательство: пробы `Reset pipeline` → `fontFamily: Arial`. Причина: у
|
||||
`button/input` не задан `font-family: inherit`. Фикс: добавить в reset
|
||||
`design2.css` `button, input, select, textarea { font-family: inherit; }`.
|
||||
Влияет на все страницы. (P1)
|
||||
- **B2. Двойной топбар.** Страница демо рендерит свой `.topbar` (brand / AUTO
|
||||
PIPELINE / help / theme / lang), который перекрывается глобальным `fixed`
|
||||
Header из `preview/+layout.svelte` (в нём только theme-toggle). В рефе — один
|
||||
топбар. Контент демо-топбара (brand, статус, язык) визуально спрятан за
|
||||
фикс-хедером. Фикс: убрать кастомный `.topbar` из демо и сделать хедер лейаута
|
||||
реф-стайл топбаром (brand + version + status + help + theme + lang) — улучшит
|
||||
и C9/C10. (P1)
|
||||
|
||||
## Расхождения дизайна (нужно решение)
|
||||
|
||||
- **F1. Фон холста.** Реф: плоский серый `#EEF1F4`. Наш: белый + blueprint-сетка.
|
||||
Решение: совпасть с рефом (убрать сетку на демо) или оставить сетку как сигнатуру нового дизайна.
|
||||
- **F2. Бейдж типа шага** (BACKGROUND/TRANSFORM/STYLE). Реф: приглушённый серый `#6D787F`.
|
||||
Решено C16: `StepCard` рендерит `type` как приглушённый моно-текст (не синий бейдж);
|
||||
индекс шага (`step-index`) — синий `#1769D2`, как в рефе.
|
||||
- **F3. Подписи превью-тайлов** (SOURCE / STEP 01 / FINAL OUTPUT). Реф: синий `#1769D2`,
|
||||
weight 400. Решено C16: `PreviewTile` label — синий `#1769D2`.
|
||||
- **F4. Раскладка preview-stack.** Реф: одна колонка (5 строк, выше). Наш: 2 колонки (grid).
|
||||
Решение: одна колонка (как реф) или оставить 2 колонки как выбор нового дизайна.
|
||||
- **F1. Фон холста.** Реф: плоский серый `#EEF1F4`. Наш: белый +
|
||||
blueprint-сетка. Решение: совпасть с рефом (убрать сетку на демо) или оставить
|
||||
сетку как сигнатуру нового дизайна.
|
||||
- **F2. Бейдж типа шага** (BACKGROUND/TRANSFORM/STYLE). Реф: приглушённый серый
|
||||
`#6D787F`. Решено C16: `StepCard` рендерит `type` как приглушённый моно-текст
|
||||
(не синий бейдж); индекс шага (`step-index`) — синий `#1769D2`, как в рефе.
|
||||
- **F3. Подписи превью-тайлов** (SOURCE / STEP 01 / FINAL OUTPUT). Реф: синий
|
||||
`#1769D2`, weight 400. Решено C16: `PreviewTile` label — синий `#1769D2`.
|
||||
- **F4. Раскладка preview-stack.** Реф: одна колонка (5 строк, выше). Наш: 2
|
||||
колонки (grid). Решение: одна колонка (как реф) или оставить 2 колонки как
|
||||
выбор нового дизайна.
|
||||
- **F5. Вес лейблов контролов.** Реф: 400. Наш (`MonoLabel`): 500. Мелко.
|
||||
- **F6. Мета-подписи** (DIMENSIONS/FORMAT/SIZE): реф fs 9px, наш 10px. Мелко.
|
||||
- **F7. Копирайт:** `Add tool` у нас `+ Add tool`; кнопка `Download result` — сверить стиль
|
||||
(в рефе синяя с белым текстом 478×42). Пробы `Download result`/`Add tool`/`DIRECTION`
|
||||
вернули NOT FOUND — скорее артефакт замера (вложенный текст/чип размера), проверить визуально.
|
||||
- **F7. Копирайт:** `Add tool` у нас `+ Add tool`; кнопка `Download result` —
|
||||
сверить стиль (в рефе синяя с белым текстом 478×42). Пробы
|
||||
`Download result`/`Add tool`/`DIRECTION` вернули NOT FOUND — скорее артефакт
|
||||
замера (вложенный текст/чип размера), проверить визуально.
|
||||
- **F8. h1 line-height** чуть выше (наш 150px vs реф 115px) — мелкий зазор.
|
||||
- **F9. Вертикальный ритм:** контент на ~152px выше, зазоры между секциями отличаются
|
||||
(pipeline-head y 434 vs 400 и т.д.) — подровнять паддинги под реф.
|
||||
- **F10. Внутренние отступы preview-панели** (Hide intermediate x 907 vs 1218) — паддинг панели.
|
||||
- **F9. Вертикальный ритм:** контент на ~152px выше, зазоры между секциями
|
||||
отличаются (pipeline-head y 434 vs 400 и т.д.) — подровнять паддинги под реф.
|
||||
- **F10. Внутренние отступы preview-панели** (Hide intermediate x 907 vs 1218) —
|
||||
паддинг панели.
|
||||
|
||||
## Темы (отложено — F1-F4)
|
||||
|
||||
Серый фон `#EEF1F4` — это **светлая** тема. Уточнение по F1: наш токен `--background`
|
||||
уже равен `#EEF1F4` (совпадает с рефом); видимое отличие — только blueprint-сетка
|
||||
поверх фона, а не «белый vs серый». То есть F1 = «сетка против плоского», не цвет фона.
|
||||
Серый фон `#EEF1F4` — это **светлая** тема. Уточнение по F1: наш токен
|
||||
`--background` уже равен `#EEF1F4` (совпадает с рефом); видимое отличие — только
|
||||
blueprint-сетка поверх фона, а не «белый vs серый». То есть F1 = «сетка против
|
||||
плоского», не цвет фона.
|
||||
|
||||
Перед тем как сверять цвета бейджей/тайлов/раскладок (F2-F4), нужно сначала
|
||||
зафиксировать палитры 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/demo.html` (и соседние `gradient.html`, `background-remover.html`)
|
||||
через Playwright/браузер.
|
||||
- Открыть `refs-html/demo.html` (и соседние `gradient.html`,
|
||||
`background-remover.html`) через Playwright/браузер.
|
||||
- Нажать theme-toggle в топбаре рефа (иконка луны/солнца) — переключает тему
|
||||
(скорее всего меняет атрибут/класс на `<html>` или `.page` и переопределяет CSS-переменные).
|
||||
- Снять скриншоты светлой и тёмной темы, замерить computed-стиль `:root` переменных
|
||||
рефа (через `getComputedStyle(document.documentElement).getPropertyValue('--...')`)
|
||||
и сопоставить с нашими токенами в `design2.css`.
|
||||
(скорее всего меняет атрибут/класс на `<html>` или `.page` и переопределяет
|
||||
CSS-переменные).
|
||||
- Снять скриншоты светлой и тёмной темы, замерить computed-стиль `:root`
|
||||
переменных рефа (через
|
||||
`getComputedStyle(document.documentElement).getPropertyValue('--...')`) и
|
||||
сопоставить с нашими токенами в `design2.css`.
|
||||
- Рефы, похоже, не доделаны по тёмной теме — зафиксировать, какие именно токены
|
||||
меняются и где расхождения. На основе этого выровнять наши токены, затем вернуться
|
||||
к F2 (бейдж типа), F3 (тайлы), F4 (раскладка preview-stack).
|
||||
меняются и где расхождения. На основе этого выровнять наши токены, затем
|
||||
вернуться к F2 (бейдж типа), F3 (тайлы), F4 (раскладка preview-stack).
|
||||
|
||||
Решение по F1-F4 принимается только после сверки токенов светлой/тёмной темы с рефом.
|
||||
Решение по F1-F4 принимается только после сверки токенов светлой/тёмной темы с
|
||||
рефом.
|
||||
|
||||
**Состояние тем (уточнение):** наша dark-тема реализована в `design2.css`
|
||||
(`[data-theme="dark"]`), но пока подключена только на preview-маршрутах (старые
|
||||
страницы грузят `app.css`). Этого достаточно для сверки: токены, отличные от light,
|
||||
соберём по всем `refs-html/*.html` (не только `demo.html`), остальные токены
|
||||
не меняются. Light-токены уже совпадают с рефом (`--background`, `--panel`,
|
||||
`--foreground`, `--muted`, `--line`, `--blue` равны).
|
||||
страницы грузят `app.css`). Этого достаточно для сверки: токены, отличные от
|
||||
light, соберём по всем `refs-html/*.html` (не только `demo.html`), остальные
|
||||
токены не меняются. Light-токены уже совпадают с рефом (`--background`,
|
||||
`--panel`, `--foreground`, `--muted`, `--line`, `--blue` равны).
|
||||
|
||||
## Состояние (C16)
|
||||
|
||||
Цветовые расхождения против `refs-html/demo.html` устранены: стек шрифтов сверен с
|
||||
рефом (`"IBM Plex Sans", sans-serif` / `"IBM Plex Mono", monospace`), бренд топбара —
|
||||
`--foreground`, индекс шага — синий, тип шага и подпись плитки — как в рефе,
|
||||
`Segmented` — 10px/32px. Аудит: токены light 10 / dark 16, элементы 47. Остаток —
|
||||
sub-pixel (10px→9px лейблы, letter-spacing) и структурные (`display`/`rect` от наших
|
||||
flex/grid раскладок против статичного мока); визуально незначимы, правим точечно при
|
||||
желании. Старые тесты (583) — без регрессий.
|
||||
Цветовые расхождения против `refs-html/demo.html` устранены: стек шрифтов сверен
|
||||
с рефом (`"IBM Plex Sans", sans-serif` / `"IBM Plex Mono", monospace`), бренд
|
||||
топбара — `--foreground`, индекс шага — синий, тип шага и подпись плитки — как в
|
||||
рефе, `Segmented` — 10px/32px. Аудит: токены light 10 / dark 16, элементы 47.
|
||||
Остаток — sub-pixel (10px→9px лейблы, letter-spacing) и структурные
|
||||
(`display`/`rect` от наших flex/grid раскладок против статичного мока);
|
||||
визуально незначимы, правим точечно при желании. Старые тесты (583) — без
|
||||
регрессий.
|
||||
|
||||
## План действий (предлагаю)
|
||||
|
||||
> Актуальный пошаговый план фикса вынесен в `docs/plan-design-fix.md`
|
||||
> (Этапы 0–1: рефакторинг + верность). Ниже — исходные наброски.
|
||||
> Актуальный пошаговый план фикса вынесен в `docs/plan-design-fix.md` (Этапы
|
||||
> 0–1: рефакторинг + верность). Ниже — исходные наброски.
|
||||
|
||||
1. Сразу поправить B1 + B2 (чёткие баги, улучшают все страницы).
|
||||
2. По F1–F4 — решение за тобой (совпадать с рефом дословно ИЛИ оставить как сигнатуру нового дизайна).
|
||||
3. F5–F10 — мелочи, подчистить в том же проходе, если решаем «совпадать с рефом».
|
||||
2. По F1–F4 — решение за тобой (совпадать с рефом дословно ИЛИ оставить как
|
||||
сигнатуру нового дизайна).
|
||||
3. F5–F10 — мелочи, подчистить в том же проходе, если решаем «совпадать с
|
||||
рефом».
|
||||
4. Переснять скриншоты и перепроверить пиксель-дифф.
|
||||
|
||||
@@ -4,20 +4,36 @@
|
||||
|
||||
## 1. Цели и принципы
|
||||
|
||||
1. **Один источник правды** — все визуальные константы живут в токенах `web/src/app.css`; ни один компонент не пишет «магические» цвета/размеры напрямую.
|
||||
2. **Никакого дублирования** — повторяющийся визуальный рецепт оформляется один раз: либо токен, либо глобальный утилитарный класс, либо компонент в `ui/`.
|
||||
3. **Простые компоненты важнее умных** — если проще сделать отдельные `SliderField` и `TextField`, делаем два маленьких компонента, а не один конфигурируемый монстр.
|
||||
4. **Scoped-стили — только для раскладки** конкретного компонента (сетка, отступы между блоками). Внешний вид контролов и текстов — из дизайн-системы.
|
||||
1. **Один источник правды** — все визуальные константы живут в токенах
|
||||
`web/src/app.css`; ни один компонент не пишет «магические» цвета/размеры
|
||||
напрямую.
|
||||
2. **Никакого дублирования** — повторяющийся визуальный рецепт оформляется один
|
||||
раз: либо токен, либо глобальный утилитарный класс, либо компонент в `ui/`.
|
||||
3. **Простые компоненты важнее умных** — если проще сделать отдельные
|
||||
`SliderField` и `TextField`, делаем два маленьких компонента, а не один
|
||||
конфигурируемый монстр.
|
||||
4. **Scoped-стили — только для раскладки** конкретного компонента (сетка,
|
||||
отступы между блоками). Внешний вид контролов и текстов — из дизайн-системы.
|
||||
|
||||
## 2. Аудит: что дублируется прямо сейчас
|
||||
|
||||
- **«Приглушённый текст»** (`--text-muted` + размер 0.85–0.95rem) — повторён в семи местах: `ToolPage` (.description, .hint), главная (.lead, .card-desc), лейаут (.nav-link, footer), `Preview` (.dims), `ParamForm` (label), `InfoPanel` (dt).
|
||||
- **«Заголовок секции капсом»** (uppercase, letter-spacing, muted) — точная копипаста в `ToolPage` и на главной.
|
||||
- **Базовые инпуты** (padding, border, radius) — живут только внутри `ParamForm`; любые новые типы контролов (слайдеры, пипетка цвета) пришлось бы копировать вручную.
|
||||
- **Блок «label + отступы»** — дублировался бы в каждом `*Field`; решается обёрткой `Field.svelte`.
|
||||
- **«Панель»** (surface + border + radius) — три ручные реализации одного паттерна: `DropZone`, карточки главной, строки `InfoPanel`.
|
||||
- **Кнопки** — глобальные классы `.primary`/`.secondary` без компонента; состояния «занят/недоступен» каждый описывает заново.
|
||||
- **Ошибка-баннер** — только в `ToolPage`, а понадобится минимум ещё раз в workspace.
|
||||
- **«Приглушённый текст»** (`--text-muted` + размер 0.85–0.95rem) — повторён в
|
||||
семи местах: `ToolPage` (.description, .hint), главная (.lead, .card-desc),
|
||||
лейаут (.nav-link, footer), `Preview` (.dims), `ParamForm` (label),
|
||||
`InfoPanel` (dt).
|
||||
- **«Заголовок секции капсом»** (uppercase, letter-spacing, muted) — точная
|
||||
копипаста в `ToolPage` и на главной.
|
||||
- **Базовые инпуты** (padding, border, radius) — живут только внутри
|
||||
`ParamForm`; любые новые типы контролов (слайдеры, пипетка цвета) пришлось бы
|
||||
копировать вручную.
|
||||
- **Блок «label + отступы»** — дублировался бы в каждом `*Field`; решается
|
||||
обёрткой `Field.svelte`.
|
||||
- **«Панель»** (surface + border + radius) — три ручные реализации одного
|
||||
паттерна: `DropZone`, карточки главной, строки `InfoPanel`.
|
||||
- **Кнопки** — глобальные классы `.primary`/`.secondary` без компонента;
|
||||
состояния «занят/недоступен» каждый описывает заново.
|
||||
- **Ошибка-баннер** — только в `ToolPage`, а понадобится минимум ещё раз в
|
||||
workspace.
|
||||
|
||||
## 3. Токены (расширение `app.css`)
|
||||
|
||||
@@ -55,11 +71,13 @@
|
||||
- `.text-muted` — приглушённый текст (размер наследуется);
|
||||
- `.text-caption` — мелкий текст `--text-s`;
|
||||
- `.heading-section` — заголовок секции капсом (вместо двух копипаст);
|
||||
- `.panel` — поверхность с бордером/радиусом (DropZone, карточки, InfoPanel-строки);
|
||||
- `.panel` — поверхность с бордером/радиусом (DropZone, карточки,
|
||||
InfoPanel-строки);
|
||||
- `.error-banner` — баннер ошибки;
|
||||
- `.visually-hidden` — скрытый, но читаемый скринридерами.
|
||||
|
||||
Элементные стили из текущего `app.css` (голые `input, select { ... }`) убрать — внешний вид контролов переезжает в компоненты `ui/`.
|
||||
Элементные стили из текущего `app.css` (голые `input, select { ... }`) убрать —
|
||||
внешний вид контролов переезжает в компоненты `ui/`.
|
||||
|
||||
## 5. Состав UI-компонентов (`web/src/lib/components/ui/`)
|
||||
|
||||
@@ -76,15 +94,23 @@
|
||||
| `ColorField.svelte` | `id`, `label`, `value = $bindable()` | color-ветка `ParamForm`; точка будущего расширения пипеткой |
|
||||
| `EmptyState.svelte` | `title`, `hint`, слот действия | «Обработка…», будущие пустые панели исходника/результата |
|
||||
|
||||
`Field` — чисто раскладочная обёртка (`<div class="field"><label for>…</label><slot /></div>`), ничего не знает о типе контрола. На ней построены `TextField`, `SliderField`, `SelectField`, `ColorField`. Исключение — `CheckboxField`: у чекбокса label оборачивает контрол (кликабельная строка), поэтому у него своя раскладка.
|
||||
`Field` — чисто раскладочная обёртка
|
||||
(`<div class="field"><label for>…</label><slot /></div>`), ничего не знает о
|
||||
типе контрола. На ней построены `TextField`, `SliderField`, `SelectField`,
|
||||
`ColorField`. Исключение — `CheckboxField`: у чекбокса label оборачивает контрол
|
||||
(кликабельная строка), поэтому у него своя раскладка.
|
||||
|
||||
## 6. Миграция существующего
|
||||
|
||||
1. `ParamForm` — переписывается на `Field` + `*Field`-компоненты; собственная стилизация контролов и label удаляется.
|
||||
1. `ParamForm` — переписывается на `Field` + `*Field`-компоненты; собственная
|
||||
стилизация контролов и label удаляется.
|
||||
2. `DownloadButton` и кнопки `ToolPage` — на `Button`.
|
||||
3. `DropZone`, карточки главной, строки `InfoPanel` — на `.panel` + свои раскладочные стили.
|
||||
4. `ToolPage`, главная, лейаут — приглушённые тексты на `.text-*`, заголовки секций на `.heading-section`, баннер на `.error-banner`.
|
||||
5. Из `app.css` удалить глобальные классы кнопок и голые стили инпутов после перехода.
|
||||
3. `DropZone`, карточки главной, строки `InfoPanel` — на `.panel` + свои
|
||||
раскладочные стили.
|
||||
4. `ToolPage`, главная, лейаут — приглушённые тексты на `.text-*`, заголовки
|
||||
секций на `.heading-section`, баннер на `.error-banner`.
|
||||
5. Из `app.css` удалить глобальные классы кнопок и голые стили инпутов после
|
||||
перехода.
|
||||
|
||||
## 7. Порядок работ
|
||||
|
||||
@@ -97,13 +123,18 @@
|
||||
|
||||
## 8. Критерии готовности
|
||||
|
||||
- Ни один компонент не задаёт цвет/радиус/шрифт мимо токенов или классов системы.
|
||||
- Ни один компонент не задаёт цвет/радиус/шрифт мимо токенов или классов
|
||||
системы.
|
||||
- Все интерактивные контролы на страницах — экземпляры `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. Что сознательно не делаем
|
||||
|
||||
- Никаких CSS-фреймворков и препроцессоров — обычный CSS, как решено на старте.
|
||||
- Никаких тем/тёмного режима — но токены сразу семантические, чтобы тема добавилась заменой значений `:root`.
|
||||
- Никаких сложных составных компонентов (модалки, таблицы) — они появятся, когда потребуется реальная задача.
|
||||
- Никаких тем/тёмного режима — но токены сразу семантические, чтобы тема
|
||||
добавилась заменой значений `:root`.
|
||||
- Никаких сложных составных компонентов (модалки, таблицы) — они появятся, когда
|
||||
потребуется реальная задача.
|
||||
|
||||
+116
-45
@@ -4,38 +4,59 @@
|
||||
>
|
||||
> **Что сделано по плану:**
|
||||
>
|
||||
> - Шаг 0 — каркас: SvelteKit + adapter-static, design-токены на CSS, лейаут с навигацией по категориям ✔
|
||||
> - Шаг 1 — ядро: `PixelImage`, декодирование файлов, кодирование PNG/JPEG/WebP, скачивание ✔
|
||||
> - Шаг 2 — операции ядра: color / geometry / alpha / analyze, чистые функции без DOM ✔
|
||||
> - Шаг 0 — каркас: SvelteKit + adapter-static, design-токены на CSS, лейаут с
|
||||
> навигацией по категориям ✔
|
||||
> - Шаг 1 — ядро: `PixelImage`, декодирование файлов, кодирование PNG/JPEG/WebP,
|
||||
> скачивание ✔
|
||||
> - Шаг 2 — операции ядра: color / geometry / alpha / analyze, чистые функции
|
||||
> без DOM ✔
|
||||
> - Шаг 3 — реестр из 11 инструментов + санитизация параметров ✔
|
||||
> - Шаг 4 — универсальные UI-компоненты (DropZone, Preview, ParamForm, DownloadButton, InfoPanel, ToolPage) ✔
|
||||
> - Шаг 5 — маршруты: `/tools/[id]` с prerender всех страниц, главная-каталог по категориям ✔
|
||||
> - Шаг 4 — универсальные UI-компоненты (DropZone, Preview, ParamForm,
|
||||
> DownloadButton, InfoPanel, ToolPage) ✔
|
||||
> - Шаг 5 — маршруты: `/tools/[id]` с prerender всех страниц, главная-каталог по
|
||||
> категориям ✔
|
||||
> - Шаги 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. Зафиксированные решения
|
||||
|
||||
- **Объём MVP:** только одиночные инструменты «загрузил → настроил → скачал». Пайплайн-workspace (`/workspace`) вне скоупа этого плана.
|
||||
- **Набор инструментов первого среза:** ~11 базовых EASY (см. §3). Достаточно, чтобы проверить насквозь ядро → реестр → универсальную страницу.
|
||||
- **Стили:** обычный CSS без фреймворка — design-токены на CSS custom properties, ноль зависимостей.
|
||||
- **Стек:** SvelteKit + `@sveltejs/adapter-static` + TypeScript, пакетный менеджер pnpm (уже зафиксирован в корневом `package.json`). Весь код сайта — в `web/`.
|
||||
- **Ядро без DOM:** все пиксельные операции — чистые функции над `PixelImage`; DOM/canvas допускается только в модуле IO и компонентах превью.
|
||||
- **Объём MVP:** только одиночные инструменты «загрузил → настроил → скачал».
|
||||
Пайплайн-workspace (`/workspace`) вне скоупа этого плана.
|
||||
- **Набор инструментов первого среза:** ~11 базовых EASY (см. §3). Достаточно,
|
||||
чтобы проверить насквозь ядро → реестр → универсальную страницу.
|
||||
- **Стили:** обычный CSS без фреймворка — design-токены на CSS custom
|
||||
properties, ноль зависимостей.
|
||||
- **Стек:** SvelteKit + `@sveltejs/adapter-static` + TypeScript, пакетный
|
||||
менеджер pnpm (уже зафиксирован в корневом `package.json`). Весь код сайта — в
|
||||
`web/`.
|
||||
- **Ядро без DOM:** все пиксельные операции — чистые функции над `PixelImage`;
|
||||
DOM/canvas допускается только в модуле IO и компонентах превью.
|
||||
|
||||
---
|
||||
|
||||
## 1. Шаг 0 — Каркас проекта
|
||||
|
||||
1. Создать SvelteKit-приложение в `web/`: `npx sv create web` (шаблон minimal, TypeScript, без лишних аддонов).
|
||||
1. Создать SvelteKit-приложение в `web/`: `npx sv create web` (шаблон minimal,
|
||||
TypeScript, без лишних аддонов).
|
||||
2. `pnpm install` внутри `web/`.
|
||||
3. `svelte.config.js`: заменить адаптер на `@sveltejs/adapter-static`.
|
||||
4. `src/routes/+layout.ts`: `export const prerender = true;` — весь сайт prerenderится в статику.
|
||||
5. `src/app.css`: дизайн-токены (палитра, типографика, отступы, радиусы через CSS custom properties) + минимальный сброс стилей.
|
||||
6. `src/routes/+layout.svelte`: шапка с названием сайта и навигацией по категориям инструментов (категории — константа из реестра), футер.
|
||||
4. `src/routes/+layout.ts`: `export const prerender = true;` — весь сайт
|
||||
prerenderится в статику.
|
||||
5. `src/app.css`: дизайн-токены (палитра, типографика, отступы, радиусы через
|
||||
CSS custom properties) + минимальный сброс стилей.
|
||||
6. `src/routes/+layout.svelte`: шапка с названием сайта и навигацией по
|
||||
категориям инструментов (категории — константа из реестра), футер.
|
||||
|
||||
**Проверка:** `pnpm dev` открывает пустой каркас; `pnpm build && pnpm preview` отдаёт статический экспорт.
|
||||
**Проверка:** `pnpm dev` открывает пустой каркас; `pnpm build && pnpm preview`
|
||||
отдаёт статический экспорт.
|
||||
|
||||
---
|
||||
|
||||
@@ -46,28 +67,42 @@
|
||||
- `types.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:
|
||||
- `decodeFile(file: File): Promise<PixelImage>` — `createImageBitmap(file)` → отрисовка на canvas → `getImageData`;
|
||||
- `encode(img: PixelImage, mime: 'image/png' | 'image/jpeg' | 'image/webp', quality?: number): Promise<Blob>` — `putImageData` → `canvas.toBlob`;
|
||||
- `decodeFile(file: File): Promise<PixelImage>` — `createImageBitmap(file)` →
|
||||
отрисовка на canvas → `getImageData`;
|
||||
- `encode(img: PixelImage, mime: 'image/png' | 'image/jpeg' | 'image/webp', quality?: number): Promise<Blob>`
|
||||
— `putImageData` → `canvas.toBlob`;
|
||||
- `downloadBlob(blob: Blob, filename: string): void`.
|
||||
|
||||
**Проверка:** юнит-смоук — декодировать фикстурный PNG, закодировать обратно, размеры совпадают.
|
||||
**Проверка:** юнит-смоук — декодировать фикстурный PNG, закодировать обратно,
|
||||
размеры совпадают.
|
||||
|
||||
---
|
||||
|
||||
## 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)`.
|
||||
- `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.
|
||||
- `color.ts`: `grayscale(img)` (luma), `invert(img)`,
|
||||
`brightnessContrast(img, brightness −100..100, contrast −100..100)`.
|
||||
- `geometry.ts`: `flip(img, axis)`, `rotate90(img, turns)`,
|
||||
`crop(img, x, y, w, h)` — индексная математика; `resize(img, w, h)` —
|
||||
билинейная интерполяция на TS (не `drawImage`, чтобы ядро осталось DOM-free и
|
||||
воспроизводимым бит-в-бит).
|
||||
- `alpha.ts`: `removeColorToAlpha(img, hex, tolerancePercent)` — цветовое
|
||||
расстояние → альфа 0.
|
||||
- `analyze.ts`: `imageInfo(img)` → `{ width, height, hasAlpha, colorCount }`.
|
||||
|
||||
**Проверка:** vitest на маленьких матрицах пикселей (2×2, 3×3): flip, rotate, crop, grayscale — ожидаемые значения руками.
|
||||
**Проверка:** vitest на маленьких матрицах пикселей (2×2, 3×3): flip, rotate,
|
||||
crop, grayscale — ожидаемые значения руками.
|
||||
|
||||
---
|
||||
|
||||
@@ -77,8 +112,22 @@
|
||||
|
||||
```ts
|
||||
export type ParamDef =
|
||||
| { id: string; label: string; type: "number"; min?: number; max?: number; step?: number; default: number }
|
||||
| { id: string; label: string; type: "select"; options: { value: string; label: string }[]; default: string }
|
||||
| {
|
||||
id: string;
|
||||
label: string;
|
||||
type: "number";
|
||||
min?: number;
|
||||
max?: number;
|
||||
step?: number;
|
||||
default: number;
|
||||
}
|
||||
| {
|
||||
id: string;
|
||||
label: string;
|
||||
type: "select";
|
||||
options: { value: string; label: string }[];
|
||||
default: string;
|
||||
}
|
||||
| { id: string; label: string; type: "checkbox"; default: boolean }
|
||||
| { id: string; label: string; type: "color"; default: string };
|
||||
|
||||
@@ -88,11 +137,15 @@ export type ToolEntry = {
|
||||
description: string;
|
||||
category: CategoryId;
|
||||
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 инструментов
|
||||
|
||||
@@ -110,9 +163,11 @@ export type ToolEntry = {
|
||||
| 10 | `remove-color-from-png` | alpha | hex color, tolerance % |
|
||||
| 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/`:
|
||||
|
||||
- `DropZone.svelte` — drag&drop + клик; эмитит `File`; проверка MIME (PNG/JPEG/WebP/GIF/BMP).
|
||||
- `Preview.svelte` — рендер `PixelImage` на canvas с fit-масштабированием; шахматный фон под прозрачность.
|
||||
- `ParamForm.svelte` — генерирует контролы из `ParamDef[]`, двусторонний `bind:values`.
|
||||
- `DownloadButton.svelte` — encode выбранного формата + `downloadBlob`, имя `<tool-id>.<ext>`.
|
||||
- `DropZone.svelte` — drag&drop + клик; эмитит `File`; проверка MIME
|
||||
(PNG/JPEG/WebP/GIF/BMP).
|
||||
- `Preview.svelte` — рендер `PixelImage` на canvas с fit-масштабированием;
|
||||
шахматный фон под прозрачность.
|
||||
- `ParamForm.svelte` — генерирует контролы из `ParamDef[]`, двусторонний
|
||||
`bind:values`.
|
||||
- `DownloadButton.svelte` — encode выбранного формата + `downloadBlob`, имя
|
||||
`<tool-id>.<ext>`.
|
||||
- `InfoPanel.svelte` — текстовый вывод для `resultType: 'info'`.
|
||||
- `ToolPage.svelte` — оркестратор страницы инструмента; состояния `idle → loaded → processing → done | error`; раскладка: слева DropZone, справа превью + форма параметров + кнопки «Применить» / «Скачать».
|
||||
- `ToolPage.svelte` — оркестратор страницы инструмента; состояния
|
||||
`idle → loaded → processing → done | error`; раскладка: слева DropZone, справа
|
||||
превью + форма параметров + кнопки «Применить» / «Скачать».
|
||||
|
||||
**Правило:** новый инструмент = новая запись в реестре (+ функция операции). Компоненты и маршрут не трогаем.
|
||||
**Правило:** новый инструмент = новая запись в реестре (+ функция операции).
|
||||
Компоненты и маршрут не трогаем.
|
||||
|
||||
---
|
||||
|
||||
## 6. Шаг 5 — Маршруты
|
||||
|
||||
- `/` (`src/routes/+page.svelte`) — сетка карточек инструментов, сгруппированных по категориям; данные из реестра.
|
||||
- `/` (`src/routes/+page.svelte`) — сетка карточек инструментов, сгруппированных
|
||||
по категориям; данные из реестра.
|
||||
- `/tools/[id]` — универсальная страница:
|
||||
- `+page.ts`: `entries()` возвращает все `id` из реестра (обязательное условие prerender при adapter-static);
|
||||
- `+page.ts`: `entries()` возвращает все `id` из реестра (обязательное условие
|
||||
prerender при adapter-static);
|
||||
- `load()` возвращает запись реестра по `id`, неизвестный id → 404;
|
||||
- `+page.svelte` — тонкая обёртка над `ToolPage`.
|
||||
|
||||
**Проверка:** `pnpm build` — в `build/tools/` лежат все 11 страниц; главная содержит ссылки на каждую.
|
||||
**Проверка:** `pnpm build` — в `build/tools/` лежат все 11 страниц; главная
|
||||
содержит ссылки на каждую.
|
||||
|
||||
---
|
||||
|
||||
## 7. Шаг 6 — Ошибки и минимальная полировка
|
||||
|
||||
- Обработка ошибок: неподдерживаемый формат, ошибка декода, некорректные параметры crop (выход за границы — клампы или понятная ошибка).
|
||||
- Состояния: «перетащите файл», спиннер во время обработки, disabled-кнопки до загрузки.
|
||||
- Обработка ошибок: неподдерживаемый формат, ошибка декода, некорректные
|
||||
параметры crop (выход за границы — клампы или понятная ошибка).
|
||||
- Состояния: «перетащите файл», спиннер во время обработки, disabled-кнопки до
|
||||
загрузки.
|
||||
- Тексты описаний для всех 11 инструментов (title/description уже в реестре).
|
||||
|
||||
---
|
||||
@@ -161,6 +228,10 @@ cd web && pnpm build # статический экспорт должен
|
||||
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
@@ -2,15 +2,14 @@
|
||||
|
||||
> **СТАТУС: РЕАЛИЗОВАН (2026-09-07) — устарел.** Скрипты
|
||||
> `web/scripts/audit-dom.mjs`, `audit-css.mjs`, `audit-cdp.mjs`,
|
||||
> `audit-cdp-responsive.mjs` и npm-скрипты `refs-dom-audit` /
|
||||
> `refs-css-audit` / `refs-cdp-audit` на месте; старый текстовый `audit.mjs`
|
||||
> удалён. Актуальная доводка верности и статус пунктов F — в
|
||||
> `plan-design-fix.md`.
|
||||
> `audit-cdp-responsive.mjs` и npm-скрипты `refs-dom-audit` / `refs-css-audit` /
|
||||
> `refs-cdp-audit` на месте; старый текстовый `audit.mjs` удалён. Актуальная
|
||||
> доводка верности и статус пунктов F — в `plan-design-fix.md`.
|
||||
|
||||
> Статус: к выполнению. Ветка `feat/redesign`.
|
||||
> Источник правды по пикселям: `refs-html/*.html` (статический экспорт рефа).
|
||||
> Актуальный рабочий аудит сейчас — `pnpm refs-audit` (`scripts/audit.mjs`), но он
|
||||
> **сломан** (см. «Почему текущий аудит не работает») и подлежит замене.
|
||||
> Статус: к выполнению. Ветка `feat/redesign`. Источник правды по пикселям:
|
||||
> `refs-html/*.html` (статический экспорт рефа). Актуальный рабочий аудит сейчас
|
||||
> — `pnpm refs-audit` (`scripts/audit.mjs`), но он **сломан** (см. «Почему
|
||||
> текущий аудит не работает») и подлежит замене.
|
||||
|
||||
## Почему текущий аудит не работает
|
||||
|
||||
@@ -37,13 +36,13 @@
|
||||
Заводим их через `t()` — совпадёт с рефом (EN).
|
||||
2. **Имена классов НЕ сводим к рефу.** Матчинг делаем по **структурному пути**,
|
||||
а не по классу. Компоненты `kit/` остаются идиоматичными, Stage 0 не
|
||||
откатываем. Требуется только эквивалентность *структуры* (теги, вложенность,
|
||||
откатываем. Требуется только эквивалентность _структуры_ (теги, вложенность,
|
||||
порядок сиблингов, наличие узлов, текст).
|
||||
3. **Пиксельный дифф (Playwright + pixelmatch) пока НЕ делаем.** DOM+CSS-аудита
|
||||
достаточно; добавим как сеть позже, если стили что-то упустят.
|
||||
4. **Порядок прогона маршрутов:** `demo` → `list-tools` → затем 2 инструмента
|
||||
(`tools/linear-gradient-png` ↔ `gradient.html`,
|
||||
`tools/remove-background-png` ↔ `background-remover.html`).
|
||||
(`tools/linear-gradient-png` ↔ `gradient.html`, `tools/remove-background-png`
|
||||
↔ `background-remover.html`).
|
||||
|
||||
## Принципы
|
||||
|
||||
@@ -106,15 +105,15 @@ Svelte-атрибутов и комментов гидрации), чтобы п
|
||||
- `moved` — тот же текст/тег, другой путь (перепутанный порядок/родитель);
|
||||
- `textMismatch` — нормализованный текст различается (в т.ч. RU vs EN).
|
||||
|
||||
Класс-мисматч — **только информационно**, не блокирует (классы свои по
|
||||
решению №2). Счётчики: `added / removed / moved / textMismatch`.
|
||||
Класс-мисматч — **только информационно**, не блокирует (классы свои по решению
|
||||
№2). Счётчики: `added / removed / moved / textMismatch`.
|
||||
|
||||
### Рендеринг
|
||||
|
||||
- **наше:** dev-сервер на `5179` (как сейчас в `audit.mjs`).
|
||||
- **реф:** `pathToFileURL` к `refs-html/<ref>.html` (`file://`). Реф —
|
||||
самодостаточный (инлайн `<style>`), рендерится полностью. Шрифты для фазы A
|
||||
не важны (сравниваем структуру/текст, не растеризацию).
|
||||
самодостаточный (инлайн `<style>`), рендерится полностью. Шрифты для фазы A не
|
||||
важны (сравниваем структуру/текст, не растеризацию).
|
||||
- Переиспользуем из текущего `audit.mjs`: `discoverRoutes()`, `EXCLUDE`
|
||||
(`/preview`), `DYNAMIC` (`tools/[id]` → `gradient.html` /
|
||||
`background-remover.html`), `refNameFor()`.
|
||||
@@ -122,8 +121,8 @@ Svelte-атрибутов и комментов гидрации), чтобы п
|
||||
### Шаги (на каждом маршруте)
|
||||
|
||||
1. Прогнать `pnpm refs-dom-audit`, получить дельты.
|
||||
2. Поправить структуру Svelte под реф (добавить/убрать/переставить узлы,
|
||||
вынести тексты в `t()` с EN-базой).
|
||||
2. Поправить структуру Svelte под реф (добавить/убрать/переставить узлы, вынести
|
||||
тексты в `t()` с EN-базой).
|
||||
3. Перезапустить, пока `added/removed/moved/textMismatch` ≃ 0 (кроме `IGNORE`).
|
||||
|
||||
---
|
||||
@@ -135,17 +134,17 @@ Svelte-атрибутов и комментов гидрации), чтобы п
|
||||
|
||||
### Сравниваемые свойства (computed)
|
||||
|
||||
`borderRadius`, позиция/размер (`getBoundingClientRect`: x/y/w/h),
|
||||
`padding` (все стороны), `margin`, `color`, `backgroundColor`,
|
||||
`borderRadius`, позиция/размер (`getBoundingClientRect`: x/y/w/h), `padding`
|
||||
(все стороны), `margin`, `color`, `backgroundColor`,
|
||||
`borderTop/Right/Bottom/Left Width|Color`, `fontFamily`, `fontSize`,
|
||||
`fontWeight`, `letterSpacing`, `lineHeight`, `gap`, `display`, `boxShadow`.
|
||||
|
||||
### Дифф
|
||||
|
||||
Для каждого состыкованного по пути элемента — список свойств с дельтой
|
||||
(наше значение → реф). Элементы, не совпавшие по пути (остаток из фазы A),
|
||||
идут в `unmatched`. Отчёт: элемент → свойство → наше/реф. Правится одной
|
||||
строкой CSS, без брутфорса.
|
||||
Для каждого состыкованного по пути элемента — список свойств с дельтой (наше
|
||||
значение → реф). Элементы, не совпавшие по пути (остаток из фазы A), идут в
|
||||
`unmatched`. Отчёт: элемент → свойство → наше/реф. Правится одной строкой CSS,
|
||||
без брутфорса.
|
||||
|
||||
### Шаги
|
||||
|
||||
@@ -167,17 +166,17 @@ Svelte-атрибутов и комментов гидрации), чтобы п
|
||||
(шаги 1–5 плана list-tools). То есть фаза A/B для list-tools стартует после
|
||||
landing этих шагов.
|
||||
3. **`tools/linear-gradient-png`** (↔ `gradient.html`) и
|
||||
**`tools/remove-background-png`** (↔ `background-remover.html`) — **последние**.
|
||||
Известные расхождения: наши инструменты могут иметь **другие наборы
|
||||
параметров / UI**, чем конкретные рефовые тулзы. Ожидаются структурные и
|
||||
стилевые дельты, которые не сводятся к «подровнять паддинг». Для них —
|
||||
**`tools/remove-background-png`** (↔ `background-remover.html`) —
|
||||
**последние**. Известные расхождения: наши инструменты могут иметь **другие
|
||||
наборы параметров / UI**, чем конкретные рефовые тулзы. Ожидаются структурные
|
||||
и стилевые дельты, которые не сводятся к «подровнять паддинг». Для них —
|
||||
триаж после прогона:
|
||||
- реальный разрыв верности → фиксим;
|
||||
- принципиальное отличие (параметр, которого нет в реф-тулзе; иной контрол)
|
||||
→ либо аккуратно приводим наш UI параметра к рефу, либо **документируем
|
||||
как допустимое расхождение / заносим в allowlist** конфига аудита
|
||||
(селектор + причина). «Скипы/фиксы» для инструментов оформляем явно, чтобы
|
||||
аудит не шумел на осознанных отличиях.
|
||||
- принципиальное отличие (параметр, которого нет в реф-тулзе; иной контрол) →
|
||||
либо аккуратно приводим наш UI параметра к рефу, либо **документируем как
|
||||
допустимое расхождение / заносим в allowlist** конфига аудита (селектор +
|
||||
причина). «Скипы/фиксы» для инструментов оформляем явно, чтобы аудит не
|
||||
шумел на осознанных отличиях.
|
||||
|
||||
---
|
||||
|
||||
@@ -185,25 +184,25 @@ Svelte-атрибутов и комментов гидрации), чтобы п
|
||||
|
||||
- `scripts/audit-dom.mjs` — фаза A (нормализация + структурный дифф).
|
||||
- `scripts/audit-css.mjs` — фаза B (матчинг по пути + дифф стилей).
|
||||
- `package.json`: `refs-dom-audit`, `refs-css-audit` (опционально
|
||||
`refs-audit`, прогоняющий обе фазы).
|
||||
- Переиспользуем из старого `audit.mjs`: `discoverRoutes`, `EXCLUDE`,
|
||||
`DYNAMIC`, `refNameFor`, `waitFor` (fast-fail на 4xx/5xx).
|
||||
- `package.json`: `refs-dom-audit`, `refs-css-audit` (опционально `refs-audit`,
|
||||
прогоняющий обе фазы).
|
||||
- Переиспользуем из старого `audit.mjs`: `discoverRoutes`, `EXCLUDE`, `DYNAMIC`,
|
||||
`refNameFor`, `waitFor` (fast-fail на 4xx/5xx).
|
||||
- Отчёты: `web/audit/dom-report.{md,json}` и `web/audit/css-report.{md,json}`
|
||||
(старый `audit-report.*` заменяем).
|
||||
|
||||
## Верификация (после каждого коммита)
|
||||
|
||||
- `pnpm lint`, `pnpm exec svelte-check --tsconfig ./tsconfig.json`,
|
||||
`pnpm build` — без ошибок.
|
||||
- `pnpm refs-dom-audit` (фаза A) / `pnpm refs-css-audit` (фаза B) по
|
||||
затронутому маршруту: счётчики расхождений не растут без причины.
|
||||
- `pnpm lint`, `pnpm exec svelte-check --tsconfig ./tsconfig.json`, `pnpm build`
|
||||
— без ошибок.
|
||||
- `pnpm refs-dom-audit` (фаза A) / `pnpm refs-css-audit` (фаза B) по затронутому
|
||||
маршруту: счётчики расхождений не растут без причины.
|
||||
|
||||
## Риски / заметки
|
||||
|
||||
- **Стабильность матчинга:** нужна аккуратная нормализация (сортировка
|
||||
классов, игнор svelte-атрибутов/`data-*`, игнор whitespace-текстовых узлов,
|
||||
игнор внутренностей SVG-иконок). Подстраивается по первым прогонам demo.
|
||||
- **Стабильность матчинга:** нужна аккуратная нормализация (сортировка классов,
|
||||
игнор svelte-атрибутов/`data-*`, игнор whitespace-текстовых узлов, игнор
|
||||
внутренностей SVG-иконок). Подстраивается по первым прогонам demo.
|
||||
- **Оболочка** (TopBar/footer/язык) сейчас отличается по дизайну — сводим к рефу
|
||||
в рамках полного соответствия (без IGNORE); финальная унификация компонентов —
|
||||
отдельным рефакторингом позже.
|
||||
|
||||
@@ -5,56 +5,54 @@
|
||||
> `schema.layout`, изоляция веток и вся миграция инструментов (121/125,
|
||||
> остальные 4 — отложены решениями) выполнены. Ключевые архитектурные решения
|
||||
> (параллельная сборка, правило копий, изоляция old↔preview) закреплены в
|
||||
> `AGENTS.md`.
|
||||
> **Незакрытое из этого плана:** шаг 37 — физический переезд старых файлов
|
||||
> в папки `old/` (перенесён в `backlog.md`), и удаление старого кода —
|
||||
> по `plan-redesign.md` (C17–C21). Отложенные инструменты —
|
||||
> `watermark-image-png` (overlay), `png-info`, `reduce-to-size` — в `backlog.md`.
|
||||
> Ниже — полное состояние на момент закрытия.
|
||||
> `AGENTS.md`. **Незакрытое из этого плана:** шаг 37 — физический переезд старых
|
||||
> файлов в папки `old/` (перенесён в `backlog.md`), и удаление старого кода — по
|
||||
> `plan-redesign.md` (C17–C21). Отложенные инструменты — `watermark-image-png`
|
||||
> (overlay), `png-info`, `reduce-to-size` — в `backlog.md`. Ниже — полное
|
||||
> состояние на момент закрытия.
|
||||
|
||||
> Статус: **в реализации.** Фаза 0 (фундамент) ✔, Фаза 1 (рабочий инструмент
|
||||
> в preview) ✔, Фаза 2 (простые инструменты без составных типов) — переведены
|
||||
> все одиночные инструменты (18 шт). Фаза 3 — **составной тип `dimension`
|
||||
> полностью переведён** (12 инструментов: create-empty, single-color,
|
||||
> random-noise, linear-gradient, color-spectrum, random-colors, draw-grid,
|
||||
> placeholder, fit-on-background, change-canvas-size, resize, crop); **составной
|
||||
> тип `color-pair` полностью переведён** (4 инструмента: blend-two,
|
||||
> step-colors, linear-gradient, two-colors); **составной тип `offset`
|
||||
> полностью переведён** (4 инструмента: circle-mask, square-mask,
|
||||
> star-mask, wavy-mask); **составной тип `position9` — переведены
|
||||
> add-text-png и date-stamp-png** (водяной знак-картинка — отдельный шаг:
|
||||
> требует overlay-механику в новом превью); **составной тип `font-style`
|
||||
> полностью переведён** (text-to-png, add-text, date-stamp); **составной тип
|
||||
> `plate` полностью переведён** (add-text, date-stamp); **составной тип
|
||||
> `gradient` полностью переведён** (linear-gradient): вместо горизонтального/
|
||||
> вертикального select — **угол 0..360°** (новый виджет `AngleControl`:
|
||||
> slider + кнопки-пресеты 0°/90°/180°/270°, переиспользуемый компонент);
|
||||
> preview
|
||||
> научен применять **генераторы** (`executeGenerate`, кнопка Generate) и
|
||||
> рендерить все kinds схемы (slider/number/color/select/checkbox/dimension/
|
||||
> Статус: **в реализации.** Фаза 0 (фундамент) ✔, Фаза 1 (рабочий инструмент в
|
||||
> preview) ✔, Фаза 2 (простые инструменты без составных типов) — переведены все
|
||||
> одиночные инструменты (18 шт). Фаза 3 — **составной тип `dimension` полностью
|
||||
> переведён** (12 инструментов: create-empty, single-color, random-noise,
|
||||
> linear-gradient, color-spectrum, random-colors, draw-grid, placeholder,
|
||||
> fit-on-background, change-canvas-size, resize, crop); **составной тип
|
||||
> `color-pair` полностью переведён** (4 инструмента: blend-two, step-colors,
|
||||
> linear-gradient, two-colors); **составной тип `offset` полностью переведён**
|
||||
> (4 инструмента: circle-mask, square-mask, star-mask, wavy-mask); **составной
|
||||
> тип `position9` — переведены add-text-png и date-stamp-png** (водяной
|
||||
> знак-картинка — отдельный шаг: требует overlay-механику в новом превью);
|
||||
> **составной тип `font-style` полностью переведён** (text-to-png, add-text,
|
||||
> date-stamp); **составной тип `plate` полностью переведён** (add-text,
|
||||
> date-stamp); **составной тип `gradient` полностью переведён**
|
||||
> (linear-gradient): вместо горизонтального/ вертикального select — **угол
|
||||
> 0..360°** (новый виджет `AngleControl`: slider + кнопки-пресеты
|
||||
> 0°/90°/180°/270°, переиспользуемый компонент); preview научен применять
|
||||
> **генераторы** (`executeGenerate`, кнопка Generate) и рендерить все kinds
|
||||
> схемы (slider/number/color/select/checkbox/dimension/
|
||||
> color-pair/offset/position9/font-style/plate/gradient). **Пер-инструмент
|
||||
> раскладка `schema.layout` реализована** (шаг 32): тип `ToolSchemaLayout`
|
||||
> {groups: [{title?, cols?, fields}]}, рендер групп в `SchemaFields.svelte`
|
||||
> (заголовок группы + сетка колонок, неупомянутые поля — в общей группе),
|
||||
> пилот — `add-text` (группы Text/Placement/Plate). **UI-макеты расставлены
|
||||
> по переведённым инструментам** (шаг 33): geometry (fit-on-background,
|
||||
> (заголовок группы + сетка колонок, неупомянутые поля — в общей группе), пилот
|
||||
> — `add-text` (группы Text/Placement/Plate). **UI-макеты расставлены по
|
||||
> переведённым инструментам** (шаг 33): geometry (fit-on-background,
|
||||
> change-canvas-size, resize, crop), маски alpha (Shape/Position), generate
|
||||
> (create-empty, linear-gradient, color-spectrum, random-colors, draw-grid,
|
||||
> step-colors, placeholder, text-to-png), text (add-text, date-stamp),
|
||||
> filters (randomize-pixels, add-noise) — везде, где канвас отделён от
|
||||
> параметров эффекта либо фигура от позиции.
|
||||
> Следующее закрыто в `archive/plan-migrate-remaining.md` (2026-09-07):
|
||||
> перевод оставшихся инструментов выполнен (всего 121/125 в preview; 4 хвоста —
|
||||
> step-colors, placeholder, text-to-png), text (add-text, date-stamp), filters
|
||||
> (randomize-pixels, add-noise) — везде, где канвас отделён от параметров
|
||||
> эффекта либо фигура от позиции. Следующее закрыто в
|
||||
> `archive/plan-migrate-remaining.md` (2026-09-07): перевод оставшихся
|
||||
> инструментов выполнен (всего 121/125 в preview; 4 хвоста —
|
||||
> `watermark-image-png`, `png-info`, `reduce-to-size-png`, `compress-png` —
|
||||
> отложены решениями в `backlog.md`). Незакрытые продолжения: переезд старых
|
||||
> компонентов в папки `old/` (шаг 37 ниже), закрытие долгов и сбор фидбека —
|
||||
> см. также `plan-design-fix.md`.
|
||||
> компонентов в папки `old/` (шаг 37 ниже), закрытие долгов и сбор фидбека — см.
|
||||
> также `plan-design-fix.md`.
|
||||
>
|
||||
> Ключевые файлы нового registry: `web/src/lib/registry-new/{types,index,*}.ts`
|
||||
> (по файлу на категорию: geometry/alpha/convert/analyze/filters/color/generate)
|
||||
>
|
||||
> - `web/src/lib/preview/categories.ts`,
|
||||
> `web/src/lib/registry-schema.ts` (kind `dimension`), `kit/fields/DimensionField.svelte`,
|
||||
> - `web/src/lib/preview/categories.ts`, `web/src/lib/registry-schema.ts` (kind
|
||||
> `dimension`), `kit/fields/DimensionField.svelte`,
|
||||
> `preview/executor/index.ts` (`executeGenerate`), `SchemaFields.svelte`
|
||||
> (полный рендер kinds). Старый `web/src/lib/registry.ts` разбит по категориям
|
||||
> в `web/src/lib/registry/` (см. `registry.ts` — тонкий баррель).
|
||||
@@ -62,23 +60,23 @@
|
||||
## Ключевая стратегия: параллельная сборка, старый UI не трогаем
|
||||
|
||||
Старый UI (группа `(old)/`, `ParamForm.svelte` + старый pipeline) **продолжает
|
||||
работать как сейчас, без рефакторинга**. Новый `ToolSchema<P>` строится
|
||||
рядом и служит источником для **нового UI**. Инструменты переписываются под
|
||||
новый registry по мере нужды — для нового UI.
|
||||
работать как сейчас, без рефакторинга**. Новый `ToolSchema<P>` строится рядом и
|
||||
служит источником для **нового UI**. Инструменты переписываются под новый
|
||||
registry по мере нужды — для нового UI.
|
||||
|
||||
Это та же логика, что в `plan-redesign.md` применена к дизайну: параллельная
|
||||
сборка, изоляция от старого, затем новый становится основным и старый
|
||||
удаляется вместе со старым дизайном.
|
||||
сборка, изоляция от старого, затем новый становится основным и старый удаляется
|
||||
вместе со старым дизайном.
|
||||
|
||||
**Правило копий (важно!):** если для нового UI/registry нужно внести изменения
|
||||
в файл, который **прямо или косвенно** уже используется `(old)` веткой, — этот
|
||||
файл **НЕ трогаем**. Вместо этого делаем **копию** в новом месте (например,
|
||||
в `lib/preview/`) и правим копию. Это приводит к дублированию, но
|
||||
**гарантированно не задевает старую ветку сайта**. Пример: категории — новый
|
||||
**Правило копий (важно!):** если для нового UI/registry нужно внести изменения в
|
||||
файл, который **прямо или косвенно** уже используется `(old)` веткой, — этот
|
||||
файл **НЕ трогаем**. Вместо этого делаем **копию** в новом месте (например, в
|
||||
`lib/preview/`) и правим копию. Это приводит к дублированию, но **гарантированно
|
||||
не задевает старую ветку сайта**. Пример: категории — новый
|
||||
`lib/preview/categories.ts` (object as const) копирует и заменяет собой
|
||||
`../categories` для нового кода, старый `categories.ts` обслуживает `(old)` и
|
||||
остаётся без изменений. После перехода (Фаза 5) копия становится основной,
|
||||
а исходник продолжает обслуживать `(old)` UI (он не удаляется — см. шаг 35).
|
||||
остаётся без изменений. После перехода (Фаза 5) копия становится основной, а
|
||||
исходник продолжает обслуживать `(old)` UI (он не удаляется — см. шаг 35).
|
||||
|
||||
**Разделение схем: две независимые схемы.**
|
||||
|
||||
@@ -98,14 +96,14 @@
|
||||
Помимо двух схем, registry тоже разделён по UI (по факту миграции):
|
||||
|
||||
- **Старый registry** (`web/src/lib/registry.ts` + `registry/` +
|
||||
`registry-helpers.ts`) — работает на старом UI, использует `ParamDef[]`.
|
||||
**Не трогаем**; остаётся обслуживать `(old)/` маршруты (см. шаг 35).
|
||||
- **Новый registry** (`web/src/lib/registry-new/`) — строится **с нуля «как надо»**:
|
||||
`ToolEntry<P>` с обязательным `schema`, типизированный `run`, **без** `ParamDef[]`
|
||||
и **без** связи со старым. Импортирует core-функции (`expandCanvas`,
|
||||
`strokeImage`, …) напрямую. Наполняется **по-инструментно** по мере миграции
|
||||
(не разовым переводом всех 130). Preview показывает только переведённые
|
||||
инструменты.
|
||||
`registry-helpers.ts`) — работает на старом UI, использует `ParamDef[]`. **Не
|
||||
трогаем**; остаётся обслуживать `(old)/` маршруты (см. шаг 35).
|
||||
- **Новый registry** (`web/src/lib/registry-new/`) — строится **с нуля «как
|
||||
надо»**: `ToolEntry<P>` с обязательным `schema`, типизированный `run`, **без**
|
||||
`ParamDef[]` и **без** связи со старым. Импортирует core-функции
|
||||
(`expandCanvas`, `strokeImage`, …) напрямую. Наполняется **по-инструментно**
|
||||
по мере миграции (не разовым переводом всех 130). Preview показывает только
|
||||
переведённые инструменты.
|
||||
|
||||
Preview (`catalog.ts`, маршруты `preview/**`, `SchemaToolView`, `SchemaFields`)
|
||||
импортируют из `$lib/registry-new`. После удаления старого UI `registry-new/`
|
||||
@@ -122,12 +120,12 @@ Preview (`catalog.ts`, маршруты `preview/**`, `SchemaToolView`, `SchemaF
|
||||
|
||||
## Проблема (текущая)
|
||||
|
||||
- `ParamDef` — плоский union из 6 примитивов (`number | slider | select |
|
||||
checkbox | color | text`). Параметры инструмента — отдельные записи в плоском
|
||||
массиве.
|
||||
- `ParamDef` — плоский union из 6 примитивов
|
||||
(`number | slider | select | checkbox | color | text`). Параметры инструмента
|
||||
— отдельные записи в плоском массиве.
|
||||
- `run()`/`generate()` получают `Record<string, unknown>`; значения достаются
|
||||
через `num(p, "fromColor")` — runtime string lookup **без type safety**,
|
||||
легко напутать id.
|
||||
через `num(p, "fromColor")` — runtime string lookup **без type safety**, легко
|
||||
напутать id.
|
||||
- Поля инструмента **слабо связаны** (например «ширина» и «цвет» рамки — два
|
||||
независимых поля без общего контракта) → общий генерализованный рендер, нет
|
||||
возможности сделать идеальный пер-инструмент UI.
|
||||
@@ -139,16 +137,16 @@ checkbox | color | text`). Параметры инструмента — отд
|
||||
1. **Полная типизация.** `interface Params` (runtime-тип) объявляется явно в
|
||||
каждом инструменте. `run`/`generate` типизируются прямо на него, а не на
|
||||
`Record<string, unknown>`.
|
||||
2. **Отдельная типизированная схема для UI/дефолтов/валидации.** Два понятия
|
||||
на инструмент: runtime-тип + схема. Схема проверяется компилятором на
|
||||
2. **Отдельная типизированная схема для UI/дефолтов/валидации.** Два понятия на
|
||||
инструмент: runtime-тип + схема. Схема проверяется компилятором на
|
||||
соответствие `Params` (чтобы не рассинхронизировались).
|
||||
3. **Общий рендер,** но с пер-инструмент layout: общие компоненты рисуют поля
|
||||
по схеме; схема инструмента задаёт layout и группировку. Без отдельного
|
||||
3. **Общий рендер,** но с пер-инструмент layout: общие компоненты рисуют поля по
|
||||
схеме; схема инструмента задаёт layout и группировку. Без отдельного
|
||||
Svelte-компонента на каждый инструмент.
|
||||
4. **Составные типы** (`gradient`, `color-pair`, `dimension`, `offset`,
|
||||
`position9`, `font-style`, `plate`) остаются в плане — это способ выразить
|
||||
в схеме связанную группу полей и переиспользовать её на нескольких
|
||||
инструментах и в UI.
|
||||
`position9`, `font-style`, `plate`) остаются в плане — это способ выразить в
|
||||
схеме связанную группу полей и переиспользовать её на нескольких инструментах
|
||||
и в UI.
|
||||
5. Рано или поздно `tool-views.ts` поглощается registry (preview/lede/layout
|
||||
переезжают в meta инструмента) — но это отдельный шаг, см. ниже.
|
||||
6. **Старый UI не рефакторим.** Две независимые схемы: старый `ParamDef[]`
|
||||
@@ -157,8 +155,8 @@ checkbox | color | text`). Параметры инструмента — отд
|
||||
|
||||
## Целевая архитектура (на примере «добавить рамку»)
|
||||
|
||||
Цель: «добавить рамку: ширина и цвет» — **один интерфейс**, а не два
|
||||
слабо связанных поля.
|
||||
Цель: «добавить рамку: ширина и цвет» — **один интерфейс**, а не два слабо
|
||||
связанных поля.
|
||||
|
||||
```ts
|
||||
// 1. Runtime-тип — что получает run/generate
|
||||
@@ -232,8 +230,8 @@ function toolSchema<P>(fields: FieldsOf<P>): ToolSchema<P> { ... }
|
||||
```
|
||||
|
||||
Ключевая идея связки: **builder `field.x<T>()` связывает runtime-тип поля с его
|
||||
схемой на этапе компиляции**, а `toolSchema<P>` заставляет ключи схемы
|
||||
совпадать с ключами `P`. Это и есть защита от рассинхрона.
|
||||
схемой на этапе компиляции**, а `toolSchema<P>` заставляет ключи схемы совпадать
|
||||
с ключами `P`. Это и есть защита от рассинхрона.
|
||||
|
||||
## Составные типы (для переиспользования и группировки)
|
||||
|
||||
@@ -247,9 +245,9 @@ function toolSchema<P>(fields: FieldsOf<P>): ToolSchema<P> { ... }
|
||||
| `font-style` | font + size + bold + color | 3-4 |
|
||||
| `plate` | enabled + color + opacity | 2-3 |
|
||||
|
||||
Составной тип = группы полей, которые в интерфейсе `Params` могут разворачиваться
|
||||
во вложенный объект или плоские ключи — решается на этапе реализации.
|
||||
Пример для `gradient`:
|
||||
Составной тип = группы полей, которые в интерфейсе `Params` могут
|
||||
разворачиваться во вложенный объект или плоские ключи — решается на этапе
|
||||
реализации. Пример для `gradient`:
|
||||
|
||||
```ts
|
||||
interface GradientParams {
|
||||
@@ -293,27 +291,27 @@ add-text-png, date-stamp-png
|
||||
|
||||
## Пошаговый план перевода инструментов
|
||||
|
||||
> Каждый шаг — **атомарное, маленькое изменение** (в идеале 1 инструмент =
|
||||
> 1 diff, укладывается в правило «коммиты < ~500 строк»). После каждого шага —
|
||||
> Каждый шаг — **атомарное, маленькое изменение** (в идеале 1 инструмент = 1
|
||||
> diff, укладывается в правило «коммиты < ~500 строк»). После каждого шага —
|
||||
> ревью; параллельно можно делать UI нового инструмента.
|
||||
>
|
||||
> **Принцип малых шагов:** перевод инструмента не меняет поведение старой
|
||||
> схемы (`params` остаётся), не ломает старый UI. Инструмент получает ДОПОЛНИТЕЛЬНО
|
||||
> **Принцип малых шагов:** перевод инструмента не меняет поведение старой схемы
|
||||
> (`params` остаётся), не ломает старый UI. Инструмент получает ДОПОЛНИТЕЛЬНО
|
||||
> новое поле `schema` + `interface Params`, старое `run`/`params` не трогаем.
|
||||
> Это гарантирует, что старый UI продолжает работать без регресса.
|
||||
>
|
||||
> **Новый подход (решение пользователя):** сначала делаем **один полноценный
|
||||
> рабочий инструмент в preview** — не эталон «на бумаге», а реально юзабельный,
|
||||
> чтобы руками оценить, насколько это рабочее решение и какие возникнут сложности.
|
||||
> Оцениваем, при необходимости правим инфраструктуру, и только потом переводим
|
||||
> остальные инструменты тем же проверенным паттерном.
|
||||
> чтобы руками оценить, насколько это рабочее решение и какие возникнут
|
||||
> сложности. Оцениваем, при необходимости правим инфраструктуру, и только потом
|
||||
> переводим остальные инструменты тем же проверенным паттерном.
|
||||
|
||||
### Фаза 0 — фундамент ✔ (сделано)
|
||||
|
||||
Typed field builders + `Field<T>` + `toolSchema<P>()` + `ToolSchema<P>` —
|
||||
в `web/src/lib/registry-schema.ts`. `ToolEntry<P>` generic + опциональное
|
||||
поле `schema`. Новые `defaultSchemaParams`/`sanitizeSchemaParams`.
|
||||
Старый `ParamDef[]`/pipeline не тронуты. Проверено: compile-time-защита
|
||||
Typed field builders + `Field<T>` + `toolSchema<P>()` + `ToolSchema<P>` — в
|
||||
`web/src/lib/registry-schema.ts`. `ToolEntry<P>` generic + опциональное поле
|
||||
`schema`. Новые `defaultSchemaParams`/`sanitizeSchemaParams`. Старый
|
||||
`ParamDef[]`/pipeline не тронуты. Проверено: compile-time-защита
|
||||
(лишний/неверный/отсутствующий ключ ловит TS), тесты, svelte-check без новых
|
||||
ошибок.
|
||||
|
||||
@@ -326,18 +324,18 @@ Typed field builders + `Field<T>` + `toolSchema<P>()` + `ToolSchema<P>` —
|
||||
5. **Инструмент «добавить рамку» (`add-border-png`)** — сделать полностью
|
||||
рабочим в preview:
|
||||
- `interface AddBorderParams { thickness: number; color: string }`
|
||||
- `addBorderSchema = toolSchema<AddBorderParams>(...)` + `schema: ...` в entry
|
||||
(старый `params`/`run` остаются — старый UI не регрессит).
|
||||
- `addBorderSchema = toolSchema<AddBorderParams>(...)` + `schema: ...` в
|
||||
entry (старый `params`/`run` остаются — старый UI не регрессит).
|
||||
- **Новый рендер в preview** (`kit/ToolView` → `ParamControl` или новый
|
||||
компонент схемы): читает `ToolSchema<P>`, рисует поля рамки, применение
|
||||
реально работает (end-to-end до картинки).
|
||||
- Дефолты/валидация/значения — из схемы (`defaultSchemaParams`,
|
||||
`sanitizeSchemaParams`).
|
||||
- **Ревью-оценка:** удобно ли это, что сломалось/усложнилось, что поправить
|
||||
в инфраструктуре до масштабирования. Принимается макет/решение.
|
||||
- **Ревью-оценка:** удобно ли это, что сломалось/усложнилось, что поправить в
|
||||
инфраструктуре до масштабирования. Принимается макет/решение.
|
||||
|
||||
Результат: один реально рабочий инструмент на новой системе = доказываем паттерн,
|
||||
прежде чем переводить остальных.
|
||||
Результат: один реально рабочий инструмент на новой системе = доказываем
|
||||
паттерн, прежде чем переводить остальных.
|
||||
|
||||
### Фаза 2 — простые инструменты без составных типов (1 инструмент = 1 шаг)
|
||||
|
||||
@@ -345,8 +343,8 @@ Typed field builders + `Field<T>` + `toolSchema<P>()` + `ToolSchema<P>` —
|
||||
составных типов. Каждый — отдельный маленький diff (~15-30 строк), тем самым
|
||||
проверенным в Фазе 1 паттерном. **Переезжают в `registry-new`.**
|
||||
|
||||
Переведено: `add-border-png` (Фаза 1), `add-stroke-png`, `find-contour-png`, плюс
|
||||
весь блок простых инструментов ниже (пункты 8-22). Следующее — Фаза 3.
|
||||
Переведено: `add-border-png` (Фаза 1), `add-stroke-png`, `find-contour-png`,
|
||||
плюс весь блок простых инструментов ниже (пункты 8-22). Следующее — Фаза 3.
|
||||
|
||||
6. ~~add-stroke-png (color + slider)~~ → переведён в `registry-new` ✔
|
||||
7. ~~find-contour-png (color + slider)~~ → переведён в `registry-new` ✔
|
||||
@@ -369,8 +367,8 @@ Typed field builders + `Field<T>` + `toolSchema<P>()` + `ToolSchema<P>` —
|
||||
> **Замечание (конвертеры):** старые `convert-png-to-jpg`/`convert-png-to-webp`
|
||||
> несли `output`-метаданные (`mime`/`ext`/`qualityParamId`) для выбора формата
|
||||
> вывода. В новом `ToolEntry<P>` поля `output` пока нет, поэтому при переводе
|
||||
> эти метаданные не перенесены — preview пока отдаёт результат как PNG.
|
||||
> Механика выбора формата/качества в новом UI — отдельный шаг (не блокирует Фазу 2).
|
||||
> эти метаданные не перенесены — preview пока отдаёт результат как PNG. Механика
|
||||
> выбора формата/качества в новом UI — отдельный шаг (не блокирует Фазу 2).
|
||||
|
||||
### Фаза 3 — инструменты с составными типами (по типу, затем по инструментам)
|
||||
|
||||
@@ -378,19 +376,19 @@ Typed field builders + `Field<T>` + `toolSchema<P>()` + `ToolSchema<P>` —
|
||||
`single-color-png`, `random-noise-png`, `linear-gradient-png`,
|
||||
`color-spectrum-png`, `random-colors-png`, `draw-grid-png`,
|
||||
`placeholder-png` — генераторы в `registry-new/generate.ts`;
|
||||
`fit-on-background-png`, `change-canvas-size-png`, `resize-png`,
|
||||
`crop-png` — в `registry-new/geometry.ts`). Составной тип во всех видах:
|
||||
вложенный объект `size: { width, height }` в Params + `field.dimension`,
|
||||
виджет `kit/fields/DimensionField.svelte`.
|
||||
Preview: генераторы применяются через `executeGenerate` (кнопка Generate),
|
||||
`SchemaFields` рендерит все kinds схемы (slider/number/color/select/
|
||||
checkbox/dimension). Тесты: 592 passed.
|
||||
`fit-on-background-png`, `change-canvas-size-png`, `resize-png`, `crop-png`
|
||||
— в `registry-new/geometry.ts`). Составной тип во всех видах: вложенный
|
||||
объект `size: { width, height }` в Params + `field.dimension`, виджет
|
||||
`kit/fields/DimensionField.svelte`. Preview: генераторы применяются через
|
||||
`executeGenerate` (кнопка Generate), `SchemaFields` рендерит все kinds схемы
|
||||
(slider/number/color/select/ checkbox/dimension). Тесты: 592 passed.
|
||||
25. `color-pair` — **все 4 инструмента переведены** ✔ (`blend-two-png`,
|
||||
`step-colors-png`, `linear-gradient-png` — генераторы в
|
||||
`registry-new/generate.ts`, `two-colors-png` — run в `registry-new/color.ts`).
|
||||
Составной тип во всех видах: вложенный объект `pair: { from, to }` +
|
||||
`field.colorPair`, виджет `kit/fields/schema/ColorPairControl.svelte`,
|
||||
kind `color-pair` в схеме (default/sanitize). Тесты: +4 (596 passed).
|
||||
`registry-new/generate.ts`, `two-colors-png` — run в
|
||||
`registry-new/color.ts`). Составной тип во всех видах: вложенный объект
|
||||
`pair: { from, to }` + `field.colorPair`, виджет
|
||||
`kit/fields/schema/ColorPairControl.svelte`, kind `color-pair` в схеме
|
||||
(default/sanitize). Тесты: +4 (596 passed).
|
||||
26. `offset` — **все 4 инструмента переведены** ✔ (`circle-mask-png`,
|
||||
`square-mask-png`, `star-mask-png`, `wavy-mask-png` — run в
|
||||
`registry-new/alpha.ts`). Составной тип во всех видах: вложенный объект
|
||||
@@ -399,40 +397,39 @@ Typed field builders + `Field<T>` + `toolSchema<P>()` + `ToolSchema<P>` —
|
||||
(default/sanitize). Тесты: +4 (600 passed).
|
||||
27. `position9` — **переведены `add-text-png` и `date-stamp-png`** ✔ (run в
|
||||
`registry-new/text.ts`, оба `domOnly`). Составной тип во всех видах:
|
||||
значение — строка `Position9`, kind `position9` в схеме (default/sanitize
|
||||
по `POSITION9_VALUES`), виджет 3×3 `kit/fields/schema/PositionControl.svelte`.
|
||||
значение — строка `Position9`, kind `position9` в схеме (default/sanitize по
|
||||
`POSITION9_VALUES`), виджет 3×3 `kit/fields/schema/PositionControl.svelte`.
|
||||
В `ToolEntry` добавлен флаг `domOnly` (превью-executor держит такие
|
||||
инструменты вне worker). `watermark-image-png` — отдельный под-шаг:
|
||||
ему нужен overlay-source (`getOverlay`/store), которого в новом превью пока
|
||||
нет. Тесты: +2 (602 passed). `font-style` и `plate` на этих инструментах
|
||||
сводятся в шаги 28-29.
|
||||
28. `font-style` — **все 3 инструмента переведены** ✔ (`text-to-png` —
|
||||
генератор в `registry-new/generate.ts` (domOnly), `add-text-png`/
|
||||
`date-stamp-png` — рефакторинг в `registry-new/text.ts`). Составной тип
|
||||
во всех видах: вложенный объект `style: { font, size, bold, color }` +
|
||||
`field.fontStyle`, виджет `kit/fields/schema/FontStyleControl.svelte`,
|
||||
kind `font-style` в схеме (default/sanitize, clamp размера к min/max).
|
||||
Тесты: +1 (603 passed).
|
||||
инструменты вне worker). `watermark-image-png` — отдельный под-шаг: ему
|
||||
нужен overlay-source (`getOverlay`/store), которого в новом превью пока нет.
|
||||
Тесты: +2 (602 passed). `font-style` и `plate` на этих инструментах сводятся
|
||||
в шаги 28-29.
|
||||
28. `font-style` — **все 3 инструмента переведены** ✔ (`text-to-png` — генератор
|
||||
в `registry-new/generate.ts` (domOnly), `add-text-png`/ `date-stamp-png` —
|
||||
рефакторинг в `registry-new/text.ts`). Составной тип во всех видах:
|
||||
вложенный объект `style: { font, size, bold, color }` + `field.fontStyle`,
|
||||
виджет `kit/fields/schema/FontStyleControl.svelte`, kind `font-style` в
|
||||
схеме (default/sanitize, clamp размера к min/max). Тесты: +1 (603 passed).
|
||||
29. `plate` — **все 2 инструмента переведены** ✔ (`add-text-png`,
|
||||
`date-stamp-png` — рефакторинг в `registry-new/text.ts`). Составной тип
|
||||
во всех видах: вложенный объект `plate: { enabled, color, opacity }` +
|
||||
`date-stamp-png` — рефакторинг в `registry-new/text.ts`). Составной тип во
|
||||
всех видах: вложенный объект `plate: { enabled, color, opacity }` +
|
||||
`field.plate`, виджет `kit/fields/schema/PlateControl.svelte` (чекбокс +
|
||||
цвет + слайдер непрозрачности, деактивируется при выключенной плашке),
|
||||
kind `plate` в схеме (default/sanitize, clamp opacity к 0..100).
|
||||
Покрытие тестов расширено (дефолты/sanitize plate в существующих тестах).
|
||||
30. `gradient` — **переведён** ✔ (linear-gradient-png). Направление градиента
|
||||
— не горизонталь/вертикаль, а **произвольный угол**: новый переиспользуемый
|
||||
цвет + слайдер непрозрачности, деактивируется при выключенной плашке), kind
|
||||
`plate` в схеме (default/sanitize, clamp opacity к 0..100). Покрытие тестов
|
||||
расширено (дефолты/sanitize plate в существующих тестах).
|
||||
30. `gradient` — **переведён** ✔ (linear-gradient-png). Направление градиента —
|
||||
не горизонталь/вертикаль, а **произвольный угол**: новый переиспользуемый
|
||||
виджет `kit/fields/schema/AngleControl.svelte` (слайдер 0..360° + кнопки
|
||||
стандартных углов 0°/90°/180°/270° с активным состоянием). Составной тип
|
||||
`gradient: { from, to, angle }` во всех видах (`field.gradient`, kind
|
||||
`gradient` в схеме — default/sanitize, clamp угла к 0..360, валидация
|
||||
цветов; виджет `kit/fields/schema/GradientControl.svelte` — пара цветов +
|
||||
AngleControl). Рендер по углу — локальный `angleGradient` в
|
||||
`registry-new/generate.ts` (core-`gradientImage` не трогали: он умеет
|
||||
только horizontal/vertical и используется старым UI). Семантика: 0° слева
|
||||
направо, 90° сверху вниз, рост угла по часовой (ось Y вниз); нормализация
|
||||
по полному диапазону проекции на ось (180°/270° разворачивают градиент).
|
||||
Тесты: дефолты, направление по углу (0°/90°/180°), sanitize/кламп.
|
||||
`registry-new/generate.ts` (core-`gradientImage` не трогали: он умеет только
|
||||
horizontal/vertical и используется старым UI). Семантика: 0° слева направо,
|
||||
90° сверху вниз, рост угла по часовой (ось Y вниз); нормализация по полному
|
||||
диапазону проекции на ось (180°/270° разворачивают градиент). Тесты:
|
||||
дефолты, направление по углу (0°/90°/180°), sanitize/кламп.
|
||||
|
||||
### Фаза 4 — масштаб UI на остальные инструменты
|
||||
|
||||
@@ -443,40 +440,41 @@ Typed field builders + `Field<T>` + `toolSchema<P>()` + `ToolSchema<P>` —
|
||||
`ToolSchemaLayout.groups: { title?, cols?, fields[] }` в registry-schema;
|
||||
`SchemaFields.svelte` рендерит группы (подпись + сетка `1..n` колонок,
|
||||
неупомянутые поля — в общей группе в конце, без заголовка). Пилот —
|
||||
`add-text-png` (Text: text+style; Placement: position+margin; Plate).
|
||||
Тесты: schema-layout в registry-schema.test.ts; полные запуски чисты.
|
||||
`add-text-png` (Text: text+style; Placement: position+margin; Plate). Тесты:
|
||||
schema-layout в registry-schema.test.ts; полные запуски чисты.
|
||||
33. Для каждого переведённого инструмента — UI-макет **расставлен** ✔.
|
||||
Layout-группы заданы там, где группировка содержательна (канвас отдельно
|
||||
от параметров эффекта, фигура отдельно от позиции; составные виджеты —
|
||||
внутри групп целиком):
|
||||
Layout-группы заданы там, где группировка содержательна (канвас отдельно от
|
||||
параметров эффекта, фигура отдельно от позиции; составные виджеты — внутри
|
||||
групп целиком):
|
||||
- geometry: fit-on-background (Canvas/Background), change-canvas-size
|
||||
(Canvas/Anchor), resize (Canvas/Scaling), crop (Offset/Crop area);
|
||||
- alpha-маски: circle/square/star/wavy (Shape — с 2 колонками где плотно,
|
||||
Position — offset);
|
||||
- generate: create-empty (Canvas/Fill), linear-gradient (Canvas/Colors),
|
||||
color-spectrum (Canvas/Spectrum), random-colors (Canvas/Random),
|
||||
draw-grid (Canvas/Grid), step-colors (Colors/Output), placeholder
|
||||
color-spectrum (Canvas/Spectrum), random-colors (Canvas/Random), draw-grid
|
||||
(Canvas/Grid), step-colors (Colors/Output), placeholder
|
||||
(Canvas/Colors/Text), text-to-png (Text/Background/Padding);
|
||||
- text: add-text, date-stamp (Text/Placement/Plate);
|
||||
- filters: randomize-pixels (Blocks), add-noise (Noise/Seed).
|
||||
Инструменты с 1–3 простыми полями остались без layout (одна общая группа).
|
||||
- filters: randomize-pixels (Blocks), add-noise (Noise/Seed). Инструменты с
|
||||
1–3 простыми полями остались без layout (одна общая группа).
|
||||
|
||||
### Фаза 5 — изоляция старого UI
|
||||
|
||||
34. Поглощение `tool-views.ts` (preview/lede/layout → meta инструмента) ✔.
|
||||
Отдельного `tool-views.ts` в репо нет: preview сразу строился на
|
||||
`registry-new`. Meta инструмента живёт в `ToolEntry` (`title`, `description`,
|
||||
`category`, `run`/`generate`), layout — в `schema.layout` (шаг 32), рендер —
|
||||
`SchemaToolView`/`SchemaFields`/`SchemaPreview`. Дублирующей системы нет.
|
||||
`registry-new`. Meta инструмента живёт в `ToolEntry` (`title`,
|
||||
`description`, `category`, `run`/`generate`), layout — в `schema.layout`
|
||||
(шаг 32), рендер — `SchemaToolView`/`SchemaFields`/`SchemaPreview`.
|
||||
Дублирующей системы нет.
|
||||
35. Старый UI **не удаляется**, а выносится в `(old)/`-маршруты и остаётся там
|
||||
временно (посмотреть, как работает, сравнить с новым; старые тесты
|
||||
продолжают проходить):
|
||||
- маршруты `(old)/{+page,demo,list-tools,tools/[id]}` — старый дизайн,
|
||||
тянет `old.css` (не `design2.css`), старый header/footer;
|
||||
- маршруты `(old)/{+page,demo,list-tools,tools/[id]}` — старый дизайн, тянет
|
||||
`old.css` (не `design2.css`), старый header/footer;
|
||||
- старый `registry.ts`/`registry/` + `ParamDef[]` обслуживают только
|
||||
`(old)/`-инструменты — не удаляются, не рефакторятся;
|
||||
- удаление происходит позже, отдельным решением (когда новый UI покроет
|
||||
все инструменты и ревью завершено).
|
||||
- удаление происходит позже, отдельным решением (когда новый UI покроет все
|
||||
инструменты и ревью завершено).
|
||||
36. **Линтер-изоляция веток** (гарантия, что old и preview не смешиваются) ✔.
|
||||
Кастомный ESLint-плагин `web/eslint-plugins/isolation/no-mixed-imports`
|
||||
Резолвит каждый импорт (и `$lib/...`, и относительные `./`/`../`) до
|
||||
@@ -490,12 +488,12 @@ Typed field builders + `Field<T>` + `toolSchema<P>()` + `ToolSchema<P>` —
|
||||
`lib/registry-schema.ts`, `lib/registry-schema.test.ts`,
|
||||
`lib/components/kit/**`;
|
||||
- общее (разрешено обоим): всё прочее — `core/`, `i18n/`, `theme`,
|
||||
`assets/`, корневой `lib` (`index.ts`, тесты).
|
||||
Достигнутая полная изоляция (одиночные пересечения устранены):
|
||||
`assets/`, корневой `lib` (`index.ts`, тесты). Достигнутая полная изоляция
|
||||
(одиночные пересечения устранены):
|
||||
- старые пилоты `registry/geometry.ts` (`add-border`) и `registry/alpha.ts`
|
||||
(`add-stroke`) получали `schema` из нового `registry-schema` — убрано;
|
||||
оба инструмента работают в старом UI через `params: ParamDef[]`,
|
||||
в preview — через свои schema-версии в `registry-new/`;
|
||||
(`add-stroke`) получали `schema` из нового `registry-schema` — убрано; оба
|
||||
инструмента работают в старом UI через `params: ParamDef[]`, в preview —
|
||||
через свои schema-версии в `registry-new/`;
|
||||
- `registry.ts` больше не импортирует `ToolSchema` из `registry-schema`;
|
||||
- preview `list-tools` тянул `TOOL_ICONS` из старого `lib/tools/tool-icons`
|
||||
→ создана копия `lib/preview/tool-icons.ts` (правило копий).
|
||||
@@ -507,10 +505,10 @@ Typed field builders + `Field<T>` + `toolSchema<P>()` + `ToolSchema<P>` —
|
||||
### Как ревьюить каждый шаг
|
||||
|
||||
- Diff **только один инструмент** (или фундамент без инструментов).
|
||||
- Старый `params`/`run`/`defaultParams`/`sanitizeParams` — **не изменены** (кроме
|
||||
добаление `schema`, если это пилот).
|
||||
- Старый UI и старые тесты продолжают работать — запустить
|
||||
`pnpm --dir web test` и `pnpm --dir web exec svelte-check`.
|
||||
- Старый `params`/`run`/`defaultParams`/`sanitizeParams` — **не изменены**
|
||||
(кроме добаление `schema`, если это пилот).
|
||||
- Старый UI и старые тесты продолжают работать — запустить `pnpm --dir web test`
|
||||
и `pnpm --dir web exec svelte-check`.
|
||||
- Порядок полей в `interface Params` и в `schema` совпадает; дефолты в сeme
|
||||
равны старым `default` из `params`.
|
||||
- Пилот из Фазы 1 проверяется **руками в preview**: применяется рамка,
|
||||
@@ -539,8 +537,8 @@ Typed field builders + `Field<T>` + `toolSchema<P>()` + `ToolSchema<P>` —
|
||||
на `ParamDef[]` как раньше.
|
||||
- **Новый UI** (kit/`SchemaToolView` + `SchemaFields` + `SchemaPreview`, читает
|
||||
`ToolSchema<P>`) — сейчас рендерит поля по схеме (number/slider/color).
|
||||
- `tool-views.ts` отсутствует — preview/lede/layout уже живут в meta
|
||||
инструмента (`ToolEntry` + `schema.layout`), дублирования нет.
|
||||
- `tool-views.ts` отсутствует — preview/lede/layout уже живут в meta инструмента
|
||||
(`ToolEntry` + `schema.layout`), дублирования нет.
|
||||
|
||||
## Оценка трудозатрат
|
||||
|
||||
@@ -557,12 +555,12 @@ Typed field builders + `Field<T>` + `toolSchema<P>()` + `ToolSchema<P>` —
|
||||
| **Итого** | | **~22-32ч** (поэтапно) |
|
||||
|
||||
> Оценка выросла по сравнению с ранней версией плана, потому что принят путь
|
||||
> «явный interface Params + схема + общий рендер с пер-инструмент layout» —
|
||||
> это полный рефакторинг pipeline, а не только добавление составных типов.
|
||||
> «явный interface Params + схема + общий рендер с пер-инструмент layout» — это
|
||||
> полный рефакторинг pipeline, а не только добавление составных типов.
|
||||
>
|
||||
> Старый UI/`ParamDef[]`/старый pipeline в смету **не входят** — они не
|
||||
> рефакторятся, а продолжают работать на `(old)/`-маршрутах до перехода
|
||||
> (затем убираются отдельным решением — см. шаг 35).
|
||||
> рефакторятся, а продолжают работать на `(old)/`-маршрутах до перехода (затем
|
||||
> убираются отдельным решением — см. шаг 35).
|
||||
|
||||
## Порядок реализации (кратко)
|
||||
|
||||
@@ -571,20 +569,19 @@ Typed field builders + `Field<T>` + `toolSchema<P>()` + `ToolSchema<P>` —
|
||||
|
||||
1. **Фаза 0** — фундамент: builders + `toolSchema<P>` + `ToolEntry<P>.schema` +
|
||||
новые default/sanitize. Ни одного инструмента не изменено. ✔
|
||||
2. **Фаза 1** — **один полноценный рабочий инструмент в preview**
|
||||
(«добавить рамку»): schema + рендер + end-to-end применение. Ручная оценка
|
||||
2. **Фаза 1** — **один полноценный рабочий инструмент в preview** («добавить
|
||||
рамку»): schema + рендер + end-to-end применение. Ручная оценка
|
||||
работоспособности решения/сложностей.
|
||||
3. **Фаза 2** — простые/одиночные инструменты (проверенный паттерн Фазы 1),
|
||||
1 инструмент = 1 диф.
|
||||
4. **Фаза 3** — инструменты с составными типами (dimension → color-pair →
|
||||
offset → position9 → font-style → plate → gradient), пилот каждого типа
|
||||
отдельно.
|
||||
3. **Фаза 2** — простые/одиночные инструменты (проверенный паттерн Фазы 1), 1
|
||||
инструмент = 1 диф.
|
||||
4. **Фаза 3** — инструменты с составными типами (dimension → color-pair → offset
|
||||
→ position9 → font-style → plate → gradient), пилот каждого типа отдельно.
|
||||
5. **Фаза 4** — масштаб UI на остальные: составные виджеты, пер-инструмент
|
||||
layout (для каждого инструмента — UI-макет).
|
||||
6. **Фаза 5** — изоляция: старый UI на `(old)/`-маршруты (не удаляется,
|
||||
остаётся для ревью); preview на `registry-new` полностью;
|
||||
линтер-изоляция веток (плагин `isolation`), затем перенос старых
|
||||
компонентов/библиотек в папки `old/`.
|
||||
6. **Фаза 5** — изоляция: старый UI на `(old)/`-маршруты (не удаляется, остаётся
|
||||
для ревью); preview на `registry-new` полностью; линтер-изоляция веток
|
||||
(плагин `isolation`), затем перенос старых компонентов/библиотек в папки
|
||||
`old/`.
|
||||
|
||||
## Зависимости
|
||||
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
# План: доводка дизайна до верности (фикс по refs)
|
||||
|
||||
> **СТАТУС: ЗАКРЫТ (2026-09-07).** Актуальный дизайн (`preview/*`) ушёл вперёд
|
||||
> от референсов `refs-html/` — сверка с ними больше не информативна
|
||||
> (новые компоненты `SchemaToolView`/`SchemaPreview`/каталог не имеют
|
||||
> рефовой копии). Аудит против рефа прекращается; качество дизайна дальше
|
||||
> гоняется ревью руками и специализированными проверками
|
||||
> (`lint:all` — stylelint/design-tokens/токен-аудит). Порядок удаления старого
|
||||
> и перехода — по `plan-redesign.md` (C17–C21).
|
||||
> от референсов `refs-html/` — сверка с ними больше не информативна (новые
|
||||
> компоненты `SchemaToolView`/`SchemaPreview`/каталог не имеют рефовой копии).
|
||||
> Аудит против рефа прекращается; качество дизайна дальше гоняется ревью руками
|
||||
> и специализированными проверками (`lint:all` —
|
||||
> stylelint/design-tokens/токен-аудит). Порядок удаления старого и перехода — по
|
||||
> `plan-redesign.md` (C17–C21).
|
||||
|
||||
> Статус: к выполнению. Ветка `feat/redesign`.
|
||||
> Источники: `docs/archive/demo-fidelity.md` (архивная копия старого отчёта по
|
||||
> верности, неактуальна), `docs/plan-redesign.md` (общая стратегия, фазы 1–4
|
||||
> выполнены до C16). Актуальный аудит верности — `pnpm refs-cdp-audit` /
|
||||
> Статус: к выполнению. Ветка `feat/redesign`. Источники:
|
||||
> `docs/archive/demo-fidelity.md` (архивная копия старого отчёта по верности,
|
||||
> неактуальна), `docs/plan-redesign.md` (общая стратегия, фазы 1–4 выполнены до
|
||||
> C16). Актуальный аудит верности — `pnpm refs-cdp-audit` /
|
||||
> `pnpm refs-css-audit` / `pnpm refs-dom-audit` → `web/audit/audit-report.md`
|
||||
> (текущий список скриптов — в `web/package.json`, см. `refs-*audit`).
|
||||
|
||||
@@ -22,11 +22,10 @@
|
||||
- `refs/` и `refs-html/` приведены к консистентному виду — текущая «правда по
|
||||
пикселям». Механизм синхронизации ref→код не создаётся (см. §9 plan-redesign).
|
||||
- Аудит верности автоматизирован: `pnpm refs-cdp-audit` (1440×900,
|
||||
`scripts/audit-cdp.mjs`) пишет
|
||||
`web/audit/audit-report.md` — light/dark токены + поэлементный дифф стилей;
|
||||
пиксель-дифф не используется. Палитры токенов уже сверены с рефом (light
|
||||
совпадает, dark собран). Темы в `refs-html` переключаются — скрипт инжектится
|
||||
в `scripts/extract-static.mjs`.
|
||||
`scripts/audit-cdp.mjs`) пишет `web/audit/audit-report.md` — light/dark
|
||||
токены + поэлементный дифф стилей; пиксель-дифф не используется. Палитры
|
||||
токенов уже сверены с рефом (light совпадает, dark собран). Темы в `refs-html`
|
||||
переключаются — скрипт инжектится в `scripts/extract-static.mjs`.
|
||||
- Исходный отчёт `demo-fidelity.md` перенесён в `docs/archive/` как утративший
|
||||
актуальность; его данные актуализированы ниже (Этап 1).
|
||||
- Зафиксированные решения (подтверждены автором):
|
||||
@@ -38,13 +37,14 @@
|
||||
- Никаких правок старого дизайна `(old)/` и старого `ui/`.
|
||||
- Компоненты пишем в `kit/` (переименование в `ui/` — на C19 по plan-redesign).
|
||||
- Коммиты атомарные (< ~500 строк), по 1–2 компонента; после каждого —
|
||||
`svelte-check` + `lint` + `build` + `refs-cdp-audit` (diff% не должен расти без причины).
|
||||
`svelte-check` + `lint` + `build` + `refs-cdp-audit` (diff% не должен расти
|
||||
без причины).
|
||||
- Коммиты делает автор (агент не коммитит).
|
||||
|
||||
## Этап 0 — Рефакторинг (SRP / дизайн-обёртки), без изменения визуала
|
||||
|
||||
Гигантские страницы (`demo` ~513, `tools/[id]` ~451 строк) дробим на
|
||||
компоненты единой ответственности; убираем дублирующиеся inline-стили.
|
||||
Гигантские страницы (`demo` ~513, `tools/[id]` ~451 строк) дробим на компоненты
|
||||
единой ответственности; убираем дублирующиеся inline-стили.
|
||||
|
||||
Новые обёртки в `src/lib/components/kit/`:
|
||||
|
||||
@@ -62,9 +62,8 @@
|
||||
Декомпозиция страниц:
|
||||
|
||||
- `tools/[id]`: вынести `ParamControl` (диспетчер полей по `ParamDef`),
|
||||
`SettingsPanel` (левая панель), `PreviewPanel` (правая панель) —
|
||||
колоцированно в `routes/preview/tools/[id]/`. Страница сжимается до
|
||||
состояния + глёв.
|
||||
`SettingsPanel` (левая панель), `PreviewPanel` (правая панель) — колоцированно
|
||||
в `routes/preview/tools/[id]/`. Страница сжимается до состояния + глёв.
|
||||
- `demo`: переписать на те же обёртки + `WorkspaceHeader` (eyebrow + h1 + lede +
|
||||
file-chip).
|
||||
|
||||
@@ -81,10 +80,9 @@
|
||||
|
||||
Аудит: `pnpm refs-cdp-audit` (1440×900) пишет `web/audit/audit-report.md` —
|
||||
light/dark токены + поэлементный дифф стилей. Пиксель-дифф не используется.
|
||||
Палитры токенов уже сверены с рефом: light совпадает (`--background`,
|
||||
`--panel`, `--foreground`, `--muted`, `--line`, `--blue`), dark собран (16
|
||||
токенов). Темы в `refs-html` переключаются (скрипт инжектится в
|
||||
`extract-static.mjs`).
|
||||
Палитры токенов уже сверены с рефом: light совпадает (`--background`, `--panel`,
|
||||
`--foreground`, `--muted`, `--line`, `--blue`), dark собран (16 токенов). Темы в
|
||||
`refs-html` переключаются (скрипт инжектится в `extract-static.mjs`).
|
||||
|
||||
Открытые / решённые пункты (исходник — `docs/archive/demo-fidelity.md`):
|
||||
|
||||
@@ -94,26 +92,26 @@ light/dark токены + поэлементный дифф стилей. Пик
|
||||
рендерит ровно один `TopBar`; `demo` свой `.topbar` не рендерит. Подтвердить
|
||||
аудитом/визуально при Этапе 1.
|
||||
- **F1** (решено — плоский фон): наш `--background` уже `#EEF1F4` (как реф);
|
||||
видимое отличие было в blueprint-сетке поверх фона. Фикс: убрать `BlueprintGrid`
|
||||
из `AppShell`.
|
||||
видимое отличие было в blueprint-сетке поверх фона. Фикс: убрать
|
||||
`BlueprintGrid` из `AppShell`.
|
||||
- **F2** (сделано C16): тип шага (`StepCard`) — приглушённый mono-текст, не
|
||||
синий бейдж; индекс шага — синий `#1769D2`.
|
||||
- **F3** (сделано C16): подпись превью-тайла (`PreviewTile`) — синяя `#1769D2`.
|
||||
- **F4** (решено — одна колонка): реализуется в `PreviewStack` (Этап 0),
|
||||
колонка = `1fr`.
|
||||
- **F4** (решено — одна колонка): реализуется в `PreviewStack` (Этап 0), колонка
|
||||
= `1fr`.
|
||||
- **F5** (открыт): вес `MonoLabel` 500 → 400 (как в рефе).
|
||||
- **F6** (открыт): мета-подписи (MetaRow/MetaList, DIMENSIONS/FORMAT/SIZE)
|
||||
fs 10px → 9px.
|
||||
- **F7** (низкий): копирайт/стиль — у нас `+ Add tool` (реф `Add tool`);
|
||||
кнопка `Download result` — сверить с рефом (синяя, ~478×42). Проверить
|
||||
визуально (замеры NOT FOUND — артефакт вложенного чипа размера).
|
||||
- **F6** (открыт): мета-подписи (MetaRow/MetaList, DIMENSIONS/FORMAT/SIZE) fs
|
||||
10px → 9px.
|
||||
- **F7** (низкий): копирайт/стиль — у нас `+ Add tool` (реф `Add tool`); кнопка
|
||||
`Download result` — сверить с рефом (синяя, ~478×42). Проверить визуально
|
||||
(замеры NOT FOUND — артефакт вложенного чипа размера).
|
||||
- **F8** (низкий): `h1` line-height чуть выше рефа (150px vs 115px).
|
||||
- **F9** (открыт): вертикальный ритм — подровнять паддинги секций под реф
|
||||
(pipeline-head и пр.).
|
||||
- **F10** (открыт): внутренние отступы preview-панели (паддинг панели).
|
||||
|
||||
После Этапа 1 — повторный `refs-cdp-audit`, зафиксировать остаточный diff (sub-pixel /
|
||||
структурный) и обновить статус пунктов выше прямо в этом файле.
|
||||
После Этапа 1 — повторный `refs-cdp-audit`, зафиксировать остаточный diff
|
||||
(sub-pixel / структурный) и обновить статус пунктов выше прямо в этом файле.
|
||||
|
||||
## Этап 2 — plan-redesign.md (C17–C21), ОТЛОЖЕНО
|
||||
|
||||
|
||||
@@ -4,65 +4,96 @@
|
||||
|
||||
## 1. Принципы отбора
|
||||
|
||||
- Максимум переиспользования: готовые контракты записей (`run`, `preview`, формат вывода, info-режим) и существующие операции ядра.
|
||||
- Новые механизмы ядра — минимальные, чистые, покрытые тестами до подключения инструментов.
|
||||
- Canvas/DOM-зависимые утилиты (SVG-вход, надписи и водяные знаки, эффекты через фильтры контекста) — вне этого пакета, отдельная волна.
|
||||
- Целевой объём волны: около 37 новых инструментов, каталог вырастает с 11 до ~48.
|
||||
- Максимум переиспользования: готовые контракты записей (`run`, `preview`,
|
||||
формат вывода, info-режим) и существующие операции ядра.
|
||||
- Новые механизмы ядра — минимальные, чистые, покрытые тестами до подключения
|
||||
инструментов.
|
||||
- Canvas/DOM-зависимые утилиты (SVG-вход, надписи и водяные знаки, эффекты через
|
||||
фильтры контекста) — вне этого пакета, отдельная волна.
|
||||
- Целевой объём волны: около 37 новых инструментов, каталог вырастает с 11 до
|
||||
~48.
|
||||
|
||||
## 2. Новые механизмы
|
||||
|
||||
- **Генераторы без входного файла**: у записи появляется признак «источник не нужен»; страница такого инструмента не показывает левую панель и зону перетаскивания — только параметры и результат.
|
||||
- **Текстовый ввод и вывод**: часть утилит принимает вставленный текст вместо файла и выдаёт текстовый результат (панель с полем только для чтения, кнопки «копировать» и «скачать как текст»). Распространяется на base64, data-uri, hex.
|
||||
- **BMP-энкодер в ядре**: 24-битный, чистая функция из пикселей в байты, с тестами на маленьких матрицах (заголовок, порядок каналов BGR, паддинг строки).
|
||||
- **Генераторы без входного файла**: у записи появляется признак «источник не
|
||||
нужен»; страница такого инструмента не показывает левую панель и зону
|
||||
перетаскивания — только параметры и результат.
|
||||
- **Текстовый ввод и вывод**: часть утилит принимает вставленный текст вместо
|
||||
файла и выдаёт текстовый результат (панель с полем только для чтения, кнопки
|
||||
«копировать» и «скачать как текст»). Распространяется на base64, data-uri,
|
||||
hex.
|
||||
- **BMP-энкодер в ядре**: 24-битный, чистая функция из пикселей в байты, с
|
||||
тестами на маленьких матрицах (заголовок, порядок каналов BGR, паддинг
|
||||
строки).
|
||||
|
||||
## 3. Состав пакета
|
||||
|
||||
**Конвертация — 6**
|
||||
`jpg-to-png`, `webp-to-png`, `gif-to-png` (первый кадр), `bmp-to-png`, `ico-to-png` — всё через уже умеющий декодировать ввод, сохранение в PNG; плюс `png-to-bmp` на новом энкодере.
|
||||
**Конвертация — 6** `jpg-to-png`, `webp-to-png`, `gif-to-png` (первый кадр),
|
||||
`bmp-to-png`, `ico-to-png` — всё через уже умеющий декодировать ввод, сохранение
|
||||
в PNG; плюс `png-to-bmp` на новом энкодере.
|
||||
|
||||
**Текстовые представления — 6**
|
||||
`png-to-base64`, `base64-to-png`, `png-to-data-uri`, `data-uri-to-png`, `png-to-hex`, `hex-to-png`.
|
||||
**Текстовые представления — 6** `png-to-base64`, `base64-to-png`,
|
||||
`png-to-data-uri`, `data-uri-to-png`, `png-to-hex`, `hex-to-png`.
|
||||
|
||||
**Цвет — 9**
|
||||
`change-opacity` (множитель прозрачности), `sepia`, `change-hue` (сдвиг тона), `extract-channel` (выбор R/G/B в оттенках серого), `swap-channels` (перестановка парой), `threshold-black-white` (порог яркости), `posterize` (число уровней), `two-colors` (два цвета + порог), `invert-alpha`.
|
||||
**Цвет — 9** `change-opacity` (множитель прозрачности), `sepia`, `change-hue`
|
||||
(сдвиг тона), `extract-channel` (выбор R/G/B в оттенках серого), `swap-channels`
|
||||
(перестановка парой), `threshold-black-white` (порог яркости), `posterize`
|
||||
(число уровней), `two-colors` (два цвета + порог), `invert-alpha`.
|
||||
|
||||
**Прозрачность и холст — 7**
|
||||
`remove-alpha-channel` (подложка белым), `set-alpha-channel` (константная прозрачность), `extract-alpha-mask` (альфа в чёрно-белую маску), `add-padding` (отступ + цвет/прозрачность), `add-border` (толщина + цвет), `round-corners` (радиус), `fit-on-background` (вписать на полотно цвета с полями).
|
||||
**Прозрачность и холст — 7** `remove-alpha-channel` (подложка белым),
|
||||
`set-alpha-channel` (константная прозрачность), `extract-alpha-mask` (альфа в
|
||||
чёрно-белую маску), `add-padding` (отступ + цвет/прозрачность), `add-border`
|
||||
(толщина + цвет), `round-corners` (радиус), `fit-on-background` (вписать на
|
||||
полотно цвета с полями).
|
||||
|
||||
**Композиция — 2**
|
||||
`tile` (повтор по сетке), `center-by-alpha` (центрирование по непрозрачному содержимому).
|
||||
**Композиция — 2** `tile` (повтор по сетке), `center-by-alpha` (центрирование по
|
||||
непрозрачному содержимому).
|
||||
|
||||
**Генераторы — 4**
|
||||
`create-empty` (размер + цвет или прозрачность), `single-color`, `random-noise` (с фиксированным зерном для воспроизводимости), `linear-gradient` (два цвета, направление).
|
||||
**Генераторы — 4** `create-empty` (размер + цвет или прозрачность),
|
||||
`single-color`, `random-noise` (с фиксированным зерном для воспроизводимости),
|
||||
`linear-gradient` (два цвета, направление).
|
||||
|
||||
**Анализ — 3**
|
||||
`is-grayscale`, `is-transparent`, `orientation` (портрет/ландшафт/квадрат) — текстовые сводки в info-режиме.
|
||||
**Анализ — 3** `is-grayscale`, `is-transparent`, `orientation`
|
||||
(портрет/ландшафт/квадрат) — текстовые сводки в info-режиме.
|
||||
|
||||
## 4. Новые операции ядра (все — чистые функции с тестами)
|
||||
|
||||
- Холст: расширение полотна (общая база для padding/border/fit), тайлинг, центр по bounding-box альфы, скругление углов маской радиуса.
|
||||
- Цвет: сепия, сдвиг тона через HSL, множитель альфы, извлечение/перестановка каналов, пороги (яркость и двухцветный), постеризация уровней, инверсия альфы.
|
||||
- Холст: расширение полотна (общая база для padding/border/fit), тайлинг, центр
|
||||
по bounding-box альфы, скругление углов маской радиуса.
|
||||
- Цвет: сепия, сдвиг тона через HSL, множитель альфы, извлечение/перестановка
|
||||
каналов, пороги (яркость и двухцветный), постеризация уровней, инверсия альфы.
|
||||
- Генерация: заливка цветом, шум с зерном, линейный градиент.
|
||||
- Форматы: BMP-энкодер; кодеки текстовых представлений (base64/data-uri/hex в обе стороны).
|
||||
- Форматы: BMP-энкодер; кодеки текстовых представлений (base64/data-uri/hex в
|
||||
обе стороны).
|
||||
- Анализ: проверка монохромности, полной непрозрачности, ориентации.
|
||||
|
||||
## 5. Этапы и проверки
|
||||
|
||||
- **A. Механизмы** — генераторный режим страницы, текстовый ввод/вывод, BMP-энкодер. Проверка: `test/check/build`.
|
||||
- **B. Конвертация и текстовые представления** — 12 инструментов. Проверка: ручной прогон каждой конвертации туда-обратно.
|
||||
- **A. Механизмы** — генераторный режим страницы, текстовый ввод/вывод,
|
||||
BMP-энкодер. Проверка: `test/check/build`.
|
||||
- **B. Конвертация и текстовые представления** — 12 инструментов. Проверка:
|
||||
ручной прогон каждой конвертации туда-обратно.
|
||||
- **C. Цвет** — 9 инструментов. Проверка: юнит-тесты операций + визуально.
|
||||
- **D. Прозрачность, холст, композиция** — 9 инструментов. Проверка: юнит-тесты + визуально.
|
||||
- **E. Генераторы и анализ** — 7 инструментов. Проверка: детерминизм шума (одно зерно — одно изображение), своды анализа.
|
||||
- **F. Итог** — полный прогон, смоук всех новых инструментов «настроить → скачать», регресс старых одиннадцати.
|
||||
- **D. Прозрачность, холст, композиция** — 9 инструментов. Проверка:
|
||||
юнит-тесты + визуально.
|
||||
- **E. Генераторы и анализ** — 7 инструментов. Проверка: детерминизм шума (одно
|
||||
зерно — одно изображение), своды анализа.
|
||||
- **F. Итог** — полный прогон, смоук всех новых инструментов «настроить →
|
||||
скачать», регресс старых одиннадцати.
|
||||
|
||||
## 6. Критерии готовности
|
||||
|
||||
- Каталог содержит ~48 рабочих инструментов, каждый доступен страницей с формы из реестра.
|
||||
- Все новые операции ядра покрыты тестами; обратимые конвертации проверены круговым прогоном (например PNG → base64 → PNG даёт те же пиксели).
|
||||
- Генераторы работают без загрузки файла; текстовые представления копируются и скачиваются.
|
||||
- Каталог содержит ~48 рабочих инструментов, каждый доступен страницей с формы
|
||||
из реестра.
|
||||
- Все новые операции ядра покрыты тестами; обратимые конвертации проверены
|
||||
круговым прогоном (например PNG → base64 → PNG даёт те же пиксели).
|
||||
- Генераторы работают без загрузки файла; текстовые представления копируются и
|
||||
скачиваются.
|
||||
- `test/check/build` зелёные.
|
||||
|
||||
## 7. Что сознательно не делаем
|
||||
|
||||
- SVG-вход, рисование текста и водяных знаков, эффекты через фильтры контекста, GIF/APNG-энкодинг, пакетная обработка — последующие волны.
|
||||
- Семейства logo/icon/signature/stamp как отдельные страницы — они будут конфигурациями тех же операций после этой волны.
|
||||
- SVG-вход, рисование текста и водяных знаков, эффекты через фильтры контекста,
|
||||
GIF/APNG-энкодинг, пакетная обработка — последующие волны.
|
||||
- Семейства logo/icon/signature/stamp как отдельные страницы — они будут
|
||||
конфигурациями тех же операций после этой волны.
|
||||
|
||||
@@ -4,41 +4,69 @@
|
||||
|
||||
## 1. Что получается
|
||||
|
||||
Одиннадцать новых инструментов: свободные трансформации формы (наклон, поворот на произвольный угол, зум, сдвиг), светокоррекция (гамма, автоконтраст, температура, тонирование), виньетка, эффект «артефактов сжатия» и вход SVG. Каталог вырастает с 58 до ~69.
|
||||
Одиннадцать новых инструментов: свободные трансформации формы (наклон, поворот
|
||||
на произвольный угол, зум, сдвиг), светокоррекция (гамма, автоконтраст,
|
||||
температура, тонирование), виньетка, эффект «артефактов сжатия» и вход SVG.
|
||||
Каталог вырастает с 58 до ~69.
|
||||
|
||||
Текстовые инструменты и водяные знаки в волну не входят — им нужен рендер текста на канвасе, это отдельная четвёртая волна.
|
||||
Текстовые инструменты и водяные знаки в волну не входят — им нужен рендер текста
|
||||
на канвасе, это отдельная четвёртая волна.
|
||||
|
||||
## 2. Новое ядро
|
||||
|
||||
- **Аффинные трансформации** (`core/affine.ts`): единая функция обратного отображения — для каждого пикселя результата вычисляется исходная координата матрицей и семплируется билинейно (общий семплер, вынесенный из `resize`). Поверх неё: наклон по осям, поворот на произвольный угол (с расширением холста под новые габариты), зум к центру, сдвиг.
|
||||
- **Свет и цвет** (`core/color.ts` дополнения): гамма-коррекция через LUT, автоконтраст (растяжка по гистограмме яркости с отсечкой хвостов), температура (множители тёплых/холодных каналов), тонирование (умножение RGB на цвет с сохранением яркостного баланса).
|
||||
- **Виньетка** (`core/effects.ts`): радиальное затемнение от центра, сила слайдером; чистая математика без canvas.
|
||||
- **IO-расширения** (`core/io.ts`): декодирование SVG из текста/файла через blob-URL и `Image`; раундтрип «закодировать JPEG с качеством Q → декодировать обратно» для эффекта артефактов сжатия.
|
||||
- **Аффинные трансформации** (`core/affine.ts`): единая функция обратного
|
||||
отображения — для каждого пикселя результата вычисляется исходная координата
|
||||
матрицей и семплируется билинейно (общий семплер, вынесенный из `resize`).
|
||||
Поверх неё: наклон по осям, поворот на произвольный угол (с расширением холста
|
||||
под новые габариты), зум к центру, сдвиг.
|
||||
- **Свет и цвет** (`core/color.ts` дополнения): гамма-коррекция через LUT,
|
||||
автоконтраст (растяжка по гистограмме яркости с отсечкой хвостов), температура
|
||||
(множители тёплых/холодных каналов), тонирование (умножение RGB на цвет с
|
||||
сохранением яркостного баланса).
|
||||
- **Виньетка** (`core/effects.ts`): радиальное затемнение от центра, сила
|
||||
слайдером; чистая математика без canvas.
|
||||
- **IO-расширения** (`core/io.ts`): декодирование SVG из текста/файла через
|
||||
blob-URL и `Image`; раундтрип «закодировать JPEG с качеством Q → декодировать
|
||||
обратно» для эффекта артефактов сжатия.
|
||||
|
||||
## 3. Инструменты — 11
|
||||
|
||||
- **Геометрия:** `skew-png` (углы X/Y), `rotate-free-png` (любой угол, холст расширяется), `zoom-png` (масштаб ≥100 % к центру), `shift-png` (сдвиг со смещением содержимого).
|
||||
- **Фильтры:** `vignette-png` (сила затемнения), `jpeg-artifacts-png` (имитация пережатия).
|
||||
- **Цвет:** `gamma-png` (0.1–3.0), `auto-contrast-png` (без параметров), `temperature-png` (−100 тёплый…+100 холодный или наоборот — подпись честная), `tint-png` (цвет + сила).
|
||||
- **Геометрия:** `skew-png` (углы X/Y), `rotate-free-png` (любой угол, холст
|
||||
расширяется), `zoom-png` (масштаб ≥100 % к центру), `shift-png` (сдвиг со
|
||||
смещением содержимого).
|
||||
- **Фильтры:** `vignette-png` (сила затемнения), `jpeg-artifacts-png` (имитация
|
||||
пережатия).
|
||||
- **Цвет:** `gamma-png` (0.1–3.0), `auto-contrast-png` (без параметров),
|
||||
`temperature-png` (−100 тёплый…+100 холодный или наоборот — подпись честная),
|
||||
`tint-png` (цвет + сила).
|
||||
- **Конвертация:** `svg-to-png` (текст SVG слева, как base64-режим).
|
||||
|
||||
## 4. Этапы и проверки
|
||||
|
||||
- **A. Ядро** — семплер, аффинный модуль, свет/цвет, эффекты + точные тесты: поворот квадрата на 90° совпадает с существующим `rotate90`, зум ×2 удваивает размеры и центр остаётся центром, гамма 1 — идентичность, автоконтраст растягивает известный диапазон, виньетка симметрична и не трогает центр при нулевой силе. Проверка: `pnpm test/check`.
|
||||
- **B. IO** — SVG-декод и JPEG-раундтрип. Проверка: сборка; живой прогон на этапе D.
|
||||
- **C. Реестр** — одиннадцать записей, иконки, тесты целостности. Проверка: `test/build`, страницы в экспорте.
|
||||
- **A. Ядро** — семплер, аффинный модуль, свет/цвет, эффекты + точные тесты:
|
||||
поворот квадрата на 90° совпадает с существующим `rotate90`, зум ×2 удваивает
|
||||
размеры и центр остаётся центром, гамма 1 — идентичность, автоконтраст
|
||||
растягивает известный диапазон, виньетка симметрична и не трогает центр при
|
||||
нулевой силе. Проверка: `pnpm test/check`.
|
||||
- **B. IO** — SVG-декод и JPEG-раундтрип. Проверка: сборка; живой прогон на
|
||||
этапе D.
|
||||
- **C. Реестр** — одиннадцать записей, иконки, тесты целостности. Проверка:
|
||||
`test/build`, страницы в экспорте.
|
||||
- **D. Итог** — полный прогон, смоук §7.
|
||||
|
||||
## 5. Смоук-чеклист
|
||||
|
||||
1. Наклон X/Y по отдельности и вместе — форма искажается предсказуемо, без чёрных полос при малых углах.
|
||||
1. Наклон X/Y по отдельности и вместе — форма искажается предсказуемо, без
|
||||
чёрных полос при малых углах.
|
||||
2. Поворот на 30° — холст вырос, углы прозрачны, содержимое повернуто.
|
||||
3. Зум 200 % — содержимое увеличено к центру, холст прежнего размера, края обрезаны.
|
||||
3. Зум 200 % — содержимое увеличено к центру, холст прежнего размера, края
|
||||
обрезаны.
|
||||
4. Сдвиг уводит содержимое за край, противоположный край заполняется фоном.
|
||||
5. Гамма 0.5 темнее, 2.0 светлее, 1.0 — без изменений.
|
||||
6. Автоконтраст на тусклом фото вытягивает диапазон.
|
||||
7. Температура ±50 заметно теплеет/холодеет.
|
||||
8. Тонирование красным даёт равномерный оттенок, белые области окрашиваются сильнее всего.
|
||||
8. Тонирование красным даёт равномерный оттенок, белые области окрашиваются
|
||||
сильнее всего.
|
||||
9. Виньетка затемняет только края, центр не трогает.
|
||||
10. Артефакты: качество 10 даёт видимые квадраты JPEG.
|
||||
11. SVG вставленный слева декодируется в результат справа; битый SVG — баннер.
|
||||
@@ -46,8 +74,10 @@
|
||||
|
||||
## 6. Критерии готовности
|
||||
|
||||
- Одиннадцать инструментов работают и покрыты юнит-тестами ядра (кроме IO-дом-части).
|
||||
- Аффинные операции опираются на общий билинейный семплер; дублирования сэмплинга нет.
|
||||
- Одиннадцать инструментов работают и покрыты юнит-тестами ядра (кроме
|
||||
IO-дом-части).
|
||||
- Аффинные операции опираются на общий билинейный семплер; дублирования
|
||||
сэмплинга нет.
|
||||
- Каталог ~69 инструментов; `test/check/build` зелёные, смоук §5 пройден.
|
||||
|
||||
## 7. Что сознательно не делаем
|
||||
|
||||
@@ -1,35 +1,42 @@
|
||||
# План: закрытие EASY/MEDIUM-пробелов из сравнения с onlinepngtools
|
||||
|
||||
> **СТАТУС: ВЫПОЛНЕН (2026-09-07) — устарел.** Все волны W1–W9 закрыты.
|
||||
> Идеи из раздела «Вне очереди» (region-инструменты, мультифайловый вывод,
|
||||
> анимационные, HARD-серия) перенесены в `backlog.md`.
|
||||
> **СТАТУС: ВЫПОЛНЕН (2026-09-07) — устарел.** Все волны W1–W9 закрыты. Идеи из
|
||||
> раздела «Вне очереди» (region-инструменты, мультифайловый вывод, анимационные,
|
||||
> HARD-серия) перенесены в `backlog.md`.
|
||||
|
||||
> Статус: черновик на ревью
|
||||
|
||||
## Принципы
|
||||
|
||||
- Волны собираются вокруг общего математического ядра: одна волна = один движок + N инструментов на нём.
|
||||
- Каждая волна оформляется отдельным планом при старте; здесь — очередь и состав.
|
||||
- Порядок: сначала кластеры, дающие много инструментов малой кровью, затем одиночные эффекты, MEDIUM-пакеты в конце.
|
||||
- Полный список параметров каждого инструмента живёт в карте инструментов (tools-map.md); здесь только состав и ядро.
|
||||
- Волны собираются вокруг общего математического ядра: одна волна = один
|
||||
движок + N инструментов на нём.
|
||||
- Каждая волна оформляется отдельным планом при старте; здесь — очередь и
|
||||
состав.
|
||||
- Порядок: сначала кластеры, дающие много инструментов малой кровью, затем
|
||||
одиночные эффекты, MEDIUM-пакеты в конце.
|
||||
- Полный список параметров каждого инструмента живёт в карте инструментов
|
||||
(tools-map.md); здесь только состав и ядро.
|
||||
|
||||
## Очередь волн
|
||||
|
||||
### W1. Палитры — ВЫПОЛНЕНА (11 инструментов)
|
||||
|
||||
Ядро: RGB↔HSL + гармонии цветового круга. Вывод: свотч-полотно (генератор).
|
||||
Состав: color-wheel, complementary, monochromatic, analogous, triadic, tetradic, similar-shades, sort-colors.
|
||||
Плюс утилиты смешения тем же ядром: mix-colors, average-color, blend-two, step-between (+4, итого 12).
|
||||
Состав: color-wheel, complementary, monochromatic, analogous, triadic, tetradic,
|
||||
similar-shades, sort-colors. Плюс утилиты смешения тем же ядром: mix-colors,
|
||||
average-color, blend-two, step-between (+4, итого 12).
|
||||
|
||||
### W2. Каналы и пространства — ВЫПОЛНЕНА (6 инструментов)
|
||||
|
||||
Ядро: матрицы преобразования RGB→(HSL/HSV/HSI/CMYK/YCbCr/LAB) + визуализация выбранного компонента серым или окрашенно.
|
||||
Общий select «компонент» + select «режим отображения».
|
||||
Ядро: матрицы преобразования RGB→(HSL/HSV/HSI/CMYK/YCbCr/LAB) + визуализация
|
||||
выбранного компонента серым или окрашенно. Общий select «компонент» + select
|
||||
«режим отображения».
|
||||
|
||||
### W3. Маски по свойствам пикселей — ВЫПОЛНЕНА (7 инструментов)
|
||||
|
||||
Ядро: предикат над пикселем → бинарная маска (с инверсией и подсветкой цветом).
|
||||
Состав: show-transparent, show-grayscale, show-color, light-mask, dark-mask, unique-color-mask, extract-by-color.
|
||||
Состав: show-transparent, show-grayscale, show-color, light-mask, dark-mask,
|
||||
unique-color-mask, extract-by-color.
|
||||
|
||||
### W4. Фигурные маски — ВЫПОЛНЕНА (4 инструмента)
|
||||
|
||||
@@ -39,38 +46,52 @@
|
||||
### W5. Геометрия-добивки — ВЫПОЛНЕНА (5 инструментов)
|
||||
|
||||
Ядро: 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)
|
||||
|
||||
Состав: pixelate, color-blocks, randomize-pixels (seed), add-noise, feather-edges, clean-edges, silhouette.
|
||||
Shadow/glow — сюда же, если потянет этап: оба = размытая альфа + смещение + цвет (ядро blur уже есть).
|
||||
Состав: pixelate, color-blocks, randomize-pixels (seed), add-noise,
|
||||
feather-edges, clean-edges, silhouette. Shadow/glow — сюда же, если потянет
|
||||
этап: оба = размытая альфа + смещение + цвет (ядро blur уже есть).
|
||||
|
||||
### W7. Конвертеры и генераторы — ВЫПОЛНЕНА (11 инструментов)
|
||||
|
||||
PNG↔bytes, PNG↔rgb-values (текстовые результаты), verify-is-png (анализ сигнатуры),
|
||||
text-to-png (domText без входной картинки), emoji-to-png, placeholder-png,
|
||||
color-spectrum, colorful-random (seed), draw-grid.
|
||||
multi-color-gradient — если успеем новый тип параметра «список цветов», иначе перенос.
|
||||
PNG↔bytes, PNG↔rgb-values (текстовые результаты), verify-is-png (анализ
|
||||
сигнатуры), text-to-png (domText без входной картинки), emoji-to-png,
|
||||
placeholder-png, color-spectrum, colorful-random (seed), draw-grid.
|
||||
multi-color-gradient — если успеем новый тип параметра «список цветов», иначе
|
||||
перенос.
|
||||
|
||||
### W8. Цветовые MEDIUM — ВЫПОЛНЕНА (4 инструмента; median-cut + Floyd–Steinberg/Bayer, custom-palette через text-параметр)
|
||||
|
||||
Ядро: квантование (median-cut или k-means).
|
||||
Состав: quantize (k), decrease-color-count (=quantize с пресетами), custom-palette (маппинг на список цветов — нужен тип параметра «список»), dithering (Floyd–Steinberg/Bayer поверх квантования).
|
||||
Ядро: квантование (median-cut или k-means). Состав: quantize (k),
|
||||
decrease-color-count (=quantize с пресетами), custom-palette (маппинг на список
|
||||
цветов — нужен тип параметра «список»), dithering (Floyd–Steinberg/Bayer поверх
|
||||
квантования).
|
||||
|
||||
### W9. Сжатие — ВЫПОЛНЕНА (3 инструмента; optimize/change-quality осознанно не выделены — покрыты пресетами compress и W8)
|
||||
|
||||
Без wasm честное управление размером PNG ограничено: реальный рычаг — квантование (W8) + обрезка метаданных + итеративный подбор под целевой KB.
|
||||
Состав: compress (пресеты усилий), reduce-to-size (целевой KB, бинарный поиск по k), optimize (re-encode), change-quality/low-quality — переосмыслить как пресеты W8; jpeg-artifacts уже покрывает «испортить».
|
||||
Без wasm честное управление размером PNG ограничено: реальный рычаг —
|
||||
квантование (W8) + обрезка метаданных + итеративный подбор под целевой KB.
|
||||
Состав: compress (пресеты усилий), reduce-to-size (целевой KB, бинарный поиск по
|
||||
k), optimize (re-encode), change-quality/low-quality — переосмыслить как пресеты
|
||||
W8; jpeg-artifacts уже покрывает «испортить».
|
||||
|
||||
## Вне очереди
|
||||
|
||||
- Region-инструменты (censor/erase/pixelate-area/blur-area/sharpen-area/reverse-area) — ждут UI выделения области на превью; отдельное решение.
|
||||
- Мультифайловый вывод (split-parts, gif-frames, separate-colors) — ждут механизм «результат = набор файлов».
|
||||
- Анимационные (slow-reveal/fade/scrolling) — выход не PNG; отдельное решение о формате.
|
||||
- Region-инструменты
|
||||
(censor/erase/pixelate-area/blur-area/sharpen-area/reverse-area) — ждут UI
|
||||
выделения области на превью; отдельное решение.
|
||||
- Мультифайловый вывод (split-parts, gif-frames, separate-colors) — ждут
|
||||
механизм «результат = набор файлов».
|
||||
- Анимационные (slow-reveal/fade/scrolling) — выход не PNG; отдельное решение о
|
||||
формате.
|
||||
- HARD: glitch, barcode, signature-extract, handwritten-digital.
|
||||
- Нишевые серии logo/icon/stamp/signature — не копируем.
|
||||
|
||||
## Оценка масштаба
|
||||
|
||||
W1–W7 дают **~47 новых инструментов**, почти все EASY. W8 добавляет ядро квантования (открывает custom-palette/dithering и улучшает W9). Суммарно каталог достигает ~120+ при текущих 71.
|
||||
W1–W7 дают **~47 новых инструментов**, почти все EASY. W8 добавляет ядро
|
||||
квантования (открывает custom-palette/dithering и улучшает W9). Суммарно каталог
|
||||
достигает ~120+ при текущих 71.
|
||||
|
||||
@@ -4,43 +4,65 @@
|
||||
|
||||
## 1. Что получается
|
||||
|
||||
Выдача поиска на главной превращается из списка строк в сетку карточек: слева иконка инструмента, справа название и описание. Инструменты получают два новых поля реестра — `popularity` (приоритет в выдаче) и `icon` (идентификатор иконки). Иконки переиспользуются в каталоге `/list-tools` и в легендах шагов цепочки. Поиск работает по-прежнему: нечёткий скоринг, клавиатура ↑↓ Enter Esc.
|
||||
Выдача поиска на главной превращается из списка строк в сетку карточек: слева
|
||||
иконка инструмента, справа название и описание. Инструменты получают два новых
|
||||
поля реестра — `popularity` (приоритет в выдаче) и `icon` (идентификатор
|
||||
иконки). Иконки переиспользуются в каталоге `/list-tools` и в легендах шагов
|
||||
цепочки. Поиск работает по-прежнему: нечёткий скоринг, клавиатура ↑↓ Enter Esc.
|
||||
|
||||
## 2. Данные реестра
|
||||
|
||||
- **`popularity?: number`** (0–100, по умолчанию 50) — приоритет внутри равного скоринга запроса. Значения проставляются вручную осознанно: базовые операции (обрезка, размер, удаление фона, конвертации JPG/PNG) — 80–95; цветовые и геометрия — 40–70; узкие и генераторы — 10–30.
|
||||
- **`icon?: string`** — имя иконки из набора Lucide. Явный словарь-маппинг для всех инструментов в одном месте; отсутствие записи даёт нейтральную заглушку (Wrench).
|
||||
- Тест целостности расширяется: популярность в диапазоне, иконка — непустая строка из разрешённого набора.
|
||||
- **`popularity?: number`** (0–100, по умолчанию 50) — приоритет внутри равного
|
||||
скоринга запроса. Значения проставляются вручную осознанно: базовые операции
|
||||
(обрезка, размер, удаление фона, конвертации JPG/PNG) — 80–95; цветовые и
|
||||
геометрия — 40–70; узкие и генераторы — 10–30.
|
||||
- **`icon?: string`** — имя иконки из набора Lucide. Явный словарь-маппинг для
|
||||
всех инструментов в одном месте; отсутствие записи даёт нейтральную заглушку
|
||||
(Wrench).
|
||||
- Тест целостности расширяется: популярность в диапазоне, иконка — непустая
|
||||
строка из разрешённого набора.
|
||||
|
||||
## 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. Карточки выдачи
|
||||
|
||||
- Сетка: одна колонка на узком экране, две — начиная со среднего героя.
|
||||
- Композиция карточки: квадрат иконки слева, справа название (полужирное) и однострочное описание с обрезкой.
|
||||
- Композиция карточки: квадрат иконки слева, справа название (полужирное) и
|
||||
однострочное описание с обрезкой.
|
||||
- Состояния hover и активной с клавиатуры — тот же акцент подсветки, что сейчас.
|
||||
- Сортировка выдачи: скоринг запроса ↓, затем популярность ↓, затем название.
|
||||
|
||||
## 5. Этапы и проверки
|
||||
|
||||
- **A. Реестр** — поля `popularity` и `icon`, словарь иконок на все 51, расширение тестов целостности. Проверка: `pnpm test/check`.
|
||||
- **B. Карточки поиска** — сетка выдачи с иконками и учётом популярности, клавиатура без изменений. Проверка: визуально, `check/build`.
|
||||
- **C. Распространение** — иконки в каталоге `/list-tools` и легендах звеньев. Проверка: визуально, `build`.
|
||||
- **A. Реестр** — поля `popularity` и `icon`, словарь иконок на все 51,
|
||||
расширение тестов целостности. Проверка: `pnpm test/check`.
|
||||
- **B. Карточки поиска** — сетка выдачи с иконками и учётом популярности,
|
||||
клавиатура без изменений. Проверка: визуально, `check/build`.
|
||||
- **C. Распространение** — иконки в каталоге `/list-tools` и легендах звеньев.
|
||||
Проверка: визуально, `build`.
|
||||
- **D. Итог** — полный прогон и смоук §8.
|
||||
|
||||
## 6. Смоук-чеклист
|
||||
|
||||
1. Пустой запрос на герое показывает популярные первыми после первого символа.
|
||||
2. Запрос «обрез» поднимает карточку обрезки выше прочих совпадений.
|
||||
3. У каждой карточки есть иконка; у большинства — разные, соответствующие смыслу.
|
||||
3. У каждой карточки есть иконка; у большинства — разные, соответствующие
|
||||
смыслу.
|
||||
4. Наведение и стрелки подсвечивают карточку; Enter открывает инструмент.
|
||||
5. Открытый инструмент работает как раньше: параметры в колонках, результат, скачивание.
|
||||
5. Открытый инструмент работает как раньше: параметры в колонках, результат,
|
||||
скачивание.
|
||||
6. Цепочка: легенда каждого шага показывает иконку выбранного инструмента.
|
||||
7. `/list-tools`: карточки категорий с иконками, ссылки работают.
|
||||
8. Регресс: старые сценарии (загрузка файла, цепочка, скачивание) не изменились.
|
||||
@@ -54,7 +76,8 @@
|
||||
|
||||
## 8. Что сознательно не делаем
|
||||
|
||||
- Автоматический расчёт популярности из статистики использования — значения ручные.
|
||||
- Автоматический расчёт популярности из статистики использования — значения
|
||||
ручные.
|
||||
- Собственный набор SVG-иконок — берём готовый открытый.
|
||||
- Переводы названий и поиск на других языках — отдельная i18n-волна.
|
||||
- Изображения-превью в карточках поиска — только иконка и текст.
|
||||
|
||||
+76
-22
@@ -1,52 +1,106 @@
|
||||
# План: i18n — русский и английский
|
||||
|
||||
> **СТАТУС: ВЫПОЛНЕН 25.08.2026.** Базовая локаль — английский (реестр), русский — перевод в словаре; строки инструментов в реестре переведены на английский в рамках этапа C.
|
||||
> **СТАТУС: ВЫПОЛНЕН 25.08.2026.** Базовая локаль — английский (реестр), русский
|
||||
> — перевод в словаре; строки инструментов в реестре переведены на английский в
|
||||
> рамках этапа C.
|
||||
|
||||
## 1. Что получается
|
||||
|
||||
Сайт работает на двух языках: русский (базовый) и английский. Переключатель в шапке, выбор сохраняется в localStorage, тег `html lang` обновляется на клиенте. Переведено всё: каркас интерфейса, карточки инструментов, цепочки, поиск, тексты ошибок. Поиск находит инструменты по запросу на любом из двух языков независимо от активной локали.
|
||||
Сайт работает на двух языках: русский (базовый) и английский. Переключатель в
|
||||
шапке, выбор сохраняется в localStorage, тег `html lang` обновляется на клиенте.
|
||||
Переведено всё: каркас интерфейса, карточки инструментов, цепочки, поиск, тексты
|
||||
ошибок. Поиск находит инструменты по запросу на любом из двух языков независимо
|
||||
от активной локали.
|
||||
|
||||
Каталог и поведение не меняются: ни новых инструментов, ни изменений логики существующих.
|
||||
Каталог и поведение не меняются: ни новых инструментов, ни изменений логики
|
||||
существующих.
|
||||
|
||||
## 2. Решения
|
||||
|
||||
- Без сегментов языка в URL. Статика пререндерится на русском; переключение на клиенте, выбор живёт в localStorage. hreflang/canonical и вторая копия страниц для поисковиков — не в этой волне.
|
||||
- Ядро не знает о локалях. Ошибки в core выбрасываются стабильными кодами (например ERR_BAD_HEX); перевод кода в человекочитаемый текст происходит в слое исполнителя/UI.
|
||||
- Английский — базовая локаль и фолбэк: строки инструментов в реестре записаны по-английски. Переводы живут в секции tools словарей по ключу id инструмента (title, description, подписи параметров, подписи опций select, тексты-результаты анализаторов); сейчас перевод — русский в ru.ts. Доступ из компонентов только через хелперы toolTitle/toolDescription/paramLabel/optionLabel. Так каждая строка имеет единственный источник: en — реестр, переводы — словари.
|
||||
- Без сегментов языка в URL. Статика пререндерится на русском; переключение на
|
||||
клиенте, выбор живёт в localStorage. hreflang/canonical и вторая копия страниц
|
||||
для поисковиков — не в этой волне.
|
||||
- Ядро не знает о локалях. Ошибки в core выбрасываются стабильными кодами
|
||||
(например ERR_BAD_HEX); перевод кода в человекочитаемый текст происходит в
|
||||
слое исполнителя/UI.
|
||||
- Английский — базовая локаль и фолбэк: строки инструментов в реестре записаны
|
||||
по-английски. Переводы живут в секции tools словарей по ключу id инструмента
|
||||
(title, description, подписи параметров, подписи опций select,
|
||||
тексты-результаты анализаторов); сейчас перевод — русский в ru.ts. Доступ из
|
||||
компонентов только через хелперы
|
||||
toolTitle/toolDescription/paramLabel/optionLabel. Так каждая строка имеет
|
||||
единственный источник: en — реестр, переводы — словари.
|
||||
|
||||
## 3. Структура i18n-модуля
|
||||
|
||||
- web/src/lib/i18n/locale.svelte.ts — состояние активной локали (runes), персист в localStorage (ключ locale, guard на SSR), обновление document.documentElement.lang.
|
||||
- web/src/lib/i18n/t.ts — функция перевода с фолбэком на базовый язык и интерполяцией переменных вида {n}.
|
||||
- web/src/lib/i18n/ru.ts и en.ts — словари одного типа Dict; тип экспортируется из ru.ts, en.ts обязан ему соответствовать — пропущенные ключи ловятся на компиляции.
|
||||
- Секции словаря: header, home, catalog, toolPage, chain, sourceCard, resultCard, paramsCard, textInput, textResult, download, infoPanel, dropZone, search, ui (aria кнопок слайдера и пипетки), categories, errors, tools.
|
||||
- web/src/lib/i18n/locale.svelte.ts — состояние активной локали (runes), персист
|
||||
в localStorage (ключ locale, guard на SSR), обновление
|
||||
document.documentElement.lang.
|
||||
- web/src/lib/i18n/t.ts — функция перевода с фолбэком на базовый язык и
|
||||
интерполяцией переменных вида {n}.
|
||||
- web/src/lib/i18n/ru.ts и en.ts — словари одного типа Dict; тип экспортируется
|
||||
из ru.ts, en.ts обязан ему соответствовать — пропущенные ключи ловятся на
|
||||
компиляции.
|
||||
- Секции словаря: header, home, catalog, toolPage, chain, sourceCard,
|
||||
resultCard, paramsCard, textInput, textResult, download, infoPanel, dropZone,
|
||||
search, ui (aria кнопок слайдера и пипетки), categories, errors, tools.
|
||||
|
||||
## 4. Этапы и проверки
|
||||
|
||||
Каждый этап заканчивается зелёными test/check/build и ревью.
|
||||
|
||||
- A. Инфраструктура — модуль состояния, функция перевода с фолбэком и интерполяцией, словари-скелеты со всеми секциями (заполнены header и categories как пилот), переключатель RU/EN в шапке, реактивный lang. Тесты: фолбэк на ru при отсутствии ключа в en, интерполяция, персист локали.
|
||||
- B. Каркас интерфейса — перенос всех хардкод-строк компонентов и маршрутов в словари: layout (шапка, футер), главная (герой, «вернуть последний», title), каталог, карточки источника/результата/параметров, текстовый вход и результат, «Показать маску», DropZone/DropOverlay, кнопка скачивания (busyText), InfoPanel плюс форматирование чисел через тег локали вместо жёсткого ru-RU, ToolPage (легенды, «Шаг n», ошибки шага), ChainToolBlock, placeholder и пустой результат поиска, страницы инструмента (title, 404). Обёрточные сообщения исполнителя («Ошибка исполнения в воркере») — тоже здесь.
|
||||
- C. Контент реестра — переводы 69 инструментов (title/description/labels/options) в tools-секции словаря en; компоненты получают строки через хелперы toolTitle/toolDescription/paramLabel/optionLabel с фолбэком на реестр. Тест целостности: у каждого инструмента есть непустые перевод title/description, полный набор param-ключей и подписи всех опций select; лишние ключи словарей отсутствуют.
|
||||
- D. Поиск по любому языку — нормализация запроса и полей: нижний регистр, ё→е, снятие диакритики через NFD; скоринг против активной и базовой локали сразу, лучший балл побеждает; сортировка совпадений через Intl.Collator активной локали. Тесты нормализации: ё/е, диакритика, английский запрос при русской локали и наоборот.
|
||||
- E. Ошибки на кодах — около 40 мест throw в core, registry, pipeline заменяются на коды; исполнитель переводит код в текст активной локали, неизвестные сообщения показываются как есть. Пять тестов, матчащих русские подстроки ошибок (executor, pipeline, registry, io, alpha), переходят на коды — это упрощает ассерты. Русские describe/it в тестах не трогаем.
|
||||
- A. Инфраструктура — модуль состояния, функция перевода с фолбэком и
|
||||
интерполяцией, словари-скелеты со всеми секциями (заполнены header и
|
||||
categories как пилот), переключатель RU/EN в шапке, реактивный lang. Тесты:
|
||||
фолбэк на ru при отсутствии ключа в en, интерполяция, персист локали.
|
||||
- B. Каркас интерфейса — перенос всех хардкод-строк компонентов и маршрутов в
|
||||
словари: layout (шапка, футер), главная (герой, «вернуть последний», title),
|
||||
каталог, карточки источника/результата/параметров, текстовый вход и результат,
|
||||
«Показать маску», DropZone/DropOverlay, кнопка скачивания (busyText),
|
||||
InfoPanel плюс форматирование чисел через тег локали вместо жёсткого ru-RU,
|
||||
ToolPage (легенды, «Шаг n», ошибки шага), ChainToolBlock, placeholder и пустой
|
||||
результат поиска, страницы инструмента (title, 404). Обёрточные сообщения
|
||||
исполнителя («Ошибка исполнения в воркере») — тоже здесь.
|
||||
- C. Контент реестра — переводы 69 инструментов
|
||||
(title/description/labels/options) в tools-секции словаря en; компоненты
|
||||
получают строки через хелперы toolTitle/toolDescription/paramLabel/optionLabel
|
||||
с фолбэком на реестр. Тест целостности: у каждого инструмента есть непустые
|
||||
перевод title/description, полный набор param-ключей и подписи всех опций
|
||||
select; лишние ключи словарей отсутствуют.
|
||||
- D. Поиск по любому языку — нормализация запроса и полей: нижний регистр, ё→е,
|
||||
снятие диакритики через NFD; скоринг против активной и базовой локали сразу,
|
||||
лучший балл побеждает; сортировка совпадений через Intl.Collator активной
|
||||
локали. Тесты нормализации: ё/е, диакритика, английский запрос при русской
|
||||
локали и наоборот.
|
||||
- E. Ошибки на кодах — около 40 мест throw в core, registry, pipeline заменяются
|
||||
на коды; исполнитель переводит код в текст активной локали, неизвестные
|
||||
сообщения показываются как есть. Пять тестов, матчащих русские подстроки
|
||||
ошибок (executor, pipeline, registry, io, alpha), переходят на коды — это
|
||||
упрощает ассерты. Русские describe/it в тестах не трогаем.
|
||||
- F. Итог — полный прогон, смоук чеклист §6, архивация плана.
|
||||
|
||||
## 5. Смоук-чеклист
|
||||
|
||||
1. Переключение RU/EN в шапке мгновенно меняет язык без перезагрузки; после перезагрузки выбор сохранён.
|
||||
1. Переключение RU/EN в шапке мгновенно меняет язык без перезагрузки; после
|
||||
перезагрузки выбор сохранён.
|
||||
2. Тег html lang соответствует активной локали.
|
||||
3. Главная, каталог, страница инструмента, цепочка из трёх звеньев — везде переведено, без смеси языков.
|
||||
4. Форма параметров: подписи полей и опции select переведены у произвольных пяти инструментов из разных категорий.
|
||||
5. Битый вход (некорректный HEX в тонировании, нулевой размер) показывает ошибку на активном языке.
|
||||
6. Поиск: «повер» находит повороты при русской локали; «rotate» находит их же; при английской локали наоборот. Диакритика и ё не мешают совпадению.
|
||||
3. Главная, каталог, страница инструмента, цепочка из трёх звеньев — везде
|
||||
переведено, без смеси языков.
|
||||
4. Форма параметров: подписи полей и опции select переведены у произвольных пяти
|
||||
инструментов из разных категорий.
|
||||
5. Битый вход (некорректный HEX в тонировании, нулевой размер) показывает ошибку
|
||||
на активном языке.
|
||||
6. Поиск: «повер» находит повороты при русской локали; «rotate» находит их же;
|
||||
при английской локали наоборот. Диакритика и ё не мешают совпадению.
|
||||
7. Числа в панели информации отформатированы по локали.
|
||||
8. Скачивание, пипетка, маски, слайдеры работают одинаково на обеих локалях — функциональность не задета.
|
||||
8. Скачивание, пипетка, маски, слайдеры работают одинаково на обеих локалях —
|
||||
функциональность не задета.
|
||||
|
||||
## 6. Критерии готовности
|
||||
|
||||
- Оба словаря типобезопасны и полны; тест целостности секции tools зелёный.
|
||||
- Ни одного пользовательского русского строки вне словарей (кроме тестовых describe/it).
|
||||
- Ни одного пользовательского русского строки вне словарей (кроме тестовых
|
||||
describe/it).
|
||||
- test/check/build зелёные, смоук пройден.
|
||||
|
||||
## 7. Что сознательно не делаем
|
||||
|
||||
@@ -9,14 +9,26 @@
|
||||
|
||||
## 1. Что получается
|
||||
|
||||
Альтернативная раскладка этапа инструмента: три колонки «Исходник | Параметры | Результат» на широких экранах, вертикальный стек на узких. Цель — отдать вариант дизайнеру как отправную точку. Откат к текущей раскладке — замена одной строки в `ToolPage`.
|
||||
Альтернативная раскладка этапа инструмента: три колонки «Исходник | Параметры |
|
||||
Результат» на широких экранах, вертикальный стек на узких. Цель — отдать вариант
|
||||
дизайнеру как отправную точку. Откат к текущей раскладке — замена одной строки в
|
||||
`ToolPage`.
|
||||
|
||||
## 2. Решения
|
||||
|
||||
- Два взаимозаменяемых компонента этапа с одинаковым набором пропсов; `ToolPage` рендерит один тег. Попробовать новое — поменять компонент в этом теге; вернуть старое — поменять обратно. Никаких флагов и конфигов.
|
||||
- Общая типизация пропсов в одном файле рядом с компонентами; сами карточки (`SourceCard`, `ResultCard`, `TextInputCard`, `ParamsCard`) переиспользуются как есть — вся логика исполнения, маски, пипетки и автозапуска остаётся в `ToolPage` и приходит сверху.
|
||||
- Chain-звенья используют ту же панельную систему «Вход | Параметры | Результат» — общие классы `.pane` / `.pane-legend` / `.pane-params` вынесены в app.css, вертикальные поля в узкой колонке заданы один раз глобально.
|
||||
- Один порог отзывчивости: три колонки от ~75rem, ниже — стек «исходник → параметры → результат». Промежуточные перестроения не придумываем до фидбека дизайнера.
|
||||
- Два взаимозаменяемых компонента этапа с одинаковым набором пропсов; `ToolPage`
|
||||
рендерит один тег. Попробовать новое — поменять компонент в этом теге; вернуть
|
||||
старое — поменять обратно. Никаких флагов и конфигов.
|
||||
- Общая типизация пропсов в одном файле рядом с компонентами; сами карточки
|
||||
(`SourceCard`, `ResultCard`, `TextInputCard`, `ParamsCard`) переиспользуются
|
||||
как есть — вся логика исполнения, маски, пипетки и автозапуска остаётся в
|
||||
`ToolPage` и приходит сверху.
|
||||
- Chain-звенья используют ту же панельную систему «Вход | Параметры | Результат»
|
||||
— общие классы `.pane` / `.pane-legend` / `.pane-params` вынесены в app.css,
|
||||
вертикальные поля в узкой колонке заданы один раз глобально.
|
||||
- Один порог отзывчивости: три колонки от ~75rem, ниже — стек «исходник →
|
||||
параметры → результат». Промежуточные перестроения не придумываем до фидбека
|
||||
дизайнера.
|
||||
|
||||
## 3. Компоненты
|
||||
|
||||
@@ -27,28 +39,45 @@ src/lib/components/stage/
|
||||
ToolStageInline.svelte # новая сетка [source | params | result]
|
||||
```
|
||||
|
||||
- `ToolStageClassic` — вырезка существующего шаблона со стилями; после извлечения страница должна быть пиксель-в-пиксель прежней.
|
||||
- `ToolStageInline` — grid-колонки `minmax(0, 1fr) | фикс-ширина параметров | minmax(0, 1fr)`; легенда «Параметры» над средней колонкой в том же стиле edge-legend.
|
||||
- Модификаторы состояний через классы на корне компонента: `no-params` (инструменты без параметров → две колонки), `no-source` (генераторы → параметры и результат), текстовый вход до декодирования занимает левую ячейку как сейчас.
|
||||
- `ToolStageClassic` — вырезка существующего шаблона со стилями; после
|
||||
извлечения страница должна быть пиксель-в-пиксель прежней.
|
||||
- `ToolStageInline` — grid-колонки
|
||||
`minmax(0, 1fr) | фикс-ширина параметров | minmax(0, 1fr)`; легенда
|
||||
«Параметры» над средней колонкой в том же стиле edge-legend.
|
||||
- Модификаторы состояний через классы на корне компонента: `no-params`
|
||||
(инструменты без параметров → две колонки), `no-source` (генераторы →
|
||||
параметры и результат), текстовый вход до декодирования занимает левую ячейку
|
||||
как сейчас.
|
||||
|
||||
## 4. Этапы и проверки
|
||||
|
||||
Каждый этап заканчивается зелёными test/check/build и ревью.
|
||||
|
||||
- **A. Извлечение классики** — перенос разметки и стилей этапа из `ToolPage` в `ToolStageClassic` без изменений поведения; пропсы собираются в одном объекте. Критерий: страница неотличима, diff по логике `ToolPage` нулевой.
|
||||
- **B. Инлайн-раскладка** — `ToolStageInline`, переключение в `ToolPage` на новый компонент, прогон смоука §5. Ревью в браузере; решение дизайнера — потом, код уже позволяет жить в обоих вариантах.
|
||||
- **A. Извлечение классики** — перенос разметки и стилей этапа из `ToolPage` в
|
||||
`ToolStageClassic` без изменений поведения; пропсы собираются в одном объекте.
|
||||
Критерий: страница неотличима, diff по логике `ToolPage` нулевой.
|
||||
- **B. Инлайн-раскладка** — `ToolStageInline`, переключение в `ToolPage` на
|
||||
новый компонент, прогон смоука §5. Ревью в браузере; решение дизайнера —
|
||||
потом, код уже позволяет жить в обоих вариантах.
|
||||
|
||||
## 5. Смоук
|
||||
|
||||
1. Замена компонента этапа — одна строка; обе версии собираются без предупреждений.
|
||||
2. Инструмент без параметров (например Автоконтраст): две колонки, пустой середины нет.
|
||||
1. Замена компонента этапа — одна строка; обе версии собираются без
|
||||
предупреждений.
|
||||
2. Инструмент без параметров (например Автоконтраст): две колонки, пустой
|
||||
середины нет.
|
||||
3. Генератор без исходника: параметры и результат в две колонки.
|
||||
4. Текстовый инструмент до декодирования: вход слева, параметры в центре, пустое состояние результата справа.
|
||||
5. Маска, пипетка, слайдеры в средней колонке; изменение параметра перезапускает инструмент как раньше.
|
||||
6. Узкий экран: стек без горизонтального скролла, порядок исходник → параметры → результат.
|
||||
4. Текстовый инструмент до декодирования: вход слева, параметры в центре, пустое
|
||||
состояние результата справа.
|
||||
5. Маска, пипетка, слайдеры в средней колонке; изменение параметра перезапускает
|
||||
инструмент как раньше.
|
||||
6. Узкий экран: стек без горизонтального скролла, порядок исходник → параметры →
|
||||
результат.
|
||||
7. Легенды присутствуют у всех трёх зон в обеих раскладках.
|
||||
|
||||
## 6. Что сознательно не делаем
|
||||
|
||||
- Редизайн визуального языка (цвета, типографика, формы) — материал для дизайнера, не для этого плана.
|
||||
- Новая логика: исполнение, автосохранение, пресеты — раскладка ничего не знает про исполнение.
|
||||
- Редизайн визуального языка (цвета, типографика, формы) — материал для
|
||||
дизайнера, не для этого плана.
|
||||
- Новая логика: исполнение, автосохранение, пресеты — раскладка ничего не знает
|
||||
про исполнение.
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# Plan: `/preview/list-tools` → design2 (catalog)
|
||||
|
||||
> **СТАТУС: ВЫПОЛНЕН (2026-09-07) — устарел.** Каталог
|
||||
> `/preview/list-tools` переписан на `CatalogHeader` / `CatalogToolbar` /
|
||||
> `CatalogGroup` / `ToolCard` (kit), поиск и фильтры функциональны.
|
||||
> Доводка верности против рефа остаётся в рамках `plan-design-fix.md`.
|
||||
> **СТАТУС: ВЫПОЛНЕН (2026-09-07) — устарел.** Каталог `/preview/list-tools`
|
||||
> переписан на `CatalogHeader` / `CatalogToolbar` / `CatalogGroup` / `ToolCard`
|
||||
> (kit), поиск и фильтры функциональны. Доводка верности против рефа остаётся в
|
||||
> рамках `plan-design-fix.md`.
|
||||
|
||||
Ветка `feat/redesign`. Цель — привести каталог инструментов к рефу
|
||||
`refs-html/list-tools.html`. Сейчас страница вообще не на design2: по
|
||||
@@ -16,19 +16,19 @@
|
||||
`ToolCard` (kit). Нет поиска, фильтров, счётчика, навигация по категориям
|
||||
сделана через `Panel`-блоки, а не через `.catalog-groups`.
|
||||
- Реф (`list-tools.html`) строит:
|
||||
- `.catalog-page` → `.catalog-head` (`.eyebrow`, `h1` «Tool catalog»,
|
||||
`p` lede, `.catalog-total` «NN TOOLS AVAILABLE» с большим числом),
|
||||
- `.catalog-page` → `.catalog-head` (`.eyebrow`, `h1` «Tool catalog», `p`
|
||||
lede, `.catalog-total` «NN TOOLS AVAILABLE» с большим числом),
|
||||
- `.catalog-toolbar` (`.catalog-search` + `.catalog-filters` с кнопками),
|
||||
- `.catalog-groups` (grid `repeat(2,minmax(0,1fr))`, gap `56px 28px`) →
|
||||
`.catalog-group` (`.group-title` «CAT · NN TOOLS») → `.tool-cards` →
|
||||
`.tool-card`.
|
||||
- `.tool-card` (реф) — `display:grid`, колонки `42px minmax(0,1fr) 24px 18px`,
|
||||
`min-height:106px`, `gap:14px`, `padding:16px`: `.tool-icon` (42×42
|
||||
синий размытый квадрат), `.tool-copy` (`strong` 13px mono + `span` 12px mono
|
||||
muted), `.tool-index` (10px mono, сверху), `.tool-arrow` (появляется при hover).
|
||||
`min-height:106px`, `gap:14px`, `padding:16px`: `.tool-icon` (42×42 синий
|
||||
размытый квадрат), `.tool-copy` (`strong` 13px mono + `span` 12px mono muted),
|
||||
`.tool-index` (10px mono, сверху), `.tool-arrow` (появляется при hover).
|
||||
- Токены `design2.css` уже совпадают с рефом (`--background #eef1f4`,
|
||||
`--panel #f8fafb`, `--foreground #17212b`, `--muted #6d7883`, `--line #cbd3da`,
|
||||
`--blue #1769d2`, `--radius 4px`) — править токены не нужно.
|
||||
`--panel #f8fafb`, `--foreground #17212b`, `--muted #6d7883`,
|
||||
`--line #cbd3da`, `--blue #1769d2`, `--radius 4px`) — править токены не нужно.
|
||||
|
||||
## Шаги
|
||||
|
||||
@@ -37,20 +37,21 @@
|
||||
- grid-колонки `42px minmax(0,1fr) 24px 18px`, `align-items:center`,
|
||||
`gap:14px`, `min-height:106px`, `padding:16px`, `border:1px solid line`,
|
||||
`background:panel`, `border-radius:radius`.
|
||||
- `.tool-icon`: 42×42 `place-items:center`, `background:color-mix(in srgb,
|
||||
var(--blue) 12%, transparent)`, `color:var(--blue)` (реф использует
|
||||
`--panel-strong`; вместо нового токена берём этот `color-mix`).
|
||||
- `.tool-icon`: 42×42 `place-items:center`,
|
||||
`background:color-mix(in srgb, var(--blue) 12%, transparent)`,
|
||||
`color:var(--blue)` (реф использует `--panel-strong`; вместо нового токена
|
||||
берём этот `color-mix`).
|
||||
- `.tool-copy strong`: `600 13px var(--font-mono)`; `span`: `12px/1.5 mono`
|
||||
`muted`.
|
||||
- `.tool-index`: `10px mono muted`, `align-self:start`.
|
||||
- `.tool-arrow`: `color:var(--blue)`, `opacity:0` → `1` + `translate(2px,-2px)`
|
||||
на `.tool-card:hover`.
|
||||
- `.tool-arrow`: `color:var(--blue)`, `opacity:0` → `1` +
|
||||
`translate(2px,-2px)` на `.tool-card:hover`.
|
||||
- DOM-порядок как в рефе: `icon, copy, index, arrow`.
|
||||
- `href` через `resolve` из `$app/paths` (убрать `svelte-ignore`).
|
||||
|
||||
2. **`kit/CatalogHeader.svelte`**: `.eyebrow` «EASY-PNG-TOOLS / CATALOG»,
|
||||
`h1` «Tool catalog», lede; `.catalog-total` с динамическим `TOOLS.length`
|
||||
(реф показывает `32`).
|
||||
2. **`kit/CatalogHeader.svelte`**: `.eyebrow` «EASY-PNG-TOOLS / CATALOG», `h1`
|
||||
«Tool catalog», lede; `.catalog-total` с динамическим `TOOLS.length` (реф
|
||||
показывает `32`).
|
||||
|
||||
3. **`kit/CatalogToolbar.svelte`**: `.catalog-search` (иконка + `<input>`) и
|
||||
`.catalog-filters` (кнопки по категориям). Состояние `query` / `category`
|
||||
@@ -68,8 +69,8 @@
|
||||
- `<CatalogToolbar bind:query bind:category />`;
|
||||
- `.catalog-groups` из отфильтрованных групп (по `CATEGORIES`, пересчитав
|
||||
`tools` с учётом `query`/`category`);
|
||||
- внутри `<CatalogGroup>` — `<ToolCard>` на каждый tool (`href=
|
||||
"/preview/tools/{id}"`, `icon` из `tool-icons`, `index` по порядку,
|
||||
- внутри `<CatalogGroup>` — `<ToolCard>` на каждый tool
|
||||
(`href= "/preview/tools/{id}"`, `icon` из `tool-icons`, `index` по порядку,
|
||||
`title`/`description` из registry);
|
||||
- убрать `Panel`/`PanelHeading` из этого маршрута.
|
||||
|
||||
@@ -77,9 +78,9 @@
|
||||
TRANSPARENCY / COLOR / GEOMETRY / FILTERS / ANALYZE). Наши `CATEGORIES`
|
||||
(`convert/alpha/color/geometry/filters/text/analyze/generate`) — сопоставить
|
||||
(`alpha`→TRANSPARENCY и т.п.); точное сопоставление уточнить по рефу и
|
||||
`i18n/ru.ts`. Фильтры = ALL + те же label'ы. Тексты заголовков/lede — по
|
||||
рефу (англ. «Tool catalog» / «Focused utilities for working with PNG…»),
|
||||
либо оставить RU-вариант, если это сознательное отклонение (зафиксировать).
|
||||
`i18n/ru.ts`. Фильтры = ALL + те же label'ы. Тексты заголовков/lede — по рефу
|
||||
(англ. «Tool catalog» / «Focused utilities for working with PNG…»), либо
|
||||
оставить RU-вариант, если это сознательное отклонение (зафиксировать).
|
||||
|
||||
7. **Адаптив**: перенести правила рефа (`@media max-width:800px`) —
|
||||
`.catalog-groups` → 1fr, `.tool-card` → `38px minmax(0,1fr) 18px`, скрыть
|
||||
@@ -87,16 +88,16 @@
|
||||
|
||||
## Верификация
|
||||
|
||||
- `pnpm lint`, `pnpm exec svelte-check --tsconfig ./tsconfig.json`,
|
||||
`pnpm build` — без ошибок.
|
||||
- `pnpm lint`, `pnpm exec svelte-check --tsconfig ./tsconfig.json`, `pnpm build`
|
||||
— без ошибок.
|
||||
- `pnpm refs-audit` (теперь мульти-маршрутный) → глянуть секцию
|
||||
`/preview/list-tools`: `onlyOurs`/`onlyRef` должны упасть с `257/54`
|
||||
примерно до уровня demo (`18/11`). Остаточный diff (если будет) зафиксировать
|
||||
в этом файле.
|
||||
`/preview/list-tools`: `onlyOurs`/`onlyRef` должны упасть с `257/54` примерно
|
||||
до уровня demo (`18/11`). Остаточный diff (если будет) зафиксировать в этом
|
||||
файле.
|
||||
|
||||
## Примечания
|
||||
|
||||
- `ToolCard` правится на месте — влияет на `preview/+page` и `preview/kit`
|
||||
(там тоже карточки каталога, унификация уместна).
|
||||
- `ToolCard` правится на месте — влияет на `preview/+page` и `preview/kit` (там
|
||||
тоже карточки каталога, унификация уместна).
|
||||
- Старый дизайн (`routes/(old)/**`, `components/search/**`) не трогать.
|
||||
- Коммиты делает автор (агент не коммитит).
|
||||
|
||||
@@ -4,15 +4,25 @@
|
||||
|
||||
## 1. Зачем
|
||||
|
||||
- Первые операции настоящего алгоритмического уровня: свёртки и заливка связной области.
|
||||
- Витринная фича каталога — удаление однотонного фона без нейросетей: порог по цвету + заливка от краёв изображения + сглаживание границы маски.
|
||||
- Эти операции реально блокируют UI-поток на больших картинках — повод включить давно подготовленный путь: исполнитель `executeStep` переезжает в Web Worker, страницы не меняются вовсе.
|
||||
- Первые операции настоящего алгоритмического уровня: свёртки и заливка связной
|
||||
области.
|
||||
- Витринная фича каталога — удаление однотонного фона без нейросетей: порог по
|
||||
цвету + заливка от краёв изображения + сглаживание границы маски.
|
||||
- Эти операции реально блокируют UI-поток на больших картинках — повод включить
|
||||
давно подготовленный путь: исполнитель `executeStep` переезжает в Web Worker,
|
||||
страницы не меняются вовсе.
|
||||
|
||||
## 2. Инструменты — 3
|
||||
|
||||
- **Удалить фон PNG** (`remove-background-png`, категория «Прозрачность») — параметры: цвет фона, допуск похожести %, режим «только внешние области» (заливка от краёв вместо глобального совпадения), сглаживание границы (проходы мажоритарного фильтра маски). Даёт превью маски через существующий контракт `preview`.
|
||||
- **Размытие PNG** (`blur-png`, новая категория «Фильтры») — гауссово приближение тремя проходами разделяемого бокса; радиус слайдером 1–32 px.
|
||||
- **Резкость PNG** (`sharpen-png`, «Фильтры») — свёртка ядром резкости с регулируемой силой (смесь исходного и обработанного), сила слайдером 0–100 %.
|
||||
- **Удалить фон PNG** (`remove-background-png`, категория «Прозрачность») —
|
||||
параметры: цвет фона, допуск похожести %, режим «только внешние области»
|
||||
(заливка от краёв вместо глобального совпадения), сглаживание границы (проходы
|
||||
мажоритарного фильтра маски). Даёт превью маски через существующий контракт
|
||||
`preview`.
|
||||
- **Размытие PNG** (`blur-png`, новая категория «Фильтры») — гауссово
|
||||
приближение тремя проходами разделяемого бокса; радиус слайдером 1–32 px.
|
||||
- **Резкость PNG** (`sharpen-png`, «Фильтры») — свёртка ядром резкости с
|
||||
регулируемой силой (смесь исходного и обработанного), сила слайдером 0–100 %.
|
||||
|
||||
## 3. Новое ядро (чистые функции, детерминированные, покрытые тестами)
|
||||
|
||||
@@ -21,46 +31,67 @@
|
||||
- разделяемый бокс-блюр и сборка гауссова приближения из трёх проходов;
|
||||
- ядро резкости и смешивание с оригиналом по силе.
|
||||
- `core/background.ts`
|
||||
- построение маски удаления по цветовому расстоянию (та же метрика, что у удаления цвета);
|
||||
- построение маски удаления по цветовому расстоянию (та же метрика, что у
|
||||
удаления цвета);
|
||||
- заливка связной области от всех граничных пикселей (очередь, без рекурсии);
|
||||
- мажоритарное сглаживание маски заданным числом проходов;
|
||||
- сборка результата: альфа 0 у удалённых пикселей; плюс функция маски для превью.
|
||||
- сборка результата: альфа 0 у удалённых пикселей; плюс функция маски для
|
||||
превью.
|
||||
|
||||
## 4. Web Worker
|
||||
|
||||
- Точка исполнения одна — `tools/executor.ts`; меняется только её внутренность.
|
||||
- Воркер-файл импортирует реестр и исполняет операцию по `toolId`; картинка приходит копией (структурный клон), результат возвращается передаваемым буфером.
|
||||
- Ленивая инициализация воркера при первом вызове; если воркер недоступен (старый браузер/тестовая среда) — автоматический откат на прямое исполнение в главном потоке. Обе ветви обязаны давать побайтово одинаковый результат.
|
||||
- Генераторы и текстовые представления остаются в главном потоке — они мгновенные.
|
||||
- Воркер-файл импортирует реестр и исполняет операцию по `toolId`; картинка
|
||||
приходит копией (структурный клон), результат возвращается передаваемым
|
||||
буфером.
|
||||
- Ленивая инициализация воркера при первом вызове; если воркер недоступен
|
||||
(старый браузер/тестовая среда) — автоматический откат на прямое исполнение в
|
||||
главном потоке. Обе ветви обязаны давать побайтово одинаковый результат.
|
||||
- Генераторы и текстовые представления остаются в главном потоке — они
|
||||
мгновенные.
|
||||
- Пипетка и лупа работают с уже готовыми картинками и не зависят от переноса.
|
||||
|
||||
## 5. Этапы и проверки
|
||||
|
||||
- **A. Ядро** — свёртки и фон с тестами на малых матрицах (точные ожидания руками). Проверка: `pnpm test/check`.
|
||||
- **B. Реестр** — категория «Фильтры», три записи, превью маски. Проверка: тесты целостности, визуально.
|
||||
- **C. Web Worker** — перенос исполнителя, отккат-ветвь. Проверка: результаты до/после переключения идентичны байт-в-байт на фиксированной картинке и параметрах; `test/build` зелёные.
|
||||
- **A. Ядро** — свёртки и фон с тестами на малых матрицах (точные ожидания
|
||||
руками). Проверка: `pnpm test/check`.
|
||||
- **B. Реестр** — категория «Фильтры», три записи, превью маски. Проверка: тесты
|
||||
целостности, визуально.
|
||||
- **C. Web Worker** — перенос исполнителя, отккат-ветвь. Проверка: результаты
|
||||
до/после переключения идентичны байт-в-байт на фиксированной картинке и
|
||||
параметрах; `test/build` зелёные.
|
||||
- **D. Итог** — полный прогон и смоук §8.
|
||||
|
||||
## 6. Смоук-чеклист
|
||||
|
||||
1. Размытие: радиус 1 и 32 — плавно усиливается, прозрачные края не темнеют.
|
||||
2. Резкость: сила 0 не меняет картинку; сила 100 заметно подчёркивает края без цветового мусора.
|
||||
2. Резкость: сила 0 не меняет картинку; сила 100 заметно подчёркивает края без
|
||||
цветового мусора.
|
||||
3. Удаление фона на логотипе с однотонным фоном: фон исчез, содержимое цело.
|
||||
4. Режим «только внешние области»: дырка внутри объекта того же цвета остаётся непрозрачной.
|
||||
5. Сглаживание 0/2/4 прохода — граница от жёсткой к мягкой, результат воспроизводим.
|
||||
6. Превью маски показывает белым удаляемую область; скачивание всегда отдаёт результат.
|
||||
7. Во время размытия большой картинки интерфейс остаётся отзывчивым (чип «Пересчёт…», скролл живой).
|
||||
8. Цепочка: размытие → резкость → удаление фона работает; каждый блок скачивает свой промежуток.
|
||||
4. Режим «только внешние области»: дырка внутри объекта того же цвета остаётся
|
||||
непрозрачной.
|
||||
5. Сглаживание 0/2/4 прохода — граница от жёсткой к мягкой, результат
|
||||
воспроизводим.
|
||||
6. Превью маски показывает белым удаляемую область; скачивание всегда отдаёт
|
||||
результат.
|
||||
7. Во время размытия большой картинки интерфейс остаётся отзывчивым (чип
|
||||
«Пересчёт…», скролл живой).
|
||||
8. Цепочка: размытие → резкость → удаление фона работает; каждый блок скачивает
|
||||
свой промежуток.
|
||||
9. Регресс старых инструментов и главной-поиска без изменений поведения.
|
||||
|
||||
## 7. Критерии готовности
|
||||
|
||||
- Три новых инструмента проходят смоук §6; операции ядра покрыты точными юнит-тестами.
|
||||
- Исполнение изображений идёт через воркер там, где он доступен, с гарантированно одинаковым результатом обеих ветвей.
|
||||
- Три новых инструмента проходят смоук §6; операции ядра покрыты точными
|
||||
юнит-тестами.
|
||||
- Исполнение изображений идёт через воркер там, где он доступен, с
|
||||
гарантированно одинаковым результатом обеих ветвей.
|
||||
- `test/check/build` зелёные.
|
||||
|
||||
## 8. Что сознательно не делаем
|
||||
|
||||
- Многопроходное гауссово ядро «по-настоящему» (O(r²)) — трёхпроходного боксового приближения достаточно визуально и оно быстрее.
|
||||
- Многопроходное гауссово ядро «по-настоящему» (O(r²)) — трёхпроходного
|
||||
боксового приближения достаточно визуально и оно быстрее.
|
||||
- Хромакей по нескольким цветам, ИИ-сегментация — следующие волны.
|
||||
- Отмена выполняющегося прогона воркера — устаревшие результаты отбрасываются токеном, сама работа завершается фоном.
|
||||
- Отмена выполняющегося прогона воркера — устаревшие результаты отбрасываются
|
||||
токеном, сама работа завершается фоном.
|
||||
|
||||
@@ -4,17 +4,26 @@
|
||||
|
||||
## 1. Зачем
|
||||
|
||||
Одна пара функций — расширение и сужение непрозрачных областей — открывает целое семейство пользовательских операций: обводка логотипов и иконок, контуры, утолщение и утончение подписей, удаление мелкого мусора и закрытие дырок. Всё это частые запросы в наборах PNG-утилит, а ядро получается компактным и легко покрывается тестами.
|
||||
Одна пара функций — расширение и сужение непрозрачных областей — открывает целое
|
||||
семейство пользовательских операций: обводка логотипов и иконок, контуры,
|
||||
утолщение и утончение подписей, удаление мелкого мусора и закрытие дырок. Всё
|
||||
это частые запросы в наборах PNG-утилит, а ядро получается компактным и легко
|
||||
покрывается тестами.
|
||||
|
||||
## 2. Инструменты — 7 (все в категории «Прозрачность»)
|
||||
|
||||
- **Обвести PNG** (`add-stroke-png`) — кольцо выбранного цвета вокруг непрозрачного содержимого; параметры: цвет, толщина 1–10 px.
|
||||
- **Найти контур PNG** (`find-contour-png`) — линия по краю непрозрачных областей; параметр: толщина линии.
|
||||
- **Обвести PNG** (`add-stroke-png`) — кольцо выбранного цвета вокруг
|
||||
непрозрачного содержимого; параметры: цвет, толщина 1–10 px.
|
||||
- **Найти контур PNG** (`find-contour-png`) — линия по краю непрозрачных
|
||||
областей; параметр: толщина линии.
|
||||
- **Утолщить PNG** (`make-thicker-png`) — расширение содержимого на N пикселей.
|
||||
- **Утончить PNG** (`make-thinner-png`) — сужение содержимого на N пикселей.
|
||||
- **Жёсткие края PNG** (`harden-alpha-png`) — бинаризация альфы по порогу: полупрозрачность уходит, остаётся два состояния.
|
||||
- **Убрать мусор PNG** (`despeckle-alpha-png`) — открытие: удаляет одиночные полупрозрачные пиксели и мелкие крапинки.
|
||||
- **Закрыть дыры PNG** (`close-holes-png`) — закрытие: заполняет одиночные прозрачные точки внутри объекта.
|
||||
- **Жёсткие края PNG** (`harden-alpha-png`) — бинаризация альфы по порогу:
|
||||
полупрозрачность уходит, остаётся два состояния.
|
||||
- **Убрать мусор PNG** (`despeckle-alpha-png`) — открытие: удаляет одиночные
|
||||
полупрозрачные пиксели и мелкие крапинки.
|
||||
- **Закрыть дыры PNG** (`close-holes-png`) — закрытие: заполняет одиночные
|
||||
прозрачные точки внутри объекта.
|
||||
|
||||
Все операции применяются к альфа-каналу; RGB пикселей сохраняется.
|
||||
|
||||
@@ -23,49 +32,64 @@
|
||||
Чистые функции над бинарной маской и над альфа-каналом:
|
||||
|
||||
- `buildAlphaMask(img)` — 1 там, где альфа > 0.
|
||||
- `dilateMask(mask, w, h, radius, shape)` — расширение; `shape`: квадрат или диск (для обводок диск даёт ровную круглую линию).
|
||||
- `dilateMask(mask, w, h, radius, shape)` — расширение; `shape`: квадрат или
|
||||
диск (для обводок диск даёт ровную круглую линию).
|
||||
- `erodeMask(...)` — зеркальная операция.
|
||||
- `dilateImage / erodeImage(img, radiusPx)` — применяют маскирование к альфе изображения, RGB сохраняется.
|
||||
- `dilateImage / erodeImage(img, radiusPx)` — применяют маскирование к альфе
|
||||
изображения, RGB сохраняется.
|
||||
- Сборщики:
|
||||
- обводка: `dilate(mask) − mask` → новое кольцо заливается цветом;
|
||||
- контур: `mask − erode(mask)` → линия;
|
||||
- открытие: `erode → dilate`; закрытие: `dilate → erode`.
|
||||
- `hardenAlpha(img, thresholdPercent)` — отдельно от морфологии, но той же волной.
|
||||
- `hardenAlpha(img, thresholdPercent)` — отдельно от морфологии, но той же
|
||||
волной.
|
||||
|
||||
Структурирующий элемент при радиусе r — диск радиуса r (расстояние до центра ≤ r); квадрат не используем, чтобы углы обводок не были угловатыми.
|
||||
Структурирующий элемент при радиусе r — диск радиуса r (расстояние до центра ≤
|
||||
r); квадрат не используем, чтобы углы обводок не были угловатыми.
|
||||
|
||||
## 4. Web Worker
|
||||
|
||||
Ничего не требуется: новые операции попадают в реестр, исполнитель автоматически исполняет их в воркере. Тесты ядра идут мимо воркера (чистые функции).
|
||||
Ничего не требуется: новые операции попадают в реестр, исполнитель автоматически
|
||||
исполняет их в воркере. Тесты ядра идут мимо воркера (чистые функции).
|
||||
|
||||
## 5. Этапы и проверки
|
||||
|
||||
- **A. Ядро** — `morphology.ts` + точные тесты на матрицах 5×5: расширение диска, сужение, открытие убирает одиночный пиксель, закрытие закрывает одиночную дыру, кольцо обводки нужного цвета и толщины. Проверка: `pnpm test/check`.
|
||||
- **B. Реестр** — семь записей, тест целостности обновляется (51 → 58). Проверка: `test/build`, визуально.
|
||||
- **A. Ядро** — `morphology.ts` + точные тесты на матрицах 5×5: расширение
|
||||
диска, сужение, открытие убирает одиночный пиксель, закрытие закрывает
|
||||
одиночную дыру, кольцо обводки нужного цвета и толщины. Проверка:
|
||||
`pnpm test/check`.
|
||||
- **B. Реестр** — семь записей, тест целостности обновляется (51 → 58).
|
||||
Проверка: `test/build`, визуально.
|
||||
- **C. Итог** — полный прогон, смоук §7.
|
||||
|
||||
## 6. Критерии готовности
|
||||
|
||||
- Семь новых инструментов работают на прозрачных PNG: обводка, контур, толще/тоньше, жёсткие края, убрать мусор, закрыть дыры.
|
||||
- Семь новых инструментов работают на прозрачных PNG: обводка, контур,
|
||||
толще/тоньше, жёсткие края, убрать мусор, закрыть дыры.
|
||||
- Все операции детерминированы и покрыты юнит-тестами с ручными ожиданиями.
|
||||
- Исполнение автоматически идёт в воркере.
|
||||
- `test/check/build` зелёные, смоук §7 пройден.
|
||||
|
||||
## 7. Смоук-чеклист
|
||||
|
||||
1. Обводка толщиной 3 вокруг круглого стикера — ровное кольцо выбранного цвета, без угловатости.
|
||||
2. Толщина 1 — тонкая аккуратная линия; 10 — толстая, форма содержимого не искажается сверх роста.
|
||||
1. Обводка толщиной 3 вокруг круглого стикера — ровное кольцо выбранного цвета,
|
||||
без угловатости.
|
||||
2. Толщина 1 — тонкая аккуратная линия; 10 — толстая, форма содержимого не
|
||||
искажается сверх роста.
|
||||
3. Контур показывает только край содержимого линией.
|
||||
4. Толще/тоньше на тонкой надписи — штрихи растут/сжимаются без разрывов при умеренных значениях.
|
||||
4. Толще/тоньше на тонкой надписи — штрихи растут/сжимаются без разрывов при
|
||||
умеренных значениях.
|
||||
5. Жёсткие края: полупрозрачность исчезает, силуэт сохраняется.
|
||||
6. Убрать мусор убирает одиночные точки-крапинки, не трогая крупные области.
|
||||
7. Закрыть дыры заполняет одиночные прозрачные точки внутри логотипа.
|
||||
8. Цепочка: обводка после удаления фона работает; каждое звено скачивает свой промежуток.
|
||||
8. Цепочка: обводка после удаления фона работает; каждое звено скачивает свой
|
||||
промежуток.
|
||||
9. Регресс остальных инструментов без изменений.
|
||||
|
||||
## 8. Что сознательно не делаем
|
||||
|
||||
- Морфология по яркости для непрозрачных сканов — волна фокусируется на альфе прозрачных PNG.
|
||||
- Морфология по яркости для непрозрачных сканов — волна фокусируется на альфе
|
||||
прозрачных PNG.
|
||||
- Выбор структурирующего элемента пользователем — всегда диск.
|
||||
- Feather/свечение краёв — отдельная будущая задача поверх размытия.
|
||||
- Произвольные SVG-маски и градиентные обводки.
|
||||
|
||||
@@ -11,10 +11,9 @@
|
||||
> Архивная копия, ниже — состояние на момент закрытия.
|
||||
|
||||
> Статус: **переведено 121 из 125; остальные 4 — отложены/закрыты решениями.**
|
||||
> Текущее
|
||||
> состояние: **121 инструмент переведён в `registry-new`**, старый UI уже живёт
|
||||
> на `(old)/`-маршрутах, линтер-изоляция old↔preview на месте
|
||||
> (плагин `isolation/no-mixed-imports`). Здесь — что сделать, чтобы:
|
||||
> Текущее состояние: **121 инструмент переведён в `registry-new`**, старый UI
|
||||
> уже живёт на `(old)/`-маршрутах, линтер-изоляция old↔preview на месте (плагин
|
||||
> `isolation/no-mixed-imports`). Здесь — что сделать, чтобы:
|
||||
>
|
||||
> 1. в preview был **весь** каталог инструментов (плюс закрыты известные долги);
|
||||
> 2. физически перенести старую ветку в папки `old/` (шаг 37 основного плана);
|
||||
@@ -31,44 +30,44 @@
|
||||
Не переведены только: `png-info` (отложен, хвост-фича с exif), `compress-png`
|
||||
и `reduce-to-size-png` (закрыто решениями, см. ниже), `watermark-image-png`
|
||||
(overlay — единственный реальный остаток).
|
||||
- Preview (`/preview`) показывает переведённые; старый UI работает
|
||||
на `/`-маршрутах (группа `(old)/`): `+page`, `demo`, `list-tools`,
|
||||
`tools/[id]`. Переведено всё кроме четырёх хвостов: color 18, filters 3,
|
||||
geometry 13, alpha 13 (+watermark-image в остатке), generate 10 (mix/sort),
|
||||
analyze 12 (6 масок + verify + 5 вердиктов), convert 14 (jpg/webp/bmp +
|
||||
5 png→texт + 6 text→png), text `watermark-tile-png`.
|
||||
- Preview (`/preview`) показывает переведённые; старый UI работает на
|
||||
`/`-маршрутах (группа `(old)/`): `+page`, `demo`, `list-tools`, `tools/[id]`.
|
||||
Переведено всё кроме четырёх хвостов: color 18, filters 3, geometry 13, alpha
|
||||
13 (+watermark-image в остатке), generate 10 (mix/sort), analyze 12 (6 масок +
|
||||
verify + 5 вердиктов), convert 14 (jpg/webp/bmp + 5 png→texт + 6 text→png),
|
||||
text `watermark-tile-png`.
|
||||
- Плагин `isolation/no-mixed-imports` (`web/eslint-plugins/isolation/`)
|
||||
полностью разделяет ветки: старый `lib/registry.ts|/registry/**`,
|
||||
`lib/registry-helpers.ts`, `lib/categories.ts`, `lib/tools/**`,
|
||||
`lib/components/**`(кроме `kit/`) vs новый `routes/preview/**`,
|
||||
`lib/registry-new/**`, `lib/preview/**`, `lib/registry-schema.ts`,
|
||||
`lib/components/kit/**`. Общее: `core/`, `i18n/`, `theme`, `assets/`,
|
||||
корневой `lib`.
|
||||
`lib/components/kit/**`. Общее: `core/`, `i18n/`, `theme`, `assets/`, корневой
|
||||
`lib`.
|
||||
- Правило копий (из основного плана): файл, используемый `(old)`, не трогаем —
|
||||
делаем копию в `lib/preview/` (пример: `lib/preview/tool-icons.ts`).
|
||||
- Известные долги (предсуществующие, не создавались нами): ошибка
|
||||
`ToolCard.svelte:19:36` (svelte-check), фейл `palette.test.ts` (ENOENT
|
||||
`src/app.css` — из-за него `pnpm test` красный при 607 passed), ~335
|
||||
design-tokens ошибок `lint:all` в старых kit-компонентах, a11y-warning в
|
||||
`ColorField.svelte`, пустой ruleset в `Toggle.svelte`, неиспользуемые токены
|
||||
в `preview.css`.
|
||||
`ColorField.svelte`, пустой ruleset в `Toggle.svelte`, неиспользуемые токены в
|
||||
`preview.css`.
|
||||
|
||||
## Скоуп перевода: переведено всё переводимое (отложено 4 хвоста)
|
||||
|
||||
Каталог дожат до предела текущих решений: переведено 121 из 125. Оставшиеся
|
||||
4 инструмента **отложены осознанно** (не делаем сейчас):
|
||||
Каталог дожат до предела текущих решений: переведено 121 из 125. Оставшиеся 4
|
||||
инструмента **отложены осознанно** (не делаем сейчас):
|
||||
|
||||
- **`watermark-image-png` (alpha)** — **отложен**. Нужна overlay-механика
|
||||
(второе изображение-знак через `getOverlay`/store + параметры
|
||||
scale/opacity/position/margin/aspect); решается отдельно, вместе с вопросом,
|
||||
как второй источник изображения вписывается в schema-driven preview.
|
||||
- **`png-info`** — **отложен осознанно**: будет серьёзно дорабатываться
|
||||
отдельным райзом (exif-теги, редактирование, структурированный вывод),
|
||||
для него нужен свой отдельный случай в UI, не «ещё один text/verdict».
|
||||
отдельным райзом (exif-теги, редактирование, структурированный вывод), для
|
||||
него нужен свой отдельный случай в UI, не «ещё один text/verdict».
|
||||
- **`compress-png`** — **закрыт как дубликат** `decrease-color-count-png`:
|
||||
пресеты сжатия 192/96/44 добавлены прямо в схему `decrease-color-count`.
|
||||
- **`reduce-to-size-png`** — **отложен** до достройки кнопки Download
|
||||
(backlog №7, «экспорт с лимитом размера»).
|
||||
- **`reduce-to-size-png`** — **отложен** до достройки кнопки Download (backlog
|
||||
№7, «экспорт с лимитом размера»).
|
||||
|
||||
### Переведено в этом проходе (text-механика)
|
||||
|
||||
@@ -77,10 +76,10 @@
|
||||
qualityParamId) для download.
|
||||
- **Executor:** `executeFromText` / `executeToText` / `executeTextToText`
|
||||
(прямые, без worker), экспорт в `preview/executor/index.ts`.
|
||||
- **UI:** `SchemaTextSource` (textarea + Render), `SchemaTextResult`
|
||||
(кол-блок с Copy/Download .txt или бейдж-вердикт по префиксу Yes/No),
|
||||
интеграция в `SchemaPreview`/`SchemaToolView` (режимы file/text/generate,
|
||||
рендер image/text/verdict, скрытие image-download для text-результата).
|
||||
- **UI:** `SchemaTextSource` (textarea + Render), `SchemaTextResult` (кол-блок с
|
||||
Copy/Download .txt или бейдж-вердикт по префиксу Yes/No), интеграция в
|
||||
`SchemaPreview`/`SchemaToolView` (режимы file/text/generate, рендер
|
||||
image/text/verdict, скрытие image-download для text-результата).
|
||||
- **Инструменты:** convert 5×png→text (base64/data-uri/hex/bytes/rgb-values),
|
||||
6×text→png (base64/data-uri/hex/bytes/rgb-values/svg, +width), analyze
|
||||
verify-is-png (text→text) и 5 вердиктов (grayscale/file-size/transparent/
|
||||
@@ -92,9 +91,9 @@
|
||||
## Известные долги перед переездом (завести tasks и закрыть)
|
||||
|
||||
По AGENTS.md техдолг «чинится только по заведённым tasks, не игнорируется»:
|
||||
`lint:all` (~335 design-tokens в kit), неиспользуемые токены preview.css,
|
||||
a11y `ColorField`, пустой ruleset `Toggle`. Плюс два, мешающих зелёной
|
||||
проверке до/после переезда: svelte-check error `ToolCard.svelte:19:36` и фейл
|
||||
`lint:all` (~335 design-tokens в kit), неиспользуемые токены preview.css, a11y
|
||||
`ColorField`, пустой ruleset `Toggle`. Плюс два, мешающих зелёной проверке
|
||||
до/после переезда: svelte-check error `ToolCard.svelte:19:36` и фейл
|
||||
`palette.test.ts` (ENOENT `src/app.css`). После их закрытия целевое состояние
|
||||
проверок перед переездом: `pnpm --dir web exec svelte-check` → 0 errors,
|
||||
`pnpm --dir web test` → зелёный, `pnpm --dir web lint:all` → только задокумент.
|
||||
@@ -102,9 +101,9 @@ a11y `ColorField`, пустой ruleset `Toggle`. Плюс два, мешающ
|
||||
|
||||
## Шаги перевода остатка (порядок)
|
||||
|
||||
1. **Простые пачки** — **выполнено**: color (18) → filters (3) → geometry (13)
|
||||
→ alpha (13/14, кроме watermark-image) → generate (10, включая mix/sort
|
||||
через kind `colors`) → analyze-маски (6) → text `watermark-tile-png`.
|
||||
1. **Простые пачки** — **выполнено**: color (18) → filters (3) → geometry (13) →
|
||||
alpha (13/14, кроме watermark-image) → generate (10, включая mix/sort через
|
||||
kind `colors`) → analyze-маски (6) → text `watermark-tile-png`.
|
||||
2. **Text-механика** — **выполнено**: типы `input`/`result` + output-формат,
|
||||
executor `executeFromText`/`toText`/`textToText`, UI
|
||||
`SchemaTextSource`/`SchemaTextResult` + интеграция в preview, перевод
|
||||
@@ -113,8 +112,8 @@ a11y `ColorField`, пустой ruleset `Toggle`. Плюс два, мешающ
|
||||
3. **Шаг про overlay не делается** — `watermark-image-png` **отложен решением**
|
||||
(см. «Скоуп перевода» выше), отдельным райзом вместе с вопросом второго
|
||||
источника изображения в schema-driven preview.
|
||||
4. После категории — `schema.layout` для сгруппированных инструментов
|
||||
(шаг 32-33 паттерн) и ручная проверка в `/preview`.
|
||||
4. После категории — `schema.layout` для сгруппированных инструментов (шаг 32-33
|
||||
паттерн) и ручная проверка в `/preview`.
|
||||
|
||||
## Переезд в `old/` (шаг 37 основного плана)
|
||||
|
||||
@@ -130,19 +129,19 @@ a11y `ColorField`, пустой ruleset `Toggle`. Плюс два, мешающ
|
||||
`lib/registry/**…` заменить на `lib/old/**` (+ `lib/old/components/**` и
|
||||
т.п.), «новое» и «общее» не меняются. Проверить, что правило по-прежнему
|
||||
разделяет (тест фолс-позитивов: временный импорт old↔new ловится).
|
||||
3. **Импорты внутри старого кода** — относительные пути остаются рабочими
|
||||
(папки двигаются целиком); поправить только то, что ссылалось на `$lib/...`
|
||||
в другом стиле. Старые тесты (`registry.test.ts` и др.) переезжают вместе с
|
||||
3. **Импорты внутри старого кода** — относительные пути остаются рабочими (папки
|
||||
двигаются целиком); поправить только то, что ссылалось на `$lib/...` в другом
|
||||
стиле. Старые тесты (`registry.test.ts` и др.) переезжают вместе с
|
||||
исходниками и продолжают проходить.
|
||||
4. **Гейты переезда:** `svelte-check` 0 errors, `pnpm --dir web test` зелёный,
|
||||
`prettier --check .` чистый, `lint:all` = задокументированный остаток,
|
||||
обе ветки открываются (старая `/`, новая `/preview`).
|
||||
`prettier --check .` чистый, `lint:all` = задокументированный остаток, обе
|
||||
ветки открываются (старая `/`, новая `/preview`).
|
||||
|
||||
## Проверка и сбор отзывов после переезда
|
||||
|
||||
1. **Регресс-чеклист по каталогу** — пройти все инструменты в `/preview`:
|
||||
дефолты совпадают со старыми, валидация/клампы поведения не меняют,
|
||||
результат эквивалентен старому UI (сверить на одинаковых входах).
|
||||
дефолты совпадают со старыми, валидация/клампы поведения не меняют, результат
|
||||
эквивалентен старому UI (сверить на одинаковых входах).
|
||||
2. **Тяжёлые кейсы руками:** генераторы, маски, font-style/plate/gradient на
|
||||
всем наборе, text-source, output-формат. (`watermark-image` — позже, после
|
||||
overlay-райза.)
|
||||
@@ -150,16 +149,16 @@ a11y `ColorField`, пустой ruleset `Toggle`. Плюс два, мешающ
|
||||
зафиксировать в этом документе по мере находок (или ссылкой на tasks).
|
||||
4. Сверка каталога с `tools-map.md` (покрытие/дедупликация
|
||||
`convert-png-to-jpg`/`webp`), при необходимости — обновить cards/мета.
|
||||
5. **Решение об удалении старого** — после завершения ревью и фидбека,
|
||||
отдельным шагом: тогда чистятся `lib/old/**`, `routes/(old)/**`,
|
||||
`old.css`, `exports`, изоляционный плагин (glob-ы больше не нужны) и
|
||||
открываются «правило копий»-дубли.
|
||||
5. **Решение об удалении старого** — после завершения ревью и фидбека, отдельным
|
||||
шагом: тогда чистятся `lib/old/**`, `routes/(old)/**`, `old.css`, `exports`,
|
||||
изоляционный плагин (glob-ы больше не нужны) и открываются «правило
|
||||
копий»-дубли.
|
||||
|
||||
## Как ревьюить каждый шаг (гайды)
|
||||
|
||||
- Диф на **один инструмент** (или фундамент без инструментов) < ~500 строк.
|
||||
- Старый `params`/`run`/`defaultParams`/`sanitizeParams` не изменены;
|
||||
в `registry-new` копия логики + `schema` (дефолты равны старым).
|
||||
- Старый `params`/`run`/`defaultParams`/`sanitizeParams` не изменены; в
|
||||
`registry-new` копия логики + `schema` (дефолты равны старым).
|
||||
- Старый UI и старые тесты продолжают работать: `pnpm --dir web test`,
|
||||
`pnpm --dir web exec svelte-check`.
|
||||
- Новый инструмент проверяется **руками в `/preview`** end-to-end (применение +
|
||||
|
||||
@@ -4,36 +4,63 @@
|
||||
|
||||
## 1. Целевое поведение
|
||||
|
||||
1. «Процентные» параметры (яркость, контраст, качество JPEG/WebP, порог похожести) — слайдеры с числовым значением рядом; точные координаты и размеры (resize/crop) остаются полями ввода.
|
||||
2. У цветовых параметров появляется кнопка-переключатель «Пипетка»: активна → клик по превью исходника снимает цвет пикселя и подставляет его в поле цвета; после выбора режим гаснет.
|
||||
3. В режиме пипетки возле курсора видна лупа: увеличенный блок пикселей вокруг точки с перекрестием и подписью hex текущего пикселя; клик выбирает центр перекрестия.
|
||||
4. Активная пипетка видна на превью перекрестием курсора; повторный клик по кнопке отменяет режим без выбора.
|
||||
5. Всё работает поверх текущего процесса: авто-применение с задержкой, токен запусков, маски — без изменений.
|
||||
1. «Процентные» параметры (яркость, контраст, качество JPEG/WebP, порог
|
||||
похожести) — слайдеры с числовым значением рядом; точные координаты и размеры
|
||||
(resize/crop) остаются полями ввода.
|
||||
2. У цветовых параметров появляется кнопка-переключатель «Пипетка»: активна →
|
||||
клик по превью исходника снимает цвет пикселя и подставляет его в поле цвета;
|
||||
после выбора режим гаснет.
|
||||
3. В режиме пипетки возле курсора видна лупа: увеличенный блок пикселей вокруг
|
||||
точки с перекрестием и подписью hex текущего пикселя; клик выбирает центр
|
||||
перекрестия.
|
||||
4. Активная пипетка видна на превью перекрестием курсора; повторный клик по
|
||||
кнопке отменяет режим без выбора.
|
||||
5. Всё работает поверх текущего процесса: авто-применение с задержкой, токен
|
||||
запусков, маски — без изменений.
|
||||
|
||||
## 2. Слайдеры
|
||||
|
||||
- В реестре новый тип параметра `'slider'` с обязательными `min`/`max`; валидируется как число (дефолт в диапазоне, минимум меньше максимума).
|
||||
- Форма маппит его на готовый `SliderField` из `ui/`; санитизация параметров обрабатывает slider так же, как number.
|
||||
- Переводятся: яркость и контраст, качество JPEG, качество WebP, порог похожести. Остальные инструменты — без изменений.
|
||||
- В реестре новый тип параметра `'slider'` с обязательными `min`/`max`;
|
||||
валидируется как число (дефолт в диапазоне, минимум меньше максимума).
|
||||
- Форма маппит его на готовый `SliderField` из `ui/`; санитизация параметров
|
||||
обрабатывает slider так же, как number.
|
||||
- Переводятся: яркость и контраст, качество JPEG, качество WebP, порог
|
||||
похожести. Остальные инструменты — без изменений.
|
||||
|
||||
## 3. Пипетка и лупа
|
||||
|
||||
- В ядро добавляется чистая функция `rgbToHex(r, g, b)` → строка вида `#rrggbb`, с юнит-тестами; чтение пикселя — из уже отрисованного canvas превью исходника через `getImageData(x, y, 1, 1)`.
|
||||
- `Preview.svelte` получает опциональный режим пипетки: пропсы «активна» и колбэк «цвет выбран»; курсор перекрестие, клик вычисляет координату пикселя с учётом разницы CSS-размера canvas и его внутренней ширины.
|
||||
- **Лупа** — компонент `PipetteLoupe.svelte`: канвас около 110×110 px у курсора (позиция fixed, `pointer-events: none`); рисует блок 15×15 пикселей из исходника с увеличением ×8 без сглаживания, перекрестие в центре, подпись hex текущего пикселя. Координаты блока клампятся у краёв изображения; лупа скрыта вне превью и вне режима пипетки. Тач-устройства: прямой тап выбирает пиксель без лупы.
|
||||
- Проводка: кнопка у каждого цветового поля в карточке параметров → состояние «активное цветовое поле» живёт в оркестраторе → выбор цвета записывает hex в это поле и выключает режим.
|
||||
- Пипетка работает только по левой панели (исходнику): удаляемый цвет и подложка берутся с оригинала.
|
||||
- В ядро добавляется чистая функция `rgbToHex(r, g, b)` → строка вида `#rrggbb`,
|
||||
с юнит-тестами; чтение пикселя — из уже отрисованного canvas превью исходника
|
||||
через `getImageData(x, y, 1, 1)`.
|
||||
- `Preview.svelte` получает опциональный режим пипетки: пропсы «активна» и
|
||||
колбэк «цвет выбран»; курсор перекрестие, клик вычисляет координату пикселя с
|
||||
учётом разницы CSS-размера canvas и его внутренней ширины.
|
||||
- **Лупа** — компонент `PipetteLoupe.svelte`: канвас около 110×110 px у курсора
|
||||
(позиция fixed, `pointer-events: none`); рисует блок 15×15 пикселей из
|
||||
исходника с увеличением ×8 без сглаживания, перекрестие в центре, подпись hex
|
||||
текущего пикселя. Координаты блока клампятся у краёв изображения; лупа скрыта
|
||||
вне превью и вне режима пипетки. Тач-устройства: прямой тап выбирает пиксель
|
||||
без лупы.
|
||||
- Проводка: кнопка у каждого цветового поля в карточке параметров → состояние
|
||||
«активное цветовое поле» живёт в оркестраторе → выбор цвета записывает hex в
|
||||
это поле и выключает режим.
|
||||
- Пипетка работает только по левой панели (исходнику): удаляемый цвет и подложка
|
||||
берутся с оригинала.
|
||||
|
||||
## 4. Этапы и проверки
|
||||
|
||||
- **A. Слайдеры** — тип в реестре, перевод четырёх инструментов, тесты реестра и санитизации, тест `rgbToHex`. Проверка: `pnpm test/check`, визуально слайдеры.
|
||||
- **B. Пипетка** — режим в Preview, проводка через карточки до полей цвета. Проверка: смоук §6 пп. 5–7.
|
||||
- **B2. Лупа** — компонент лупы поверх режима пипетки. Проверка: смоук §6 пп. 8–10.
|
||||
- **A. Слайдеры** — тип в реестре, перевод четырёх инструментов, тесты реестра и
|
||||
санитизации, тест `rgbToHex`. Проверка: `pnpm test/check`, визуально слайдеры.
|
||||
- **B. Пипетка** — режим в Preview, проводка через карточки до полей цвета.
|
||||
Проверка: смоук §6 пп. 5–7.
|
||||
- **B2. Лупа** — компонент лупы поверх режима пипетки. Проверка: смоук §6 пп.
|
||||
8–10.
|
||||
- **C. Итог** — полный прогон `test/check/build`.
|
||||
|
||||
## 5. Изменения по файлам
|
||||
|
||||
- `registry.ts` — тип `'slider'` в `ParamDef`, перевод четырёх записей, ветка в `sanitizeParams`.
|
||||
- `registry.ts` — тип `'slider'` в `ParamDef`, перевод четырёх записей, ветка в
|
||||
`sanitizeParams`.
|
||||
- `ParamForm.svelte` — ветка slider → `SliderField`.
|
||||
- `core/color.ts` — `rgbToHex`.
|
||||
- `Preview.svelte` — режим пипетки (курсор, клик, колбэк).
|
||||
@@ -44,21 +71,29 @@
|
||||
|
||||
## 6. Смоук-чеклист
|
||||
|
||||
1. Яркость/контраст — слайдеры со значением справа, результат обновляется сам при перетаскивании.
|
||||
1. Яркость/контраст — слайдеры со значением справа, результат обновляется сам
|
||||
при перетаскивании.
|
||||
2. Качество JPEG/WebP — слайдер от 1 до 100.
|
||||
3. Порог похожести — слайдер от 0 до 100.
|
||||
4. Resize и crop остались числовыми полями.
|
||||
5. Удаление цвета: «Пипетка» → клик по красной области исходника → поле цвета получил hex этого пикселя, режим погас, результат пересчитался.
|
||||
6. Повторный клик по кнопке или выбор без подтверждения — режим снят, цвет не изменился.
|
||||
5. Удаление цвета: «Пипетка» → клик по красной области исходника → поле цвета
|
||||
получил hex этого пикселя, режим погас, результат пересчитался.
|
||||
6. Повторный клик по кнопке или выбор без подтверждения — режим снят, цвет не
|
||||
изменился.
|
||||
7. Подложка JPEG: пипетка проставляет цвет подложки тем же способом.
|
||||
8. В режиме пипетки лупа следует за курсором, перекрестие показывает пиксель под ним, подпись hex совпадает с выбранным при клике.
|
||||
9. У края картинки блок лупи корректно клампится — за границей тянется чёрный/прозрачный фон, выбор по-прежнему точен.
|
||||
10. Курсор ушёл с превью или режим выключен — лупа скрыта; клики по кнопкам под превью не перехватываются лупой.
|
||||
11. Слайдеры и пипетка дружат с быстрыми правками: одна финальная версия результата, чип «Пересчёт…» на месте.
|
||||
8. В режиме пипетки лупа следует за курсором, перекрестие показывает пиксель под
|
||||
ним, подпись hex совпадает с выбранным при клике.
|
||||
9. У края картинки блок лупи корректно клампится — за границей тянется
|
||||
чёрный/прозрачный фон, выбор по-прежнему точен.
|
||||
10. Курсор ушёл с превью или режим выключен — лупа скрыта; клики по кнопкам под
|
||||
превью не перехватываются лупой.
|
||||
11. Слайдеры и пипетка дружат с быстрыми правками: одна финальная версия
|
||||
результата, чип «Пересчёт…» на месте.
|
||||
|
||||
## 7. Критерии готовности
|
||||
|
||||
- Все процентные параметры — слайдеры; числовые поля остались только там, где нужен точный ввод.
|
||||
- Все процентные параметры — слайдеры; числовые поля остались только там, где
|
||||
нужен точный ввод.
|
||||
- Цвет любого color-параметра можно взять кликом с исходника.
|
||||
- `test/check/build` зелёные, чеклист §6 пройден.
|
||||
|
||||
|
||||
@@ -1,23 +1,26 @@
|
||||
# План: тестирование preview перед промоушеном в главную
|
||||
|
||||
> Статус: **реализация завершена**. Playwright-набор в `web/e2e/`
|
||||
> (108 тестов: 104 прошли, 4 `test.fixme` — известные баги №1–4 ниже),
|
||||
> ручной чек-лист — `docs/checklist-manual-testing.md`. План ветки preview
|
||||
> Статус: **реализация завершена**. Playwright-набор в `web/e2e/` (108 тестов:
|
||||
> 104 прошли, 4 `test.fixme` — известные баги №1–4 ниже), ручной чек-лист —
|
||||
> `docs/checklist-manual-testing.md`. План ветки preview
|
||||
> (`web/src/routes/preview/**`, регистр `lib/registry-new/`, 121 инструмент)
|
||||
> перед шагом C17 из `docs/plan-redesign.md` (переезд preview → реальные
|
||||
> маршруты).
|
||||
>
|
||||
> Охват: **документация плана** — реализация тестов (Playwright-раннер,
|
||||
> сами тесты) делается отдельными задачами после ревью этого документа.
|
||||
> Охват: **документация плана** — реализация тестов (Playwright-раннер, сами
|
||||
> тесты) делается отдельными задачами после ревью этого документа.
|
||||
>
|
||||
> Приоритет: **отсутствие падений/ошибок** > пиксельная корректность.
|
||||
> Дизайн доделываем по ходу, визуальная сверка — ручная и вторичная.
|
||||
> Приоритет: **отсутствие падений/ошибок** > пиксельная корректность. Дизайн
|
||||
> доделываем по ходу, визуальная сверка — ручная и вторичная.
|
||||
>
|
||||
> Команды для проверки в процессе:
|
||||
>
|
||||
> - `pnpm --dir web test` — существующий Vitest (42 файла, 121 инструмент частично покрыт юнит-тестами).
|
||||
> - `pnpm --dir web test:e2e` — Playwright (`web/e2e/`), сборка + `scripts/serve-static.mjs` на 4173.
|
||||
> - `pnpm --dir web exec svelte-check --tsconfig ./tsconfig.json` — проверка типов.
|
||||
> - `pnpm --dir web test` — существующий Vitest (42 файла, 121 инструмент
|
||||
> частично покрыт юнит-тестами).
|
||||
> - `pnpm --dir web test:e2e` — Playwright (`web/e2e/`), сборка +
|
||||
> `scripts/serve-static.mjs` на 4173.
|
||||
> - `pnpm --dir web exec svelte-check --tsconfig ./tsconfig.json` — проверка
|
||||
> типов.
|
||||
> - `pnpm --dir web lint:all` — ESLint + stylelint + токен-аудит.
|
||||
> - `pnpm --dir web build` — статическая сборка (adapter-static).
|
||||
|
||||
@@ -44,21 +47,21 @@ GEOMETRY / FILTERS / TEXT / ANALYZE / GENERATE. Режимы инструмен
|
||||
- **text-output** (`result: "text"`, file): `toText` → текстовый результат;
|
||||
- **generate**: без входа, `executeGenerate` → картинка.
|
||||
|
||||
Исполнение: `preview/executor` — worker с fallback на main-thread
|
||||
(domOnly / нет Worker / ошибка worker). Auto-run с debounce 200 мс.
|
||||
Исполнение: `preview/executor` — worker с fallback на main-thread (domOnly / нет
|
||||
Worker / ошибка worker). Auto-run с debounce 200 мс.
|
||||
|
||||
## 1. Функциональные блоки для проверки
|
||||
|
||||
### A. Инфраструктура и навигация
|
||||
|
||||
1. Все 4 маршрута + 404 открываются без console errors, uncaught exceptions
|
||||
и failed requests.
|
||||
1. Все 4 маршрута + 404 открываются без console errors, uncaught exceptions и
|
||||
failed requests.
|
||||
2. Поиск на `/preview` — фильтрация по названию/описанию, пустой результат →
|
||||
аккуратное empty-state.
|
||||
3. Каталог `/preview/list-tools` — 8 групп, счётчик 121, фильтр категории,
|
||||
иконки не падают.
|
||||
4. Тема light/dark и язык RU/EN переключаются на превью-страницах без
|
||||
регресса рендера.
|
||||
4. Тема light/dark и язык RU/EN переключаются на превью-страницах без регресса
|
||||
рендера.
|
||||
5. 404 на несуществующий id инструмента → EmptyState, без краша.
|
||||
6. Prerender: `pnpm --dir web build` проходит; все 121 страницы
|
||||
`/preview/tools/[id]` генерируются (проверка `entries()`/`getTool`).
|
||||
@@ -66,7 +69,8 @@ GEOMETRY / FILTERS / TEXT / ANALYZE / GENERATE. Режимы инструмен
|
||||
### B. Конвейер инструмента (file-инструменты)
|
||||
|
||||
7. Upload → авто-run → результатный тайл появился, поле ошибки пустое.
|
||||
8. Download — событие download, имя `<toolId>.<ext>`, MIME соответствует `output`.
|
||||
8. Download — событие download, имя `<toolId>.<ext>`, MIME соответствует
|
||||
`output`.
|
||||
9. Reset — возвращает дефолты, сбрасывает результат и ошибку.
|
||||
10. Смена параметра → авто-перезапуск (debounce 200 мс) без «залипания»
|
||||
running-стейта.
|
||||
@@ -75,8 +79,8 @@ GEOMETRY / FILTERS / TEXT / ANALYZE / GENERATE. Режимы инструмен
|
||||
### C. Текстовые инструменты и вердикты
|
||||
|
||||
12. Text-source: ввод → текстовый результат или картинка; copy/download .txt.
|
||||
13. Вердикты (analyze): корректный вердикт на PNG с альфой, на PNG без альфы,
|
||||
на не-PNG входе → без падений.
|
||||
13. Вердикты (analyze): корректный вердикт на PNG с альфой, на PNG без альфы, на
|
||||
не-PNG входе → без падений.
|
||||
14. Пустой text-source → run пропускается без ошибки.
|
||||
|
||||
### D. Генераторы
|
||||
@@ -103,11 +107,11 @@ GEOMETRY / FILTERS / TEXT / ANALYZE / GENERATE. Режимы инструмен
|
||||
|
||||
### ✅ Playwright — детерминированные сквозные потоки и «нет падений»
|
||||
|
||||
- Все маршруты + 404: открытие, отсутствие console errors / uncaught
|
||||
exceptions / failed requests (слушатели `page.on('console'/'pageerror')`).
|
||||
- Все маршруты + 404: открытие, отсутствие console errors / uncaught exceptions
|
||||
/ failed requests (слушатели `page.on('console'/'pageerror')`).
|
||||
- Поиск и каталог: фильтрация, счётчики, переходы.
|
||||
- Upload (`setInputFiles` с фикстурой PNG) → авто-результат → Download
|
||||
(ожидание `download` события, проверка имени/расширения/suggestedFilename).
|
||||
- Upload (`setInputFiles` с фикстурой PNG) → авто-результат → Download (ожидание
|
||||
`download` события, проверка имени/расширения/suggestedFilename).
|
||||
- Reset / смена параметра → перегенерация, running не «залипает».
|
||||
- Тема light/dark и язык RU/EN.
|
||||
- Текстовые инструменты: ввод → текстовый результат → копия/скачивание.
|
||||
@@ -129,12 +133,12 @@ GEOMETRY / FILTERS / TEXT / ANALYZE / GENERATE. Режимы инструмен
|
||||
- **Краевые PNG:** большие (мегапиксели), 1×1, анимация, палитровые,
|
||||
чекерборд/полупрозрачность, повреждённые файлы → нет зависаний, понятные
|
||||
сообщения об ошибке.
|
||||
- **Края параметров:** экстремальные значения, NaN/пустые поля, инвалидные
|
||||
цвета в плашках/градиентах → корректная санитизация (silent repair).
|
||||
- **Производительность/отзывчивость:** задержка авто-реранов на слайдерах,
|
||||
нет «залипания» UI, скролл при длинных настройках.
|
||||
- **Визуальный слой дизайна** (доделываем по ходу): сетки, выравнивание,
|
||||
отступы на брейкпоинтах mobile/tablet/desktop (640/800/1100).
|
||||
- **Края параметров:** экстремальные значения, NaN/пустые поля, инвалидные цвета
|
||||
в плашках/градиентах → корректная санитизация (silent repair).
|
||||
- **Производительность/отзывчивость:** задержка авто-реранов на слайдерах, нет
|
||||
«залипания» UI, скролл при длинных настройках.
|
||||
- **Визуальный слой дизайна** (доделываем по ходу): сетки, выравнивание, отступы
|
||||
на брейкпоинтах mobile/tablet/desktop (640/800/1100).
|
||||
- **Буфер обмена** (copy) в реальном браузере.
|
||||
- **Drag-n-drop** файла на dropzone (UX-ощущения).
|
||||
- **Разные браузеры:** Firefox/Safari-нюансы (webkit в Playwright опционален).
|
||||
@@ -144,8 +148,8 @@ GEOMETRY / FILTERS / TEXT / ANALYZE / GENERATE. Режимы инструмен
|
||||
## 4. Критерии приёмки (доступ в главную)
|
||||
|
||||
1. Все 4 маршрута + 404 открываются без console errors и исключений.
|
||||
2. Репрезентативный прогон по 8 категориям: ни один инструмент не падает и
|
||||
не показывает поле ошибки.
|
||||
2. Репрезентативный прогон по 8 категориям: ни один инструмент не падает и не
|
||||
показывает поле ошибки.
|
||||
3. Upload → результат → download завершается для file-инструментов;
|
||||
текст/вердикты/генераторы дают корректный output-тип.
|
||||
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 детерминированно
|
||||
(маршруты, сквозные потоки, отсутствие ошибок).
|
||||
- **~30–40%** — руками: краевые PNG/параметры, реальные пользовательские
|
||||
файлы, буфер обмена, кросс-браузеры, визуальный/производительный слой,
|
||||
субъективная корректность.
|
||||
- **~30–40%** — руками: краевые PNG/параметры, реальные пользовательские файлы,
|
||||
буфер обмена, кросс-браузеры, визуальный/производительный слой, субъективная
|
||||
корректность.
|
||||
|
||||
## 6. Шаги реализации (после ревью)
|
||||
|
||||
1. ✅ Playwright-раннер: `web/playwright.config.ts` (webServer на `pnpm build +
|
||||
`scripts/serve-static.mjs` `--port 4173`), папка `web/e2e/`, npm-скрипт
|
||||
`test:e2e`.
|
||||
2. ✅ Фикстуры PNG (генерируются в рантайме, `web/e2e/helpers/fixtures.ts`):
|
||||
с альфой, без альфы, 1×1, большой, повреждённый, ландшафт.
|
||||
1. ✅ Playwright-раннер: `web/playwright.config.ts` (webServer на
|
||||
`pnpm build + `scripts/serve-static.mjs` `--port
|
||||
4173`), папка `web/e2e/`, npm-скрипт `test:e2e`.
|
||||
2. ✅ Фикстуры PNG (генерируются в рантайме, `web/e2e/helpers/fixtures.ts`): с
|
||||
альфой, без альфы, 1×1, большой, повреждённый, ландшафт.
|
||||
3. ✅ E2E по блокам A–D и секции 3 (авто-часть): `navigation.spec.ts`,
|
||||
`catalog.spec.ts`, `pipeline.spec.ts`, `text-and-verdicts.spec.ts`,
|
||||
`tools-smoke.spec.ts` (матрица ~71 инструмента), `generators.spec.ts`,
|
||||
@@ -174,14 +178,13 @@ GEOMETRY / FILTERS / TEXT / ANALYZE / GENERATE. Режимы инструмен
|
||||
4. ✅ Ручной чек-лист из секции 3 (ручная часть) —
|
||||
`docs/checklist-manual-testing.md` (разделы A–G, включая известные баги).
|
||||
5. ✅ Найденные баги (зафиксированы как `test.fixme` + чек-лист §G):
|
||||
1. генераторы (21/121) не имеют кнопки «Generate» и полей схемы —
|
||||
результат через UI недостижим;
|
||||
1. генераторы (21/121) не имеют кнопки «Generate» и полей схемы — результат
|
||||
через UI недостижим;
|
||||
2. `resize-png` с дефолтом 0×0 → `errors.resizeSize` при любом входе;
|
||||
3. `crop-png` с дефолтом 0×0 → `errors.cropSize` при любом входе;
|
||||
4. ключи ошибок показываются сырыми (`errors.*`) вместо переведённого
|
||||
текста из `en.ts`/`ru.ts`.
|
||||
Исправления — отдельными атомарными задачами по конвенции AGENTS.md
|
||||
(после правок: `lint:all`, `test:e2e`).
|
||||
4. ключи ошибок показываются сырыми (`errors.*`) вместо переведённого текста
|
||||
из `en.ts`/`ru.ts`. Исправления — отдельными атомарными задачами по
|
||||
конвенции AGENTS.md (после правок: `lint:all`, `test:e2e`).
|
||||
|
||||
## 7. Итоговое состояние на момент сдачи
|
||||
|
||||
|
||||
@@ -4,41 +4,78 @@
|
||||
|
||||
## 1. Что получается
|
||||
|
||||
Четыре инструмента новой категории «Текст»: надпись на изображении, дата-штамп, защитная плитка (повторяющийся полупрозрачный текст под углом), водяной знак картинкой. Каталог вырастает с 69 до 73. Это первые операции, рисующие через canvas 2D — класс perceptual по роадмапу: точные пиксельные тесты на них не пишутся, юнит-тестами покрывается чистая математика раскладки.
|
||||
Четыре инструмента новой категории «Текст»: надпись на изображении, дата-штамп,
|
||||
защитная плитка (повторяющийся полупрозрачный текст под углом), водяной знак
|
||||
картинкой. Каталог вырастает с 69 до 73. Это первые операции, рисующие через
|
||||
canvas 2D — класс perceptual по роадмапу: точные пиксельные тесты на них не
|
||||
пишутся, юнит-тестами покрывается чистая математика раскладки.
|
||||
|
||||
## 2. Решения
|
||||
|
||||
- Вся геометрия — чистые функции без DOM: якорь из 9-позиционной сетки → координаты, перенос текста по ширине с инъекцией измерителя, раскладка плитки с углом и шагом. Canvas-вызовы — тонкий слой рядом (`core/textdraw.ts`), в юнит-тесты не попадает.
|
||||
- Один движок рендера текста на все три текстовых инструмента: шрифт из системного набора (sans/serif/mono), размер, цвет, жирность, прозрачность, поворот. Различия инструментов — только в пресетах параметров и количестве слоёв (одна надпись vs плитка).
|
||||
- Дата-штамп — надпись с автотекстом: строка формата (`YYYY-MM-DD hh:mm`) разворачивается в текущую дату в момент запуска; свой мини-форматтер без зависимостей.
|
||||
- Водяной знак картинкой — первый двухисточниковый инструмент: вторая картинка грузится отдельной зоной в этапе, живёт только в состоянии страницы и сознательно не сохраняется ни в workspace-pipeline, ни в экспорте JSON; после восстановления цепочки знак нужно выбрать заново. Ограничение фиксируем в описании инструмента.
|
||||
- Новая категория `text` встаёт после «Фильтров»; названия в обоих словарях, порядок в CATEGORIES один.
|
||||
- Вся геометрия — чистые функции без DOM: якорь из 9-позиционной сетки →
|
||||
координаты, перенос текста по ширине с инъекцией измерителя, раскладка плитки
|
||||
с углом и шагом. Canvas-вызовы — тонкий слой рядом (`core/textdraw.ts`), в
|
||||
юнит-тесты не попадает.
|
||||
- Один движок рендера текста на все три текстовых инструмента: шрифт из
|
||||
системного набора (sans/serif/mono), размер, цвет, жирность, прозрачность,
|
||||
поворот. Различия инструментов — только в пресетах параметров и количестве
|
||||
слоёв (одна надпись vs плитка).
|
||||
- Дата-штамп — надпись с автотекстом: строка формата (`YYYY-MM-DD hh:mm`)
|
||||
разворачивается в текущую дату в момент запуска; свой мини-форматтер без
|
||||
зависимостей.
|
||||
- Водяной знак картинкой — первый двухисточниковый инструмент: вторая картинка
|
||||
грузится отдельной зоной в этапе, живёт только в состоянии страницы и
|
||||
сознательно не сохраняется ни в workspace-pipeline, ни в экспорте JSON; после
|
||||
восстановления цепочки знак нужно выбрать заново. Ограничение фиксируем в
|
||||
описании инструмента.
|
||||
- Новая категория `text` встаёт после «Фильтров»; названия в обоих словарях,
|
||||
порядок в CATEGORIES один.
|
||||
|
||||
## 3. Инструменты
|
||||
|
||||
- `add-text-png` — параметры: текст (строка), размер (слайдер 8–200), цвет, шрифт (select), жирность (чекбокс), позиция (select 3×3: углы/стороны/центр), отступ (слайдер). Опциональная подложка: цвет + прозрачность плашки за текстом.
|
||||
- `date-stamp-png` — тот же набор минус текст, плюс формат даты; дефолт — нижний правый угол.
|
||||
- `watermark-tile-png` — текст, размер, цвет, прозрачность (0–100), угол (-90…90), шаг по X/Y; плитка заполняет весь холст.
|
||||
- `watermark-image-png` — второй источник (загрузка PNG с альфой), масштаб знака (% от ширины холста), прозрачность, позиция 3×3, отступ.
|
||||
- `add-text-png` — параметры: текст (строка), размер (слайдер 8–200), цвет,
|
||||
шрифт (select), жирность (чекбокс), позиция (select 3×3: углы/стороны/центр),
|
||||
отступ (слайдер). Опциональная подложка: цвет + прозрачность плашки за
|
||||
текстом.
|
||||
- `date-stamp-png` — тот же набор минус текст, плюс формат даты; дефолт — нижний
|
||||
правый угол.
|
||||
- `watermark-tile-png` — текст, размер, цвет, прозрачность (0–100), угол
|
||||
(-90…90), шаг по X/Y; плитка заполняет весь холст.
|
||||
- `watermark-image-png` — второй источник (загрузка PNG с альфой), масштаб знака
|
||||
(% от ширины холста), прозрачность, позиция 3×3, отступ.
|
||||
|
||||
## 4. Этапы и проверки
|
||||
|
||||
Каждый этап заканчивается зелёными test/check/build и ревью.
|
||||
|
||||
- **A. Фундамент** — категория `text` (categories + словари + порядок), `core/textdraw.ts`: anchor9, wrap, tileLayout с юнит-тестами на математике (координаты углов/центра, переносы слов, плотность плитки), тонкий canvas-модуль `domText.ts` (без тестов). Реестр пока пуст в категории.
|
||||
- **B. Надпись и дата** — `add-text-png`, `date-stamp-png` на общем движке; иконки, переводы, записи реестра. Смоук: обе локали, тёмная тема (плашка читаема), длинный текст переносится и не вылезает за край.
|
||||
- **C. Плитка** — `watermark-tile-png`; смоук: отрицательные и нулевой угол, прозрачность 0/50/100, мелкий шаг не подвешивает страницу на больших холстах (ограничение количества плиток капом).
|
||||
- **D. Знак картинкой** — двухисточниковый UX: вторая drop-зона в этапе (компонент рядом с SourceCard, виден только у этого инструмента), масштаб/позиция/отступ; ограничение про сохранение — в описании и подсказке. Этап режется без ущерба остальным, если решим отложить.
|
||||
- **A. Фундамент** — категория `text` (categories + словари + порядок),
|
||||
`core/textdraw.ts`: anchor9, wrap, tileLayout с юнит-тестами на математике
|
||||
(координаты углов/центра, переносы слов, плотность плитки), тонкий
|
||||
canvas-модуль `domText.ts` (без тестов). Реестр пока пуст в категории.
|
||||
- **B. Надпись и дата** — `add-text-png`, `date-stamp-png` на общем движке;
|
||||
иконки, переводы, записи реестра. Смоук: обе локали, тёмная тема (плашка
|
||||
читаема), длинный текст переносится и не вылезает за край.
|
||||
- **C. Плитка** — `watermark-tile-png`; смоук: отрицательные и нулевой угол,
|
||||
прозрачность 0/50/100, мелкий шаг не подвешивает страницу на больших холстах
|
||||
(ограничение количества плиток капом).
|
||||
- **D. Знак картинкой** — двухисточниковый UX: вторая drop-зона в этапе
|
||||
(компонент рядом с SourceCard, виден только у этого инструмента),
|
||||
масштаб/позиция/отступ; ограничение про сохранение — в описании и подсказке.
|
||||
Этап режется без ущерба остальным, если решим отложить.
|
||||
- **E. Итог** — полный прогон, смоук §6, архивация плана.
|
||||
|
||||
## 5. Критерии готовности
|
||||
|
||||
- 73 инструмента; вся математика раскладки покрыта юнит-тестами; canvas-слой изолирован в одном модуле.
|
||||
- test/check/build зелёные; новые инструменты работают в цепочке как обычные шаги (кроме источника знака — см. ограничение).
|
||||
- 73 инструмента; вся математика раскладки покрыта юнит-тестами; canvas-слой
|
||||
изолирован в одном модуле.
|
||||
- test/check/build зелёные; новые инструменты работают в цепочке как обычные
|
||||
шаги (кроме источника знака — см. ограничение).
|
||||
|
||||
## 6. Что сознательно не делаем
|
||||
|
||||
- Выбор произвольных пользовательских шрифтов (файлами) и веб-шрифтов — только системный набор.
|
||||
- Кернинг/линии/многострочное редактирование с абзацами — один абзац с автопереносом.
|
||||
- Выбор произвольных пользовательских шрифтов (файлами) и веб-шрифтов — только
|
||||
системный набор.
|
||||
- Кернинг/линии/многострочное редактирование с абзацами — один абзац с
|
||||
автопереносом.
|
||||
- Сохранение картинки-знака в пайплайне и её передача между сессиями.
|
||||
- Текст по контуру, градиентные заливки текста.
|
||||
|
||||
+25
-10
@@ -4,30 +4,44 @@
|
||||
|
||||
## 1. Что получается
|
||||
|
||||
Переключатель light/dark в шапке рядом с языковым. Выбор в localStorage (`theme`), первый визит — по `prefers-color-scheme`. Тёмная палитра — переопределение токенов селектором `[data-theme='dark']`; компоненты не правятся.
|
||||
Переключатель light/dark в шапке рядом с языковым. Выбор в localStorage
|
||||
(`theme`), первый визит — по `prefers-color-scheme`. Тёмная палитра —
|
||||
переопределение токенов селектором `[data-theme='dark']`; компоненты не
|
||||
правятся.
|
||||
|
||||
## 2. Решения
|
||||
|
||||
- Два состояния, без «авто»: ключ хранит только явный выбор, отсутствие ключа = системная тема.
|
||||
- Анти-вспышка: инлайн-скрипт в `app.html` до гидрации ставит `data-theme` из localStorage или `matchMedia`; пререндеренная разметка остаётся светлой.
|
||||
- `color-scheme: dark` на корне — нативные контролы (input[type=color], range, select, скроллбары) темнеют без правок.
|
||||
- Модуль `theme.svelte.ts` по образцу locale: состояние, персист, применение атрибута. Стартовую установку делает скрипт из `app.html`, модуль синхронизирует runes-состояние после гидрации.
|
||||
- Два состояния, без «авто»: ключ хранит только явный выбор, отсутствие ключа =
|
||||
системная тема.
|
||||
- Анти-вспышка: инлайн-скрипт в `app.html` до гидрации ставит `data-theme` из
|
||||
localStorage или `matchMedia`; пререндеренная разметка остаётся светлой.
|
||||
- `color-scheme: dark` на корне — нативные контролы (input[type=color], range,
|
||||
select, скроллбары) темнеют без правок.
|
||||
- Модуль `theme.svelte.ts` по образцу locale: состояние, персист, применение
|
||||
атрибута. Стартовую установку делает скрипт из `app.html`, модуль
|
||||
синхронизирует runes-состояние после гидрации.
|
||||
|
||||
## 3. Палитра
|
||||
|
||||
- Переопределяем: `--bg`, `--surface`, `--border`, `--text`, `--text-muted`.
|
||||
- Акцент `#2563eb` не меняется; `--accent-hover` — светлее для тёмного фона.
|
||||
- `--danger` осветлить для читаемости.
|
||||
- `--check-a`/`--check-b` шахматки — приглушённые серые: паттерн прозрачности остаётся узнаваемым, но не слепит.
|
||||
- `--check-a`/`--check-b` шахматки — приглушённые серые: паттерн прозрачности
|
||||
остаётся узнаваемым, но не слепит.
|
||||
- `--shadow-card` — почти чёрная с низкой альфой вместо синей.
|
||||
- Все `color-mix(… var(--surface))` пересчитаются сами — проверить визуально легенды, recalc-бейдж, drag-оверлеи.
|
||||
- Все `color-mix(… var(--surface))` пересчитаются сами — проверить визуально
|
||||
легенды, recalc-бейдж, drag-оверлеи.
|
||||
|
||||
## 4. Этапы и проверки
|
||||
|
||||
Каждый этап заканчивается зелёными test/check/build и ревью.
|
||||
|
||||
- **A. Инфраструктура** — `theme.svelte.ts`, скрипт в `app.html`, переключатель в шапке, черновая тёмная палитра. Тесты: персист выбора, игнор мусора в localStorage, применение атрибута (стаб document).
|
||||
- **B. Палитра** — выверка значений: герой, каталог, страница инструмента, цепочка, баннер ошибки, маска, текстовые инструменты; фокус-ринги и hover; обе локали.
|
||||
- **A. Инфраструктура** — `theme.svelte.ts`, скрипт в `app.html`, переключатель
|
||||
в шапке, черновая тёмная палитра. Тесты: персист выбора, игнор мусора в
|
||||
localStorage, применение атрибута (стаб document).
|
||||
- **B. Палитра** — выверка значений: герой, каталог, страница инструмента,
|
||||
цепочка, баннер ошибки, маска, текстовые инструменты; фокус-ринги и hover; обе
|
||||
локали.
|
||||
|
||||
## 5. Смоук
|
||||
|
||||
@@ -43,4 +57,5 @@ Grep: ни одного цвета вне токенов в компонента
|
||||
|
||||
## 7. Не делаем
|
||||
|
||||
Три позиции в переключателе; пользовательские цветовые схемы; автосмену по времени суток; отдельную тему для превью изображений.
|
||||
Три позиции в переключателе; пользовательские цветовые схемы; автосмену по
|
||||
времени суток; отдельную тему для превью изображений.
|
||||
|
||||
@@ -4,57 +4,87 @@
|
||||
|
||||
## 1. Зачем
|
||||
|
||||
- Страница инструмента (`ToolPage.svelte`) разрослась: состояние процесса, загрузка файлов, вставка из буфера и разметка трёх зон живут в одном файле.
|
||||
- Назревают два требования к процессу: параметры применяются автоматически без кнопки; у части утилит на экране показывается одно — визуализация фильтра/маски, а скачивается (и в будущем передаётся следующему инструменту) другое — результат.
|
||||
- Страница инструмента (`ToolPage.svelte`) разрослась: состояние процесса,
|
||||
загрузка файлов, вставка из буфера и разметка трёх зон живут в одном файле.
|
||||
- Назревают два требования к процессу: параметры применяются автоматически без
|
||||
кнопки; у части утилит на экране показывается одно — визуализация
|
||||
фильтра/маски, а скачивается (и в будущем передаётся следующему инструменту)
|
||||
другое — результат.
|
||||
|
||||
## 2. Контракт «результат и маска» в реестре
|
||||
|
||||
- `run(img, params)` — как сейчас: возвращает итоговое изображение. Его скачивают и будут передавать дальше по цепочке.
|
||||
- Запись реестра получает опциональное поле `preview?(img, params)` — возвращает картинку для показа на экране (маску/фильтр).
|
||||
- Если у записи есть `preview`, в карточке результата появляется переключатель «Показать маску». Включён — на превью маска, выключен (по умолчанию) — результат. Скачивание всегда отдаёт результат `run`, независимо от положения переключателя.
|
||||
- Первая реализация — удаление цвета: в ядро добавляется чистая функция `colorMask(img, hex, tolerance)` — чёрно-белая маска, где удаляемые пиксели белые; с юнит-тестами; `preview` записи вызывает её.
|
||||
- `run(img, params)` — как сейчас: возвращает итоговое изображение. Его
|
||||
скачивают и будут передавать дальше по цепочке.
|
||||
- Запись реестра получает опциональное поле `preview?(img, params)` — возвращает
|
||||
картинку для показа на экране (маску/фильтр).
|
||||
- Если у записи есть `preview`, в карточке результата появляется переключатель
|
||||
«Показать маску». Включён — на превью маска, выключен (по умолчанию) —
|
||||
результат. Скачивание всегда отдаёт результат `run`, независимо от положения
|
||||
переключателя.
|
||||
- Первая реализация — удаление цвета: в ядро добавляется чистая функция
|
||||
`colorMask(img, hex, tolerance)` — чёрно-белая маска, где удаляемые пиксели
|
||||
белые; с юнит-тестами; `preview` записи вызывает её.
|
||||
|
||||
## 3. Декомпозиция страницы
|
||||
|
||||
Новая папка `components/tool/`; карточки — чисто отображательные, состояние живёт только в оркестраторе:
|
||||
Новая папка `components/tool/`; карточки — чисто отображательные, состояние
|
||||
живёт только в оркестраторе:
|
||||
|
||||
- **`SourceCard.svelte`** — левая половина сцены: пусто → зона перетаскивания; загружено → превью исходника под оверлеем замены с кнопкой «Заменить изображение».
|
||||
- **`ResultCard.svelte`** — правая половина: пустые состояния, превью результата или сводка, чип «Пересчёт…», кнопка скачивания, при наличии `preview` — переключатель маски.
|
||||
- **`ParamsCard.svelte`** — форма параметров и подпись «Изменения применяются автоматически»; для инструментов без параметров не выводится.
|
||||
- **`ToolPage.svelte`** остаётся оркестратором: состояние (исходник, результат, сводка, статус, ошибка, значения), декодирование файлов, вставка из буфера, сброс, токен запусков и эффект с задержкой. Ожидаемо худеет примерно вдвое.
|
||||
- **`SourceCard.svelte`** — левая половина сцены: пусто → зона перетаскивания;
|
||||
загружено → превью исходника под оверлеем замены с кнопкой «Заменить
|
||||
изображение».
|
||||
- **`ResultCard.svelte`** — правая половина: пустые состояния, превью результата
|
||||
или сводка, чип «Пересчёт…», кнопка скачивания, при наличии `preview` —
|
||||
переключатель маски.
|
||||
- **`ParamsCard.svelte`** — форма параметров и подпись «Изменения применяются
|
||||
автоматически»; для инструментов без параметров не выводится.
|
||||
- **`ToolPage.svelte`** остаётся оркестратором: состояние (исходник, результат,
|
||||
сводка, статус, ошибка, значения), декодирование файлов, вставка из буфера,
|
||||
сброс, токен запусков и эффект с задержкой. Ожидаемо худеет примерно вдвое.
|
||||
|
||||
Карточки переиспользуются будущим многошаговым режимом.
|
||||
|
||||
## 4. Реактивные параметры (в этом же проходе)
|
||||
|
||||
- Кнопки «Применить» больше нет ни у одного инструмента.
|
||||
- Изменение параметра запускает вычисление через 300 мс после последнего изменения; каждое новое изменение перезапускает таймер.
|
||||
- Токен запусков: если во время вычисления параметры поменялись, устаревший ответ отбрасывается.
|
||||
- Загрузка и замена файла считают сразу и подавляют ровно один авто-запуск эффекта.
|
||||
- Изменение параметра запускает вычисление через 300 мс после последнего
|
||||
изменения; каждое новое изменение перезапускает таймер.
|
||||
- Токен запусков: если во время вычисления параметры поменялись, устаревший
|
||||
ответ отбрасывается.
|
||||
- Загрузка и замена файла считают сразу и подавляют ровно один авто-запуск
|
||||
эффекта.
|
||||
- Ошибка пересчёта — баннер сверху, прежний результат не сбрасывается.
|
||||
|
||||
## 5. Этапы и проверки
|
||||
|
||||
- **A. Декомпозиция** — перенос разметки в три карточки один к одному, поведение не меняется. Проверка: `check/build`, визуально идентично.
|
||||
- **B. Реактивность** — токен, задержка, чип пересчёта, минус кнопка. Проверка: смоук §6 пп. 1–6.
|
||||
- **C. Маска** — типы реестра, `colorMask` с тестами, переключатель в карточке результата, пример на удалении цвета. Проверка: тесты + смоук §6 пп. 7–9.
|
||||
- **A. Декомпозиция** — перенос разметки в три карточки один к одному, поведение
|
||||
не меняется. Проверка: `check/build`, визуально идентично.
|
||||
- **B. Реактивность** — токен, задержка, чип пересчёта, минус кнопка. Проверка:
|
||||
смоук §6 пп. 1–6.
|
||||
- **C. Маска** — типы реестра, `colorMask` с тестами, переключатель в карточке
|
||||
результата, пример на удалении цвета. Проверка: тесты + смоук §6 пп. 7–9.
|
||||
- **D. Итог** — полный прогон `test/check/build`.
|
||||
|
||||
## 6. Смоук-чеклист
|
||||
|
||||
1. Загрузка файла → результат появляется сразу.
|
||||
2. Изменить параметр → около 0.3 с и результат обновился сам.
|
||||
3. Быстро изменить яркость несколько раз → одна финальная версия, промежуточные не мелькают.
|
||||
3. Быстро изменить яркость несколько раз → одна финальная версия, промежуточные
|
||||
не мелькают.
|
||||
4. Во время пересчёта чип виден, предыдущий результат не исчезает.
|
||||
5. Инструмент без параметров и информационная страница ведут себя как раньше.
|
||||
6. Некорректные параметры (обрезка вне картинки) → баннер ошибки, прежний результат цел.
|
||||
7. Переключатель «Показать маску» у удаления цвета: включён — белым по чёрному видно, что удалится; выключен — обычный полупрозрачный результат.
|
||||
8. С включённой маской скачивание всё равно сохраняет прозрачный результат, а не маску.
|
||||
6. Некорректные параметры (обрезка вне картинки) → баннер ошибки, прежний
|
||||
результат цел.
|
||||
7. Переключатель «Показать маску» у удаления цвета: включён — белым по чёрному
|
||||
видно, что удалится; выключен — обычный полупрозрачный результат.
|
||||
8. С включённой маской скачивание всё равно сохраняет прозрачный результат, а не
|
||||
маску.
|
||||
9. Замена исходника возвращает переключатель маски в выключенное состояние.
|
||||
|
||||
## 7. Критерии готовности
|
||||
|
||||
- `ToolPage` содержит только оркестрацию состояния и эффектов; вся разметка панелей — в `components/tool/*`.
|
||||
- `ToolPage` содержит только оркестрацию состояния и эффектов; вся разметка
|
||||
панелей — в `components/tool/*`.
|
||||
- Кнопки «Применить» нет ни на одной странице.
|
||||
- Скачивание всегда соответствует `run`; экран может показывать `preview`.
|
||||
- `test/check/build` зелёные, смоук §6 пройден.
|
||||
@@ -62,5 +92,7 @@
|
||||
## 8. Что сознательно не делаем
|
||||
|
||||
- Вынос вычислений в веб-воркер; настраиваемая задержка — фиксированные 300 мс.
|
||||
- Редактор масок (кисть, слои, история) — только автогенерация из параметров инструмента.
|
||||
- Передача результата между инструментами — придёт вместе с многошаговым режимом позже.
|
||||
- Редактор масок (кисть, слои, история) — только автогенерация из параметров
|
||||
инструмента.
|
||||
- Передача результата между инструментами — придёт вместе с многошаговым режимом
|
||||
позже.
|
||||
|
||||
@@ -4,48 +4,89 @@
|
||||
>
|
||||
> **Что сделано по этапам:**
|
||||
>
|
||||
> - A — общая MIME-валидация `isSupportedImage` + `unsupportedImageMessage` в `core/io.ts`; `DropZone` переведён на них ✔
|
||||
> - B — постоянная двухпанельная раскладка: одна карточка-«сцена» с равными половинами «Исходник | Результат», кнопки внутри своих половин, параметры — отдельной карточкой ниже; контент сайта растянут на всю ширину окна ✔
|
||||
> - C — Ctrl+V: `<svelte:window onpaste>`, поиск image-item в буфере, общая валидация; текстовые вставки не затрагиваются ✔
|
||||
> - D — drag-замена: компонент `DropOverlay` с вуалью-подсказкой и счётчиком `dragenter/dragleave` против мерцания; тот же счётчик в `DropZone` ✔
|
||||
> - E — верификация: тесты 85/85, svelte-check 0 ошибок, статический билд с 11 страницами; ручной смоук-чеклист §5 пройден ✔
|
||||
> - A — общая MIME-валидация `isSupportedImage` + `unsupportedImageMessage` в
|
||||
> `core/io.ts`; `DropZone` переведён на них ✔
|
||||
> - B — постоянная двухпанельная раскладка: одна карточка-«сцена» с равными
|
||||
> половинами «Исходник | Результат», кнопки внутри своих половин, параметры —
|
||||
> отдельной карточкой ниже; контент сайта растянут на всю ширину окна ✔
|
||||
> - C — Ctrl+V: `<svelte:window onpaste>`, поиск image-item в буфере, общая
|
||||
> валидация; текстовые вставки не затрагиваются ✔
|
||||
> - D — drag-замена: компонент `DropOverlay` с вуалью-подсказкой и счётчиком
|
||||
> `dragenter/dragleave` против мерцания; тот же счётчик в `DropZone` ✔
|
||||
> - E — верификация: тесты 85/85, svelte-check 0 ошибок, статический билд с 11
|
||||
> страницами; ручной смоук-чеклист §5 пройден ✔
|
||||
>
|
||||
> **Отступления от исходного плана в ходе реализации:** вместо пропа `overlay` у `DropZone` сделан отдельный компонент-обёртка `DropOverlay` (drag-события ловит контент под курсором); раскладка доработана по ревью — одна карточка равных половин вместо двух независимых колонок, сайт без ограничения ширины.
|
||||
> **Отступления от исходного плана в ходе реализации:** вместо пропа `overlay` у
|
||||
> `DropZone` сделан отдельный компонент-обёртка `DropOverlay` (drag-события
|
||||
> ловит контент под курсором); раскладка доработана по ревью — одна карточка
|
||||
> равных половин вместо двух независимых колонок, сайт без ограничения ширины.
|
||||
|
||||
## 1. Целевое поведение
|
||||
|
||||
1. **Постоянная двухпанельная раскладка** страницы инструмента — отдельного состояния «страница загрузки» больше нет:
|
||||
- **Слева — исходник**, панель есть всегда. Не загружено → пустое дроп-поле (drag&drop или клик). Загружено → превью исходника.
|
||||
- **Справа — результат**: до загрузки — `EmptyState` «Результат появится здесь»; во время обработки — `EmptyState` «Обработка…»; после — превью результата (для info-инструмента — `InfoPanel`).
|
||||
2. **Ctrl+V** — картинка из буфера обмена вставляется как исходник с любого места страницы.
|
||||
3. **Замена перетаскиванием** — новый файл, брошенный на левую панель с загруженной картинкой, заменяет исходник (с оверлеем-подсказкой во время перетаскивания).
|
||||
4. **Кнопка «Заменить»** под превью исходника (переносится туда текущая «Загрузить другое изображение»).
|
||||
1. **Постоянная двухпанельная раскладка** страницы инструмента — отдельного
|
||||
состояния «страница загрузки» больше нет:
|
||||
- **Слева — исходник**, панель есть всегда. Не загружено → пустое дроп-поле
|
||||
(drag&drop или клик). Загружено → превью исходника.
|
||||
- **Справа — результат**: до загрузки — `EmptyState` «Результат появится
|
||||
здесь»; во время обработки — `EmptyState` «Обработка…»; после — превью
|
||||
результата (для info-инструмента — `InfoPanel`).
|
||||
2. **Ctrl+V** — картинка из буфера обмена вставляется как исходник с любого
|
||||
места страницы.
|
||||
3. **Замена перетаскиванием** — новый файл, брошенный на левую панель с
|
||||
загруженной картинкой, заменяет исходник (с оверлеем-подсказкой во время
|
||||
перетаскивания).
|
||||
4. **Кнопка «Заменить»** под превью исходника (переносится туда текущая
|
||||
«Загрузить другое изображение»).
|
||||
|
||||
На мобильной ширине панели складываются в столбец: исходник сверху, результат снизу.
|
||||
На мобильной ширине панели складываются в столбец: исходник сверху, результат
|
||||
снизу.
|
||||
|
||||
## 2. Изменения по файлам
|
||||
|
||||
Новые компоненты не вводим — `EmptyState`, `Preview`, `DropZone` покрывают всё. Меняем три файла:
|
||||
Новые компоненты не вводим — `EmptyState`, `Preview`, `DropZone` покрывают всё.
|
||||
Меняем три файла:
|
||||
|
||||
- **`core/io.ts`** — экспорт `isSupportedImage(file): boolean`: общая MIME-проверка на базе `ACCEPTED_IMAGE_TYPES`.
|
||||
- **`DropZone.svelte`** — переходит на `isSupportedImage`; новый опциональный проп `overlay` — режим «невидимая зона приёма поверх контента» для замены перетаскиванием.
|
||||
- **`ToolPage.svelte`** — постоянная сетка из двух панелей; `<svelte:window onpaste>`; drag-оверлей левой панели; кнопки замены/сброса переезжают в левую панель; правая панель — три состояния через `EmptyState`.
|
||||
- **`core/io.ts`** — экспорт `isSupportedImage(file): boolean`: общая
|
||||
MIME-проверка на базе `ACCEPTED_IMAGE_TYPES`.
|
||||
- **`DropZone.svelte`** — переходит на `isSupportedImage`; новый опциональный
|
||||
проп `overlay` — режим «невидимая зона приёма поверх контента» для замены
|
||||
перетаскиванием.
|
||||
- **`ToolPage.svelte`** — постоянная сетка из двух панелей;
|
||||
`<svelte:window onpaste>`; drag-оверлей левой панели; кнопки замены/сброса
|
||||
переезжают в левую панель; правая панель — три состояния через `EmptyState`.
|
||||
|
||||
## 3. Этапы и проверки
|
||||
|
||||
- **A. Общая валидация** — `isSupportedImage` в `io.ts`, `DropZone` переведён на неё. Проверка: `pnpm check`.
|
||||
- **B. Раскладка** — две постоянные колонки в ToolPage; правая панель через EmptyState; кнопки уходят в левую панель; обёртка `{#if !source}` на всю страницу убирается. Проверка: визуальный осмотр всех инструментов, `check/build`.
|
||||
- **C. Ctrl+V** — `onpaste` на окне → поиск image-item в `clipboardData` → тот же `handleFile`; не-изображения игнорируются молча. Проверка: ручной смоук со скриншотом из буфера.
|
||||
- **D. Drag-замена** — оверлей DropZone(overlay) при dragover на левой панели; drop → `handleFile` нового файла. Проверка: ручной смоук «бросить файл на загруженный исходник».
|
||||
- **E. Итог** — полный ручной смоук-чеклист (§5). Проверка: `test/check/build` зелёные.
|
||||
- **A. Общая валидация** — `isSupportedImage` в `io.ts`, `DropZone` переведён на
|
||||
неё. Проверка: `pnpm check`.
|
||||
- **B. Раскладка** — две постоянные колонки в ToolPage; правая панель через
|
||||
EmptyState; кнопки уходят в левую панель; обёртка `{#if !source}` на всю
|
||||
страницу убирается. Проверка: визуальный осмотр всех инструментов,
|
||||
`check/build`.
|
||||
- **C. Ctrl+V** — `onpaste` на окне → поиск image-item в `clipboardData` → тот
|
||||
же `handleFile`; не-изображения игнорируются молча. Проверка: ручной смоук со
|
||||
скриншотом из буфера.
|
||||
- **D. Drag-замена** — оверлей DropZone(overlay) при dragover на левой панели;
|
||||
drop → `handleFile` нового файла. Проверка: ручной смоук «бросить файл на
|
||||
загруженный исходник».
|
||||
- **E. Итог** — полный ручной смоук-чеклист (§5). Проверка: `test/check/build`
|
||||
зелёные.
|
||||
|
||||
## 4. Нюансы и решения
|
||||
|
||||
- **Мерцание dragleave** — классическая проблема оверлеев: используем счётчик `dragenter/dragleave`, оверлей скрывается только когда счётчик обнулился и произошёл `drop`/`dragend`.
|
||||
- **Paste при фокусе в поле параметров** — обрабатываем всегда: если в буфере именно изображение, пользователь очевидно хочет заменить исходник; текстовые вставки обработчик не трогает.
|
||||
- **Форматы буфера** — скриншоты приходят как PNG, часть приложений отдаёт JPEG/WebP; все проходят общую `isSupportedImage`. GIF/BMP из буфера редки, но тоже допустимы.
|
||||
- **Info-инструменты** — ведут себя так же: слева исходник, справа после загрузки появляется `InfoPanel`.
|
||||
- **Состояние ошибки** — баннер остаётся сверху страницы; неудачная замена не сбрасывает старый исходник.
|
||||
- **Мерцание dragleave** — классическая проблема оверлеев: используем счётчик
|
||||
`dragenter/dragleave`, оверлей скрывается только когда счётчик обнулился и
|
||||
произошёл `drop`/`dragend`.
|
||||
- **Paste при фокусе в поле параметров** — обрабатываем всегда: если в буфере
|
||||
именно изображение, пользователь очевидно хочет заменить исходник; текстовые
|
||||
вставки обработчик не трогает.
|
||||
- **Форматы буфера** — скриншоты приходят как PNG, часть приложений отдаёт
|
||||
JPEG/WebP; все проходят общую `isSupportedImage`. GIF/BMP из буфера редки, но
|
||||
тоже допустимы.
|
||||
- **Info-инструменты** — ведут себя так же: слева исходник, справа после
|
||||
загрузки появляется `InfoPanel`.
|
||||
- **Состояние ошибки** — баннер остаётся сверху страницы; неудачная замена не
|
||||
сбрасывает старый исходник.
|
||||
|
||||
## 5. Чеклист ручного смоука
|
||||
|
||||
@@ -54,7 +95,8 @@
|
||||
3. Ctrl+V со скриншотом → исходник заменился, результат пересчитался.
|
||||
4. Drag файла на загруженную левую панель → оверлей виден, после drop — замена.
|
||||
5. Отмена перетаскивания (Esc / мимо) → оверлей скрылся, исходник не изменился.
|
||||
6. Бросок неподдерживаемого файла (например .txt) → баннер ошибки, старый исходник цел.
|
||||
6. Бросок неподдерживаемого файла (например .txt) → баннер ошибки, старый
|
||||
исходник цел.
|
||||
7. Кнопка «Заменить» открывает выбор файла.
|
||||
8. png-info: работает та же схема, справа сводка.
|
||||
9. Мобильная ширина: панели в столбец, порядок исходник → результат.
|
||||
@@ -62,7 +104,8 @@
|
||||
|
||||
## 6. Критерии готовности
|
||||
|
||||
- На странице инструмента нет состояния «только дропзона на всю страницу» — раскладка двухпанельная всегда.
|
||||
- На странице инструмента нет состояния «только дропзона на всю страницу» —
|
||||
раскладка двухпанельная всегда.
|
||||
- Загрузка работает тремя способами: клик, drag&drop (включая замену), Ctrl+V.
|
||||
- Ошибочные файлы и отмена перетаскивания не портят текущий исходник.
|
||||
- `test/check/build` зелёные; чеклист §5 пройден.
|
||||
@@ -71,4 +114,5 @@
|
||||
|
||||
- Множественная загрузка / пакетная обработка — вне скоупа этого плана.
|
||||
- Камера/скриншот-инструменты захвата — только буфер обмена.
|
||||
- Прогресс-бары декодирования — файлы читаются быстро, достаточно индикации «Обработка…».
|
||||
- Прогресс-бары декодирования — файлы читаются быстро, достаточно индикации
|
||||
«Обработка…».
|
||||
|
||||
@@ -1,75 +1,130 @@
|
||||
# План: главная страница как рабочая область с цепочкой
|
||||
|
||||
> **СТАТУС: ВЫПОЛНЕН 2026-08-23.** Заменяет идею отдельной страницы рабочего пространства.
|
||||
> **СТАТУС: ВЫПОЛНЕН 2026-08-23.** Заменяет идею отдельной страницы рабочего
|
||||
> пространства.
|
||||
|
||||
## 1. Модель
|
||||
|
||||
1. **Главная страница — это рабочая область.** Без выбранного инструмента на ней большой поиск: начните вводить название — список отфильтровывается; выбор разворачивает инструмент прямо здесь, без перехода.
|
||||
2. Развернутый инструмент выглядит как привычная страница: слева исходник, справа результат со своей кнопкой скачивания, параметры — в отдельной карточке под сценой.
|
||||
3. Под результатом рядом с кнопкой скачивания — кнопка **«⛓ Chain with…»**: добавляет пустую секцию, внутри которой такой же поиск; выбор заполняет секцию следующим инструментом. Так цепочка растёт сколько угодно.
|
||||
4. Каждая заполненная секция цепи — полноценная: свои параметры, своё превью, своя кнопка скачивания промежуточного результата. Крестик убирает секцию; кнопка «✂ Remove chain» снимает всю цепь разом (без подтверждения).
|
||||
5. Чекбокс «Показать маску» живёт в карточке параметров базового инструмента и влияет только на его превью: в цепь всегда передаётся сам результат.
|
||||
6. Сетка-каталог инструментов переезжает на отдельную страницу `/list-tools` — для SEO и визуального поиска. Основная работа происходит на главной; в шапке появляются ссылки на обе.
|
||||
7. Прямые адреса `/tools/<id>` сохраняются: тот же развернутый инструмент для глубоких ссылок.
|
||||
8. Цепочка переживает перезагрузку (автосохранение в localStorage). Экспорт/импорт файлом отложен до появления CLI/wasm-части.
|
||||
1. **Главная страница — это рабочая область.** Без выбранного инструмента на ней
|
||||
большой поиск: начните вводить название — список отфильтровывается; выбор
|
||||
разворачивает инструмент прямо здесь, без перехода.
|
||||
2. Развернутый инструмент выглядит как привычная страница: слева исходник,
|
||||
справа результат со своей кнопкой скачивания, параметры — в отдельной
|
||||
карточке под сценой.
|
||||
3. Под результатом рядом с кнопкой скачивания — кнопка **«⛓ Chain with…»**:
|
||||
добавляет пустую секцию, внутри которой такой же поиск; выбор заполняет
|
||||
секцию следующим инструментом. Так цепочка растёт сколько угодно.
|
||||
4. Каждая заполненная секция цепи — полноценная: свои параметры, своё превью,
|
||||
своя кнопка скачивания промежуточного результата. Крестик убирает секцию;
|
||||
кнопка «✂ Remove chain» снимает всю цепь разом (без подтверждения).
|
||||
5. Чекбокс «Показать маску» живёт в карточке параметров базового инструмента и
|
||||
влияет только на его превью: в цепь всегда передаётся сам результат.
|
||||
6. Сетка-каталог инструментов переезжает на отдельную страницу `/list-tools` —
|
||||
для SEO и визуального поиска. Основная работа происходит на главной; в шапке
|
||||
появляются ссылки на обе.
|
||||
7. Прямые адреса `/tools/<id>` сохраняются: тот же развернутый инструмент для
|
||||
глубоких ссылок.
|
||||
8. Цепочка переживает перезагрузку (автосохранение в localStorage).
|
||||
Экспорт/импорт файлом отложен до появления CLI/wasm-части.
|
||||
|
||||
## 2. Архитектура
|
||||
|
||||
- Страница инструмента уже собрана как самодостаточный компонент, принимающий запись реестра, — он и становится встраиваемым блоком. Маршруты `/tools/[id]` и главная используют один и тот же компонент: первая — по адресу из URL, вторая — по выбору из поиска.
|
||||
- Состояние главной: выбран ли инструмент; если да — рендерится блок инструмента, целиком владеющий своим исходником, параметрами, результатом и цепочкой.
|
||||
- Страница инструмента уже собрана как самодостаточный компонент, принимающий
|
||||
запись реестра, — он и становится встраиваемым блоком. Маршруты `/tools/[id]`
|
||||
и главная используют один и тот же компонент: первая — по адресу из URL,
|
||||
вторая — по выбору из поиска.
|
||||
- Состояние главной: выбран ли инструмент; если да — рендерится блок
|
||||
инструмента, целиком владеющий своим исходником, параметрами, результатом и
|
||||
цепочкой.
|
||||
- Смена инструмента на главной (через повторный поиск) сбрасывает блок целиком.
|
||||
- Цепочка хранится массивом шагов; у пустого звена инструмент ещё не выбран — такие звенья не сохраняются в localStorage и не участвуют в прогоне.
|
||||
- Исполнение — последовательная свёртка: выход каждого звена — вход следующего; результаты всех звеньев доступны их секциям. Общий авто-раннер даёт токен отмены и задержку ~300 мс.
|
||||
- Цепочка хранится массивом шагов; у пустого звена инструмент ещё не выбран —
|
||||
такие звенья не сохраняются в localStorage и не участвуют в прогоне.
|
||||
- Исполнение — последовательная свёртка: выход каждого звена — вход следующего;
|
||||
результаты всех звеньев доступны их секциям. Общий авто-раннер даёт токен
|
||||
отмены и задержку ~300 мс.
|
||||
|
||||
## 3. Переиспользование и доработки существующего
|
||||
|
||||
Готовое берётся как есть: `SourceCard`, `Preview`, `ParamForm`, `DownloadButton`, `EmptyState`, модуль пайплайна, авто-раннер, `PNG_OUTPUT`.
|
||||
Готовое берётся как есть: `SourceCard`, `Preview`, `ParamForm`,
|
||||
`DownloadButton`, `EmptyState`, модуль пайплайна, авто-раннер, `PNG_OUTPUT`.
|
||||
|
||||
Доработки:
|
||||
|
||||
- **Блок страницы инструмента** — расширяется секцией цепочки: список заполненных звеньев, каждое со своими параметрами, превью и скачиванием, плюс пустые звенья с поиском. Кнопки «⛓» и «✂ Remove chain» — рядом со скачиванием базового результата.
|
||||
- **`ParamsCard`** — опциональный блок «Показать маску» (флаг preview у инструмента + двусторонний флаг).
|
||||
- **Карточка результата** — упрощается: получает готовую картинку, маска уходит в карточку параметров.
|
||||
- **Существующая главная-каталог** — переезжает на маршрут `/list-tools` почти без изменений.
|
||||
- **Блок страницы инструмента** — расширяется секцией цепочки: список
|
||||
заполненных звеньев, каждое со своими параметрами, превью и скачиванием, плюс
|
||||
пустые звенья с поиском. Кнопки «⛓» и «✂ Remove chain» — рядом со скачиванием
|
||||
базового результата.
|
||||
- **`ParamsCard`** — опциональный блок «Показать маску» (флаг preview у
|
||||
инструмента + двусторонний флаг).
|
||||
- **Карточка результата** — упрощается: получает готовую картинку, маска уходит
|
||||
в карточку параметров.
|
||||
- **Существующая главная-каталог** — переезжает на маршрут `/list-tools` почти
|
||||
без изменений.
|
||||
|
||||
Удаляется: черновой маршрут рабочего пространства и его компоненты списка/селектора — вместо них новые, под вертикальную модель.
|
||||
Удаляется: черновой маршрут рабочего пространства и его компоненты
|
||||
списка/селектора — вместо них новые, под вертикальную модель.
|
||||
|
||||
## 4. Новые компоненты
|
||||
|
||||
- **`ToolSearch.svelte`** — поиск с нечётким совпадением: поле ввода, отфильтрованный список (регистронезависимая подпоследовательность по названию и описанию, приоритет совпадению с начала), клавиатура ↑↓ Enter Esc; два размера — герой на главной и компактный внутри пустого звена. Предлагает только инструменты-трансформеры.
|
||||
- **`ChainStep.svelte`** — секция одного звена: пустое звено показывает компактный поиск; заполненное — заголовок с номером и крестиком, форму параметров, превью результата, кнопку скачивания.
|
||||
- **`ChainBar.svelte`** — строка под результатом базового инструмента: «⛓ Chain with…» и «✂ Remove chain» (видна только при непустой цепи).
|
||||
- **`ToolSearch.svelte`** — поиск с нечётким совпадением: поле ввода,
|
||||
отфильтрованный список (регистронезависимая подпоследовательность по названию
|
||||
и описанию, приоритет совпадению с начала), клавиатура ↑↓ Enter Esc; два
|
||||
размера — герой на главной и компактный внутри пустого звена. Предлагает
|
||||
только инструменты-трансформеры.
|
||||
- **`ChainStep.svelte`** — секция одного звена: пустое звено показывает
|
||||
компактный поиск; заполненное — заголовок с номером и крестиком, форму
|
||||
параметров, превью результата, кнопку скачивания.
|
||||
- **`ChainBar.svelte`** — строка под результатом базового инструмента: «⛓ Chain
|
||||
with…» и «✂ Remove chain» (видна только при непустой цепи).
|
||||
|
||||
## 5. Этапы и проверки
|
||||
|
||||
- **A. Поиск и главная** — `ToolSearch`, герой поиска на главной, встраивание блока инструмента, перенос каталога на `/list-tools`, ссылки в шапке. Проверка: `check/build`, визуально.
|
||||
- **B. Цепочка** — состояние цепи в блоке инструмента, пустые звенья с поиском, прогон, промежуточные превью и скачивания, ошибки звеньев. Проверка: смоук §8 пп. 1–6.
|
||||
- **C. Маска и упрощение** — блок маски в `ParamsCard`, упрощение карточки результата. Проверка: смоук §8 п. 7.
|
||||
- **D. Хранение и итог** — автосохранение и восстановление цепочки, удаление чернового маршрута рабочего пространства, полный регресс каталога, `test/check/build`.
|
||||
- **A. Поиск и главная** — `ToolSearch`, герой поиска на главной, встраивание
|
||||
блока инструмента, перенос каталога на `/list-tools`, ссылки в шапке.
|
||||
Проверка: `check/build`, визуально.
|
||||
- **B. Цепочка** — состояние цепи в блоке инструмента, пустые звенья с поиском,
|
||||
прогон, промежуточные превью и скачивания, ошибки звеньев. Проверка: смоук §8
|
||||
пп. 1–6.
|
||||
- **C. Маска и упрощение** — блок маски в `ParamsCard`, упрощение карточки
|
||||
результата. Проверка: смоук §8 п. 7.
|
||||
- **D. Хранение и итог** — автосохранение и восстановление цепочки, удаление
|
||||
чернового маршрута рабочего пространства, полный регресс каталога,
|
||||
`test/check/build`.
|
||||
|
||||
## 6. Смоук-чеклист
|
||||
|
||||
1. На главной — большое поле поиска; ввод фильтрует список по мере набора; Enter выбирает подсвеченный.
|
||||
2. Выбранный инструмент разворачивается на той же странице: исходник, параметры, результат.
|
||||
3. «⛓ Chain with…» добавляет пустую секцию с компактным поиском; выбор заполняет её.
|
||||
1. На главной — большое поле поиска; ввод фильтрует список по мере набора; Enter
|
||||
выбирает подсвеченный.
|
||||
2. Выбранный инструмент разворачивается на той же странице: исходник, параметры,
|
||||
результат.
|
||||
3. «⛓ Chain with…» добавляет пустую секцию с компактным поиском; выбор заполняет
|
||||
её.
|
||||
4. У каждой секции своё превью и своё скачивание промежуточного результата.
|
||||
5. Крестик убирает секцию; «✂ Remove chain» снимает все звенья разом.
|
||||
6. Ошибка звена — баннер «Шаг N (имя): …»; последние успешные результаты секций не исчезают.
|
||||
7. «Показать маску» переключает превью базового инструмента; в цепь идёт результат, скачивание базового отдаёт результат, не маску.
|
||||
8. Перезагрузка восстанавливает цепочку заполненных звеньев; пустые звенья при этом исчезают.
|
||||
6. Ошибка звена — баннер «Шаг N (имя): …»; последние успешные результаты секций
|
||||
не исчезают.
|
||||
7. «Показать маску» переключает превью базового инструмента; в цепь идёт
|
||||
результат, скачивание базового отдаёт результат, не маску.
|
||||
8. Перезагрузка восстанавливает цепочку заполненных звеньев; пустые звенья при
|
||||
этом исчезают.
|
||||
9. Быстрые правки подряд — один финальный пересчёт всей вертикали.
|
||||
10. `/list-tools` открывает старый каталог по категориям; прямые адреса `/tools/<id>` работают.
|
||||
10. `/list-tools` открывает старый каталог по категориям; прямые адреса
|
||||
`/tools/<id>` работают.
|
||||
|
||||
## 7. Критерии готовности
|
||||
|
||||
- Работа начинается с главной: поиск → инструмент → цепочка звеньями, каждый со своим превью и скачиванием.
|
||||
- Отдельной страницы рабочего пространства нет; каталог доступен на `/list-tools`; прямые ссылки инструментов работают.
|
||||
- Работа начинается с главной: поиск → инструмент → цепочка звеньями, каждый со
|
||||
своим превью и скачиванием.
|
||||
- Отдельной страницы рабочего пространства нет; каталог доступен на
|
||||
`/list-tools`; прямые ссылки инструментов работают.
|
||||
- Пустые звенья не попадают в сохранение и не ломают прогон.
|
||||
- `test/check/build` зелёные, смоук §6 пройден.
|
||||
|
||||
## 8. Что сознательно не делаем
|
||||
|
||||
- Ветвления и прицепывание от середины цепи — новое звено всегда добавляется в конец.
|
||||
- Ветвления и прицепывание от середины цепи — новое звено всегда добавляется в
|
||||
конец.
|
||||
- Пипетка и маски внутри звеньев цепи — они остаются фичей базового инструмента.
|
||||
- Экспорт и импорт цепочки файлом — отложено до появления CLI/wasm-части.
|
||||
- Несколько параллельных инструментов на главной одновременно — один активный блок за раз.
|
||||
- Несколько параллельных инструментов на главной одновременно — один активный
|
||||
блок за раз.
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
> (`web/e2e/`, `pnpm --dir web test:e2e`). Автоматизированная часть закрывает
|
||||
> «нет падений/ошибок»; здесь — то, что руками, и что Playwright не покрывает.
|
||||
>
|
||||
> Перед прогоном: `pnpm --dir web build`, поднять локально `web/` (dev или
|
||||
> build → `node scripts/serve-static.mjs`).
|
||||
> Перед прогоном: `pnpm --dir web build`, поднять локально `web/` (dev или build
|
||||
> → `node scripts/serve-static.mjs`).
|
||||
>
|
||||
> Критерий приёмки большинства пунктов: **нет зависит, нет «залипания»,
|
||||
> сообщения об ошибках человекочитаемые**.
|
||||
@@ -25,8 +25,8 @@
|
||||
|
||||
## B. Края параметров
|
||||
|
||||
- [ ] Экстремальные значения слайдеров (0 и max) в blur/sharpen/pixelate —
|
||||
не «залипает» running.
|
||||
- [ ] Экстремальные значения слайдеров (0 и max) в blur/sharpen/pixelate — не
|
||||
«залипает» running.
|
||||
- [ ] Пустые/NaN поля (напр. width/height) — санитизация (silent repair), нет
|
||||
красного alert-текста в виде `errors.*`.
|
||||
- [ ] Невалидные цвета в плашках/градиентах (мисс-спелл `#xyz`, короткие hex,
|
||||
@@ -37,17 +37,17 @@
|
||||
|
||||
## C. Буфер обмена и drag-n-drop
|
||||
|
||||
- [ ] В реальном браузере кнопка copy на текстовом результате (png-to-base64
|
||||
и т.п.) кладёт текст в буфер (в Playwright не тестируем из-за permissions).
|
||||
- [ ] Drag-n-drop файла на dropzone страницы инструмента (не только
|
||||
файл-диалог) — UX-ощущения и корректность.
|
||||
- [ ] В реальном браузере кнопка copy на текстовом результате (png-to-base64 и
|
||||
т.п.) кладёт текст в буфер (в Playwright не тестируем из-за permissions).
|
||||
- [ ] Drag-n-drop файла на dropzone страницы инструмента (не только файл-диалог)
|
||||
— UX-ощущения и корректность.
|
||||
|
||||
## D. Производительность и отзывчивость
|
||||
|
||||
- [ ] Долгие авто-рераны на больших картинках (jpeg-artifacts, dithering,
|
||||
quantize на 4К) — UI не фризит, канселяция/дебаунс срабатывает.
|
||||
- [ ] Скролл страницы при длинных настройках (например, text-инструменты) —
|
||||
нет залипаний, панели не прыгают.
|
||||
- [ ] Скролл страницы при длинных настройках (например, text-инструменты) — нет
|
||||
залипаний, панели не прыгают.
|
||||
- [ ] Вкладка не «съедает» память при 10+ повторах blur/sharpen на мегапикселе
|
||||
(утечек blob-URL/bitmap быть не должно).
|
||||
|
||||
|
||||
+195
-181
@@ -1,40 +1,41 @@
|
||||
# План: переезд на новый дизайн (refs) — параллельная сборка
|
||||
|
||||
> Статус: план к выполнению. Шаги 5–6 (C17–C21) остаются **следующим этапом**.
|
||||
> Они больше не привязаны к «верности против рефа»: `docs/archive/plan-design-fix.md`
|
||||
> закрыт (2026-09-07, дизайн ушёл вперёд от `refs-html/`), аудит против рефа
|
||||
> не информативен и прекращён. Дальнейшее качество — ручное ревью + `lint:all`.
|
||||
> Шаг 37 (переезд старых файлов в папки `old/`) вынесен в `backlog.md`.
|
||||
> Они больше не привязаны к «верности против рефа»:
|
||||
> `docs/archive/plan-design-fix.md` закрыт (2026-09-07, дизайн ушёл вперёд от
|
||||
> `refs-html/`), аудит против рефа не информативен и прекращён. Дальнейшее
|
||||
> качество — ручное ревью + `lint:all`. Шаг 37 (переезд старых файлов в папки
|
||||
> `old/`) вынесен в `backlog.md`.
|
||||
>
|
||||
> Источники: `refs/` — Next/React-референс (правда по пикселям и интерактиву),
|
||||
> `refs-html/` — статические HTML-снимки для быстрого просмотра в браузере.
|
||||
>
|
||||
> **Стратегия изменена.** Вместо in-place рестайла (старый план A–F, правка
|
||||
> общих компонентов на месте с алиасами токенов) — **параллельная сборка**:
|
||||
> новый дизайн строится отдельно, изолируется от старого, проверяется
|
||||
> целиком, затем становится основным и старый удаляется. Старое приложение
|
||||
> остаётся рабочим и шippable на всём протяжении перехода.
|
||||
> новый дизайн строится отдельно, изолируется от старого, проверяется целиком,
|
||||
> затем становится основным и старый удаляется. Старое приложение остаётся
|
||||
> рабочим и шippable на всём протяжении перехода.
|
||||
|
||||
## 0. Почему меняем стратегию
|
||||
|
||||
- **Старый план (A–F):** правим `ui/*` и страницы на месте, держим старые
|
||||
токены как алиасы, чтобы ничего не сломалось до этапа F. Риск: общие
|
||||
компоненты редактируются вживую → регресс старого UI во время работы,
|
||||
новый дизайн нельзя принять изолированно.
|
||||
- **Новый подход (5 фаз):** (1) строим новый ui-kit, (2) строим новые
|
||||
страницы рядом, не трогая старые, (3) проверяем новый дизайн, (4) делаем
|
||||
его основным, (5) удаляем старый. Ниже фазы 1–5 — это требуемые шаги.
|
||||
- **Старый план (A–F):** правим `ui/*` и страницы на месте, держим старые токены
|
||||
как алиасы, чтобы ничего не сломалось до этапа F. Риск: общие компоненты
|
||||
редактируются вживую → регресс старого UI во время работы, новый дизайн нельзя
|
||||
принять изолированно.
|
||||
- **Новый подход (5 фаз):** (1) строим новый ui-kit, (2) строим новые страницы
|
||||
рядом, не трогая старые, (3) проверяем новый дизайн, (4) делаем его основным,
|
||||
(5) удаляем старый. Ниже фазы 1–5 — это требуемые шаги.
|
||||
- **Главный технический риск и его решение.** Сейчас `web/src/app.css`
|
||||
импортируется в **корневом** `web/src/routes/+layout.svelte:2` и потому
|
||||
грузится на ВСЕХ маршрутах, включая новые. Решение — **не префиксовать
|
||||
новый CSS (никакого `.ds2`), а разнести глобальные стили по layout'ам без
|
||||
наследования**: (а) убрать `import '../app.css'` из корневого layout;
|
||||
(б) старые маршруты обернуть группой `(old)/` с собственным layout,
|
||||
импортирующим `app.css`; (в) новые маршруты (`preview/*`) — собственный
|
||||
layout, импортирующий `design2.css`. SvelteKit делает code-splitting CSS
|
||||
по маршрутам, а стили компонентов скоупятся сами — поэтому глобальные
|
||||
стили каждого дизайна живут только в своей ветке, и префикс-обёртка
|
||||
`.ds2` не нужна. Старые `ui/` и страницы не трогаются до фазы 5.
|
||||
грузится на ВСЕХ маршрутах, включая новые. Решение — **не префиксовать новый
|
||||
CSS (никакого `.ds2`), а разнести глобальные стили по layout'ам без
|
||||
наследования**: (а) убрать `import '../app.css'` из корневого layout; (б)
|
||||
старые маршруты обернуть группой `(old)/` с собственным layout, импортирующим
|
||||
`app.css`; (в) новые маршруты (`preview/*`) — собственный layout,
|
||||
импортирующий `design2.css`. SvelteKit делает code-splitting CSS по маршрутам,
|
||||
а стили компонентов скоупятся сами — поэтому глобальные стили каждого дизайна
|
||||
живут только в своей ветке, и префикс-обёртка `.ds2` не нужна. Старые `ui/` и
|
||||
страницы не трогаются до фазы 5.
|
||||
|
||||
## 1. Источники и их оценка
|
||||
|
||||
@@ -57,10 +58,11 @@
|
||||
`panel-heading`, `setting-group`, `settings-footer`, `settings-panel`,
|
||||
`segmented`, `status`, `step-card` (`step-index`/`step-heading`/`step-type`/
|
||||
`step-body`/`drag`), `color-field` (`swatch`/`native-color`),
|
||||
`preview-toolbar`/`checker` (`tile-canvas`/`remover-canvas`/`comparison-grid`),
|
||||
`result-meta`/`meta-row`, `tool-card`/`image-card` (`tool-icon`/`tool-index`/
|
||||
`tool-copy`), `code-block`, `icon-btn`, `download-btn`, `reset-btn`, `lede`,
|
||||
`auto-note`, `label`, `toggle`/`toggle-group`, `direction-grid`, `gradient-bar`.
|
||||
`preview-toolbar`/`checker`
|
||||
(`tile-canvas`/`remover-canvas`/`comparison-grid`), `result-meta`/`meta-row`,
|
||||
`tool-card`/`image-card` (`tool-icon`/`tool-index`/ `tool-copy`),
|
||||
`code-block`, `icon-btn`, `download-btn`, `reset-btn`, `lede`, `auto-note`,
|
||||
`label`, `toggle`/`toggle-group`, `direction-grid`, `gradient-bar`.
|
||||
- **Дыры в refs-html:** нет состояний empty/error/loading, нет глобальных
|
||||
модалок/тултипов, i18n не представлен (все лейблы — английские брендовые).
|
||||
Решения — см. §9.
|
||||
@@ -68,9 +70,9 @@
|
||||
## 2. Токены (фундамент нового дизайна)
|
||||
|
||||
Живут в `src/lib/styles/design2.css` как обычный `:root { … }` (плюс
|
||||
`[data-theme='dark'] { … }`). Файл грузится **только** layout'ом новой
|
||||
ветки (§5), поэтому глобально не конфликтует со старым `app.css`. Маппинг
|
||||
старых токенов `web/src/app.css` → новые:
|
||||
`[data-theme='dark'] { … }`). Файл грузится **только** layout'ом новой ветки
|
||||
(§5), поэтому глобально не конфликтует со старым `app.css`. Маппинг старых
|
||||
токенов `web/src/app.css` → новые:
|
||||
|
||||
| Старый | Новый | Light | Dark |
|
||||
| -------------- | -------------- | --------- | ------------------------ |
|
||||
@@ -90,45 +92,45 @@
|
||||
- Радиусы: один `--radius: 4px` (мягкость старой темы уходит).
|
||||
- Шрифты: `IBM Plex Sans` + `IBM Plex Mono`, self-host через `@fontsource`
|
||||
(offline-first, CDN нельзя). Mono — рабочий шрифт для лейблов/метаданных/
|
||||
цифр; Sans — заголовки и текст. Задаются на `.app-shell` (корень новой
|
||||
ветки), чтобы не зависеть от старых глобальных правил `body`.
|
||||
- Фон-сетка (32px blueprint-grid) и шахматка канвасов — утилиты `.page-grid`
|
||||
и `.checker-canvas` внутри `.ds2`, не токены.
|
||||
- Тёмная тема — на `[data-theme='dark']` (инлайн-скрипт уже есть в
|
||||
`app.html`); класс `.dark-mode` из рефа не переносим, используем наш
|
||||
механизм. На переходный период **никаких алиасов старых токенов** (старые
|
||||
страницы сами живут на своём `app.css`).
|
||||
цифр; Sans — заголовки и текст. Задаются на `.app-shell` (корень новой ветки),
|
||||
чтобы не зависеть от старых глобальных правил `body`.
|
||||
- Фон-сетка (32px blueprint-grid) и шахматка канвасов — утилиты `.page-grid` и
|
||||
`.checker-canvas` внутри `.ds2`, не токены.
|
||||
- Тёмная тема — на `[data-theme='dark']` (инлайн-скрипт уже есть в `app.html`);
|
||||
класс `.dark-mode` из рефа не переносим, используем наш механизм. На
|
||||
переходный период **никаких алиасов старых токенов** (старые страницы сами
|
||||
живут на своём `app.css`).
|
||||
|
||||
## 3. Базовые примитивы дизайн-системы (все — компоненты)
|
||||
|
||||
Правило фазы 1: **любой повторяющийся паттерн — компонент**, даже «просто
|
||||
div с двумя стилями». Никакой копипасты классов между страницами. Каждый
|
||||
примитив ниже — отдельный `.svelte` в `src/lib/components/kit/`.
|
||||
Правило фазы 1: **любой повторяющийся паттерн — компонент**, даже «просто div с
|
||||
двумя стилями». Никакой копипасты классов между страницами. Каждый примитив ниже
|
||||
— отдельный `.svelte` в `src/lib/components/kit/`.
|
||||
|
||||
- `MonoLabel` — микро-лейбл: mono 10px, uppercase, letter-spacing .1em,
|
||||
muted; слот-модификатор accent (синие eyebrow/version).
|
||||
- `StatusDot` / `StatusLine` — точка `--success` + подпись
|
||||
(«AUTO PIPELINE», «LIVE PREVIEW»).
|
||||
- `MonoLabel` — микро-лейбл: mono 10px, uppercase, letter-spacing .1em, muted;
|
||||
слот-модификатор accent (синие eyebrow/version).
|
||||
- `StatusDot` / `StatusLine` — точка `--success` + подпись («AUTO PIPELINE»,
|
||||
«LIVE PREVIEW»).
|
||||
- `Panel` / `PanelHeading` / `SettingGroup` / `SettingsFooter` — панель с
|
||||
1px-бордером, составные части с разделителями.
|
||||
- `CheckerCanvas` — рабочая поверхность превью: тёмная подложка + шахматка.
|
||||
- `MetaRow` / `MetaList` — пары «CAPTION значение».
|
||||
- `Segmented` — сегмент-переключатель (язык, тип градиента, пресеты).
|
||||
- `Badge` — тег типа шага (BACKGROUND/TRANSFORM/STYLE), чип «✓ AUTO».
|
||||
- `StepCard` — нумерованная карточка этапа: индекс `01`, drag-handle,
|
||||
Badge типа, заголовок, кнопка удаления; слот для параметров.
|
||||
- `StepCard` — нумерованная карточка этапа: индекс `01`, drag-handle, Badge
|
||||
типа, заголовок, кнопка удаления; слот для параметров.
|
||||
- `ToolCard` — карточка каталога (icon, index, title, copy, arrow).
|
||||
- `ImageCard` / `PreviewTile` — label сверху + canvas-превью.
|
||||
- `AppShell` — корневой контейнер `.app-shell` + `.page-grid` (обёртка
|
||||
новой ветки маршрутов); задаёт шрифты и фон нового дизайна.
|
||||
- `AppShell` — корневой контейнер `.app-shell` + `.page-grid` (обёртка новой
|
||||
ветки маршрутов); задаёт шрифты и фон нового дизайна.
|
||||
- `BlueprintGrid` — фоновая сетка (используется в `AppShell`).
|
||||
- `IconButton`, `Icon` (обёртка `lucide-svelte`, 16–17px, muted).
|
||||
|
||||
## 4. Минимальный UI-kit (`src/lib/components/kit/`)
|
||||
|
||||
Контролы, поверх примитивов §3. API проектируем сразу под новый дизайн
|
||||
(обратной совместимости со старым `ui/` не нужно — старые страницы его
|
||||
продолжают использовать).
|
||||
Контролы, поверх примитивов §3. API проектируем сразу под новый дизайн (обратной
|
||||
совместимости со старым `ui/` не нужно — старые страницы его продолжают
|
||||
использовать).
|
||||
|
||||
| Компонент | Назначение |
|
||||
| ------------------------- | ----------------------------------------------------------------- |
|
||||
@@ -156,33 +158,33 @@ div с двумя стилями». Никакой копипасты класс
|
||||
- **Корневой layout `src/routes/+layout.svelte`** перестаёт импортировать
|
||||
`../app.css`. В нём остаётся только общая логика (init темы/locale) и
|
||||
нейтральный `<div>{children}</div>` без глобальных стилей дизайна.
|
||||
- **Старая ветка `src/routes/(old)/+layout.svelte`** импортирует `app.css`
|
||||
и содержит текущую шапку/подвал (разметку из нынешнего корневого layout).
|
||||
Все старые страницы переносятся внутрь `(old)/` (группа в скобках не
|
||||
меняет URL): `(old)/+page.svelte`, `(old)/demo/...`,
|
||||
`(old)/list-tools/...`, `(old)/tools/[id]/...`.
|
||||
- **Новая ветка `src/routes/preview/+layout.svelte`** импортирует
|
||||
`design2.css` и рендерит `<AppShell>`. Сюда же — новые страницы (§6).
|
||||
- Благодаря code-splitting CSS в SvelteKit: на старых маршрутах грузится
|
||||
только `app.css`, на `preview/*` — только `design2.css`. Скоуп стилей
|
||||
- **Старая ветка `src/routes/(old)/+layout.svelte`** импортирует `app.css` и
|
||||
содержит текущую шапку/подвал (разметку из нынешнего корневого layout). Все
|
||||
старые страницы переносятся внутрь `(old)/` (группа в скобках не меняет URL):
|
||||
`(old)/+page.svelte`, `(old)/demo/...`, `(old)/list-tools/...`,
|
||||
`(old)/tools/[id]/...`.
|
||||
- **Новая ветка `src/routes/preview/+layout.svelte`** импортирует `design2.css`
|
||||
и рендерит `<AppShell>`. Сюда же — новые страницы (§6).
|
||||
- Благодаря code-splitting CSS в SvelteKit: на старых маршрутах грузится только
|
||||
`app.css`, на `preview/*` — только `design2.css`. Скоуп стилей
|
||||
Svelte-компонентов добавляет второй уровень изоляции. Никаких `.ds2`.
|
||||
- Переключатель для ручной проверки: временный флаг `previewDesign` в
|
||||
`localStorage` + кнопка в старой шапке («Preview v2»), ведущая на
|
||||
`/preview`. На фазе 4 флаг убираем.
|
||||
`localStorage` + кнопка в старой шапке («Preview v2»), ведущая на `/preview`.
|
||||
На фазе 4 флаг убираем.
|
||||
|
||||
> Почему не `(new)`-группа с теми же путями вместо `preview/*`: две группы
|
||||
> не могут одновременно определять один и тот же URL (`(old)/+page.svelte`
|
||||
> и `(new)/+page.svelte` оба → `/` = конфликт маршрутов). Поэтому новая
|
||||
> ветка живёт под префиксом `preview/`, а на фазе 4 файлы переносятся на
|
||||
> реальные пути и группа `(old)` удаляется.
|
||||
> Почему не `(new)`-группа с теми же путями вместо `preview/*`: две группы не
|
||||
> могут одновременно определять один и тот же URL (`(old)/+page.svelte` и
|
||||
> `(new)/+page.svelte` оба → `/` = конфликт маршрутов). Поэтому новая ветка
|
||||
> живёт под префиксом `preview/`, а на фазе 4 файлы переносятся на реальные пути
|
||||
> и группа `(old)` удаляется.
|
||||
>
|
||||
> Это совпадает с рекомендованным в документации SvelteKit паттерном:
|
||||
> «put your entire app inside one or more groups except the routes that
|
||||
> should not inherit the common layouts» — старое целиком в `(old)`, новое
|
||||
> снаружи. Синтаксис `@`-breakout (`+layout@.svelte`, `+page@.svelte`) здесь
|
||||
> не требуется: «выломаться» можно лишь до корня, а нам нужно, чтобы именно
|
||||
> у корня НЕ было `app.css`; если позже захотим держать новые маршруты внутри
|
||||
> группы, `@` пригодится для сброса родительского layout.
|
||||
> Это совпадает с рекомендованным в документации SvelteKit паттерном: «put your
|
||||
> entire app inside one or more groups except the routes that should not inherit
|
||||
> the common layouts» — старое целиком в `(old)`, новое снаружи. Синтаксис
|
||||
> `@`-breakout (`+layout@.svelte`, `+page@.svelte`) здесь не требуется:
|
||||
> «выломаться» можно лишь до корня, а нам нужно, чтобы именно у корня НЕ было
|
||||
> `app.css`; если позже захотим держать новые маршруты внутри группы, `@`
|
||||
> пригодится для сброса родительского layout.
|
||||
|
||||
## 6. Маршрутизация параллельных страниц
|
||||
|
||||
@@ -202,55 +204,50 @@ src/routes/preview/list-tools/+page.svelte
|
||||
src/routes/preview/tools/[id]/+page.svelte
|
||||
```
|
||||
|
||||
- Каждая новая страница импортирует **только** `kit/*`. Старые страницы
|
||||
не изменяются (только перемещены в `(old)/`).
|
||||
- Фаза 4: содержимое `preview/*` переносится на реальные маршруты
|
||||
(move-файлы), группа `(old)/` удаляется, `design2.css` подключается в
|
||||
корневом `+layout.svelte` (как новый основной слой), временный флаг/кнопка
|
||||
убираются.
|
||||
- Каждая новая страница импортирует **только** `kit/*`. Старые страницы не
|
||||
изменяются (только перемещены в `(old)/`).
|
||||
- Фаза 4: содержимое `preview/*` переносится на реальные маршруты (move-файлы),
|
||||
группа `(old)/` удаляется, `design2.css` подключается в корневом
|
||||
`+layout.svelte` (как новый основной слой), временный флаг/кнопка убираются.
|
||||
|
||||
## 7. Фазы выполнения (1–5 по запросу)
|
||||
|
||||
Каждая фаза заканчивается зелёными `test`/`check`/`build` и ревью в браузере
|
||||
(обе темы) против `refs-html/`.
|
||||
|
||||
- **Фаза 1 — Фундамент + UI-kit.** Токены §2, `design2.css` по §5, все
|
||||
примитивы §3 и контролы §4 как отдельные компоненты. Критерий:
|
||||
демо-страница kit'а (`/preview` или временная) показывает все состояния
|
||||
(light/dark, hover/disabled, все варианты Button/Segmented/Badge);
|
||||
unit-тесты форм не падают; **старые страницы визуально не изменились**
|
||||
(изоляция работает).
|
||||
- **Фаза 1 — Фундамент + UI-kit.** Токены §2, `design2.css` по §5, все примитивы
|
||||
§3 и контролы §4 как отдельные компоненты. Критерий: демо-страница kit'а
|
||||
(`/preview` или временная) показывает все состояния (light/dark,
|
||||
hover/disabled, все варианты Button/Segmented/Badge); unit-тесты форм не
|
||||
падают; **старые страницы визуально не изменились** (изоляция работает).
|
||||
|
||||
- **Фаза 2 — Новые страницы.** Под `preview/*` (§6): воркспейс, demo,
|
||||
list-tools, tool-page (двухпанельная раскладка из рефа: settings-panel +
|
||||
preview-panel, step-cards для chain). Критерий: разметка соответствует
|
||||
`refs-html/index.html`, `gradient.html`, `background-remover.html`,
|
||||
`list-tools.html`; интерактив (слайдеры/тоглы/загрузка/автозапуск) работает
|
||||
в новом стиле; старые маршруты продолжают работать как раньше.
|
||||
`list-tools.html`; интерактив (слайдеры/тоглы/загрузка/автозапуск) работает в
|
||||
новом стиле; старые маршруты продолжают работать как раньше.
|
||||
|
||||
- **Фазa 3 — Проверка нового дизайна.** Смоук §8 (обе темы, узкий экран,
|
||||
800px), скриншоты против `refs-html/`, прогон старого тест-сьюта для
|
||||
доказательства отсутствия регресса, RU/EN-локализация новых лейблов.
|
||||
Критерий: визуальное совпадение с рефом, зелёные тесты, записанные
|
||||
скриншоты приняты.
|
||||
- **Фазa 3 — Проверка нового дизайна.** Смоук §8 (обе темы, узкий экран, 800px),
|
||||
скриншоты против `refs-html/`, прогон старого тест-сьюта для доказательства
|
||||
отсутствия регресса, RU/EN-локализация новых лейблов. Критерий: визуальное
|
||||
совпадение с рефом, зелёные тесты, записанные скриншоты приняты.
|
||||
|
||||
- **Фаза 4 — Сделать новый дизайн основным.** Перенос `preview/*` →
|
||||
реальные маршруты; `design2.css` подключается в корневом `+layout.svelte`;
|
||||
удаление preview-группы и временного флага/кнопки. Старые страницы теперь
|
||||
не используются (мёртвый код), но физически ещё в репо.
|
||||
Критерий: реальные маршруты отдают новый дизайн, тесты зелёные, билд без
|
||||
предупреждений.
|
||||
- **Фаза 4 — Сделать новый дизайн основным.** Перенос `preview/*` → реальные
|
||||
маршруты; `design2.css` подключается в корневом `+layout.svelte`; удаление
|
||||
preview-группы и временного флага/кнопки. Старые страницы теперь не
|
||||
используются (мёртвый код), но физически ещё в репо. Критерий: реальные
|
||||
маршруты отдают новый дизайн, тесты зелёные, билд без предупреждений.
|
||||
|
||||
- **Фаза 5 — Удалить старый дизайн.** Удаление старых страниц, старого
|
||||
`ui/`, старого `app.css` (и его токенов), неиспользуемых классов;
|
||||
переименование `kit/` → `ui/` (финальное имя) либо оставление `kit/` как
|
||||
основного — решается на фазе 1. Финальный проход по брейкпоинтам
|
||||
1200/1100/800/480. Критерий: `grep` по старым токенам/классам пуст,
|
||||
build чистый, старый дизайн нигде не упоминается.
|
||||
- **Фаза 5 — Удалить старый дизайн.** Удаление старых страниц, старого `ui/`,
|
||||
старого `app.css` (и его токенов), неиспользуемых классов; переименование
|
||||
`kit/` → `ui/` (финальное имя) либо оставление `kit/` как основного — решается
|
||||
на фазе 1. Финальный проход по брейкпоинтам 1200/1100/800/480. Критерий:
|
||||
`grep` по старым токенам/классам пуст, build чистый, старый дизайн нигде не
|
||||
упоминается.
|
||||
|
||||
Оценка объёма: Фаза 1 — фундамент+kit (~1 день), Фаза 2 — страницы
|
||||
(~1–2 дня), Фаза 3 — проверка (~полдня), Фаза 4–5 — переключение и чистка
|
||||
(~полдня).
|
||||
Оценка объёма: Фаза 1 — фундамент+kit (~1 день), Фаза 2 — страницы (~1–2 дня),
|
||||
Фаза 3 — проверка (~полдня), Фаза 4–5 — переключение и чистка (~полдня).
|
||||
|
||||
## 8. Смоук (после фазы 3, контроль на фазе 4)
|
||||
|
||||
@@ -262,35 +259,34 @@ src/routes/preview/tools/[id]/+page.svelte
|
||||
шахматке.
|
||||
4. Chain из 3+ инструментов: step-cards рендерятся, удаление/сброс работают.
|
||||
5. Узкий экран 800px: настройки над превью, sticky выключается как в рефе.
|
||||
6. RU/EN: новые лейблы локализованы; mono-лейблы рефа — либо ключи i18n,
|
||||
либо осознанно английские брендовые элементы (см. §9).
|
||||
6. RU/EN: новые лейблы локализованы; mono-лейблы рефа — либо ключи i18n, либо
|
||||
осознанно английские брендовые элементы (см. §9).
|
||||
|
||||
## 9. Открытые вопросы
|
||||
|
||||
- **Имя финального каталога компонентов:** оставить `kit/` как основное имя
|
||||
или переименовать в `ui/` на фазе 5 (удалив старое `ui/`). Рекомендую
|
||||
сразу писать в `kit/`, на фазе 5 переименовать в `ui/`. Решить на фазе 1.
|
||||
- **Имя финального каталога компонентов:** оставить `kit/` как основное имя или
|
||||
переименовать в `ui/` на фазе 5 (удалив старое `ui/`). Рекомендую сразу писать
|
||||
в `kit/`, на фазе 5 переименовать в `ui/`. Решить на фазе 1.
|
||||
- **Изоляция через layout:** `app.css` вынесен из корневого layout в
|
||||
`(old)/+layout.svelte`; новый `design2.css` — только в `preview/+layout`.
|
||||
Проверить на фазе 1, что на `/preview/*` не попадают стили старого
|
||||
дизайна (DevTools → вкладка Styles, нет правил из `app.css`).
|
||||
- **Переключатель для проверки:** рекомендую `localStorage`-флаг +
|
||||
кнопка «Preview v2» в старой шапке + группа `preview/*`. Убрать на фазе 4.
|
||||
Проверить на фазе 1, что на `/preview/*` не попадают стили старого дизайна
|
||||
(DevTools → вкладка Styles, нет правил из `app.css`).
|
||||
- **Переключатель для проверки:** рекомендую `localStorage`-флаг + кнопка
|
||||
«Preview v2» в старой шапке + группа `preview/*`. Убрать на фазе 4.
|
||||
- **Mono-лейблы рефа на английском** (`PROCESSING PIPELINE`): завести i18n-
|
||||
ключи, но допустить часть английских брендовых («приборная панель»).
|
||||
- **Версия в шапке/футере:** из `package.json` через `define` или
|
||||
захардкодить — мелочь, решить при сборке topbar.
|
||||
- **`refs/` и `refs-html/` — одноразовый источник, НЕ поддерживаемый.**
|
||||
После внедрения нового дизайна все правки дизайна делаются сразу в
|
||||
продакшен-коде (`design2.css` + `kit/`), а не в `refs/`. Механизм
|
||||
синхронизации «ref → код» не нужен и не создаётся. `refs/` и `refs-html/`
|
||||
можно удалить из репо на фазе 5 (по желанию), они не являются частью
|
||||
рантайма.
|
||||
- **Версия в шапке/футере:** из `package.json` через `define` или захардкодить —
|
||||
мелочь, решить при сборке topbar.
|
||||
- **`refs/` и `refs-html/` — одноразовый источник, НЕ поддерживаемый.** После
|
||||
внедрения нового дизайна все правки дизайна делаются сразу в продакшен-коде
|
||||
(`design2.css` + `kit/`), а не в `refs/`. Механизм синхронизации «ref → код»
|
||||
не нужен и не создаётся. `refs/` и `refs-html/` можно удалить из репо на фазе
|
||||
5 (по желанию), они не являются частью рантайма.
|
||||
|
||||
## 10. План исполнения (мелкие коммиты)
|
||||
|
||||
Ветка `feat/redesign`. Каждый шаг — отдельный коммит, цель < ~500 строк.
|
||||
Перед стартом: `test`/`build` зелёные на базе (якорь регрессии).
|
||||
Ветка `feat/redesign`. Каждый шаг — отдельный коммит, цель < ~500 строк. Перед
|
||||
стартом: `test`/`build` зелёные на базе (якорь регрессии).
|
||||
|
||||
### Шаг 0. Изоляция layout (без дизайна)
|
||||
|
||||
@@ -299,13 +295,15 @@ src/routes/preview/tools/[id]/+page.svelte
|
||||
`<div>{children}</div>`.
|
||||
- [x] **[C2]** Создать `routes/(old)/+layout.svelte` (импорт `app.css` + старая
|
||||
шапка/подвал) и перенести старые страницы (`+page`, `demo`, `list-tools`,
|
||||
`tools/[id]`) в `(old)/`. URL не меняются. Проверка: старый сайт идентичен.
|
||||
_(файлы — move, объём строк минимален)_
|
||||
`tools/[id]`) в `(old)/`. URL не меняются. Проверка: старый сайт
|
||||
идентичен. _(файлы — move, объём строк минимален)_
|
||||
|
||||
### Шаг 1. Фундамент нового дизайна
|
||||
|
||||
- [x] **[C3]** Создать `src/lib/styles/design2.css` (токены `:root` + `[data-theme='dark']` по §2) и подключить `@fontsource/ibm-plex-sans` + `@fontsource/ibm-plex-mono` (self-host, offline-first).
|
||||
подключить шрифты в `design2.css` (self-host, offline-first).
|
||||
- [x] **[C3]** Создать `src/lib/styles/design2.css` (токены `:root` +
|
||||
`[data-theme='dark']` по §2) и подключить `@fontsource/ibm-plex-sans` +
|
||||
`@fontsource/ibm-plex-mono` (self-host, offline-first). подключить шрифты
|
||||
в `design2.css` (self-host, offline-first).
|
||||
- [x] **[C4]** _(слит в C3: шрифты нельзя подключить до создания `design2.css`)_
|
||||
`[data-theme='dark']` (таблица §2). Без утилитарных классов.
|
||||
- [x] **[C5]** Примитивы batch 1: `AppShell`, `BlueprintGrid`, `CheckerCanvas`,
|
||||
@@ -320,87 +318,103 @@ src/routes/preview/tools/[id]/+page.svelte
|
||||
|
||||
### Шаг 2. Витрина kit (проверка компонентов)
|
||||
|
||||
- [x] **[C8]** `preview/+layout.svelte` (импорт `design2.css` + `<AppShell>` + переключатель темы) и
|
||||
временная `preview/kit` страница со всеми состояниями (light/dark,
|
||||
hover/disabled, варианты Button/Segmented/Badge и пр.)
|
||||
- [x] **[C8]** `preview/+layout.svelte` (импорт `design2.css` + `<AppShell>` +
|
||||
переключатель темы) и временная `preview/kit` страница со всеми
|
||||
состояниями (light/dark, hover/disabled, варианты Button/Segmented/Badge и
|
||||
пр.)
|
||||
|
||||
### Шаг 3. Новые страницы
|
||||
|
||||
- [x] **[C9]** `/preview` — воркспейс (eyebrow, h1, lede, поиск, «открыть последний»).
|
||||
Поиск фильтрует `TOOLS` из `$lib/registry`; карточки результата пока
|
||||
ведут на `#` (реальные `/preview/tools/[id]` подключим на C12, иначе
|
||||
prerender падает на 404).
|
||||
- [x] **[C9]** `/preview` — воркспейс (eyebrow, h1, lede, поиск, «открыть
|
||||
последний»). Поиск фильтрует `TOOLS` из `$lib/registry`; карточки
|
||||
результата пока ведут на `#` (реальные `/preview/tools/[id]` подключим на
|
||||
C12, иначе prerender падает на 404).
|
||||
- [x] **[C10]** `/preview/list-tools` — каталог карточек (группировка по
|
||||
`CATEGORIES`, `ToolCard`). Ссылки пока `#` (реальные `/preview/tools/[id]`
|
||||
подключим на C12).
|
||||
- [x] **[C11]** `/preview/demo` — зеркало воркспейса (4 шага + панель превью), статичный приёмочный стенд.
|
||||
- [x] **[C12]** `/preview/tools/[id]` — data-driven settings-panel + preview-panel (gradient live, остальные — плейсхолдер); `NumberField`, `entries()` для prerender, ToolCard-ссылки подключены.
|
||||
- [x] **[C13]** `/preview/tools/[id]` — comparison-grid для file-source инструментов (`Dropzone` + before/after `CheckerCanvas`, `result-meta`); охватывает background-remover и прочие.
|
||||
- [x] **[C14]** `/preview/tools/[id]` — исполняемая цепочка: `StepCard`-шаги (параметры inline) + «Add step» (chainable), живой прогон пайплайна на исходнике, превью каждого шага + итог + Download.
|
||||
- [x] **[C11]** `/preview/demo` — зеркало воркспейса (4 шага + панель превью),
|
||||
статичный приёмочный стенд.
|
||||
- [x] **[C12]** `/preview/tools/[id]` — data-driven settings-panel +
|
||||
preview-panel (gradient live, остальные — плейсхолдер); `NumberField`,
|
||||
`entries()` для prerender, ToolCard-ссылки подключены.
|
||||
- [x] **[C13]** `/preview/tools/[id]` — comparison-grid для file-source
|
||||
инструментов (`Dropzone` + before/after `CheckerCanvas`, `result-meta`);
|
||||
охватывает background-remover и прочие.
|
||||
- [x] **[C14]** `/preview/tools/[id]` — исполняемая цепочка: `StepCard`-шаги
|
||||
(параметры inline) + «Add step» (chainable), живой прогон пайплайна на
|
||||
исходнике, превью каждого шага + итог + Download.
|
||||
|
||||
### Шаг 4. Переключатель и проверка
|
||||
|
||||
- [x] **[C15]** В старой шапке ссылка «Preview v2» → `/preview`, ставит `localStorage['easy-png-tools:preview-v2']='1'`.
|
||||
- [x] **[C16]** База под дизайн: рабочий переключатель тем (persist в localStorage, применяется на `<html>`; проверено — фон `#eef1f4`↔`#11171d`), `TopBar` приведён к виду `demo.html` (бренд + синий хлебный крош `/* страница */`, безрамочные иконки, язык — active = `--foreground`/`--background`), страницы `/preview/*` на всю ширину (убран `max-width`). Точечное выравнивание к рефу отложено: реф-дизайн неконсистентен.
|
||||
(доказательство отсутствия регресса). Фиксы по результатам — отдельными
|
||||
мелкими коммитами.
|
||||
- [x] **[C15]** В старой шапке ссылка «Preview v2» → `/preview`, ставит
|
||||
`localStorage['easy-png-tools:preview-v2']='1'`.
|
||||
- [x] **[C16]** База под дизайн: рабочий переключатель тем (persist в
|
||||
localStorage, применяется на `<html>`; проверено — фон
|
||||
`#eef1f4`↔`#11171d`), `TopBar` приведён к виду `demo.html` (бренд + синий
|
||||
хлебный крош `/* страница */`, безрамочные иконки, язык — active =
|
||||
`--foreground`/`--background`), страницы `/preview/*` на всю ширину (убран
|
||||
`max-width`). Точечное выравнивание к рефу отложено: реф-дизайн
|
||||
неконсистентен. (доказательство отсутствия регресса). Фиксы по результатам
|
||||
— отдельными мелкими коммитами.
|
||||
|
||||
### Шаг 5. Сделать новый дизайн основным — СЛЕДУЮЩИЙ ЭТАП
|
||||
|
||||
> Раньше планировалось после «верности к рефу» (Этапы 0–1
|
||||
> `docs/archive/plan-design-fix.md`) — план закрыт, гейт снят (2026-09-07).
|
||||
> Перед стартом C17 выполнить переезд старых файлов в папки `old/`
|
||||
> (backlog: «Переезд старых файлов в `old/`», шаг 37 закрытого
|
||||
> `docs/archive/plan-composite-params.md`). Пока не трогаем `preview/*` и `(old)/`.
|
||||
> Перед стартом C17 выполнить переезд старых файлов в папки `old/` (backlog:
|
||||
> «Переезд старых файлов в `old/`», шаг 37 закрытого
|
||||
> `docs/archive/plan-composite-params.md`). Пока не трогаем `preview/*` и
|
||||
> `(old)/`.
|
||||
|
||||
- [ ] **[C17]** Перенести `preview/*` → реальные маршруты (move), удалить
|
||||
`(old)/`, корневой `+layout.svelte` импортирует `design2.css` + новый shell.
|
||||
Убрать флаг/кнопку. **И обновить `newCode` в `web/eslint.config.js`:** заменить
|
||||
`src/routes/preview/**` на `src/routes/**` (после переезда все маршруты —
|
||||
новый дизайн), компонентный glob `src/lib/components/kit/**` пока оставить.
|
||||
`(old)/`, корневой `+layout.svelte` импортирует `design2.css` + новый
|
||||
shell. Убрать флаг/кнопку. **И обновить `newCode` в
|
||||
`web/eslint.config.js`:** заменить `src/routes/preview/**` на
|
||||
`src/routes/**` (после переезда все маршруты — новый дизайн), компонентный
|
||||
glob `src/lib/components/kit/**` пока оставить.
|
||||
- [ ] **[C18]** Правки импортов/редиректов после переноса, проверка билда.
|
||||
|
||||
### Шаг 6. Удаление старого
|
||||
|
||||
- [ ] **[C19]** Удалить старое `ui/`, прочие старые компоненты, старый `app.css`,
|
||||
неиспользуемые классы. Переименовать `kit/` → `ui/` (если решено на C1 —
|
||||
тогда в `eslint.config.js` обновить и компонентный glob).
|
||||
- [ ] **[C19]** Удалить старое `ui/`, прочие старые компоненты, старый
|
||||
`app.css`, неиспользуемые классы. Переименовать `kit/` → `ui/` (если
|
||||
решено на C1 — тогда в `eslint.config.js` обновить и компонентный glob).
|
||||
**И раскрыть ESLint `recommended` на весь код** — убрать scoped-блок
|
||||
`newCode` в `web/eslint.config.js` (см. AGENTS.md), прогнать `lint` и
|
||||
починить всплывшие ошибки в оставшемся коде.
|
||||
_Примечание: доменные модули (`lib/core`, `lib/registry`, тесты и т.п.) —
|
||||
это не «старый дизайн», у них свой линт-долг; расширение `recommended` на
|
||||
них может выдать много ошибок. Решить на C19: либо чиним сразу, либо
|
||||
расширяем scoped только на `src/routes/**` + `src/lib/components/**`._
|
||||
починить всплывшие ошибки в оставшемся коде. _Примечание: доменные модули
|
||||
(`lib/core`, `lib/registry`, тесты и т.п.) — это не «старый дизайн», у них
|
||||
свой линт-долг; расширение `recommended` на них может выдать много ошибок.
|
||||
Решить на C19: либо чиним сразу, либо расширяем scoped только на
|
||||
`src/routes/**` + `src/lib/components/**`._
|
||||
- [ ] **[C20]** Финальный проход по брейкпоинтам 1200/1100/800/480; build без
|
||||
предупреждений; `grep` по старым токенам/классам пуст.
|
||||
- [ ] **[C21]** _(опционально)_ Удалить `refs/` и `refs-html/` из репо — они
|
||||
больше не источник правды.
|
||||
|
||||
> Если какой-то коммит тянет за собой > 500 строк (например, C6/C7),
|
||||
> разбивать на под-коммиты по 2–4 компонента. Тесты/билд прогонять после
|
||||
> каждого коммита, чтобы регрессия локализовалась одним шагом назад.
|
||||
> Если какой-то коммит тянет за собой > 500 строк (например, C6/C7), разбивать
|
||||
> на под-коммиты по 2–4 компонента. Тесты/билд прогонять после каждого коммита,
|
||||
> чтобы регрессия локализовалась одним шагом назад.
|
||||
>
|
||||
> **Атомарность и точки «поломки» между коммитами.**
|
||||
>
|
||||
> - C1+C2 уже сделаны вместе (связаны: нейтральный root без `(old)`-layout
|
||||
> ломает старый сайт). Аналогично связанных пар больше нет.
|
||||
> - C3 и C4 слиты: шрифты подключаются внутри `design2.css`, поэтому файл
|
||||
> должен существовать в том же коммите.
|
||||
> - Коммиты **C3→C16 не трогают старый сайт** `(old)/` — они только
|
||||
> добавляют новые файлы/маршруты, сломать старый сайт на них нельзя.
|
||||
> - C3 и C4 слиты: шрифты подключаются внутри `design2.css`, поэтому файл должен
|
||||
> существовать в том же коммите.
|
||||
> - Коммиты **C3→C16 не трогают старый сайт** `(old)/` — они только добавляют
|
||||
> новые файлы/маршруты, сломать старый сайт на них нельзя.
|
||||
> - **C17 — единственный атомарный переключатель**: перенос `preview/*` на
|
||||
> реальные пути + удаление `(old)/` + смена root-layout должны быть в
|
||||
> ОДНОМ коммите (иначе конфликт маршрутов или пропажа страниц). Не дробить.
|
||||
> - **C19 должен идти строго после C17** (старый `app.css`/`ui/` удаляем
|
||||
> только когда old-сайт уже выключен).
|
||||
> - Очерёдность зависимостей: `design2.css` (C3) и kit-компоненты (C5–C7)
|
||||
> должны существовать до страниц, которые их импортируют (C8–C14).
|
||||
> реальные пути + удаление `(old)/` + смена root-layout должны быть в ОДНОМ
|
||||
> коммите (иначе конфликт маршрутов или пропажа страниц). Не дробить.
|
||||
> - **C19 должен идти строго после C17** (старый `app.css`/`ui/` удаляем только
|
||||
> когда old-сайт уже выключен).
|
||||
> - Очерёдность зависимостей: `design2.css` (C3) и kit-компоненты (C5–C7) должны
|
||||
> существовать до страниц, которые их импортируют (C8–C14).
|
||||
|
||||
## Бэклог полировки (после C9–C21, отдельным проходом)
|
||||
|
||||
Конкретные расхождения витрины `preview/kit` с `refs-html/`. Править
|
||||
системно в конце, а не по ходу сборки базы.
|
||||
Конкретные расхождения витрины `preview/kit` с `refs-html/`. Править системно в
|
||||
конце, а не по ходу сборки базы.
|
||||
|
||||
- **Toggle** в дизайне прямоугольный, у нас — закруглённый. Сделать
|
||||
прямоугольным (или уточнить радиус у свича отдельно от общих токенов).
|
||||
@@ -408,11 +422,11 @@ src/routes/preview/tools/[id]/+page.svelte
|
||||
ненулевой `--radius` для всех бордеров. Проверить токен `--radius` против
|
||||
референсов; возможно он должен быть 0 (или близко), а скругления оставить
|
||||
только там, где они реально есть в дизайне.
|
||||
- **DownloadButton**: нет отступа между текстом и размером файла
|
||||
(`size`-чип). Добавить `gap`/отступ внутри кнопки.
|
||||
- **DownloadButton**: нет отступа между текстом и размером файла (`size`-чип).
|
||||
Добавить `gap`/отступ внутри кнопки.
|
||||
- **Поля ввода (controls)**: выглядят без паддингов (возможно артефакт самой
|
||||
витрины, но визуально плохо). Проверить паддинги в `TextField`/`SelectField`/
|
||||
`ColorField`/`SliderField` против референса и поправить.
|
||||
- **Segmented**: на витрине последняя кнопка растянута и нечитаемо, что это
|
||||
сегмент. Проверить, как `Segmented` выглядит в реальных раскладках
|
||||
(C9–C14); возможно нужен фиксированный/авто размер сегментов или перенос.
|
||||
сегмент. Проверить, как `Segmented` выглядит в реальных раскладках (C9–C14);
|
||||
возможно нужен фиксированный/авто размер сегментов или перенос.
|
||||
|
||||
+32
-32
@@ -1,12 +1,11 @@
|
||||
# План: закрытие техдолга перед переездом `old/` (C17)
|
||||
|
||||
> Статус: план к выполнению. Ветка `feat/redesign`.
|
||||
> Входит в гейт перед переездом старых файлов в `old/` (backlog №14; см. также
|
||||
> Статус: план к выполнению. Ветка `feat/redesign`. Входит в гейт перед
|
||||
> переездом старых файлов в `old/` (backlog №14; см. также
|
||||
> `archive/plan-migrate-remaining.md` — техдолг «чинить по заведённым задачам,
|
||||
> не игнорировать правилами»).
|
||||
> Целевое состояние по завершении: `svelte-check` → 0 errors, `pnpm test` →
|
||||
> зелёный, `lint:all` → без ошибок (допустим только задокументированный остаток),
|
||||
> дизайн-токены → единый словарь `preview.css`.
|
||||
> не игнорировать правилами»). Целевое состояние по завершении: `svelte-check` →
|
||||
> 0 errors, `pnpm test` → зелёный, `lint:all` → без ошибок (допустим только
|
||||
> задокументированный остаток), дизайн-токены → единый словарь `preview.css`.
|
||||
|
||||
## Состав техдолга (на 2026-09-07)
|
||||
|
||||
@@ -15,9 +14,9 @@
|
||||
svelte-check, висит на каждом прогоне.
|
||||
- **фейл теста** — `src/lib/palette.test.ts`: `ENOENT src/app.css` (файл
|
||||
разъехался на `old.css` + `design2.css` при редизайне). Тест проверял
|
||||
контрасты палитры по токенам старого `app.css` (`text/surface/bg/link/
|
||||
accent/danger/border`). При гейтах `pnpm test` красный, хотя 607 тестов
|
||||
проходят.
|
||||
контрасты палитры по токенам старого `app.css`
|
||||
(`text/surface/bg/link/ accent/danger/border`). При гейтах `pnpm test`
|
||||
красный, хотя 607 тестов проходят.
|
||||
- **design-tokens в `kit/**`** — ~335 ошибок `lint:all` от локального плагина
|
||||
`eslint-plugins/design-tokens` (захардкоженные цвета/размеры/длительности/
|
||||
z-index/`color-mix()` в `<style>`-блоках svelte), плюс stylelint по CSS.
|
||||
@@ -43,46 +42,47 @@
|
||||
|
||||
### Этап 2 — миграция `kit/**` на design-токены (main объём)
|
||||
|
||||
Цель — `lint:all` без ошибок design-tokens (все примитивы/`color-mix()`
|
||||
уходят в токены `preview.css`).
|
||||
Цель — `lint:all` без ошибок design-tokens (все примитивы/`color-mix()` уходят в
|
||||
токены `preview.css`).
|
||||
|
||||
**Вариант А (основной): авто-фикс одноразовым скриптом.**
|
||||
Писать скрипт ТОЛЬКО если он гарантированно ничего не испортит. Гарантии:
|
||||
**Вариант А (основной): авто-фикс одноразовым скриптом.** Писать скрипт ТОЛЬКО
|
||||
если он гарантированно ничего не испортит. Гарантии:
|
||||
|
||||
- Скрипт — одноразовый (`web/scripts/fix-design-tokens.mjs`), после прогона
|
||||
удаляется либо помечается неактуальным.
|
||||
- Строгая карта замен: примитив → токен **1:1, только если значение точно
|
||||
совпадает с уже существующим токеном словаря** `preview.css`
|
||||
(цвет → `--color-*`/`--brand-*`, радиус → `--radius-*`, размер →
|
||||
совпадает с уже существующим токеном словаря** `preview.css` (цвет →
|
||||
`--color-*`/`--brand-*`, радиус → `--radius-*`, размер →
|
||||
`--space-*`/`--size-*`, длительность → `--duration-*`, z-index → `--z-*`).
|
||||
Никаких новых токенов скрипт не генерирует.
|
||||
- Замены — только примитивы, не содержащие `var()`/`calc()`/`color-mix()`;
|
||||
исключения по правилам плагина (0, 0px, 1px, проценты, unitless) не
|
||||
трогаются. `color-mix()` → выносится в `preview.css` как производный
|
||||
`--color-*` токен (может требовать ручного решения имени).
|
||||
исключения по правилам плагина (0, 0px, 1px, проценты, unitless) не трогаются.
|
||||
`color-mix()` → выносится в `preview.css` как производный `--color-*` токен
|
||||
(может требовать ручного решения имени).
|
||||
- Скрипт обрабатывает только `src/lib/components/kit/**` и `src/preview.css`
|
||||
(scoped-пути плагина) и только `<style>`-блоки/`.css` — логику/разметку не
|
||||
меняет, только строки стилей.
|
||||
- После прогона: обязательный дифф-ревью (все изменения — замена значений на
|
||||
`var(--x)`, ничего удалено/переставлено), затем полный `lint:all` +
|
||||
`svelte-check` + `pnpm test`. Если хоть что-то вне «замена примитива на
|
||||
токен» — скрипт считается проваленным, откатываем и переходим на вариант Б.
|
||||
`svelte-check` + `pnpm test`. Если хоть что-то вне «замена примитива на токен»
|
||||
— скрипт считается проваленным, откатываем и переходим на вариант Б.
|
||||
|
||||
**Вариант Б (страховка): руками, пакетами по компонентам.**
|
||||
Если скрипт не удался — чистим/заводим токены вручную, пакетами по компонентам
|
||||
(1–2 на задачу, диф < ~500 строк). Отсутствующие токены (например
|
||||
`--space-2xl`/`--space-panel`, уже дважды обходились в SchemaTextSource)
|
||||
добавляем в словарь `preview.css` осознанно.
|
||||
**Вариант Б (страховка): руками, пакетами по компонентам.** Если скрипт не
|
||||
удался — чистим/заводим токены вручную, пакетами по компонентам (1–2 на задачу,
|
||||
диф < ~500 строк). Отсутствующие токены (например `--space-2xl`/`--space-panel`,
|
||||
уже дважды обходились в SchemaTextSource) добавляем в словарь `preview.css`
|
||||
осознанно.
|
||||
|
||||
Гейт этапа: `pnpm --dir web lint:all` — только допустимый остаток (неиспользуемые
|
||||
токены — варнинг), плюс регресс-прогон тестов и svelte-check.
|
||||
Гейт этапа: `pnpm --dir web lint:all` — только допустимый остаток
|
||||
(неиспользуемые токены — варнинг), плюс регресс-прогон тестов и svelte-check.
|
||||
|
||||
### Этап 3 — чистота словаря `preview.css`
|
||||
|
||||
- Разобрать варнинги неиспользуемых токенов (check-tokens): либо использовать,
|
||||
либо удалить из словаря.
|
||||
- После этапа 2 — повторный аудит токенов (`pnpm --dir web exec node
|
||||
scripts/check-tokens.mjs`): parity light/dark, hct-only, без мёртвых токенов.
|
||||
- После этапа 2 — повторный аудит токенов
|
||||
(`pnpm --dir web exec node scripts/check-tokens.mjs`): parity light/dark,
|
||||
hct-only, без мёртвых токенов.
|
||||
|
||||
### Этап 4 — финальные гейты (гейт переезда `old/`)
|
||||
|
||||
@@ -92,12 +92,12 @@
|
||||
4. `pnpm --dir web lint:all` → 0 (или только задокументированный остаток)
|
||||
5. Обе ветки открываются: старый UI `/`, новый `/preview`
|
||||
|
||||
После зеленых гейтов — закрытие задач и переход к `backlog` №14
|
||||
(переезд старых файлов в `old/`).
|
||||
После зеленых гейтов — закрытие задач и переход к `backlog` №14 (переезд старых
|
||||
файлов в `old/`).
|
||||
|
||||
## Порядок и коммиты
|
||||
|
||||
- Коммиты атомарные, < ~500 строк (AGENTS). По завершении каждой мини-задачи —
|
||||
ревью (агент не коммитит).
|
||||
- Техдолг чинится по заведённым задачам, правила линтеров не отключаются и не
|
||||
игнорируются.
|
||||
игнорируются.
|
||||
|
||||
+83
-37
@@ -1,23 +1,39 @@
|
||||
# План разработки: easy-png-tools
|
||||
|
||||
> **Статус (2026-09-07):** Фаза 1 (полноценный TS-сайт) в основном выполнена —
|
||||
> сайт живёт, каталог переведён в типизированный `registry-new` (121/125),
|
||||
> идёт редизайн на `preview/*` по `plan-redesign.md` (параллельная ветка,
|
||||
> старый UI на `(old)/`). Фазы 2–8 (эталоны, Rust/wasm, CLI, harness) —
|
||||
> будущие, разделам ниже не запущены.
|
||||
> сайт живёт, каталог переведён в типизированный `registry-new` (121/125), идёт
|
||||
> редизайн на `preview/*` по `plan-redesign.md` (параллельная ветка, старый UI
|
||||
> на `(old)/`). Фазы 2–8 (эталоны, Rust/wasm, CLI, harness) — будущие, разделам
|
||||
> ниже не запущены.
|
||||
|
||||
## 0. Решения
|
||||
|
||||
- **Набор PNG-утилит, всё в браузере** (SvelteKit + `adapter-static`), данные не покидают машину.
|
||||
- **Первый и главный приоритет — полностью рабочий TS-сайт**: максимум простых инструментов уровня EASY, UX/UI, пайплайны (несколько инструментов подряд), описания инструментов. Сайтом можно пользоваться уже после этой фазы.
|
||||
- **Никакого wasm/Rust на этом этапе.** TS-реализация на canvas + `ImageData` — эталон алгоритмов, из которого позже генерируются эталонные файлы для проверки Rust-порта. Rust-ядро, WASM и CLI — отдельные фазы позже, когда сайт живёт.
|
||||
- **Пайплайн = JSON-список шагов** `{tool, params}`. Формат закладывается сейчас и потом станет общим для браузера, wasm и CLI.
|
||||
- **Реестр инструментов** (id, title, description, схема параметров, `run`) — источник истины: из него генерируются страницы, формы, пайплайны. Позже — общий формат для Rust-парсера.
|
||||
- Валидация будущего Rust-порта — эталонные файлы, сгенерированные из TS-реализации. Сравнение: **бит-в-бит** для чистых пиксельных операций, **perceptual diff** для canvas-зависимых.
|
||||
- **Единая точка исполнения инструментов.** Страницы вызывают операции только через `web/src/lib/tools/executor.ts::executeStep` — нигде напрямую `tool.run`. Контракт уже асинхронный (`Promise<PixelImage>`), поэтому:
|
||||
- когда появятся тяжёлые MEDIUM-операции (свёртки, квантование), внутренности `executeStep` переезжают в Web Worker — воркер импортирует тот же чистый core как TS-фолбэк, алгоритмы не дублируются, вызывающий код не меняется;
|
||||
- в WASM-фазе тот же воркер принимает wasm-модуль и диспетчеризует по `toolId` — свитч A/B из фазы 5 сводится к замене реализации внутри исполнителя;
|
||||
- до появления MEDIUM-операций воркер не вводится: EASY-операции быстры и не блокируют UI.
|
||||
- **Набор PNG-утилит, всё в браузере** (SvelteKit + `adapter-static`), данные не
|
||||
покидают машину.
|
||||
- **Первый и главный приоритет — полностью рабочий TS-сайт**: максимум простых
|
||||
инструментов уровня EASY, UX/UI, пайплайны (несколько инструментов подряд),
|
||||
описания инструментов. Сайтом можно пользоваться уже после этой фазы.
|
||||
- **Никакого wasm/Rust на этом этапе.** TS-реализация на canvas + `ImageData` —
|
||||
эталон алгоритмов, из которого позже генерируются эталонные файлы для проверки
|
||||
Rust-порта. Rust-ядро, WASM и CLI — отдельные фазы позже, когда сайт живёт.
|
||||
- **Пайплайн = JSON-список шагов** `{tool, params}`. Формат закладывается сейчас
|
||||
и потом станет общим для браузера, wasm и CLI.
|
||||
- **Реестр инструментов** (id, title, description, схема параметров, `run`) —
|
||||
источник истины: из него генерируются страницы, формы, пайплайны. Позже —
|
||||
общий формат для Rust-парсера.
|
||||
- Валидация будущего Rust-порта — эталонные файлы, сгенерированные из
|
||||
TS-реализации. Сравнение: **бит-в-бит** для чистых пиксельных операций,
|
||||
**perceptual diff** для canvas-зависимых.
|
||||
- **Единая точка исполнения инструментов.** Страницы вызывают операции только
|
||||
через `web/src/lib/tools/executor.ts::executeStep` — нигде напрямую
|
||||
`tool.run`. Контракт уже асинхронный (`Promise<PixelImage>`), поэтому:
|
||||
- когда появятся тяжёлые MEDIUM-операции (свёртки, квантование), внутренности
|
||||
`executeStep` переезжают в Web Worker — воркер импортирует тот же чистый
|
||||
core как TS-фолбэк, алгоритмы не дублируются, вызывающий код не меняется;
|
||||
- в WASM-фазе тот же воркер принимает wasm-модуль и диспетчеризует по `toolId`
|
||||
— свитч A/B из фазы 5 сводится к замене реализации внутри исполнителя;
|
||||
- до появления MEDIUM-операций воркер не вводится: EASY-операции быстры и не
|
||||
блокируют UI.
|
||||
|
||||
## 1. Структура репозитория (этап TS)
|
||||
|
||||
@@ -39,15 +55,28 @@ easy-png-tools/
|
||||
|
||||
### Фаза 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-инструментов** по категориям:
|
||||
|
||||
@@ -56,29 +85,36 @@ easy-png-tools/
|
||||
- цвет: замена, тон, каналы, оттенки серого, инверсии;
|
||||
- геометрия и холст: resize, crop, rotate, flip, border, padding, background;
|
||||
- текст и простые эффекты: watermark, add-text, рамки, шум, pixelate;
|
||||
- анализ: размеры, палитра, проверки, просмотр.
|
||||
Приоритет — ширина, не глубина: как можно больше простых инструментов.
|
||||
- анализ: размеры, палитра, проверки, просмотр. Приоритет — ширина, не глубина:
|
||||
как можно больше простых инструментов.
|
||||
|
||||
1.6 **Полировка.** Описания и подсказки для всех инструментов, состояния загрузки/ошибок, доступность, пустые состояния.
|
||||
1.6 **Полировка.** Описания и подсказки для всех инструментов, состояния
|
||||
загрузки/ошибок, доступность, пустые состояния.
|
||||
|
||||
**Checkpoint:** загрузил PNG → применил пайплайн из N шагов → сохранил и вернул пайплайн → скачал результат. Все инструменты 1.5 работают на сайте. Ни одного wasm.
|
||||
**Checkpoint:** загрузил PNG → применил пайплайн из N шагов → сохранил и вернул
|
||||
пайплайн → скачал результат. Все инструменты 1.5 работают на сайте. Ни одного
|
||||
wasm.
|
||||
|
||||
### Фаза 2 — Тестовые PNG и генератор эталонов
|
||||
|
||||
- Фикстурные входы: градиенты/паттерны/шум + пара «настоящих» PNG.
|
||||
- Правило: сравниваем **распакованные пиксели**, не байты файла.
|
||||
- **Checkpoint:** в `tests/reference/<op>/` лежат `input-*.png` + `expected-*.png`.
|
||||
- **Checkpoint:** в `tests/reference/<op>/` лежат `input-*.png` +
|
||||
`expected-*.png`.
|
||||
|
||||
### Фаза 3 — Playwright-тесты
|
||||
|
||||
- `tools/gen-references.ts`: грузит страницу, гоняет TS-эталон, сохраняет `expected-*.png`.
|
||||
- `tools/gen-references.ts`: грузит страницу, гоняет TS-эталон, сохраняет
|
||||
`expected-*.png`.
|
||||
- Регресс: изменение TS-ядра ловится тестами.
|
||||
- **Checkpoint:** регресс-тесты падают при изменении выхода эталона.
|
||||
|
||||
### Фаза 4 — Rust-ядро
|
||||
|
||||
- Порт 1:1 чистых операций (первым делом те, что покрыты эталонными файлами) с `Image` ↔ TS `ImageData`.
|
||||
- `clamp_to_u8` с семантикой `ToUint8Clamp` (round-half-to-even, ≥255 → 255) + юнит-тесты на краях.
|
||||
- Порт 1:1 чистых операций (первым делом те, что покрыты эталонными файлами) с
|
||||
`Image` ↔ TS `ImageData`.
|
||||
- `clamp_to_u8` с семантикой `ToUint8Clamp` (round-half-to-even, ≥255 → 255) +
|
||||
юнит-тесты на краях.
|
||||
- `cargo test` читает эталонные файлы напрямую, **без браузера**.
|
||||
- **Checkpoint:** `cargo test` зелёный на всех эталонных файлах.
|
||||
|
||||
@@ -97,14 +133,18 @@ easy-png-tools/
|
||||
|
||||
### Фаза 7 — Сквозная проверка идентичности
|
||||
|
||||
- Матрица: **эталонные файлы (из TS) ↔ Rust-native ↔ Rust-wasm**, одна команда `harness compare`.
|
||||
- Матрица: **эталонные файлы (из TS) ↔ Rust-native ↔ Rust-wasm**, одна команда
|
||||
`harness compare`.
|
||||
- **Checkpoint:** сходимость по всем покрытым операциям во всех трёх рантаймах.
|
||||
|
||||
### Фаза 8 — Масштабирование и продвинутое
|
||||
|
||||
- Порт оставшихся EASY → MEDIUM (свёртки, морфология, квантование; кодеки через крейт `image`).
|
||||
- Canvas-зависимые операции (текст, градиенты, штампы, `ctx.filter`): остаются в TS с perceptual-классом либо реимплементация в Rust.
|
||||
- batch-страница: дроп папки → wasm по всем файлам → zip. localStorage-пресеты (тот же JSON, что у CLI).
|
||||
- Порт оставшихся EASY → MEDIUM (свёртки, морфология, квантование; кодеки через
|
||||
крейт `image`).
|
||||
- Canvas-зависимые операции (текст, градиенты, штампы, `ctx.filter`): остаются в
|
||||
TS с perceptual-классом либо реимплементация в Rust.
|
||||
- batch-страница: дроп папки → wasm по всем файлам → zip. localStorage-пресеты
|
||||
(тот же JSON, что у CLI).
|
||||
- HARD: compress/optimize через oxipng/zopfli (в Rust — нативно).
|
||||
|
||||
## 3. Правила сравнения (для фаз 4–7)
|
||||
@@ -120,12 +160,18 @@ easy-png-tools/
|
||||
|
||||
## 4. Риски и контрмеры
|
||||
|
||||
- **Реестр разрастается, страницы дублируются** — один универсальный рендерер страниц/форм из записей реестра; инструмент = данные + `run`.
|
||||
- **Семантика пайплайна** — зафиксировать: каждый шаг применяется к результату предыдущего; единый тип `Image` на всём пути; схема параметров на шаге.
|
||||
- **Canvas-операции не бит-в-бит** — класс perceptual закладывается на Фазе 2–3, а не в конце.
|
||||
- **WASM + Vite** — спайк на Фазе 5 до массовой интеграции (риск отложен сознательно: сайт уже живёт на TS).
|
||||
- **Расползание реестра при будущем Rust-порте** — схема параметров в JSON, её можно парсить и из Rust.
|
||||
- **Разные декодеры PNG** — фикстурные PNG генерировать самим, чтобы не тащить чужие артефакты.
|
||||
- **Реестр разрастается, страницы дублируются** — один универсальный рендерер
|
||||
страниц/форм из записей реестра; инструмент = данные + `run`.
|
||||
- **Семантика пайплайна** — зафиксировать: каждый шаг применяется к результату
|
||||
предыдущего; единый тип `Image` на всём пути; схема параметров на шаге.
|
||||
- **Canvas-операции не бит-в-бит** — класс perceptual закладывается на Фазе 2–3,
|
||||
а не в конце.
|
||||
- **WASM + Vite** — спайк на Фазе 5 до массовой интеграции (риск отложен
|
||||
сознательно: сайт уже живёт на TS).
|
||||
- **Расползание реестра при будущем Rust-порте** — схема параметров в JSON, её
|
||||
можно парсить и из Rust.
|
||||
- **Разные декодеры PNG** — фикстурные PNG генерировать самим, чтобы не тащить
|
||||
чужие артефакты.
|
||||
|
||||
## 5. Команды верификации
|
||||
|
||||
|
||||
@@ -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
@@ -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-data-uri / data-uri-to-png — строка
|
||||
- png-to-hex — rrggbbaa по строкам / hex-to-png — tokens + width
|
||||
- png-to-bytes — десятичные RGBA-байты по строкам / bytes-to-png — tokens + width
|
||||
- png-to-rgb-values — rgba(r,g,b,a) по пикселям / rgb-values-to-png — числа + width
|
||||
- png-to-bytes — десятичные RGBA-байты по строкам / bytes-to-png — tokens +
|
||||
width
|
||||
- png-to-rgb-values — rgba(r,g,b,a) по пикселям / rgb-values-to-png — числа +
|
||||
width
|
||||
- svg-to-png — width результата
|
||||
- verify-is-png — текстовый источник (base64/data-uri), вердикт по сигнатуре
|
||||
|
||||
@@ -41,7 +45,8 @@
|
||||
- harden-alpha-png — threshold
|
||||
- feather-edges-png — radius (размытие только альфы)
|
||||
- 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
|
||||
- center-by-alpha-png — без параметров
|
||||
- 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
|
||||
- change-png-hue — degrees
|
||||
- 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-ycbcr — component (y/cb/cr), display
|
||||
- png-to-lab — component (l/a/b), display
|
||||
@@ -117,12 +124,15 @@
|
||||
### Маски по свойствам пикселей
|
||||
|
||||
- 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
|
||||
- light-pixel-mask-png — threshold, mode, highlightColor, highlightOpacity
|
||||
- dark-pixel-mask-png — threshold, mode, highlightColor, highlightOpacity
|
||||
- unique-color-mask-png — rarity (макс. повторов), mode, highlightColor, highlightOpacity
|
||||
- extract-color-from-png — color, tolerance (оставить близкие, остальное прозрачным)
|
||||
- unique-color-mask-png — rarity (макс. повторов), mode, highlightColor,
|
||||
highlightOpacity
|
||||
- extract-color-from-png — color, tolerance (оставить близкие, остальное
|
||||
прозрачным)
|
||||
|
||||
### Генерация
|
||||
|
||||
@@ -130,7 +140,8 @@
|
||||
- single-color-png — width, height, color
|
||||
- random-noise-png — width, height, seed
|
||||
- linear-gradient-png — width, height, fromColor, toColor, direction
|
||||
- text-to-png — text, fontSize, font, bold, color, transparentBg, backgroundColor, padding
|
||||
- text-to-png — text, fontSize, font, bold, color, transparentBg,
|
||||
backgroundColor, padding
|
||||
- emoji-to-png — emoji, size
|
||||
- placeholder-png — width, height, backgroundColor, color, showText
|
||||
- color-spectrum-png — width, height, direction, saturation, lightness
|
||||
@@ -139,13 +150,17 @@
|
||||
|
||||
### Текст
|
||||
|
||||
- add-text-png — text, fontSize, color, font, bold, position (3×3), margin, plate, plateColor, plateOpacity
|
||||
- date-stamp-png — format, fontSize, color, font, bold, position, margin, plate, plateColor, plateOpacity
|
||||
- watermark-tile-png — text, fontSize, color, opacity, angle, stepX, stepY, font, bold
|
||||
- add-text-png — text, fontSize, color, font, bold, position (3×3), margin,
|
||||
plate, plateColor, plateOpacity
|
||||
- date-stamp-png — format, fontSize, color, font, bold, position, margin, plate,
|
||||
plateColor, plateOpacity
|
||||
- watermark-tile-png — text, fontSize, color, opacity, angle, stepX, stepY,
|
||||
font, bold
|
||||
|
||||
### Палитры и цветовые утилиты
|
||||
|
||||
- color-wheel-generator — size, кольца/сектора, показ hex при клике (у нас — статичный свотч-полотно)
|
||||
- color-wheel-generator — size, кольца/сектора, показ hex при клике (у нас —
|
||||
статичный свотч-полотно)
|
||||
- complementary-palette — baseColor
|
||||
- monochromatic-palette — baseColor, count
|
||||
- analogous-palette — baseColor, spread
|
||||
@@ -154,7 +169,8 @@
|
||||
- similar-shades-palette — baseColor, count, range
|
||||
- sort-colors — источник (палитра-картинка или список), порядок (hsl/luma)
|
||||
- mix-colors — colors[], веса?
|
||||
- average-color — colors[]; blend-two — a, b, steps; step-between — a, b, steps (три частных случая одного движка)
|
||||
- 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 на цветных фонах — фича превью, не инструмент (решить позже)
|
||||
- extract-barcode — HARD, вне планов
|
||||
|
||||
@@ -201,8 +218,17 @@
|
||||
|
||||
## 3. Идеи на рассмотрение (нужна архитектура или спорная ценность)
|
||||
|
||||
- **Region-инструменты** — требуется UI выделения прямоугольника/ластика на превью: censor-region, erase-region, pixelate-area, blur-area, sharpen-area, reverse-colors-area. Один раз делаем selection-компонент — получаем сразу шесть инструментов.
|
||||
- **Мультифайловый вывод** — сейчас инструмент отдаёт одну картинку: 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 клонов у источника) — сознательно не копируем: это обычные операции над конкретным контентом, у нас они доступны через базовые инструменты + цепочки.
|
||||
- **Region-инструменты** — требуется UI выделения прямоугольника/ластика на
|
||||
превью: censor-region, erase-region, pixelate-area, blur-area, sharpen-area,
|
||||
reverse-colors-area. Один раз делаем selection-компонент — получаем сразу
|
||||
шесть инструментов.
|
||||
- **Мультифайловый вывод** — сейчас инструмент отдаёт одну картинку:
|
||||
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 клонов у источника) —
|
||||
сознательно не копируем: это обычные операции над конкретным контентом, у нас
|
||||
они доступны через базовые инструменты + цепочки.
|
||||
|
||||
Reference in New Issue
Block a user