mirror of
https://github.com/Ku6epXBOCTuK/easy-png-tools.git
synced 2026-11-13 17:08:20 +00:00
docs: update a lot of plans, readmes, descriptions
This commit is contained in:
+1
-8
@@ -1,13 +1,6 @@
|
|||||||
node_modules
|
node_modules
|
||||||
|
|
||||||
# Reference source (legacy Next.js design in refs/): branding icons are not needed
|
# Отчёты разовых аудитов и артефакты прогонов
|
||||||
refs/public/icon.svg
|
|
||||||
refs/public/icon-dark-32x32.png
|
|
||||||
refs/public/icon-light-32x32.png
|
|
||||||
refs/public/apple-icon.png
|
|
||||||
refs/public/placeholder-logo.svg
|
|
||||||
refs/public/placeholder-logo.png
|
|
||||||
refs/public/placeholder.svg
|
|
||||||
web/audit/cdp-audit*.txt
|
web/audit/cdp-audit*.txt
|
||||||
css-lint.txt
|
css-lint.txt
|
||||||
web/coverage
|
web/coverage
|
||||||
|
|||||||
@@ -1,129 +1,102 @@
|
|||||||
# AGENTS.md
|
# AGENTS.md
|
||||||
|
|
||||||
Инструкции для агентов (и IDE-ассистентов) по работе с этим репозиторием.
|
Краткий обязательный контракт. Правило: **у каждой темы ровно один источник
|
||||||
|
истины**; здесь только то, что агент обязан знать до чтения остального, и то,
|
||||||
|
что линтер не проверяет. Подробности — по ссылкам ниже.
|
||||||
|
|
||||||
## Стек
|
## Стек и команды
|
||||||
|
|
||||||
- **SvelteKit** (`web/`) — основное приложение, Svelte 5 (runes).
|
- **SvelteKit** (`web/`) — основное приложение, Svelte 5 (runes).
|
||||||
- Менеджер пакетов: **pnpm**. Запускать команды из `web/`, либо через обёртки в
|
- **pnpm** обязателен, npm не использовать. Команды — из корня (`pnpm build`,
|
||||||
корне: `pnpm build`, `pnpm dev` (это `pnpm --dir web ...`). Не использовать
|
`pnpm dev`, `pnpm verify`) или из `web/` с префиксом `pnpm --dir web`.
|
||||||
npm.
|
|
||||||
- Сборка: `pnpm --dir web build`, проверка типов: `pnpm --dir web check`, тесты:
|
|
||||||
`pnpm --dir web test` (Vitest), тесты с coverage-гейтом:
|
|
||||||
`pnpm --dir web test:coverage`, тесты lint-правил:
|
|
||||||
`pnpm --dir web test:rules`, e2e: `pnpm --dir web test:e2e` (Playwright,
|
|
||||||
`web/e2e/`, свой webServer на `pnpm build` + `scripts/serve-static.mjs`, порт
|
|
||||||
4173).
|
|
||||||
- Форматирование: `pnpm format` в корне (оба прогона) или
|
|
||||||
`pnpm --dir web format` (только код web/). Проверка: `pnpm check:docs`
|
|
||||||
(docs) + `pnpm --dir web format:check` (код).
|
|
||||||
- Полный быстрый gate: `pnpm verify`; подробная матрица команд и CI —
|
|
||||||
`docs/quality-gates.md`.
|
|
||||||
- После пуша результат CI проверяется явно: смотреть статусы отдельных job, а не
|
|
||||||
общий conclusion прогона. У `e2e` стоит `continue-on-error`, поэтому прогон с
|
|
||||||
упавшим e2e помечается как `success`. `Quality` workflow запускается на каждом
|
|
||||||
пуше в `main` и на `pull_request`, поэтому browser matrix проверяется сама;
|
|
||||||
ручной запуск нужен только для старого коммита.
|
|
||||||
- Линтинг: `pnpm --dir web lint` (ESLint). Полный прогон дизайн-проверок:
|
|
||||||
`pnpm --dir web lint:all` (ESLint + stylelint + токен-аудит). Каждый шаг
|
|
||||||
выполняется, даже если предыдущий упал. Новые изменения не должны добавлять
|
|
||||||
предупреждения без записи в baseline.
|
|
||||||
|
|
||||||
## Правила кода
|
| Что | Команда |
|
||||||
|
| ----------------------- | ------------------------------------------------- |
|
||||||
|
| Полный быстрый gate | `pnpm verify` |
|
||||||
|
| Типы | `pnpm --dir web check` |
|
||||||
|
| Юнит-тесты | `pnpm --dir web test` |
|
||||||
|
| Тесты lint-правил | `pnpm --dir web test:rules` |
|
||||||
|
| Линтинг | `pnpm --dir web lint:all` |
|
||||||
|
| E2E | `pnpm --dir web test:e2e` |
|
||||||
|
| Форматирование | `pnpm format` (код + docs) |
|
||||||
|
| Проверка форматирования | `pnpm check:docs` + `pnpm --dir web format:check` |
|
||||||
|
|
||||||
Синтаксические ограничения описывать не нужно — они проверяются линтером (весь
|
## Что читать под задачу
|
||||||
список правил и токены-префиксы в `web/eslint-plugins/README.md`, прогон:
|
|
||||||
`pnpm --dir web lint` / `pnpm --dir web lint:all`). Здесь — только то, что
|
|
||||||
линтер не умеет.
|
|
||||||
|
|
||||||
### Svelte 5
|
Читать только нужную строку, а не все документы подряд.
|
||||||
|
|
||||||
Типизация пропсов через локальный `interface Props` закреплена правилом
|
| Задача | Документ |
|
||||||
`conventions/interface-props`; руками его нигде дублировать не надо.
|
| --------------------------------------------- | ---------------------------------- |
|
||||||
|
| Слои, поток данных, новый инструмент/поле | `docs/architecture.md` |
|
||||||
|
| Какие проверки запускать, пороги, CI | `docs/quality-gates.md` |
|
||||||
|
| Правка самих гейтов: «почему так, а не иначе» | `docs/decisions.md` |
|
||||||
|
| Порядок работы, нагрузка на машину | `docs/agent-workflow.md` |
|
||||||
|
| Locator-ы, fixtures, browser matrix | `docs/testing-strategy.md` |
|
||||||
|
| Детали lint-правил и токенов | `web/eslint-plugins/README.md` |
|
||||||
|
| Задачи и идеи | `docs/backlog.md` |
|
||||||
|
| Актуальный список инструментов | `docs/tools-map.md` |
|
||||||
|
| Ручная приёмка UI | `docs/checklist-manual-testing.md` |
|
||||||
|
| Индекс остальных документов | `docs/README.md` |
|
||||||
|
|
||||||
### E2E
|
## Инварианты, которые линтер не проверяет
|
||||||
|
|
||||||
- Сначала используй `role`, `aria-*`, label и стабильный `href`; `data-testid`
|
- **Сначала искать.** Новая константа, тип или утилита не заводится, пока не
|
||||||
разрешён только для source/result/verdict/empty-state.
|
найдено существующее определение — по ключу в соседних модулях и по всему
|
||||||
- `test.fixme` допускается только для зарегистрированной задачи в backlog или
|
`web/src/`.
|
||||||
checklist; после исправления тест и ссылку обновляют вместе.
|
- **Комментарий объясняет «почему»**, а не пересказывает код. `TODO`/`FIXME` —
|
||||||
- Полные переводы проверяются в unit-тестах; e2e проверяет поведение.
|
только для зарегистрированной задачи: ссылка на `docs/backlog.md` и критерий
|
||||||
- Playwright-прогоны запускаются только по явной команде пользователя, по одному
|
закрытия.
|
||||||
прогону, без циклов повторов и без фонового запуска: локально matrix идёт с
|
- **Один шаг — одна проблема**, примерно до 500 строк; не смешивать lint, i18n,
|
||||||
`workers: 1`. Отзывчивость компьютера пользователя важнее скорости проверки —
|
UI и рефакторинг.
|
||||||
подробности в `docs/agent-workflow.md`.
|
- **Не коммитить.** Коммит и пуш делает разработчик после ревью.
|
||||||
|
- **Не запускать тяжёлое без спроса.** Playwright — только по явной команде
|
||||||
|
пользователя, один прогон, без циклов повторов и фонового запуска.
|
||||||
|
- **Не писать изменяемые числа** (количество инструментов, тестов, локалей,
|
||||||
|
проценты покрытия): вместо числа — ссылка на источник или команда. Число
|
||||||
|
допустимо, только если его проверяет конфиг или тест. Обоснование и границы
|
||||||
|
автоматизации — `docs/decisions.md`, раздел 3.
|
||||||
|
- **Результат CI смотреть по отдельным job**: у `e2e` стоит `continue-on-error`,
|
||||||
|
поэтому общий conclusion может быть зелёным при красном e2e
|
||||||
|
(`docs/decisions.md`, раздел 10).
|
||||||
|
|
||||||
### Сначала искать
|
## Границы изменений
|
||||||
|
|
||||||
Новая константа или тип не заводится, пока не произведён поиск существующего
|
- Production-UI — `web/src/routes/**` и `web/src/lib/components/**`. Старый UI
|
||||||
определения (по ключу в соседних модулях и по всему `web/src/`). Это правило в
|
(`/v1/**`) удалён и не возвращается.
|
||||||
первую очередь для ИИ-агентов: дублирующее определение ухудшает правки в
|
- `web/src/lib/core/**` и `web/src/lib/theme.svelte.ts` — общие модули: их
|
||||||
нескольких местах и запутывает. (Синтаксис самих определений — под линтером:
|
правка проверяется unit-тестами и smoke-сценариями production-потребителей.
|
||||||
строка `conventions/no-string-union-alias` и т.д.)
|
- Направление зависимостей, точки расширения и рецепт правки (инструмент, вид
|
||||||
|
поля, маршрут, токен) — `docs/architecture.md`, разделы 1–2, 7, 8.
|
||||||
|
|
||||||
### Границы изменений
|
## Что проверяется линтером
|
||||||
|
|
||||||
- Production — только текущие маршруты и `web/src/lib/components/**`. Старый
|
`pnpm --dir web lint:all` — ESLint с `--max-warnings=0` (warn-only правила тоже
|
||||||
UI (`/v1/**`) удалён и не возвращается: без redirects и совместимого режима;
|
блокируют gate), Stylelint и token audit. Синтаксические ограничения здесь не
|
||||||
исторические упоминания живят только в `docs/archive/`. Будущий API-контракт
|
дублируются: полный список правил и префиксы токенов —
|
||||||
`POST /v1/process` из `docs/plan-platform.md` не связан с удалённым UI-v1.
|
`web/eslint-plugins/README.md`; новое правило сначала появляется в warn-only
|
||||||
- `core/` и `theme/` — общие модули: их изменение проверяется unit-тестами и
|
режиме с fixtures и тестами (`test:rules`).
|
||||||
smoke-сценариями production-потребителей.
|
|
||||||
|
|
||||||
### i18n
|
- **E2E линтером не покрыт:** порядок locator-ов, допустимые `data-testid`,
|
||||||
|
`test.fixme` — `docs/testing-strategy.md`.
|
||||||
|
|
||||||
- Новый пользовательский текст — только через словари и `t()`. Исключения:
|
## Дизайн
|
||||||
бренд, форматы и имена файлов, технические значения, registry source strings и
|
|
||||||
`/kit`. Проверяется warn-правилом `i18n/no-hardcoded-user-text`, детали и
|
|
||||||
список исключений — в `web/eslint-plugins/README.md`.
|
|
||||||
- Новые ключи добавляются во **все** локали сразу: паритет ключей и
|
|
||||||
плейсхолдеров проверяет `i18n/dict-consistency`.
|
|
||||||
|
|
||||||
### Комментарии
|
Источник правды по визуальному языку — код: `web/src/app.css` (дизайн-токены и
|
||||||
|
глобальные стили), `web/src/lib/components/**` и витрина `/kit`. Новый дизайн
|
||||||
- Комментарий объясняет «почему», а не пересказывает код.
|
живёт в `app.css` и применяется ко всем production-маршрутам; повторяющийся
|
||||||
- `TODO`/`FIXME` — только зарегистрированные задачи: ссылка на `docs/backlog.md`
|
визуальный элемент — отдельный компонент, даже «просто div с двумя стилями»;
|
||||||
или issue и критерий закрытия. В коде незарегистрированных пометок не
|
дизайн-токены — только из CSS-переменных, новый токен появляется вместе с
|
||||||
оставляем.
|
потребителем (`pnpm --dir web lint:tokens` сообщает о неиспользуемых). Приёмка —
|
||||||
|
`docs/checklist-manual-testing.md`.
|
||||||
### Новые lint-правила
|
|
||||||
|
|
||||||
Новое правило сначала появляется в report-only/warn-only режиме, получает
|
|
||||||
fixtures и тесты (`test:rules`), и только после очистки baseline может стать
|
|
||||||
blocking. Учитывай: `lint:all` гоняет ESLint с `--max-warnings=0`, поэтому
|
|
||||||
warn-правило обязано приземлиться без срабатываний на текущем коде.
|
|
||||||
|
|
||||||
|
|
||||||
### Дизайн
|
|
||||||
|
|
||||||
Источник правды по визуальному языку — сам код: `web/src/app.css` (дизайн-токены
|
|
||||||
и глобальные стили), `web/src/lib/components/**` (компоненты и локальные стили)
|
|
||||||
и витрина `/kit`. Приёмка — `docs/checklist-manual-testing.md`, ограничения
|
|
||||||
линтера — `web/eslint-plugins/README.md`. Общие правила, которые линтер не
|
|
||||||
проверяет:
|
|
||||||
|
|
||||||
- Новый дизайн живёт в `web/src/app.css` и применяется ко всем
|
|
||||||
production-маршрутам.
|
|
||||||
- Все повторяющиеся визуальные элементы — отдельные компоненты в
|
|
||||||
`web/src/lib/components/`, даже «просто div с двумя стилями».
|
|
||||||
- Дизайн-токены (цвета, размеры, длительности, z-index) — только из
|
|
||||||
CSS-переменных; детали ограничений — в `web/eslint-plugins/README.md`.
|
|
||||||
- Новый токен появляется вместе с потребителем: `pnpm --dir web lint:tokens`
|
|
||||||
сообщает о неиспользуемых токенах, и это не блокирует CI только потому, что
|
|
||||||
проверка оставлена предупреждением.
|
|
||||||
|
|
||||||
## Документация
|
## Документация
|
||||||
|
|
||||||
- Здесь держать только обязательные инварианты, команды и ссылки на подробные
|
- Новый документ создаётся, только если темы нет в существующем; иначе
|
||||||
документы; не превращать `AGENTS.md` в полный справочник.
|
обновляется источник истины. У документа есть статус (`draft`, `active`,
|
||||||
- Индекс активных документов: `docs/README.md`; подробный workflow:
|
`completed`, `archived`).
|
||||||
`docs/agent-workflow.md`; quality gates: `docs/quality-gates.md`; тестовая
|
- Не дублировать длинные объяснения в нескольких файлах: короткая формулировка и
|
||||||
архитектура: `docs/testing-strategy.md`.
|
ссылка на канонический документ.
|
||||||
- Новые подробные правила, процедуры и таблицы хранить в `docs/`, а детали
|
- **Backlog живёт открытыми задачами:** выполненные удаляются, а критичные для
|
||||||
конкретных lint-правил — в `web/eslint-plugins/README.md`.
|
проекта факты переносятся в тематический документ. Завершённые планы и
|
||||||
- Завершённые или устаревшие документы переносить в `docs/archive/`, указывать
|
исследования — в `docs/archive/`, удаляются через 30 дней; процедура в
|
||||||
дату архивирования и срок удаления; архив — только справка, не источник
|
`docs/archive/README.md`.
|
||||||
требований.
|
|
||||||
|
|
||||||
- Trunk-based: коммиты делает разработчик после ревью, самому не коммитить.
|
|
||||||
Изменения делать небольшими (< ~500 строк), атомарными.
|
|
||||||
|
|||||||
@@ -1,35 +1,35 @@
|
|||||||
# easy-png-tools
|
# easy-png-tools
|
||||||
|
|
||||||
Browser-based PNG utility toolkit — no server, no data upload, everything runs
|
Browser-based image utility toolkit — no server, no data upload, everything runs
|
||||||
locally.
|
locally in your browser.
|
||||||
|
|
||||||
Inspired by online services like onlinepngtools.com, but fully offline-capable
|
Inspired by online services like onlinepngtools.com, but fully offline-capable
|
||||||
and open source.
|
and open source.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- **Pixel-level image processing** — format conversion, transparency/alpha
|
- **A full set of image tools** across categories: conversion,
|
||||||
channel, color transformations, geometry, filters/convolutions, morphology,
|
transparency/alpha, color, channel decomposition, geometry, filters,
|
||||||
quantization & palettes, image generation, text/watermarks, analysis & checks
|
pixel-property masks, generation, text/watermarks, analysis & verdicts — the
|
||||||
|
current list lives in [`docs/tools-map.md`](docs/tools-map.md)
|
||||||
- **Pipeline system (workspace)** — load image → chain multiple tool steps →
|
- **Pipeline system (workspace)** — load image → chain multiple tool steps →
|
||||||
sequential application → download result; pipelines saved to localStorage,
|
sequential application → download result; pipelines saved to localStorage,
|
||||||
exportable as JSON
|
exportable as JSON
|
||||||
- **Web Worker execution** — heavy operations run off the main thread with
|
- **Web Worker execution** — heavy operations run off the main thread with
|
||||||
automatic fallback to direct calls
|
automatic fallback to direct calls
|
||||||
- **i18n** — full Russian and English localization with search matching
|
- **i18n** — full Russian and English localization
|
||||||
- **Dark/Light themes** — persisted to localStorage
|
- **Dark/Light themes** — persisted to localStorage
|
||||||
- **Pure TypeScript core** — `lib/core/` operates on
|
- **Pure TypeScript core** — `lib/core/` operates on
|
||||||
`ImageData`/`Uint8ClampedArray` with no DOM dependency
|
`ImageData`/`Uint8ClampedArray` with no DOM dependency
|
||||||
- **~30+ tools** across categories: transparency, color, geometry, filters,
|
- **Fully static** — no backend, no analytics, no tracking
|
||||||
morphology, palettes, generation, text, analysis
|
|
||||||
|
|
||||||
## Tech Stack
|
## Tech Stack
|
||||||
|
|
||||||
- **SvelteKit** (Svelte 5, runes mode) with static adapter — pure static export,
|
- **SvelteKit** (Svelte 5, runes mode) with static adapter — pure static export,
|
||||||
no server
|
no server
|
||||||
- **Vite** + **TypeScript** (strict)
|
- **Vite** + **TypeScript** (strict)
|
||||||
- **Vitest** for unit tests
|
- **Vitest** for unit tests, **Playwright** for e2e
|
||||||
- **ESLint** + **Prettier**
|
- **ESLint** (incl. local plugins) + Stylelint + Prettier
|
||||||
- **pnpm** (required, ^11.20.0)
|
- **pnpm** (required, ^11.20.0)
|
||||||
- **Lucide** icons, **IBM Plex** fonts (self-hosted)
|
- **Lucide** icons, **IBM Plex** fonts (self-hosted)
|
||||||
|
|
||||||
@@ -54,54 +54,44 @@ pnpm build
|
|||||||
|
|
||||||
Static output is written to `web/build/`.
|
Static output is written to `web/build/`.
|
||||||
|
|
||||||
### Other Commands
|
### Commands
|
||||||
|
|
||||||
| Command | Description |
|
Run from the repository root; `pnpm --dir web …` targets the app package.
|
||||||
| ------------------------ | ------------------------------ |
|
|
||||||
| `pnpm verify` | Run the full fast quality gate |
|
|
||||||
| `pnpm --dir web test` | Run unit tests (Vitest) |
|
|
||||||
| `pnpm --dir web check` | Type-check (svelte-check) |
|
|
||||||
| `pnpm --dir web lint` | Lint (ESLint) |
|
|
||||||
| `pnpm --dir web format` | Format code (Prettier) |
|
|
||||||
| `pnpm format:docs` | Format docs (Prettier) |
|
|
||||||
| `pnpm --dir web preview` | Preview production build |
|
|
||||||
|
|
||||||
## Project Structure
|
```bash
|
||||||
|
pnpm install
|
||||||
```txt
|
pnpm dev # dev server
|
||||||
easy-png-tools/
|
pnpm verify # full fast quality gate — the pre-review gate
|
||||||
├── web/ # Main SvelteKit application
|
pnpm build # static production export → web/build/
|
||||||
│ ├── src/
|
|
||||||
│ │ ├── lib/
|
|
||||||
│ │ │ ├── core/ # Pure TS image processing (ImageData-based)
|
|
||||||
│ │ │ ├── components/ # UI components
|
|
||||||
│ │ │ ├── tools/ # Pipeline, executor, overlay store
|
|
||||||
│ │ │ ├── i18n/ # Localization (ru/en)
|
|
||||||
│ │ │ └── registry.ts # Tool registry (source of truth)
|
|
||||||
│ │ └── routes/ # SvelteKit routes
|
|
||||||
│ └── build/ # Static export output
|
|
||||||
├── docs/ # Architecture docs, roadmap, plans
|
|
||||||
└── refs/ # Design reference (Next.js, not part of runtime)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Полный список команд и матрица «какие проверки запускать при каком изменении» —
|
||||||
|
в `AGENTS.md` и `docs/quality-gates.md`.
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
The project is built around a **tool registry** (`registry.ts`) — each tool is a
|
Everything is built around the **tool registry** (`web/src/lib/registry/`): each
|
||||||
self-contained entry with `{id, title, description, category, params, run}`.
|
tool is a self-contained entry
|
||||||
Pages, forms, and pipelines are generated from this registry.
|
`{id, title, description, category, schema, run}`. Pages, forms, and pipelines
|
||||||
|
are generated from it, and all execution goes through a single
|
||||||
|
`executor.execute` entry point.
|
||||||
|
|
||||||
Core image processing lives in `lib/core/` and operates directly on `ImageData`
|
Core image processing lives in `web/src/lib/core/` and operates directly on
|
||||||
objects with no browser API dependencies, making it portable to other
|
`ImageData`, with no browser API dependencies.
|
||||||
environments (WASM, CLI) in the future.
|
|
||||||
|
Layering, data flow, and the recipe for adding a tool or a field kind:
|
||||||
|
[`docs/architecture.md`](docs/architecture.md).
|
||||||
|
|
||||||
## Roadmap
|
## Roadmap
|
||||||
|
|
||||||
See [docs/roadmap.md](docs/roadmap.md) for the full development plan. Current
|
See [`docs/roadmap.md`](docs/roadmap.md) for phases and
|
||||||
phases:
|
[`docs/plan-platform.md`](docs/plan-platform.md) for the API/PWA direction. The
|
||||||
|
current focus is SEO/GEO ([`docs/plan-seo.md`](docs/plan-seo.md)) plus the
|
||||||
|
product tasks in [`docs/backlog.md`](docs/backlog.md).
|
||||||
|
|
||||||
1. **Phase 1** — TypeScript core + web UI (in progress)
|
## For AI agents
|
||||||
2. **Phase 2** — Rust/WASM core for performance-critical operations
|
|
||||||
3. **Phase 3** — CLI tool
|
Read [`AGENTS.md`](AGENTS.md) first — it is the mandatory short contract.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
+42
-75
@@ -1,89 +1,56 @@
|
|||||||
# Документация проекта
|
# Документация проекта
|
||||||
|
|
||||||
> Статус: **active**. Это индекс документов и карта источников истины для
|
> Статус: **active**. Индекс документов. Обязательный краткий контракт — в
|
||||||
> разработчиков и агентов. Подробные правила находятся в самих документах, а
|
> `AGENTS.md`, подробные правила — в самих документах.
|
||||||
> обязательный краткий контракт — в `AGENTS.md`.
|
|
||||||
|
|
||||||
## Порядок чтения
|
## Порядок чтения
|
||||||
|
|
||||||
1. `AGENTS.md` — команды и обязательные инварианты.
|
1. `AGENTS.md` — команды, инварианты и карта «что читать под задачу».
|
||||||
2. Этот файл — выбрать актуальный документ по теме.
|
2. Этот файл — выбрать актуальный документ по теме.
|
||||||
3. Соответствующий план или checklist темы.
|
3. Соответствующий план или checklist темы.
|
||||||
4. Исходники, конфигурация и тесты рядом с изменяемым кодом.
|
4. Исходники, конфигурация и тесты рядом с изменяемым кодом.
|
||||||
|
|
||||||
## Активные документы
|
## Активные документы
|
||||||
|
|
||||||
| Документ | Роль | Статус |
|
| Документ | Когда читать | Статус |
|
||||||
| ---------------------------------- | ---------------------------------------------------- | ------ |
|
| ---------------------------------- | -------------------------------------------------- | ------ |
|
||||||
| `AGENTS.md` | Краткий обязательный контракт для агента | active |
|
| `AGENTS.md` | Всегда: контракт агента | active |
|
||||||
| `docs/agent-workflow.md` | Подробный порядок исследования, изменения и проверки | active |
|
| `docs/architecture.md` | Слои, поток данных, новый инструмент/поле/маршрут | active |
|
||||||
| `docs/testing-strategy.md` | Locator, fixture, i18n и fixme-policy для e2e | active |
|
| `docs/quality-gates.md` | Какие проверки запускать, пороги, coverage, CI | active |
|
||||||
| `docs/quality-gates.md` | Команды проверки, warnings и CI | active |
|
| `docs/decisions.md` | Правка самих гейтов; чтение CI после пуша | active |
|
||||||
| `docs/backlog.md` | Продуктовые, UX- и инфраструктурные задачи | active |
|
| `docs/agent-workflow.md` | Порядок работы, нагрузка на машину, протокол ревью | active |
|
||||||
| `docs/roadmap.md` | Общие направления развития | active |
|
| `docs/testing-strategy.md` | Правка e2e: locator-ы, fixtures, browser matrix | active |
|
||||||
| `docs/plan-seo.md` | SEO/GEO-план | active |
|
| `docs/backlog.md` | Продуктовые, UX- и инфраструктурные задачи | active |
|
||||||
| `docs/plan-platform.md` | PWA/API/WASM-план | active |
|
| `docs/roadmap.md` | Направления развития, фазы 1–10 | active |
|
||||||
| `docs/checklist-manual-testing.md` | Ручная приёмка production UI | active |
|
| `docs/tools-map.md` | Актуальный список инструментов против реестра | active |
|
||||||
| `docs/tools-map.md` | Живая карта инструментов | active |
|
| `docs/tools-audit.md` | Сырые UX/UI-находки по инструментам (вход аудита) | active |
|
||||||
| `web/eslint-plugins/README.md` | Детали custom lint rules и token policy | active |
|
| `docs/plan-fields-audit-fixes.md` | Разбивка аудита на волны правок | draft |
|
||||||
| `docs/archive/README.md` | Правила архивирования и удаления старых документов | active |
|
| `docs/plan-seo.md` | SEO/GEO-план | active |
|
||||||
|
| `docs/plan-platform.md` | API/PWA/шрифты-план | active |
|
||||||
|
| `docs/checklist-manual-testing.md` | Ручная приёмка production UI | active |
|
||||||
|
| `web/eslint-plugins/README.md` | Правка lint-правил, токенов и их конфигов | active |
|
||||||
|
| `docs/archive/README.md` | Архивирование и удаление старых документов | active |
|
||||||
|
|
||||||
`docs/archive/` содержит временную историческую справку и не является текущим
|
`docs/plan-domain.local.md` исключён из git через `.gitignore` (выбранное имя
|
||||||
источником критериев приёмки. Для старых документов действует срок хранения 30
|
домена не должно попасть в историю до покупки). Ссылки на него в `plan-seo.md` и
|
||||||
дней; после него их удаляют, а при необходимости достают из git history.
|
`backlog.md` при клонировании репозитория не разрешаются — это ожидаемо.
|
||||||
|
|
||||||
## Где хранить новую информацию
|
## Правила
|
||||||
|
|
||||||
- Поведение, которое агент обязан соблюдать сразу, — кратко в `AGENTS.md`.
|
- У каждой темы ровно один владелец: остальные документы ссылаются на него, а не
|
||||||
- Точные команды, параметры скриптов и критерии качества — в
|
пересказывают. Новый документ создаётся, только если темы нет в существующем.
|
||||||
`docs/quality-gates.md` или в README соответствующей подсистемы.
|
- У документа есть статус (`draft`, `active`, `completed`, `archived`) и
|
||||||
- Архитектура, потоки данных и границы модулей — в отдельном architecture-плане
|
понятная область применения. Ссылки на конкретный файл или команду проверяются
|
||||||
или документе, не дублируя существующие планы.
|
при изменении источника истины.
|
||||||
- Реализация lint-правил, fixtures и технические детали ESLint — в
|
- Подробные материалы живут в `docs/`, а не в корне. Не создавать отдельные
|
||||||
`web/eslint-plugins/README.md` и исходниках правил.
|
`docs/commands.md` или `docs/known-issues.md`: команды принадлежат
|
||||||
- Продуктовые решения и ручная приёмка — в backlog, плане и checklist.
|
`quality-gates`, известные проблемы — backlog и checklist. `README.md` в корне
|
||||||
|
остаётся кратким обзором продукта, а не вторым руководством агента.
|
||||||
У документа должен быть один источник истины: остальные файлы ссылаются на него,
|
- Ссылаясь на инструмент по id, сверять id с реестром (`web/src/lib/registry/`):
|
||||||
а не пересказывают. Подробные материалы хранятся в `docs/`, чтобы не засорять
|
исторические названия в старых планах не действуют.
|
||||||
корень репозитория. Не создавать отдельные `docs/commands.md`,
|
- `docs/backlog.md` содержит только открытые задачи: выполненные удаляются, а
|
||||||
`docs/known-issues.md` или копию backlog: команды принадлежат `quality-gates`,
|
критичные для проекта факты из них переносятся в тематический документ.
|
||||||
известные проблемы — backlog и checklist. `README.md` в корне остаётся кратким
|
- `docs/archive/` — временная историческая справка, не источник критериев
|
||||||
обзором продукта, а не вторым руководством агента.
|
приёмки; срок хранения 30 дней, файлы восстановимы из git history.
|
||||||
|
- Протокол «остановиться и показать изменения» — `docs/agent-workflow.md`,
|
||||||
Новый документ создаётся только когда темы нет в существующем файле; иначе
|
раздел 6.
|
||||||
обновляется источник истины. Активный документ имеет статус (`draft`, `active`,
|
|
||||||
`completed`) и понятную область применения.
|
|
||||||
|
|
||||||
Подробные материалы хранятся в `docs/`, а не в корне. Новый файл создаётся
|
|
||||||
только если темы нет в существующем документе; иначе обновляется источник
|
|
||||||
истины.
|
|
||||||
|
|
||||||
## Текущий baseline
|
|
||||||
|
|
||||||
- production registry содержит 122 инструмента;
|
|
||||||
- `pnpm --dir web check` проходит без ошибок и предупреждений;
|
|
||||||
- `pnpm --dir web test` — 46 test files / 534 tests, включая тесты token audit,
|
|
||||||
PostCSS HCT-плагина и правил линтинга;
|
|
||||||
- `pnpm --dir web test:rules` — 5 test files / 104 tests;
|
|
||||||
- `pnpm --dir web lint:all` — 0 errors, 0 i18n warnings и 0 unused design tokens
|
|
||||||
от token audit;
|
|
||||||
- coverage `src/lib` — statements 91.77%, branches 83.3%, functions 74.31%,
|
|
||||||
lines 92.53% при порогах 90 / 82 / 72 / 91;
|
|
||||||
- `pnpm verify` проходит;
|
|
||||||
- `pnpm --dir web test:e2e` — 276 тестов в четырёх проектах: 276 passed, 0
|
|
||||||
skipped, 0 failed. Отказ декодера битого PNG зафиксирован per-engine в
|
|
||||||
`web/e2e/png-fixtures.spec.ts`.
|
|
||||||
|
|
||||||
Состав проверок и их семантика описаны в `docs/quality-gates.md`, тестовая
|
|
||||||
архитектура — в `docs/testing-strategy.md`. Baseline обновляется здесь при
|
|
||||||
изменении любого из гейтов.
|
|
||||||
|
|
||||||
## Правила актуальности
|
|
||||||
|
|
||||||
- У активного документа есть статус: `draft`, `active`, `completed` или
|
|
||||||
`archived`.
|
|
||||||
- Ссылки на конкретный файл или команду проверяются при изменении источника
|
|
||||||
истины.
|
|
||||||
- Архивный документ не возвращается в live-критерии без явной сверки.
|
|
||||||
- После каждого шага плана агент останавливается, показывает изменённые файлы,
|
|
||||||
проверки и нерешённые решения, и ждёт ревью.
|
|
||||||
|
|||||||
+28
-71
@@ -1,46 +1,31 @@
|
|||||||
# Workflow для агентов
|
# Workflow для агентов
|
||||||
|
|
||||||
> Статус: **active**. Подробная процедура работы с репозиторием; краткие
|
> Статус: **active**. Порядок исследования, изменения и проверки. Обязательные
|
||||||
> обязательные правила остаются в `AGENTS.md`. Этот документ не заменяет source
|
> инварианты — в `AGENTS.md`, этот документ не заменяет source of truth
|
||||||
> of truth конкретной подсистемы.
|
> подсистемы.
|
||||||
|
|
||||||
## 1. Подготовка к задаче
|
## 1. Подготовка к задаче
|
||||||
|
|
||||||
1. Прочитать `AGENTS.md` и `docs/README.md`.
|
1. Прочитать `AGENTS.md` и выбрать документ по карте «что читать под задачу».
|
||||||
2. Найти релевантный активный план, backlog или checklist.
|
2. Найти релевантный активный план, backlog или checklist.
|
||||||
3. Проверить `git status`: не перезаписывать чужие незакоммиченные изменения.
|
3. Проверить `git status`: не перезаписывать чужие незакоммиченные изменения.
|
||||||
4. Найти существующие типы, константы, helpers и тестовые паттерны до создания
|
4. Определить границу изменения: production UI, shared-модуль или инфраструктура
|
||||||
нового определения.
|
— `AGENTS.md`, раздел «Границы изменений». Изменение shared-кода требует
|
||||||
5. Определить границу изменения:
|
проверки production-потребителей.
|
||||||
- production UI — `web/src/routes/**` и `web/src/lib/components/**`;
|
5. Не добавлять исключения линтера ради отдельных модулей без записи в baseline.
|
||||||
- shared — `web/src/lib/core/**` и `web/src/lib/theme.svelte.ts`.
|
|
||||||
|
|
||||||
Изменение shared-кода требует проверки production-потребителей. Не добавлять
|
|
||||||
исключения линтера ради отдельных модулей без записи в baseline.
|
|
||||||
|
|
||||||
## 2. Порядок изменения
|
## 2. Порядок изменения
|
||||||
|
|
||||||
- Сначала исправляются тесты или контракт, который фиксирует неверное поведение,
|
- Сначала исправляются тесты или контракт, который фиксирует неверное поведение,
|
||||||
затем production-код — если это не отдельная продуктовая задача.
|
затем production-код — если это не отдельная продуктовая задача.
|
||||||
- Один PR решает одну проблему и не объединяет lint, i18n, UI и архитектурный
|
- Не начинать следующий этап плана в том же шаге: изменение атомарное, ревью
|
||||||
рефакторинг без необходимости.
|
идёт между шагами (раздел 6).
|
||||||
- Изменения держатся небольшими и атомарными, примерно до 500 строк или меньше.
|
|
||||||
- Не добавлять комментарии, которые пересказывают код. Комментарий объясняет
|
|
||||||
причину решения или фиксирует неочевидный инвариант.
|
|
||||||
- Не коммитить изменения автоматически: коммит делает разработчик после ревью.
|
|
||||||
|
|
||||||
## 3. Проверки
|
## 3. Проверки
|
||||||
|
|
||||||
Команды и их область применения перечислены в `AGENTS.md`; точная матрица и
|
Матрица «изменение → минимальный набор проверок», состав `pnpm verify`, политика
|
||||||
текущая политика warnings/errors поддерживаются в `docs/quality-gates.md`.
|
warnings и чтение CI — в `docs/quality-gates.md`. Здесь только то, что относится
|
||||||
|
к работе агента.
|
||||||
| Изменение | Минимальная проверка |
|
|
||||||
| ------------------------------------------------------- | ------------------------------------------------------ |
|
|
||||||
| Только Markdown | `pnpm check:docs` |
|
|
||||||
| TypeScript/Svelte | `pnpm --dir web check`, `pnpm --dir web test` |
|
|
||||||
| Lint-конфиг или custom rule | `pnpm --dir web test:rules`, `pnpm --dir web lint:all` |
|
|
||||||
| Маршрут, загрузка, результат, download, тема или локаль | `pnpm --dir web test:e2e` |
|
|
||||||
| Перед релизом | полный quality gate из плана Q1 |
|
|
||||||
|
|
||||||
`lint:all` может завершиться успешно при warnings. Это не означает чистый
|
`lint:all` может завершиться успешно при warnings. Это не означает чистый
|
||||||
baseline: нужно отдельно показать число errors, warnings и token warnings.
|
baseline: нужно отдельно показать число errors, warnings и token warnings.
|
||||||
@@ -56,75 +41,47 @@ E2E запускается отдельно от быстрых проверок
|
|||||||
|
|
||||||
- Playwright-прогоны запускаются только по явной команде пользователя: один
|
- Playwright-прогоны запускаются только по явной команде пользователя: один
|
||||||
прогон за раз, без циклов повторов и без фонового запуска.
|
прогон за раз, без циклов повторов и без фонового запуска.
|
||||||
- Локально `workers: 1` (`web/playwright.config.ts`). Browser matrix из четырёх
|
- Локально `workers: 1` (`web/playwright.config.ts`). Browser matrix проходит
|
||||||
проектов проходит медленно, зато не забирает память и не мешает работе
|
медленно, зато не забирает память и не мешает работе пользователя. Скорость
|
||||||
пользователя. Скорость проверки уступает отзывчивости компьютера.
|
проверки уступает отзывчивости компьютера.
|
||||||
- Нельзя запускать тяжёлый прогон параллельно с другим прогоном или в момент,
|
- Нельзя запускать тяжёлый прогон параллельно с другим прогоном или в момент,
|
||||||
когда пользователь работает с приложением.
|
когда пользователь работает с приложением.
|
||||||
- Диагностика флаков идёт от меньшего к большему: один spec, один проект,
|
- Диагностика флаков идёт от меньшего к большему: один spec, один проект,
|
||||||
`--workers=1`, и только затем полный прогон.
|
`--workers=1`, и только затем полный прогон.
|
||||||
- Таймауты не подбираются под конкретную машину. Если проверка падает из-за
|
- Таймауты не подбираются под конкретную машину. Если проверка падает из-за
|
||||||
нехватки ресурсов, это фиксируется как ограничение окружения, а не лечится
|
нехватки ресурсов, это фиксируется как ограничение окружения, а не лечится
|
||||||
увеличением `expect.timeout`.
|
увеличением `expect.timeout` (обоснование — `docs/decisions.md`, раздел 7).
|
||||||
- При необходимости повторного прогона сначала спрашивается пользователь.
|
- При необходимости повторного прогона сначала спрашивается пользователь.
|
||||||
|
|
||||||
## 4. Работа с тестами
|
## 4. Работа с тестами
|
||||||
|
|
||||||
|
Политика locator-ов, запрещённые зависимости, fixtures, browser matrix и
|
||||||
|
`test.fixme` — в `docs/testing-strategy.md`. Здесь только рабочая рутина:
|
||||||
|
|
||||||
- Поведенческий тест проверяет пользовательский контракт, а не текущую
|
- Поведенческий тест проверяет пользовательский контракт, а не текущую
|
||||||
реализацию.
|
реализацию.
|
||||||
- Сначала предпочитать `role`, `aria-*`, label, stable id или href.
|
- Спецификация вызывает общие операции из `web/e2e/helpers/page.ts`, а не
|
||||||
- `data-testid` ограничен source/result/verdict/empty-state; общие
|
повторяет raw-селекторы.
|
||||||
locator-паттерны собраны в `web/e2e/helpers/page.ts`, policy — в
|
|
||||||
`docs/testing-strategy.md`.
|
|
||||||
- `data-testid` использовать только там, где семантического locator нет.
|
|
||||||
- Не фиксировать абсолютное число инструментов, если проверка не связана с этим
|
- Не фиксировать абсолютное число инструментов, если проверка не связана с этим
|
||||||
инвариантом.
|
инвариантом.
|
||||||
- Точные переводы проверять в словарях и i18n helpers; в e2e проверять поведение
|
|
||||||
и отдельный locale smoke.
|
|
||||||
- `test.fixme` допускается только для зарегистрированной проблемы или явного
|
|
||||||
тестового долга, с ссылкой на backlog или checklist. После исправления тест и
|
|
||||||
его issue обновляются вместе.
|
|
||||||
- Новые fixtures должны быть воспроизводимыми и не требовать ручного создания
|
- Новые fixtures должны быть воспроизводимыми и не требовать ручного создания
|
||||||
больших бинарных файлов.
|
больших бинарных файлов.
|
||||||
|
|
||||||
## 5. Документация
|
## 5. Документация
|
||||||
|
|
||||||
Источник истины выбирается по теме:
|
Порядок работы с документами и карта источников истины — `docs/README.md`.
|
||||||
|
Архивирование — `docs/archive/README.md`.
|
||||||
- обязательное поведение агента — `AGENTS.md`;
|
|
||||||
- индекс и карта документов — `docs/README.md`;
|
|
||||||
- качество и точные команды — `docs/quality-gates.md`;
|
|
||||||
- тестовая архитектура — `docs/testing-strategy.md`;
|
|
||||||
- lint implementation — `web/eslint-plugins/README.md`;
|
|
||||||
- продуктовые решения — backlog и активные планы.
|
|
||||||
|
|
||||||
Не дублировать длинные объяснения в нескольких файлах. Если правило требует
|
|
||||||
пояснения, в `AGENTS.md` оставить короткую понятную формулировку и ссылку на
|
|
||||||
подробный документ.
|
|
||||||
|
|
||||||
### Архивирование завершённых документов
|
|
||||||
|
|
||||||
- Если план или исследование больше не является рабочим источником, перенести
|
|
||||||
файл в `docs/archive/`.
|
|
||||||
- В начало файла добавить `Статус`, `Архивирован`, `Удалить после` и причину.
|
|
||||||
- Архив хранить 30 дней для быстрой справки; после срока удалить после проверки
|
|
||||||
ссылок. Если решение понадобится снова, использовать git history.
|
|
||||||
- Не переносить активный план в архив только потому, что он пока не начат.
|
|
||||||
|
|
||||||
## 6. Протокол ревью между шагами
|
## 6. Протокол ревью между шагами
|
||||||
|
|
||||||
После каждого шага плана агент:
|
После каждого шага плана агент:
|
||||||
|
|
||||||
1. останавливается и не начинает следующий Q-этап;
|
1. останавливается и не начинает следующий этап;
|
||||||
2. перечисляет изменённые файлы и принятые решения;
|
2. перечисляет изменённые файлы и принятые решения;
|
||||||
3. указывает выполненные проверки и их фактический результат;
|
3. указывает выполненные проверки и их фактический результат;
|
||||||
4. отдельно перечисляет warnings, fixme, долги и вопросы;
|
4. отдельно перечисляет warnings, fixme, долги и вопросы;
|
||||||
5. ждёт подтверждения на продолжение.
|
5. ждёт подтверждения на продолжение.
|
||||||
|
|
||||||
Коммит и пуш делает разработчик. После пуша агент проверяет результат CI и
|
Коммит и пуш делает разработчик. Следующий шаг начинается только после ревью
|
||||||
сообщает его вместе с локальными проверками, включая случай, когда нужный job не
|
текущего изменения; после пуша результат CI читается по процедуре из
|
||||||
запускался: статусы смотрятся по отдельным job, потому что общий conclusion
|
`docs/decisions.md`, раздел 10.
|
||||||
прогона с `continue-on-error` у `e2e` остаётся зелёным. Процедура описана в
|
|
||||||
`docs/quality-gates.md`.
|
|
||||||
|
|
||||||
Следующий шаг начинается только после ревью текущего изменения.
|
|
||||||
|
|||||||
@@ -0,0 +1,172 @@
|
|||||||
|
# Архитектура
|
||||||
|
|
||||||
|
> Статус: **active**. Описание текущего кода: границы модулей, поток данных от
|
||||||
|
> реестра до скачивания, точки расширения. Канонический источник истины — сам
|
||||||
|
> код; этот документ объясняет, **куда смотреть и что с чем связано**, и не
|
||||||
|
> дублирует сигнатуры.
|
||||||
|
|
||||||
|
## 1. Слои
|
||||||
|
|
||||||
|
```txt
|
||||||
|
web/src/lib/
|
||||||
|
registry/ # источник истины: записи ToolEntry
|
||||||
|
registry-schema.ts # декларативная схема параметров + санитизация дефолтов
|
||||||
|
catalog.ts # группировка и счётчики для /list-tools
|
||||||
|
categories.ts # id/названия категорий
|
||||||
|
tool-icons.ts # id → иконка
|
||||||
|
core/ # чистый TS: операции над ImageData, без DOM
|
||||||
|
executor/ # единственная точка исполнения инструментов
|
||||||
|
i18n/ # словари ru/en, t(), списки для i18n-правил линтера
|
||||||
|
components/ # UI (fields/, layout/, ui/, schema-компоненты)
|
||||||
|
zip.ts # сборка мультифайлового результата (1 → many)
|
||||||
|
theme.svelte.ts # light/dark + localStorage
|
||||||
|
web/src/routes/
|
||||||
|
+page.svelte # главная
|
||||||
|
list-tools/ # каталог
|
||||||
|
tools/[id]/ # страница инструмента
|
||||||
|
kit/ # витрина компонентов (production, но исключена из i18n-гейта)
|
||||||
|
```
|
||||||
|
|
||||||
|
Правило зависимостей: `components/**` и `routes/**` знают про `registry/**`,
|
||||||
|
`executor/**` и `core/**`; `core/**` не знает ни о ком и не зависит от DOM;
|
||||||
|
`registry/**` описывает данные и знает только `core/**` и `registry-schema.ts`.
|
||||||
|
Нарушение этого порядка — повод не заводить импорт, а инвертировать зависимость.
|
||||||
|
|
||||||
|
## 2. Поток данных
|
||||||
|
|
||||||
|
```txt
|
||||||
|
registry/*.ts ──(TOOLS: ToolEntry[])
|
||||||
|
│
|
||||||
|
├─► catalog.ts ──► routes/list-tools (каталог, поиск, фильтры)
|
||||||
|
│
|
||||||
|
└─► routes/tools/[id]/+page.svelte
|
||||||
|
│ getTool(id) → запись реестра
|
||||||
|
▼
|
||||||
|
SchemaToolView.svelte оркестратор страницы
|
||||||
|
│ schema = tool.schema
|
||||||
|
├─► SchemaFields.svelte поля из schema.fields (FIELDS: kind → контрол)
|
||||||
|
├─► SchemaSourceTile/SchemaTextSource.svelte вход
|
||||||
|
└─► SchemaPreview + schema-preview-model.ts вид превью
|
||||||
|
│ buildSchemaPreviewModel(inputMode, resultKind, …) → source/result/hasResult
|
||||||
|
▼
|
||||||
|
executor.execute(ctx) единственная точка исполнения
|
||||||
|
│ сериализация параметров → worker → worker-handler → tool.run(ctx)
|
||||||
|
▼
|
||||||
|
core/* пиксели
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
SchemaResultTile / SchemaTextResult + SchemaActions (download/copy)
|
||||||
|
│ кодирование по ToolEntry.output (mime/ext/quality)
|
||||||
|
└─► zip.ts, если result === "files"
|
||||||
|
```
|
||||||
|
|
||||||
|
Ключевые решения:
|
||||||
|
|
||||||
|
- **Реестр — единственный источник истины.** Страница, форма и пайплайн строятся
|
||||||
|
из записи; новый инструмент = новая запись + функция `run`.
|
||||||
|
- **Исполнение идёт только через `executor.execute`.** Компоненты не зовут
|
||||||
|
`tool.run` напрямую — иначе нельзя перенести тяжёлое в worker/wasm без правки
|
||||||
|
вызывающего кода.
|
||||||
|
- **Форма параметров не знает про инструмент.** Она рендерится из
|
||||||
|
`registry-schema.ts`, поэтому добавление вида поля не трогает страницы.
|
||||||
|
- **Модель превью отделена от вёрстки.** `schema-preview-model.ts` — чистая
|
||||||
|
функция, покрыта unit-тестами; `SchemaPreview.svelte` только рисует её вывод.
|
||||||
|
|
||||||
|
## 3. Запись реестра
|
||||||
|
|
||||||
|
`ToolEntry` (`web/src/lib/registry/types.ts`):
|
||||||
|
|
||||||
|
| Поле | Смысл |
|
||||||
|
| ------------- | ------------------------------------------------------------ |
|
||||||
|
| `id` | уникальный ключ; он же URL-anchor и ключ в пайплайне |
|
||||||
|
| `title` | EN-строка реестра, переводится через `tools.*.title` |
|
||||||
|
| `description` | EN-строка реестра, переводится через `tools.*.description` |
|
||||||
|
| `category` | id из `categories.ts` |
|
||||||
|
| `schema` | `toolSchema({ fields, layout?, meta? })` |
|
||||||
|
| `input` | `"image" \| "text" \| "none"` — что ждёт инструмент на входе |
|
||||||
|
| `result` | `"image" \| "text" \| "verdict" \| "files"` — что отдаёт |
|
||||||
|
| `output` | mime/ext/quality для download (не у всех инструментов) |
|
||||||
|
| `run(ctx)` | чистая функция: `ctx` → `ToolResult` |
|
||||||
|
|
||||||
|
Хелперы `genTool` / `imgTool` / `textGen` из `types.ts` задают `run` для типовых
|
||||||
|
случаев (приведение типа результата), чтобы запись не повторяла обвязку. `TOOLS`
|
||||||
|
собирается в `registry/index.ts` из восьми групп; `getTool(id)` — единственный
|
||||||
|
способ достать запись по id.
|
||||||
|
|
||||||
|
## 4. Схема параметров
|
||||||
|
|
||||||
|
`registry-schema.ts` — единственное место, где описаны виды полей.
|
||||||
|
|
||||||
|
- **Виды поля** (`fieldSpecs`): `number`, `slider`, `color`, `select`, `text`,
|
||||||
|
`checkbox`, `dimension`, `color-pair`, `colors`, `offset`, `position9`,
|
||||||
|
`font-style`, `plate`, `gradient`.
|
||||||
|
- **Фабрика `field.*`** создаёт `Field<T>`; UI-контрол подбирается в
|
||||||
|
`SchemaFields.svelte` по `spec.kind` через карту `FIELDS`.
|
||||||
|
- **`schema.layout`** группирует поля (`resolveLayoutGroups`); раскладка
|
||||||
|
разделов — единственное место, где живёт порядок и группировка.
|
||||||
|
- **Значения по умолчанию** даёт `defaultSchemaParams`; `applySourceDefaults`
|
||||||
|
подставляет размеры текущего source; `sanitizeSchemaParams` приводит мусор к
|
||||||
|
допустимому виду. Правка поведения полей идёт здесь, а не в компоненте.
|
||||||
|
|
||||||
|
Новый вид поля требует: спека + фабрика `field.*` → запись в `fieldSpecs` →
|
||||||
|
контрол в `components/fields/schema/` → ключ в `FIELDS` → дефолт и санитизация →
|
||||||
|
i18n-подписи → тест в `registry-schema.test.ts`.
|
||||||
|
|
||||||
|
## 5. Исполнение
|
||||||
|
|
||||||
|
`executor/executor.ts` — фабрика `createExecutor(factories?)` плюс готовый
|
||||||
|
экспорт `execute`. Контракт асинхронный (`Promise`), параметры сериализуются,
|
||||||
|
исполнение уходит в `executor.worker.ts`; `worker-handler.ts` разбирает
|
||||||
|
сообщение и зовёт `run`. Если worker недоступен — прямой вызов.
|
||||||
|
|
||||||
|
Отсюда следует: подключение wasm позже означает замену реализации **внутри**
|
||||||
|
исполнителя, а не правку компонентов.
|
||||||
|
|
||||||
|
## 6. i18n
|
||||||
|
|
||||||
|
Словари — `web/src/lib/i18n/` (`en.ts`, `ru.ts`, общий `dict.ts` с
|
||||||
|
`LOCALES`/`BASE_LOCALE`). Пользовательский текст живёт только в словарях и
|
||||||
|
читается через `t()`; строки реестра (`title`/`description`) — источник для
|
||||||
|
EN-ключей `tools.*` и словарь-переводчик. Инварианты паритета ключей,
|
||||||
|
плейсхолдеров и запрет хардкода проверяются линтером, детали —
|
||||||
|
`web/eslint-plugins/README.md`.
|
||||||
|
|
||||||
|
## 7. Точки расширения
|
||||||
|
|
||||||
|
| Задача | Где править |
|
||||||
|
| ------------------------------ | ----------------------------------------------------------------- |
|
||||||
|
| Новый инструмент | запись в `registry/<group>.ts` + i18n-ключи + тесты (рецепт ниже) |
|
||||||
|
| Новый вид поля | `registry-schema.ts` + контрол + `FIELDS` (раздел 4) |
|
||||||
|
| Новый маршрут/страница | `web/src/routes/**` + ссылка в каталоге |
|
||||||
|
| Новый визуальный элемент | компонент в `components/**` по образцу `ui/` или `layout/` |
|
||||||
|
| Новый дизайн-токен | `web/src/app.css` (только там) + потребитель сразу |
|
||||||
|
| Тяжёлая операция → worker/wasm | `executor/**`, без правок компонентов |
|
||||||
|
| Мультифайловый результат | `result: "files"` + `zip.ts` |
|
||||||
|
|
||||||
|
## 8. Рецепт: добавить инструмент
|
||||||
|
|
||||||
|
1. **Найти соседей.** Поиск по ключу в `registry/<group>.ts` и по всему
|
||||||
|
`web/src/` — не заводить дублирующую константу, тип или утилиту.
|
||||||
|
2. **Выбрать группу** (`geometry`/`alpha`/`color`/`convert`/`filters`/
|
||||||
|
`generate`/`text`/`analyze`) и положить запись в её экспорт; при новой
|
||||||
|
категории — сначала `categories.ts`.
|
||||||
|
3. **Описать схему** через `field.*`, задать `layout`, если поля нужно
|
||||||
|
сгруппировать.
|
||||||
|
4. **Написать `run`** поверх `core/*`; пиксельные операции — чистые функции в
|
||||||
|
`core/`, без DOM.
|
||||||
|
5. **Проверить обязательные контракты** для нового id: entry попадает в `TOOLS`,
|
||||||
|
страница `/tools/<id>` prerender-ится, `input`/`result` согласованы с тем,
|
||||||
|
что делает `run`.
|
||||||
|
6. **Добавить i18n-ключи во все локали** сразу.
|
||||||
|
7. **Тесты:** unit — в `registry/*.test.ts` или рядом с `core`-функцией;
|
||||||
|
e2e-smoke — существующий spec по типу `input`/`result` (правила —
|
||||||
|
`docs/testing-strategy.md`).
|
||||||
|
8. **Обновить `docs/tools-map.md`:** новый инструмент обязателен в разделе
|
||||||
|
«Реализовано», формат строки — в шапке раздела. Проверяет
|
||||||
|
`web/src/lib/registry/tools-map-doc.test.ts`: отсутствующий в карте id и
|
||||||
|
несуществующий в реестре id роняют тест. Обоснование и границы автоматизации
|
||||||
|
— `docs/decisions.md`, раздел 4.
|
||||||
|
|
||||||
|
Ожидаемый минимум проверок: `pnpm --dir web check` и `pnpm --dir web test`; для
|
||||||
|
инструмента, меняющего пользовательский поток, — e2e (см. раздел 3
|
||||||
|
`docs/quality-gates.md`).
|
||||||
+12
-11
@@ -10,7 +10,10 @@
|
|||||||
актуальных правил используются `AGENTS.md`, `docs/README.md` и активные планы.
|
актуальных правил используются `AGENTS.md`, `docs/README.md` и активные планы.
|
||||||
|
|
||||||
Если решение снова понадобится, его можно восстановить из git history или
|
Если решение снова понадобится, его можно восстановить из git history или
|
||||||
создать новый активный документ.
|
создать новый активный документ: **архивные файлы удаляются, а не хранятся
|
||||||
|
вечно.** Удалённые планы восстановимы через
|
||||||
|
`git log --diff-filter=D -- name-only -- docs/archive/` и
|
||||||
|
`git show <commit>^:<path>`.
|
||||||
|
|
||||||
## Обязательные метаданные
|
## Обязательные метаданные
|
||||||
|
|
||||||
@@ -24,19 +27,18 @@
|
|||||||
> Использование: только историческая справка
|
> Использование: только историческая справка
|
||||||
```
|
```
|
||||||
|
|
||||||
Дата `Удалить после` — дата архивирования плюс 30 дней. Старые файлы, созданные
|
Дата `Удалить после` — дата архивирования плюс 30 дней.
|
||||||
до введения политики, могут не иметь этих дат; они также считаются архивными и
|
|
||||||
не являются активными требованиями. При следующем касании их можно удалить или
|
|
||||||
привести к шаблону.
|
|
||||||
|
|
||||||
## Порядок архивирования
|
## Порядок архивирования
|
||||||
|
|
||||||
1. Убедиться, что документ действительно завершён, отменён или заменён более
|
1. Убедиться, что документ действительно завершён, отменён или заменён более
|
||||||
актуальным источником.
|
актуальным источником.
|
||||||
2. Добавить обязательные метаданные.
|
2. Убрать из него ссылки на несуществующие пути и артефакты прошлых волн.
|
||||||
3. Переместить файл в `docs/archive/`.
|
3. Добавить обязательные метаданные.
|
||||||
4. Проверить ссылки из backlog, планов, checklist и `docs/README.md`.
|
4. Переместить файл в `docs/archive/`.
|
||||||
5. Не менять архивный документ под новые требования.
|
5. Проверить ссылки из backlog, планов, checklist и `docs/README.md`: удалённые
|
||||||
|
файлы должны потерять и входящие ссылки.
|
||||||
|
6. Не менять архивный документ под новые требования.
|
||||||
|
|
||||||
## Удаление
|
## Удаление
|
||||||
|
|
||||||
@@ -44,8 +46,7 @@
|
|||||||
|
|
||||||
- проверить, что документ не нужен для текущей работы;
|
- проверить, что документ не нужен для текущей работы;
|
||||||
- найти и удалить оставшиеся ссылки на него из активных документов;
|
- найти и удалить оставшиеся ссылки на него из активных документов;
|
||||||
- удалить файл из `docs/archive/`;
|
- удалить файл из `docs/archive/`.
|
||||||
- при необходимости отметить удаление в истории изменений.
|
|
||||||
|
|
||||||
До истечения срока архивный файл не превращается обратно в live-документ: если
|
До истечения срока архивный файл не превращается обратно в live-документ: если
|
||||||
он снова актуален, создаётся новый документ или возвращается версия из git
|
он снова актуален, создаётся новый документ или возвращается версия из git
|
||||||
|
|||||||
@@ -1,118 +0,0 @@
|
|||||||
# Demo fidelity: /preview/demo vs refs-html/demo.html
|
|
||||||
|
|
||||||
> ⚠️ **УТРАТИЛ АКТУАЛЬНОСТЬ — архивная копия.** Актуальный план фикса верности и
|
|
||||||
> текущий статус пунктов B/F — в `docs/plan-design-fix.md` (Этап 1). Аудит
|
|
||||||
> верности теперь автоматизирован (`pnpm audit` → `web/audit/audit-report.md`;
|
|
||||||
> пиксель-дифф не используется); темы в `refs-html` переключаются (скрипт
|
|
||||||
> инжектится в `scripts/extract-static.mjs`). Ниже — исходный текст на момент
|
|
||||||
> написания.
|
|
||||||
|
|
||||||
Метод: Playwright, viewport 1440×900, скриншоты обеих страниц + замер
|
|
||||||
вычисленных стилей ключевых элементов + пиксель-дифф (без чтения картинки
|
|
||||||
моделью). Пиксель-дифф: **24.9%** (наш скролл 1731px, реф 1579px). Зоны различий
|
|
||||||
размазаны по всей высоте → править надо глобально, не точечно.
|
|
||||||
|
|
||||||
Теперь процесс автоматизирован: `pnpm audit` (Playwright) пишет
|
|
||||||
`web/audit/audit-report.md` — light/dark токены + поэлементный дифф стилей по
|
|
||||||
тексту. Пиксель-дифф убран (неудобен для работы). Фиксы можно делать в любой
|
|
||||||
момент и перепроверять аудитом.
|
|
||||||
|
|
||||||
## Баги (объективно, правим в любом случае)
|
|
||||||
|
|
||||||
- **B1. Кнопки/инпуты рендерятся шрифтом Arial, а не IBM Plex Sans.**
|
|
||||||
Доказательство: пробы `Reset pipeline` → `fontFamily: Arial`. Причина: у
|
|
||||||
`button/input` не задан `font-family: inherit`. Фикс: добавить в reset
|
|
||||||
`design2.css` `button, input, select, textarea { font-family: inherit; }`.
|
|
||||||
Влияет на все страницы. (P1)
|
|
||||||
- **B2. Двойной топбар.** Страница демо рендерит свой `.topbar` (brand / AUTO
|
|
||||||
PIPELINE / help / theme / lang), который перекрывается глобальным `fixed`
|
|
||||||
Header из `preview/+layout.svelte` (в нём только theme-toggle). В рефе — один
|
|
||||||
топбар. Контент демо-топбара (brand, статус, язык) визуально спрятан за
|
|
||||||
фикс-хедером. Фикс: убрать кастомный `.topbar` из демо и сделать хедер лейаута
|
|
||||||
реф-стайл топбаром (brand + version + status + help + theme + lang) — улучшит
|
|
||||||
и C9/C10. (P1)
|
|
||||||
|
|
||||||
## Расхождения дизайна (нужно решение)
|
|
||||||
|
|
||||||
- **F1. Фон холста.** Реф: плоский серый `#EEF1F4`. Наш: белый +
|
|
||||||
blueprint-сетка. Решение: совпасть с рефом (убрать сетку на демо) или оставить
|
|
||||||
сетку как сигнатуру нового дизайна.
|
|
||||||
- **F2. Бейдж типа шага** (BACKGROUND/TRANSFORM/STYLE). Реф: приглушённый серый
|
|
||||||
`#6D787F`. Решено C16: `StepCard` рендерит `type` как приглушённый моно-текст
|
|
||||||
(не синий бейдж); индекс шага (`step-index`) — синий `#1769D2`, как в рефе.
|
|
||||||
- **F3. Подписи превью-тайлов** (SOURCE / STEP 01 / FINAL OUTPUT). Реф: синий
|
|
||||||
`#1769D2`, weight 400. Решено C16: `PreviewTile` label — синий `#1769D2`.
|
|
||||||
- **F4. Раскладка preview-stack.** Реф: одна колонка (5 строк, выше). Наш: 2
|
|
||||||
колонки (grid). Решение: одна колонка (как реф) или оставить 2 колонки как
|
|
||||||
выбор нового дизайна.
|
|
||||||
- **F5. Вес лейблов контролов.** Реф: 400. Наш (`MonoLabel`): 500. Мелко.
|
|
||||||
- **F6. Мета-подписи** (DIMENSIONS/FORMAT/SIZE): реф fs 9px, наш 10px. Мелко.
|
|
||||||
- **F7. Копирайт:** `Add tool` у нас `+ Add tool`; кнопка `Download result` —
|
|
||||||
сверить стиль (в рефе синяя с белым текстом 478×42). Пробы
|
|
||||||
`Download result`/`Add tool`/`DIRECTION` вернули NOT FOUND — скорее артефакт
|
|
||||||
замера (вложенный текст/чип размера), проверить визуально.
|
|
||||||
- **F8. h1 line-height** чуть выше (наш 150px vs реф 115px) — мелкий зазор.
|
|
||||||
- **F9. Вертикальный ритм:** контент на ~152px выше, зазоры между секциями
|
|
||||||
отличаются (pipeline-head y 434 vs 400 и т.д.) — подровнять паддинги под реф.
|
|
||||||
- **F10. Внутренние отступы preview-панели** (Hide intermediate x 907 vs 1218) —
|
|
||||||
паддинг панели.
|
|
||||||
|
|
||||||
## Темы (отложено — F1-F4)
|
|
||||||
|
|
||||||
Серый фон `#EEF1F4` — это **светлая** тема. Уточнение по F1: наш токен
|
|
||||||
`--background` уже равен `#EEF1F4` (совпадает с рефом); видимое отличие — только
|
|
||||||
blueprint-сетка поверх фона, а не «белый vs серый». То есть F1 = «сетка против
|
|
||||||
плоского», не цвет фона.
|
|
||||||
|
|
||||||
Перед тем как сверять цвета бейджей/тайлов/раскладок (F2-F4), нужно сначала
|
|
||||||
зафиксировать палитры light/dark как единый источник токенов. Сейчас они есть в
|
|
||||||
`web/src/lib/styles/design2.css` (`:root` + `[data-theme="dark"]`), но не
|
|
||||||
сверены с рефом.
|
|
||||||
|
|
||||||
**Как проверить темы в refs-html:**
|
|
||||||
|
|
||||||
- Открыть `refs-html/demo.html` (и соседние `gradient.html`,
|
|
||||||
`background-remover.html`) через Playwright/браузер.
|
|
||||||
- Нажать theme-toggle в топбаре рефа (иконка луны/солнца) — переключает тему
|
|
||||||
(скорее всего меняет атрибут/класс на `<html>` или `.page` и переопределяет
|
|
||||||
CSS-переменные).
|
|
||||||
- Снять скриншоты светлой и тёмной темы, замерить computed-стиль `:root`
|
|
||||||
переменных рефа (через
|
|
||||||
`getComputedStyle(document.documentElement).getPropertyValue('--...')`) и
|
|
||||||
сопоставить с нашими токенами в `design2.css`.
|
|
||||||
- Рефы, похоже, не доделаны по тёмной теме — зафиксировать, какие именно токены
|
|
||||||
меняются и где расхождения. На основе этого выровнять наши токены, затем
|
|
||||||
вернуться к F2 (бейдж типа), F3 (тайлы), F4 (раскладка preview-stack).
|
|
||||||
|
|
||||||
Решение по F1-F4 принимается только после сверки токенов светлой/тёмной темы с
|
|
||||||
рефом.
|
|
||||||
|
|
||||||
**Состояние тем (уточнение):** наша dark-тема реализована в `design2.css`
|
|
||||||
(`[data-theme="dark"]`), но пока подключена только на preview-маршрутах (старые
|
|
||||||
страницы грузят `app.css`). Этого достаточно для сверки: токены, отличные от
|
|
||||||
light, соберём по всем `refs-html/*.html` (не только `demo.html`), остальные
|
|
||||||
токены не меняются. Light-токены уже совпадают с рефом (`--background`,
|
|
||||||
`--panel`, `--foreground`, `--muted`, `--line`, `--blue` равны).
|
|
||||||
|
|
||||||
## Состояние (C16)
|
|
||||||
|
|
||||||
Цветовые расхождения против `refs-html/demo.html` устранены: стек шрифтов сверен
|
|
||||||
с рефом (`"IBM Plex Sans", sans-serif` / `"IBM Plex Mono", monospace`), бренд
|
|
||||||
топбара — `--foreground`, индекс шага — синий, тип шага и подпись плитки — как в
|
|
||||||
рефе, `Segmented` — 10px/32px. Аудит: токены light 10 / dark 16, элементы 47.
|
|
||||||
Остаток — sub-pixel (10px→9px лейблы, letter-spacing) и структурные
|
|
||||||
(`display`/`rect` от наших flex/grid раскладок против статичного мока);
|
|
||||||
визуально незначимы, правим точечно при желании. Старые тесты (583) — без
|
|
||||||
регрессий.
|
|
||||||
|
|
||||||
## План действий (предлагаю)
|
|
||||||
|
|
||||||
> Актуальный пошаговый план фикса вынесен в `docs/plan-design-fix.md` (Этапы
|
|
||||||
> 0–1: рефакторинг + верность). Ниже — исходные наброски.
|
|
||||||
|
|
||||||
1. Сразу поправить B1 + B2 (чёткие баги, улучшают все страницы).
|
|
||||||
2. По F1–F4 — решение за тобой (совпадать с рефом дословно ИЛИ оставить как
|
|
||||||
сигнатуру нового дизайна).
|
|
||||||
3. F5–F10 — мелочи, подчистить в том же проходе, если решаем «совпадать с
|
|
||||||
рефом».
|
|
||||||
4. Переснять скриншоты и перепроверить пиксель-дифф.
|
|
||||||
@@ -1,140 +0,0 @@
|
|||||||
# План: единая дизайн-система
|
|
||||||
|
|
||||||
> **СТАТУС: ВЫПОЛНЕН 2026-08-22.**
|
|
||||||
|
|
||||||
## 1. Цели и принципы
|
|
||||||
|
|
||||||
1. **Один источник правды** — все визуальные константы живут в токенах
|
|
||||||
`web/src/app.css`; ни один компонент не пишет «магические» цвета/размеры
|
|
||||||
напрямую.
|
|
||||||
2. **Никакого дублирования** — повторяющийся визуальный рецепт оформляется один
|
|
||||||
раз: либо токен, либо глобальный утилитарный класс, либо компонент в `ui/`.
|
|
||||||
3. **Простые компоненты важнее умных** — если проще сделать отдельные
|
|
||||||
`SliderField` и `TextField`, делаем два маленьких компонента, а не один
|
|
||||||
конфигурируемый монстр.
|
|
||||||
4. **Scoped-стили — только для раскладки** конкретного компонента (сетка,
|
|
||||||
отступы между блоками). Внешний вид контролов и текстов — из дизайн-системы.
|
|
||||||
|
|
||||||
## 2. Аудит: что дублируется прямо сейчас
|
|
||||||
|
|
||||||
- **«Приглушённый текст»** (`--text-muted` + размер 0.85–0.95rem) — повторён в
|
|
||||||
семи местах: `ToolPage` (.description, .hint), главная (.lead, .card-desc),
|
|
||||||
лейаут (.nav-link, footer), `Preview` (.dims), `ParamForm` (label),
|
|
||||||
`InfoPanel` (dt).
|
|
||||||
- **«Заголовок секции капсом»** (uppercase, letter-spacing, muted) — точная
|
|
||||||
копипаста в `ToolPage` и на главной.
|
|
||||||
- **Базовые инпуты** (padding, border, radius) — живут только внутри
|
|
||||||
`ParamForm`; любые новые типы контролов (слайдеры, пипетка цвета) пришлось бы
|
|
||||||
копировать вручную.
|
|
||||||
- **Блок «label + отступы»** — дублировался бы в каждом `*Field`; решается
|
|
||||||
обёрткой `Field.svelte`.
|
|
||||||
- **«Панель»** (surface + border + radius) — три ручные реализации одного
|
|
||||||
паттерна: `DropZone`, карточки главной, строки `InfoPanel`.
|
|
||||||
- **Кнопки** — глобальные классы `.primary`/`.secondary` без компонента;
|
|
||||||
состояния «занят/недоступен» каждый описывает заново.
|
|
||||||
- **Ошибка-баннер** — только в `ToolPage`, а понадобится минимум ещё раз в
|
|
||||||
workspace.
|
|
||||||
|
|
||||||
## 3. Токены (расширение `app.css`)
|
|
||||||
|
|
||||||
Существующие остаются; добавляются недостающие группы:
|
|
||||||
|
|
||||||
```css
|
|
||||||
:root {
|
|
||||||
/* типографика */
|
|
||||||
--text-xs: 0.8rem;
|
|
||||||
--text-s: 0.85rem;
|
|
||||||
--text-m: 0.95rem;
|
|
||||||
--text-l: 1.1rem;
|
|
||||||
--text-xl: 1.25rem;
|
|
||||||
--text-xxl: 1.75rem;
|
|
||||||
--leading-tight: 1.25;
|
|
||||||
|
|
||||||
/* тени */
|
|
||||||
--shadow-card: 0 4px 14px rgb(37 99 235 / 12%);
|
|
||||||
|
|
||||||
/* фокус */
|
|
||||||
--focus-ring: 2px solid var(--accent);
|
|
||||||
|
|
||||||
/* контрольные размеры контролов */
|
|
||||||
--control-height: 2.4rem;
|
|
||||||
--control-max-width: 16rem;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Правило именования: семантика, не значение (`--text-muted`, а не `--gray-500`).
|
|
||||||
|
|
||||||
## 4. Глобальные утилиты (`app.css`)
|
|
||||||
|
|
||||||
Только то, что не требует JS и не является интерактивным:
|
|
||||||
|
|
||||||
- `.text-muted` — приглушённый текст (размер наследуется);
|
|
||||||
- `.text-caption` — мелкий текст `--text-s`;
|
|
||||||
- `.heading-section` — заголовок секции капсом (вместо двух копипаст);
|
|
||||||
- `.panel` — поверхность с бордером/радиусом (DropZone, карточки,
|
|
||||||
InfoPanel-строки);
|
|
||||||
- `.error-banner` — баннер ошибки;
|
|
||||||
- `.visually-hidden` — скрытый, но читаемый скринридерами.
|
|
||||||
|
|
||||||
Элементные стили из текущего `app.css` (голые `input, select { ... }`) убрать —
|
|
||||||
внешний вид контролов переезжает в компоненты `ui/`.
|
|
||||||
|
|
||||||
## 5. Состав UI-компонентов (`web/src/lib/components/ui/`)
|
|
||||||
|
|
||||||
Все — тонкие обёртки над нативными элементами, без состояний кроме своих:
|
|
||||||
|
|
||||||
| Компонент | Пропы | Заменяет |
|
|
||||||
| ---------------------- | -------------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
|
|
||||||
| `Field.svelte` | `id`, `label`, `hint?`, слот контрола | label + раскладка поля; единая точка для hint'ов и ошибок валидации |
|
|
||||||
| `Button.svelte` | `variant: 'primary' \| 'secondary'`, `disabled`, `busy`, слот текста | глобальные `.primary`/`.secondary`, локальный busy в `DownloadButton` |
|
|
||||||
| `TextField.svelte` | `id`, `label`, `type: 'number' \| 'text'`, `min/max/step`, `value = $bindable()` | number/text ветки `ParamForm` |
|
|
||||||
| `SliderField.svelte` | `id`, `label`, `min/max/step`, `value = $bindable()`, показ значения рядом | ничего — готовая база для будущих слайдеров (яркость, качество, порог) |
|
|
||||||
| `SelectField.svelte` | `id`, `label`, `options`, `value = $bindable()` | select-ветка `ParamForm` |
|
|
||||||
| `CheckboxField.svelte` | `id`, `label`, `checked = $bindable()` | чекбокс `ParamForm` |
|
|
||||||
| `ColorField.svelte` | `id`, `label`, `value = $bindable()` | color-ветка `ParamForm`; точка будущего расширения пипеткой |
|
|
||||||
| `EmptyState.svelte` | `title`, `hint`, слот действия | «Обработка…», будущие пустые панели исходника/результата |
|
|
||||||
|
|
||||||
`Field` — чисто раскладочная обёртка
|
|
||||||
(`<div class="field"><label for>…</label><slot /></div>`), ничего не знает о
|
|
||||||
типе контрола. На ней построены `TextField`, `SliderField`, `SelectField`,
|
|
||||||
`ColorField`. Исключение — `CheckboxField`: у чекбокса label оборачивает контрол
|
|
||||||
(кликабельная строка), поэтому у него своя раскладка.
|
|
||||||
|
|
||||||
## 6. Миграция существующего
|
|
||||||
|
|
||||||
1. `ParamForm` — переписывается на `Field` + `*Field`-компоненты; собственная
|
|
||||||
стилизация контролов и label удаляется.
|
|
||||||
2. `DownloadButton` и кнопки `ToolPage` — на `Button`.
|
|
||||||
3. `DropZone`, карточки главной, строки `InfoPanel` — на `.panel` + свои
|
|
||||||
раскладочные стили.
|
|
||||||
4. `ToolPage`, главная, лейаут — приглушённые тексты на `.text-*`, заголовки
|
|
||||||
секций на `.heading-section`, баннер на `.error-banner`.
|
|
||||||
5. Из `app.css` удалить глобальные классы кнопок и голые стили инпутов после
|
|
||||||
перехода.
|
|
||||||
|
|
||||||
## 7. Порядок работ
|
|
||||||
|
|
||||||
| Этап | Содержимое | Проверка |
|
|
||||||
| ------------ | ----------------------------------------------------- | -------------------------------------------------------------------- |
|
|
||||||
| A. Фундамент | Токены + утилиты в `app.css` | `pnpm check` |
|
|
||||||
| B. Примитивы | Восемь компонентов в `components/ui/` | `pnpm check`; каждый используется хотя бы раз после этапа C |
|
|
||||||
| C. Миграция | Переделка пяти файлов из §6 | Визуально: страницы выглядят как раньше; `test/check/build` зелёные |
|
|
||||||
| D. Чистка | Удаление устаревших классов и дублей из scoped-стилей | grep: в `components/` нет hex-цветов и `var(--space…)` вне раскладки |
|
|
||||||
|
|
||||||
## 8. Критерии готовности
|
|
||||||
|
|
||||||
- Ни один компонент не задаёт цвет/радиус/шрифт мимо токенов или классов
|
|
||||||
системы.
|
|
||||||
- Все интерактивные контролы на страницах — экземпляры `ui/*`.
|
|
||||||
- `grep -E '#[0-9a-f]{3,6}' web/src/lib/components` не находит ничего, кроме
|
|
||||||
токенов в `app.css`.
|
|
||||||
- Визуальной регрессии нет (ручная сверка всех страниц до/после), тесты и билд
|
|
||||||
зелёные.
|
|
||||||
|
|
||||||
## 9. Что сознательно не делаем
|
|
||||||
|
|
||||||
- Никаких CSS-фреймворков и препроцессоров — обычный CSS, как решено на старте.
|
|
||||||
- Никаких тем/тёмного режима — но токены сразу семантические, чтобы тема
|
|
||||||
добавилась заменой значений `:root`.
|
|
||||||
- Никаких сложных составных компонентов (модалки, таблицы) — они появятся, когда
|
|
||||||
потребуется реальная задача.
|
|
||||||
@@ -1,237 +0,0 @@
|
|||||||
# План начальной реализации: минимальный рабочий MVP
|
|
||||||
|
|
||||||
> **СТАТУС: ВЫПОЛНЕН 2026-08-22.**
|
|
||||||
>
|
|
||||||
> **Что сделано по плану:**
|
|
||||||
>
|
|
||||||
> - Шаг 0 — каркас: SvelteKit + adapter-static, design-токены на CSS, лейаут с
|
|
||||||
> навигацией по категориям ✔
|
|
||||||
> - Шаг 1 — ядро: `PixelImage`, декодирование файлов, кодирование PNG/JPEG/WebP,
|
|
||||||
> скачивание ✔
|
|
||||||
> - Шаг 2 — операции ядра: color / geometry / alpha / analyze, чистые функции
|
|
||||||
> без DOM ✔
|
|
||||||
> - Шаг 3 — реестр из 11 инструментов + санитизация параметров ✔
|
|
||||||
> - Шаг 4 — универсальные UI-компоненты (DropZone, Preview, ParamForm,
|
|
||||||
> DownloadButton, InfoPanel, ToolPage) ✔
|
|
||||||
> - Шаг 5 — маршруты: `/tools/[id]` с prerender всех страниц, главная-каталог по
|
|
||||||
> категориям ✔
|
|
||||||
> - Шаги 6–7 — обработка ошибок, состояния, полировка; верификация ✔
|
|
||||||
>
|
|
||||||
> **Checkpoint пройден:** все 11 инструментов вручную проверены в браузере
|
|
||||||
> (загрузка → применение → скачивание), включая найденный и исправленный баг
|
|
||||||
> resize с сохранением пропорций. Тесты 75/75, svelte-check 0 ошибок,
|
|
||||||
> статический экспорт собирается.
|
|
||||||
>
|
|
||||||
> **Попутные улучшения сверх плана:** корневой скрипт `pnpm dev`;
|
|
||||||
> `sanitizeParams` против пустых/невалидных значений полей; семантика «вписать в
|
|
||||||
> размеры» для resize.
|
|
||||||
|
|
||||||
## 0. Зафиксированные решения
|
|
||||||
|
|
||||||
- **Объём MVP:** только одиночные инструменты «загрузил → настроил → скачал».
|
|
||||||
Пайплайн-workspace (`/workspace`) вне скоупа этого плана.
|
|
||||||
- **Набор инструментов первого среза:** ~11 базовых EASY (см. §3). Достаточно,
|
|
||||||
чтобы проверить насквозь ядро → реестр → универсальную страницу.
|
|
||||||
- **Стили:** обычный CSS без фреймворка — design-токены на CSS custom
|
|
||||||
properties, ноль зависимостей.
|
|
||||||
- **Стек:** SvelteKit + `@sveltejs/adapter-static` + TypeScript, пакетный
|
|
||||||
менеджер pnpm (уже зафиксирован в корневом `package.json`). Весь код сайта — в
|
|
||||||
`web/`.
|
|
||||||
- **Ядро без DOM:** все пиксельные операции — чистые функции над `PixelImage`;
|
|
||||||
DOM/canvas допускается только в модуле IO и компонентах превью.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Шаг 0 — Каркас проекта
|
|
||||||
|
|
||||||
1. Создать SvelteKit-приложение в `web/`: `npx sv create web` (шаблон minimal,
|
|
||||||
TypeScript, без лишних аддонов).
|
|
||||||
2. `pnpm install` внутри `web/`.
|
|
||||||
3. `svelte.config.js`: заменить адаптер на `@sveltejs/adapter-static`.
|
|
||||||
4. `src/routes/+layout.ts`: `export const prerender = true;` — весь сайт
|
|
||||||
prerenderится в статику.
|
|
||||||
5. `src/app.css`: дизайн-токены (палитра, типографика, отступы, радиусы через
|
|
||||||
CSS custom properties) + минимальный сброс стилей.
|
|
||||||
6. `src/routes/+layout.svelte`: шапка с названием сайта и навигацией по
|
|
||||||
категориям инструментов (категории — константа из реестра), футер.
|
|
||||||
|
|
||||||
**Проверка:** `pnpm dev` открывает пустой каркас; `pnpm build && pnpm preview`
|
|
||||||
отдаёт статический экспорт.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Шаг 1 — Ядро: типы и ввод-вывод
|
|
||||||
|
|
||||||
Файлы в `web/src/lib/core/`:
|
|
||||||
|
|
||||||
- `types.ts`
|
|
||||||
|
|
||||||
```ts
|
|
||||||
export type PixelImage = {
|
|
||||||
width: number;
|
|
||||||
height: number;
|
|
||||||
data: Uint8ClampedArray;
|
|
||||||
}; // RGBA, как ImageData
|
|
||||||
```
|
|
||||||
|
|
||||||
- `io.ts` — единственное место ядра, где разрешён canvas/DOM:
|
|
||||||
- `decodeFile(file: File): Promise<PixelImage>` — `createImageBitmap(file)` →
|
|
||||||
отрисовка на canvas → `getImageData`;
|
|
||||||
- `encode(img: PixelImage, mime: 'image/png' | 'image/jpeg' | 'image/webp', quality?: number): Promise<Blob>`
|
|
||||||
— `putImageData` → `canvas.toBlob`;
|
|
||||||
- `downloadBlob(blob: Blob, filename: string): void`.
|
|
||||||
|
|
||||||
**Проверка:** юнит-смоук — декодировать фикстурный PNG, закодировать обратно,
|
|
||||||
размеры совпадают.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Шаг 2 — Операции ядра (чистые функции)
|
|
||||||
|
|
||||||
Файлы в `web/src/lib/core/`, каждая функция: `(img, params) => new PixelImage`,
|
|
||||||
без мутации входа:
|
|
||||||
|
|
||||||
- `color.ts`: `grayscale(img)` (luma), `invert(img)`,
|
|
||||||
`brightnessContrast(img, brightness −100..100, contrast −100..100)`.
|
|
||||||
- `geometry.ts`: `flip(img, axis)`, `rotate90(img, turns)`,
|
|
||||||
`crop(img, x, y, w, h)` — индексная математика; `resize(img, w, h)` —
|
|
||||||
билинейная интерполяция на TS (не `drawImage`, чтобы ядро осталось DOM-free и
|
|
||||||
воспроизводимым бит-в-бит).
|
|
||||||
- `alpha.ts`: `removeColorToAlpha(img, hex, tolerancePercent)` — цветовое
|
|
||||||
расстояние → альфа 0.
|
|
||||||
- `analyze.ts`: `imageInfo(img)` → `{ width, height, hasAlpha, colorCount }`.
|
|
||||||
|
|
||||||
**Проверка:** vitest на маленьких матрицах пикселей (2×2, 3×3): flip, rotate,
|
|
||||||
crop, grayscale — ожидаемые значения руками.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Шаг 3 — Реестр инструментов
|
|
||||||
|
|
||||||
`web/src/lib/registry.ts` — источник истины для страниц, форм и навигации:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
export type ParamDef =
|
|
||||||
| {
|
|
||||||
id: string;
|
|
||||||
label: string;
|
|
||||||
type: "number";
|
|
||||||
min?: number;
|
|
||||||
max?: number;
|
|
||||||
step?: number;
|
|
||||||
default: number;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
id: string;
|
|
||||||
label: string;
|
|
||||||
type: "select";
|
|
||||||
options: { value: string; label: string }[];
|
|
||||||
default: string;
|
|
||||||
}
|
|
||||||
| { id: string; label: string; type: "checkbox"; default: boolean }
|
|
||||||
| { id: string; label: string; type: "color"; default: string };
|
|
||||||
|
|
||||||
export type ToolEntry = {
|
|
||||||
id: string; // slug маршрута
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
category: CategoryId;
|
|
||||||
params: ParamDef[];
|
|
||||||
run: (
|
|
||||||
img: PixelImage,
|
|
||||||
params: Record<string, unknown>,
|
|
||||||
) => Promise<PixelImage> | PixelImage;
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
Категории (константа там же): `convert`, `alpha`, `color`, `geometry`,
|
|
||||||
`analyze`.
|
|
||||||
|
|
||||||
### Состав первого среза — 11 инструментов
|
|
||||||
|
|
||||||
| # | id | Категория | Параметры |
|
|
||||||
| --- | -------------------------------- | --------- | ----------------------------------------------------- |
|
|
||||||
| 1 | `resize-png` | geometry | width, height, checkbox «сохранять пропорции» |
|
|
||||||
| 2 | `crop-png` | geometry | x, y, width, height |
|
|
||||||
| 3 | `rotate-png` | geometry | select: 90 / 180 / 270 |
|
|
||||||
| 4 | `flip-png` | geometry | select: horizontal / vertical |
|
|
||||||
| 5 | `grayscale-png` | color | — |
|
|
||||||
| 6 | `invert-colors-png` | color | — |
|
|
||||||
| 7 | `adjust-brightness-contrast-png` | color | brightness −100..100, contrast −100..100 |
|
|
||||||
| 8 | `convert-png-to-jpg` | convert | background color (подложка под альфу), quality 0..100 |
|
|
||||||
| 9 | `convert-png-to-webp` | convert | quality 0..100 |
|
|
||||||
| 10 | `remove-color-from-png` | alpha | hex color, tolerance % |
|
|
||||||
| 11 | `png-info` | analyze | — особый случай: не картинка, а текстовая сводка |
|
|
||||||
|
|
||||||
Для `png-info` в записи реестра добавляется флаг `resultType: 'info'`; остальные
|
|
||||||
— `'image'`.
|
|
||||||
|
|
||||||
**Проверка:** импорт реестра в тесте — все id уникальны, у каждого `params`
|
|
||||||
имеют дефолты, `run` определён.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Шаг 4 — Универсальные UI-компоненты
|
|
||||||
|
|
||||||
`web/src/lib/components/`:
|
|
||||||
|
|
||||||
- `DropZone.svelte` — drag&drop + клик; эмитит `File`; проверка MIME
|
|
||||||
(PNG/JPEG/WebP/GIF/BMP).
|
|
||||||
- `Preview.svelte` — рендер `PixelImage` на canvas с fit-масштабированием;
|
|
||||||
шахматный фон под прозрачность.
|
|
||||||
- `ParamForm.svelte` — генерирует контролы из `ParamDef[]`, двусторонний
|
|
||||||
`bind:values`.
|
|
||||||
- `DownloadButton.svelte` — encode выбранного формата + `downloadBlob`, имя
|
|
||||||
`<tool-id>.<ext>`.
|
|
||||||
- `InfoPanel.svelte` — текстовый вывод для `resultType: 'info'`.
|
|
||||||
- `ToolPage.svelte` — оркестратор страницы инструмента; состояния
|
|
||||||
`idle → loaded → processing → done | error`; раскладка: слева DropZone, справа
|
|
||||||
превью + форма параметров + кнопки «Применить» / «Скачать».
|
|
||||||
|
|
||||||
**Правило:** новый инструмент = новая запись в реестре (+ функция операции).
|
|
||||||
Компоненты и маршрут не трогаем.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Шаг 5 — Маршруты
|
|
||||||
|
|
||||||
- `/` (`src/routes/+page.svelte`) — сетка карточек инструментов, сгруппированных
|
|
||||||
по категориям; данные из реестра.
|
|
||||||
- `/tools/[id]` — универсальная страница:
|
|
||||||
- `+page.ts`: `entries()` возвращает все `id` из реестра (обязательное условие
|
|
||||||
prerender при adapter-static);
|
|
||||||
- `load()` возвращает запись реестра по `id`, неизвестный id → 404;
|
|
||||||
- `+page.svelte` — тонкая обёртка над `ToolPage`.
|
|
||||||
|
|
||||||
**Проверка:** `pnpm build` — в `build/tools/` лежат все 11 страниц; главная
|
|
||||||
содержит ссылки на каждую.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. Шаг 6 — Ошибки и минимальная полировка
|
|
||||||
|
|
||||||
- Обработка ошибок: неподдерживаемый формат, ошибка декода, некорректные
|
|
||||||
параметры crop (выход за границы — клампы или понятная ошибка).
|
|
||||||
- Состояния: «перетащите файл», спиннер во время обработки, disabled-кнопки до
|
|
||||||
загрузки.
|
|
||||||
- Тексты описаний для всех 11 инструментов (title/description уже в реестре).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. Шаг 7 — Верификация и checkpoint
|
|
||||||
|
|
||||||
Команды:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd web && pnpm dev # разработка
|
|
||||||
cd web && pnpm build # статический экспорт должен проходить без ошибок
|
|
||||||
cd web && pnpm test # vitest: ядро (геометрия/цвет) + целостность реестра
|
|
||||||
```
|
|
||||||
|
|
||||||
Ручной smoke-тест каждого из 11 инструментов: загрузка PNG → применить → скачать
|
|
||||||
→ открыть результат.
|
|
||||||
|
|
||||||
**Checkpoint MVP:** любой из 11 инструментов доступен по прямому URL с главной
|
|
||||||
страницы; PNG загружается drag&drop; параметры применяются; результат
|
|
||||||
скачивается; всё работает офлайн со статического хостинга; ни одной серверной
|
|
||||||
зависимости.
|
|
||||||
@@ -1,213 +0,0 @@
|
|||||||
# План: аудит верности `preview/*` против `refs-html` (DOM → CSS)
|
|
||||||
|
|
||||||
> **СТАТУС: РЕАЛИЗОВАН (2026-09-07) — устарел.** Скрипты
|
|
||||||
> `web/scripts/audit-dom.mjs`, `audit-css.mjs`, `audit-cdp.mjs`,
|
|
||||||
> `audit-cdp-responsive.mjs` и npm-скрипты `refs-dom-audit` / `refs-css-audit` /
|
|
||||||
> `refs-cdp-audit` на месте; старый текстовый `audit.mjs` удалён. Актуальная
|
|
||||||
> доводка верности и статус пунктов F — в `plan-design-fix.md`.
|
|
||||||
|
|
||||||
> Статус: к выполнению. Ветка `feat/redesign`. Источник правды по пикселям:
|
|
||||||
> `refs-html/*.html` (статический экспорт рефа). Актуальный рабочий аудит сейчас
|
|
||||||
> — `pnpm refs-audit` (`scripts/audit.mjs`), но он **сломан** (см. «Почему
|
|
||||||
> текущий аудит не работает») и подлежит замене.
|
|
||||||
|
|
||||||
## Почему текущий аудит не работает
|
|
||||||
|
|
||||||
`scripts/audit.mjs` сопоставляет элементы **только по тексту** и берёт в
|
|
||||||
сравнение **только листовые узлы** (у которых нет детей). Контейнеры/карточки/
|
|
||||||
панели, несущие `border-radius`, отступы и позиционирование, в сравнение
|
|
||||||
**вообще не попадают** (`byText` кладёт в мапу только элементы с непустым
|
|
||||||
`text`). Следствия:
|
|
||||||
|
|
||||||
- разница радиусов у карточек невидима (карточка — родитель без своего текста);
|
|
||||||
- позиция кнопки (`Download result`) не ловится — это структурная разница;
|
|
||||||
- `onlyOurs / onlyRef` — это просто счётчик текстовых строк на одной стороне,
|
|
||||||
**не метрика верности**.
|
|
||||||
|
|
||||||
Плюс он не делает пиксельного сравнения, то есть принципиально не может сказать
|
|
||||||
«выглядит по-другому». Это текстовый дифф стилей листовых текстов — и всё.
|
|
||||||
|
|
||||||
## Решения (подтверждены автором)
|
|
||||||
|
|
||||||
1. **Копирайт превью — через i18n `t()` с EN-базой.** Сейчас RU-литералы
|
|
||||||
хардкодом только в `preview/+page.svelte:26-27` и
|
|
||||||
`preview/list-tools/+page.svelte:31` (эти страницы не используют i18n, в
|
|
||||||
отличие от `(old)/` и компонентов `DownloadButton`/`ChainToolBlock`/…).
|
|
||||||
Заводим их через `t()` — совпадёт с рефом (EN).
|
|
||||||
2. **Имена классов НЕ сводим к рефу.** Матчинг делаем по **структурному пути**,
|
|
||||||
а не по классу. Компоненты `kit/` остаются идиоматичными, Stage 0 не
|
|
||||||
откатываем. Требуется только эквивалентность _структуры_ (теги, вложенность,
|
|
||||||
порядок сиблингов, наличие узлов, текст).
|
|
||||||
3. **Пиксельный дифф (Playwright + pixelmatch) пока НЕ делаем.** DOM+CSS-аудита
|
|
||||||
достаточно; добавим как сеть позже, если стили что-то упустят.
|
|
||||||
4. **Порядок прогона маршрутов:** `demo` → `list-tools` → затем 2 инструмента
|
|
||||||
(`tools/linear-gradient-png` ↔ `gradient.html`, `tools/remove-background-png`
|
|
||||||
↔ `background-remover.html`).
|
|
||||||
|
|
||||||
## Принципы
|
|
||||||
|
|
||||||
- Никаких правок старого дизайна `(old)/` и старого `ui/`.
|
|
||||||
- Коммиты атомарные (< ~500 строк), по 1–2 компонента/скрипта; после каждого —
|
|
||||||
`lint` + `svelte-check` + `build` + соответствующий аудит (diff не должен
|
|
||||||
расти без причины).
|
|
||||||
- Коммиты делает автор (агент не коммитит).
|
|
||||||
- Шрифты уже совпадают: `design2.css:1-6` грузит IBM Plex Sans/Mono через
|
|
||||||
`@fontsource`, токены `:34-35` — `"IBM Plex Sans"/"IBM Plex Mono"` (как в рефе
|
|
||||||
`demo.html:67-68`). Пиксельный шум от шрифтов не предвидится.
|
|
||||||
|
|
||||||
## Подход: две фазы
|
|
||||||
|
|
||||||
Корень проблемы — нет **стабильного ключа сопоставления**. Разбиваем на две
|
|
||||||
фазы: сначала доводим DOM до структурного равенства с рефом (фаза A), затем на
|
|
||||||
стабильном DOM сравниваем стили по тому же пути (фаза B). Фаза A сама по себе
|
|
||||||
ловит именно то, из-за чего возникли претензии (кнопка не в том родителе,
|
|
||||||
лишние/недостающие обёртки, перепутанный порядок) — и без эстетических решений.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Фаза A — DOM/структурный аудит (`scripts/audit-dom.mjs`)
|
|
||||||
|
|
||||||
**Цель:** Svelte рендерит структуру, идентичную `refs-html` (дерево тегов,
|
|
||||||
вложенность, порядок сиблингов, наличие узлов, текст). Имена классов могут
|
|
||||||
отличаться.
|
|
||||||
|
|
||||||
### Нормализация узла (ключ сравнения)
|
|
||||||
|
|
||||||
Для каждого элемента (пропускаем `SCRIPT`/`STYLE`/`NOSCRIPT` и скрытые
|
|
||||||
поддеревья, игнорируем служебное):
|
|
||||||
|
|
||||||
- **тег** (`tagName` в lower-case);
|
|
||||||
- **классы** — как отсортированный набор (для инфо, не для матчинга);
|
|
||||||
- **нормализованный текст** — `trim`, `collapse-whitespace`, без SVG-иконок
|
|
||||||
внутри (текст иконок игнорируем);
|
|
||||||
- **порядок детей** — позиция среди сиблингов (`nth-child`).
|
|
||||||
|
|
||||||
Матчинг — по **структурному пути** от корня сравнения:
|
|
||||||
`tag:nth-child(i) > tag:nth-child(j) …`. Не по классу и не по тексту.
|
|
||||||
|
|
||||||
### Полное соответствие, без IGNORE
|
|
||||||
|
|
||||||
Оболочка (наш `TopBar`/`AppShell`/`preview-footer` vs рефовый
|
|
||||||
`.topbar`/`.footer`/`.language`) тоже сводим к рефу — **IGNORE-список не
|
|
||||||
заводим**. Сравниваем всё дерево от `<body>` (без пропуска служебного, кроме
|
|
||||||
Svelte-атрибутов и комментов гидрации), чтобы получить полное структурное
|
|
||||||
соответствие, а не только контента. Шапка/футер/переключатель языка — часть
|
|
||||||
верности; позже, после полного фикса дизайна, всё равно планируется рефакторинг
|
|
||||||
компонентов, где оболочку приведём в порядок и унифицируем. Аудит на этом этапе
|
|
||||||
просто показывает все расхождения, включая оболочечные.
|
|
||||||
|
|
||||||
### Дифф
|
|
||||||
|
|
||||||
Для каждого маршрута — набор дельт:
|
|
||||||
|
|
||||||
- `added` — у нас, нет в рефе;
|
|
||||||
- `removed` — в рефе, нет у нас;
|
|
||||||
- `moved` — тот же текст/тег, другой путь (перепутанный порядок/родитель);
|
|
||||||
- `textMismatch` — нормализованный текст различается (в т.ч. RU vs EN).
|
|
||||||
|
|
||||||
Класс-мисматч — **только информационно**, не блокирует (классы свои по решению
|
|
||||||
№2). Счётчики: `added / removed / moved / textMismatch`.
|
|
||||||
|
|
||||||
### Рендеринг
|
|
||||||
|
|
||||||
- **наше:** dev-сервер на `5179` (как сейчас в `audit.mjs`).
|
|
||||||
- **реф:** `pathToFileURL` к `refs-html/<ref>.html` (`file://`). Реф —
|
|
||||||
самодостаточный (инлайн `<style>`), рендерится полностью. Шрифты для фазы A не
|
|
||||||
важны (сравниваем структуру/текст, не растеризацию).
|
|
||||||
- Переиспользуем из текущего `audit.mjs`: `discoverRoutes()`, `EXCLUDE`
|
|
||||||
(`/preview`), `DYNAMIC` (`tools/[id]` → `gradient.html` /
|
|
||||||
`background-remover.html`), `refNameFor()`.
|
|
||||||
|
|
||||||
### Шаги (на каждом маршруте)
|
|
||||||
|
|
||||||
1. Прогнать `pnpm refs-dom-audit`, получить дельты.
|
|
||||||
2. Поправить структуру Svelte под реф (добавить/убрать/переставить узлы, вынести
|
|
||||||
тексты в `t()` с EN-базой).
|
|
||||||
3. Перезапустить, пока `added/removed/moved/textMismatch` ≃ 0 (кроме `IGNORE`).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Фаза B — CSS-аудит по стабильному DOM (`scripts/audit-css.mjs`)
|
|
||||||
|
|
||||||
**Цель:** на совпадающем дереве сравнить вычисленные стили по тому же
|
|
||||||
структурному пути и получить «элемент по пути X: свойство → наше/реф».
|
|
||||||
|
|
||||||
### Сравниваемые свойства (computed)
|
|
||||||
|
|
||||||
`borderRadius`, позиция/размер (`getBoundingClientRect`: x/y/w/h), `padding`
|
|
||||||
(все стороны), `margin`, `color`, `backgroundColor`,
|
|
||||||
`borderTop/Right/Bottom/Left Width|Color`, `fontFamily`, `fontSize`,
|
|
||||||
`fontWeight`, `letterSpacing`, `lineHeight`, `gap`, `display`, `boxShadow`.
|
|
||||||
|
|
||||||
### Дифф
|
|
||||||
|
|
||||||
Для каждого состыкованного по пути элемента — список свойств с дельтой (наше
|
|
||||||
значение → реф). Элементы, не совпавшие по пути (остаток из фазы A), идут в
|
|
||||||
`unmatched`. Отчёт: элемент → свойство → наше/реф. Правится одной строкой CSS,
|
|
||||||
без брутфорса.
|
|
||||||
|
|
||||||
### Шаги
|
|
||||||
|
|
||||||
1. Прогнать `pnpm refs-css-audit` (только после того, как фаза A по маршруту
|
|
||||||
дала ≃ 0 структурных расхождений).
|
|
||||||
2. Поправить стили в `design2.css` / стилях компонентов.
|
|
||||||
3. Перезапустить, пока delta% по свойствам ≃ 0.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Порядок маршрутов и особенности
|
|
||||||
|
|
||||||
1. **`/preview/demo`** (↔ `demo.html`) — уже ближе всего; идеальная «обкатка»
|
|
||||||
тулинга фаз A и B. Здесь же проверим, что `Download result` стоит в шапке
|
|
||||||
`.preview-top` (как в рефе), а не внизу панели.
|
|
||||||
2. **`/preview/list-tools`** (↔ `list-tools.html`) — объёмнее; **зависит от
|
|
||||||
`docs/plan-list-tools.md`**: реальный аудит имеет смысл только после того,
|
|
||||||
как страница переписана на `CatalogHeader`/`CatalogToolbar`/`CatalogGroup`
|
|
||||||
(шаги 1–5 плана list-tools). То есть фаза A/B для list-tools стартует после
|
|
||||||
landing этих шагов.
|
|
||||||
3. **`tools/linear-gradient-png`** (↔ `gradient.html`) и
|
|
||||||
**`tools/remove-background-png`** (↔ `background-remover.html`) —
|
|
||||||
**последние**. Известные расхождения: наши инструменты могут иметь **другие
|
|
||||||
наборы параметров / UI**, чем конкретные рефовые тулзы. Ожидаются структурные
|
|
||||||
и стилевые дельты, которые не сводятся к «подровнять паддинг». Для них —
|
|
||||||
триаж после прогона:
|
|
||||||
- реальный разрыв верности → фиксим;
|
|
||||||
- принципиальное отличие (параметр, которого нет в реф-тулзе; иной контрол) →
|
|
||||||
либо аккуратно приводим наш UI параметра к рефу, либо **документируем как
|
|
||||||
допустимое расхождение / заносим в allowlist** конфига аудита (селектор +
|
|
||||||
причина). «Скипы/фиксы» для инструментов оформляем явно, чтобы аудит не
|
|
||||||
шумел на осознанных отличиях.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Структура скриптов
|
|
||||||
|
|
||||||
- `scripts/audit-dom.mjs` — фаза A (нормализация + структурный дифф).
|
|
||||||
- `scripts/audit-css.mjs` — фаза B (матчинг по пути + дифф стилей).
|
|
||||||
- `package.json`: `refs-dom-audit`, `refs-css-audit` (опционально `refs-audit`,
|
|
||||||
прогоняющий обе фазы).
|
|
||||||
- Переиспользуем из старого `audit.mjs`: `discoverRoutes`, `EXCLUDE`, `DYNAMIC`,
|
|
||||||
`refNameFor`, `waitFor` (fast-fail на 4xx/5xx).
|
|
||||||
- Отчёты: `web/audit/dom-report.{md,json}` и `web/audit/css-report.{md,json}`
|
|
||||||
(старый `audit-report.*` заменяем).
|
|
||||||
|
|
||||||
## Верификация (после каждого коммита)
|
|
||||||
|
|
||||||
- `pnpm lint`, `pnpm exec svelte-check --tsconfig ./tsconfig.json`, `pnpm build`
|
|
||||||
— без ошибок.
|
|
||||||
- `pnpm refs-dom-audit` (фаза A) / `pnpm refs-css-audit` (фаза B) по затронутому
|
|
||||||
маршруту: счётчики расхождений не растут без причины.
|
|
||||||
|
|
||||||
## Риски / заметки
|
|
||||||
|
|
||||||
- **Стабильность матчинга:** нужна аккуратная нормализация (сортировка классов,
|
|
||||||
игнор svelte-атрибутов/`data-*`, игнор whitespace-текстовых узлов, игнор
|
|
||||||
внутренностей SVG-иконок). Подстраивается по первым прогонам demo.
|
|
||||||
- **Оболочка** (TopBar/footer/язык) сейчас отличается по дизайну — сводим к рефу
|
|
||||||
в рамках полного соответствия (без IGNORE); финальная унификация компонентов —
|
|
||||||
отдельным рефакторингом позже.
|
|
||||||
- **Динамические маршруты:** `DYNAMIC` уже есть; оставляем.
|
|
||||||
- **Hover / адаптив / тёмная тема** — вне объёма статического аудита (initial
|
|
||||||
state, 1440×900, light). Отмечаем как будущие прогоны.
|
|
||||||
- **list-tools** зависит от landing `plan-list-tools.md` (шаги 1–5) — не
|
|
||||||
аудируем вслепую.
|
|
||||||
@@ -1,114 +0,0 @@
|
|||||||
# План C17: перенос preview → корень + (old) → /v1/
|
|
||||||
|
|
||||||
> **СТАТУС: ВЫПОЛНЕН (2026-09-11) — устарел.** Перенос завершён:
|
|
||||||
> preview-страницы на реальных маршрутах, старый дизайн на `/v1/*`,
|
|
||||||
> `app.css`/`app_v1.css`, isolation-плагин на v1-путях, аудит-скрипты в
|
|
||||||
> `scripts/archive/`. C18 (правки импортов/редиректов, проверка билда) тоже
|
|
||||||
> выполнен. Следующий этап — C19–C21 (`plan-redesign.md`, §10).
|
|
||||||
>
|
|
||||||
> Оригинальный статус до выполнения: план к выполнению. Предпосылки: tech debt
|
|
||||||
> закрыт, backlog №14 (старые файлы → lib/old/) выполнен. Домен НЕ блокер (см.
|
|
||||||
> plan-redesign.md §10, plan-seo.md §6).
|
|
||||||
|
|
||||||
## Цель
|
|
||||||
|
|
||||||
Новый дизайн становится основным на корневых маршрутах (`/`, `/tools/[id]`,
|
|
||||||
`/list-tools`). Старый дизайн переезжает на `/v1/*` как архив для справки.
|
|
||||||
Удаление старого кода (C19) откладывается до полной приёмки нового дизайна.
|
|
||||||
|
|
||||||
## Структура маршрутов (до → после)
|
|
||||||
|
|
||||||
**До:** `(old)/` даёт URLs `/`, `/demo`, `/list-tools`, `/tools/[id]`.
|
|
||||||
`preview/` даёт `/preview`, `/preview/list-tools`, `/preview/tools/[id]`,
|
|
||||||
`/preview/kit`.
|
|
||||||
|
|
||||||
**После:** `v1/` даёт `/v1/`, `/v1/demo`, `/v1/list-tools`, `/v1/tools/[id]`.
|
|
||||||
Корень даёт `/`, `/list-tools`, `/tools/[id]`. Витрина kit — на реальном
|
|
||||||
маршруте `/kit` (дизайн в стадии тестирования).
|
|
||||||
|
|
||||||
## Решения
|
|
||||||
|
|
||||||
1. **`v1/` — реальная папка, НЕ route group** — group `(v1)/` не меняет URL, а
|
|
||||||
нам нужно чтобы старые страницы жили по `/v1/*`.
|
|
||||||
2. **Корневой layout: shell нового дизайна** — TopBar, Footer, тема, хлебные
|
|
||||||
крошки, импорт `app.css`, `@layer app, design;`.
|
|
||||||
3. **`v1/+layout.svelte` — только импорт `app_v1.css`** — без
|
|
||||||
header/footer/shell.
|
|
||||||
4. **Тема:** использовать `theme.svelte.ts` (не дублировать локальный `$state`).
|
|
||||||
5. **`@layer app, design;`** остаётся — design выигрывает у app везде.
|
|
||||||
6. **`noindex` остаётся** на `tools/[id]/+page.svelte` до покупки домена.
|
|
||||||
7. **CSS-файлы переименовываются:** `preview.css` → `app.css`, `old.css` →
|
|
||||||
`app_v1.css`. Все ссылки обновляются.
|
|
||||||
8. **`kit/` витрина → `/kit`** — переносится на реальный маршрут вместе с
|
|
||||||
остальными preview-страницами; отдельный `preview/+layout` удаляется (`/kit`
|
|
||||||
наследует корневой shell).
|
|
||||||
|
|
||||||
## Что делать (шаги)
|
|
||||||
|
|
||||||
### 1. Rename `(old)/` → `v1/`
|
|
||||||
|
|
||||||
Переименовать папку. Обновить `v1/+layout.svelte`: убрать «Preview v2» ссылку,
|
|
||||||
обновить пути навигации, поправить импорт CSS.
|
|
||||||
|
|
||||||
### 2. Новый корневой `+layout.svelte` + rename CSS
|
|
||||||
|
|
||||||
Перезаписать текущий нейтральный layout содержимым из `preview/+layout.svelte`
|
|
||||||
(с правками: тема через `theme.svelte.ts`, путь к CSS). Переименовать
|
|
||||||
`src/preview.css` → `src/app.css`, `src/old.css` → `src/app_v1.css`. Обновить
|
|
||||||
все ссылки на эти файлы в проекте ( layout, лайнтеры, eslint-плагины, скрипты,
|
|
||||||
тесты).
|
|
||||||
|
|
||||||
### 3. Move preview pages → корень
|
|
||||||
|
|
||||||
Переместить страницы из `preview/` в корень `routes/`: `+page` → `/`,
|
|
||||||
`list-tools` → `/`, `tools/[id]` → `/`, витрину `kit` → `/kit`. Удалить
|
|
||||||
`preview/+layout.svelte` (корневой shell заменяет его).
|
|
||||||
|
|
||||||
### 4. Update hardcoded `/preview/` URLs
|
|
||||||
|
|
||||||
В компонентах с захардкоженными путями `/preview/` заменить на корневые.
|
|
||||||
|
|
||||||
### 5. Flatten `kit/` → `components/`
|
|
||||||
|
|
||||||
Перенести компоненты из `lib/components/kit/` в `lib/components/`, обновить все
|
|
||||||
импорты, удалить пустую `kit/`.
|
|
||||||
|
|
||||||
### 6. Flatten `preview/` → `lib/`
|
|
||||||
|
|
||||||
Перенести код из `lib/previw/` в `lib/`, обновить все импорты, удалить пустую
|
|
||||||
`preview/`.
|
|
||||||
|
|
||||||
### 7. Перенести `old/` → `v1/`
|
|
||||||
|
|
||||||
- `$lib/old/` — переименовать в `lib/v1/`, обновить все импорты
|
|
||||||
|
|
||||||
### 8. E2E тесты + playwright config
|
|
||||||
|
|
||||||
Заменить `/preview/...` на `/...` во всех e2e файлах и конфиге playwright.
|
|
||||||
|
|
||||||
### 9. ESLint config + AGENTS.md
|
|
||||||
|
|
||||||
Обновить glob-паттерны: `routes/preview/**` → `routes/**`, `components/kit/**` →
|
|
||||||
`components/**`. Обновить `AGENTS.md`.
|
|
||||||
|
|
||||||
### 10. Isolation plugin
|
|
||||||
|
|
||||||
Обновить `DEFAULT_OLD`: `routes/(old)/**` → `routes/v1/**`. Убрать
|
|
||||||
`routes/preview/**` из `DEFAULT_NEW`.
|
|
||||||
|
|
||||||
### 11. Archive audit scripts
|
|
||||||
|
|
||||||
Перенести скрипты аудита в `scripts/archive/`, убрать вызовы из манифестов.
|
|
||||||
|
|
||||||
## Что НЕ меняется
|
|
||||||
|
|
||||||
- `routes/+layout.ts` — prerender = true остаётся
|
|
||||||
- Слаги инструментов (`*-png`) — без изменений
|
|
||||||
- `/kit` — витрина дизайна остаётся (в стадии тестирования)
|
|
||||||
|
|
||||||
## Риски
|
|
||||||
|
|
||||||
- **v1/ без shell** — старый дизайн без header/footer.
|
|
||||||
- **`@layer` взаимодействие** — проверить что design-токены перекрывают
|
|
||||||
app-токены на обоих маршрутах.
|
|
||||||
- **Isolation: shared-классификация** — маршруты нового дизайна shared до C19.
|
|
||||||
@@ -1,593 +0,0 @@
|
|||||||
# План: составные типы параметров и полная типизация pipeline
|
|
||||||
|
|
||||||
> **СТАТУС: ЗАКРЫТ (2026-09-07).** Типизация pipeline, составные типы
|
|
||||||
> (dimension/color-pair/offset/position9/font-style/plate/gradient),
|
|
||||||
> `schema.layout`, изоляция веток и вся миграция инструментов (121/125,
|
|
||||||
> остальные 4 — отложены решениями) выполнены. Ключевые архитектурные решения
|
|
||||||
> (параллельная сборка, правило копий, изоляция old↔preview) закреплены в
|
|
||||||
> `AGENTS.md`. **Незакрытое из этого плана:** шаг 37 — физический переезд старых
|
|
||||||
> файлов в папки `old/` (перенесён в `backlog.md`), и удаление старого кода — по
|
|
||||||
> `plan-redesign.md` (C17–C21). Отложенные инструменты — `watermark-image-png`
|
|
||||||
> (overlay), `png-info`, `reduce-to-size` — в `backlog.md`. Ниже — полное
|
|
||||||
> состояние на момент закрытия.
|
|
||||||
|
|
||||||
> Статус: **в реализации.** Фаза 0 (фундамент) ✔, Фаза 1 (рабочий инструмент в
|
|
||||||
> preview) ✔, Фаза 2 (простые инструменты без составных типов) — переведены все
|
|
||||||
> одиночные инструменты (18 шт). Фаза 3 — **составной тип `dimension` полностью
|
|
||||||
> переведён** (12 инструментов: create-empty, single-color, random-noise,
|
|
||||||
> linear-gradient, color-spectrum, random-colors, draw-grid, placeholder,
|
|
||||||
> fit-on-background, change-canvas-size, resize, crop); **составной тип
|
|
||||||
> `color-pair` полностью переведён** (4 инструмента: blend-two, step-colors,
|
|
||||||
> linear-gradient, two-colors); **составной тип `offset` полностью переведён**
|
|
||||||
> (4 инструмента: circle-mask, square-mask, star-mask, wavy-mask); **составной
|
|
||||||
> тип `position9` — переведены add-text-png и date-stamp-png** (водяной
|
|
||||||
> знак-картинка — отдельный шаг: требует overlay-механику в новом превью);
|
|
||||||
> **составной тип `font-style` полностью переведён** (text-to-png, add-text,
|
|
||||||
> date-stamp); **составной тип `plate` полностью переведён** (add-text,
|
|
||||||
> date-stamp); **составной тип `gradient` полностью переведён**
|
|
||||||
> (linear-gradient): вместо горизонтального/ вертикального select — **угол
|
|
||||||
> 0..360°** (новый виджет `AngleControl`: slider + кнопки-пресеты
|
|
||||||
> 0°/90°/180°/270°, переиспользуемый компонент); preview научен применять
|
|
||||||
> **генераторы** (`executeGenerate`, кнопка Generate) и рендерить все kinds
|
|
||||||
> схемы (slider/number/color/select/checkbox/dimension/
|
|
||||||
> color-pair/offset/position9/font-style/plate/gradient). **Пер-инструмент
|
|
||||||
> раскладка `schema.layout` реализована** (шаг 32): тип `ToolSchemaLayout`
|
|
||||||
> {groups: [{title?, cols?, fields}]}, рендер групп в `SchemaFields.svelte`
|
|
||||||
> (заголовок группы + сетка колонок, неупомянутые поля — в общей группе), пилот
|
|
||||||
> — `add-text` (группы Text/Placement/Plate). **UI-макеты расставлены по
|
|
||||||
> переведённым инструментам** (шаг 33): geometry (fit-on-background,
|
|
||||||
> change-canvas-size, resize, crop), маски alpha (Shape/Position), generate
|
|
||||||
> (create-empty, linear-gradient, color-spectrum, random-colors, draw-grid,
|
|
||||||
> step-colors, placeholder, text-to-png), text (add-text, date-stamp), filters
|
|
||||||
> (randomize-pixels, add-noise) — везде, где канвас отделён от параметров
|
|
||||||
> эффекта либо фигура от позиции. Следующее закрыто в
|
|
||||||
> `archive/plan-migrate-remaining.md` (2026-09-07): перевод оставшихся
|
|
||||||
> инструментов выполнен (всего 121/125 в preview; 4 хвоста —
|
|
||||||
> `watermark-image-png`, `png-info`, `reduce-to-size-png`, `compress-png` —
|
|
||||||
> отложены решениями в `backlog.md`). Незакрытые продолжения: переезд старых
|
|
||||||
> компонентов в папки `old/` (шаг 37 ниже), закрытие долгов и сбор фидбека — см.
|
|
||||||
> также `plan-design-fix.md`.
|
|
||||||
>
|
|
||||||
> Ключевые файлы нового registry: `web/src/lib/registry-new/{types,index,*}.ts`
|
|
||||||
> (по файлу на категорию: geometry/alpha/convert/analyze/filters/color/generate)
|
|
||||||
>
|
|
||||||
> - `web/src/lib/preview/categories.ts`, `web/src/lib/registry-schema.ts` (kind
|
|
||||||
> `dimension`), `kit/fields/DimensionField.svelte`,
|
|
||||||
> `preview/executor/index.ts` (`executeGenerate`), `SchemaFields.svelte`
|
|
||||||
> (полный рендер kinds). Старый `web/src/lib/registry.ts` разбит по категориям
|
|
||||||
> в `web/src/lib/registry/` (см. `registry.ts` — тонкий баррель).
|
|
||||||
|
|
||||||
## Ключевая стратегия: параллельная сборка, старый UI не трогаем
|
|
||||||
|
|
||||||
Старый UI (группа `(old)/`, `ParamForm.svelte` + старый pipeline) **продолжает
|
|
||||||
работать как сейчас, без рефакторинга**. Новый `ToolSchema<P>` строится рядом и
|
|
||||||
служит источником для **нового UI**. Инструменты переписываются под новый
|
|
||||||
registry по мере нужды — для нового UI.
|
|
||||||
|
|
||||||
Это та же логика, что в `plan-redesign.md` применена к дизайну: параллельная
|
|
||||||
сборка, изоляция от старого, затем новый становится основным и старый удаляется
|
|
||||||
вместе со старым дизайном.
|
|
||||||
|
|
||||||
**Правило копий (важно!):** если для нового UI/registry нужно внести изменения в
|
|
||||||
файл, который **прямо или косвенно** уже используется `(old)` веткой, — этот
|
|
||||||
файл **НЕ трогаем**. Вместо этого делаем **копию** в новом месте (например, в
|
|
||||||
`lib/preview/`) и правим копию. Это приводит к дублированию, но **гарантированно
|
|
||||||
не задевает старую ветку сайта**. Пример: категории — новый
|
|
||||||
`lib/preview/categories.ts` (object as const) копирует и заменяет собой
|
|
||||||
`../categories` для нового кода, старый `categories.ts` обслуживает `(old)` и
|
|
||||||
остаётся без изменений. После перехода (Фаза 5) копия становится основной, а
|
|
||||||
исходник продолжает обслуживать `(old)` UI (он не удаляется — см. шаг 35).
|
|
||||||
|
|
||||||
**Разделение схем: две независимые схемы.**
|
|
||||||
|
|
||||||
| Схема | Источник | Использование |
|
|
||||||
| --------------------- | ------------------------------------------------- | ---------------------------------------------------------------- |
|
|
||||||
| Старый `ParamDef[]` | В `ToolEntry.params` — **остаётся без изменений** | Старый UI, старый pipeline (`ParamForm.svelte`, executor, chain) |
|
|
||||||
| Новый `ToolSchema<P>` | Новое поле `ToolEntry.schema` — добавляется рядом | Новый UI (kit/`ParamControl`, новый pipeline) |
|
|
||||||
|
|
||||||
- Старая и новая схемы **не выводятся друг из друга** — это два независимых
|
|
||||||
описания инструмента.
|
|
||||||
- Пока новый UI не готов — `schema` просто отсутствует у большинства
|
|
||||||
инструментов, старый UI ничего не замечает.
|
|
||||||
- Удаление старого UI (после перехода) попутно удаляет и старый `ParamDef[]`
|
|
||||||
|
|
||||||
### Разделение registry по UI (новое решение, Фаза 2+)
|
|
||||||
|
|
||||||
Помимо двух схем, registry тоже разделён по UI (по факту миграции):
|
|
||||||
|
|
||||||
- **Старый registry** (`web/src/lib/registry.ts` + `registry/` +
|
|
||||||
`registry-helpers.ts`) — работает на старом UI, использует `ParamDef[]`. **Не
|
|
||||||
трогаем**; остаётся обслуживать `(old)/` маршруты (см. шаг 35).
|
|
||||||
- **Новый registry** (`web/src/lib/registry-new/`) — строится **с нуля «как
|
|
||||||
надо»**: `ToolEntry<P>` с обязательным `schema`, типизированный `run`, **без**
|
|
||||||
`ParamDef[]` и **без** связи со старым. Импортирует core-функции
|
|
||||||
(`expandCanvas`, `strokeImage`, …) напрямую. Наполняется **по-инструментно**
|
|
||||||
по мере миграции (не разовым переводом всех 130). Preview показывает только
|
|
||||||
переведённые инструменты.
|
|
||||||
|
|
||||||
Preview (`catalog.ts`, маршруты `preview/**`, `SchemaToolView`, `SchemaFields`)
|
|
||||||
импортируют из `$lib/registry-new`. После удаления старого UI `registry-new/`
|
|
||||||
переименуется в `registry`.
|
|
||||||
|
|
||||||
**Executor/worker для нового UI (осознанное дублирование):** общий
|
|
||||||
`executor.worker.ts` резолвит инструменты по `id` в **старом** registry — для
|
|
||||||
новых инструментов их там нет. Поэтому `executor.ts` + `executor.worker.ts`
|
|
||||||
**скопированы** в `web/src/lib/preview/` и переписаны под `registry-new` +
|
|
||||||
`sanitizeSchemaParams`. Старый executor/worker под старый UI остаются
|
|
||||||
нетронутыми; `SchemaToolView` использует `$lib/preview/executor`. Это
|
|
||||||
дублирование необходимо до ухода старого UI (после — preview-executor заменяет
|
|
||||||
общий).
|
|
||||||
|
|
||||||
## Проблема (текущая)
|
|
||||||
|
|
||||||
- `ParamDef` — плоский union из 6 примитивов
|
|
||||||
(`number | slider | select | checkbox | color | text`). Параметры инструмента
|
|
||||||
— отдельные записи в плоском массиве.
|
|
||||||
- `run()`/`generate()` получают `Record<string, unknown>`; значения достаются
|
|
||||||
через `num(p, "fromColor")` — runtime string lookup **без type safety**, легко
|
|
||||||
напутать id.
|
|
||||||
- Поля инструмента **слабо связаны** (например «ширина» и «цвет» рамки — два
|
|
||||||
независимых поля без общего контракта) → общий генерализованный рендер, нет
|
|
||||||
возможности сделать идеальный пер-инструмент UI.
|
|
||||||
- Preview-система (`tool-views.ts`) **дублирует** registry (FieldDef vs
|
|
||||||
ParamDef) — одни и те же инструменты описаны дважды.
|
|
||||||
|
|
||||||
## Решения (принятые в обсуждении)
|
|
||||||
|
|
||||||
1. **Полная типизация.** `interface Params` (runtime-тип) объявляется явно в
|
|
||||||
каждом инструменте. `run`/`generate` типизируются прямо на него, а не на
|
|
||||||
`Record<string, unknown>`.
|
|
||||||
2. **Отдельная типизированная схема для UI/дефолтов/валидации.** Два понятия на
|
|
||||||
инструмент: runtime-тип + схема. Схема проверяется компилятором на
|
|
||||||
соответствие `Params` (чтобы не рассинхронизировались).
|
|
||||||
3. **Общий рендер,** но с пер-инструмент layout: общие компоненты рисуют поля по
|
|
||||||
схеме; схема инструмента задаёт layout и группировку. Без отдельного
|
|
||||||
Svelte-компонента на каждый инструмент.
|
|
||||||
4. **Составные типы** (`gradient`, `color-pair`, `dimension`, `offset`,
|
|
||||||
`position9`, `font-style`, `plate`) остаются в плане — это способ выразить в
|
|
||||||
схеме связанную группу полей и переиспользовать её на нескольких инструментах
|
|
||||||
и в UI.
|
|
||||||
5. Рано или поздно `tool-views.ts` поглощается registry (preview/lede/layout
|
|
||||||
переезжают в meta инструмента) — но это отдельный шаг, см. ниже.
|
|
||||||
6. **Старый UI не рефакторим.** Две независимые схемы: старый `ParamDef[]`
|
|
||||||
остаётся источником для старого UI без изменений; новый `ToolSchema<P>`
|
|
||||||
строится рядом для нового UI (см. «Ключевая стратегия» выше).
|
|
||||||
|
|
||||||
## Целевая архитектура (на примере «добавить рамку»)
|
|
||||||
|
|
||||||
Цель: «добавить рамку: ширина и цвет» — **один интерфейс**, а не два слабо
|
|
||||||
связанных поля.
|
|
||||||
|
|
||||||
```ts
|
|
||||||
// 1. Runtime-тип — что получает run/generate
|
|
||||||
interface AddBorderParams {
|
|
||||||
thickness: number; // рамка: ширина
|
|
||||||
color: string; // рамка: цвет
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Типизированная UI-схема, проверяемая компилятором против Params
|
|
||||||
// Ключи схемы обязаны совпадать с полями Params; default совместим с типом.
|
|
||||||
const addBorderSchema = toolSchema<AddBorderParams>({
|
|
||||||
thickness: field.slider({ min: 1, max: 500, default: 5 }),
|
|
||||||
color: field.color({ default: "#000000" }),
|
|
||||||
layout: { group: "border", cols: 2 }, // пер-инструмент раскладка
|
|
||||||
});
|
|
||||||
|
|
||||||
// 3. Инструмент в registry
|
|
||||||
const addBorder: ToolEntry<AddBorderParams> = {
|
|
||||||
id: "add-border-png",
|
|
||||||
title: "Add border to PNG",
|
|
||||||
category: "geometry",
|
|
||||||
schema: addBorderSchema,
|
|
||||||
run: (img, p) => expandCanvas(img, 4x..., p.color), // p.thickness — type safe
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
### Что даёт
|
|
||||||
|
|
||||||
- `p.thickness`, `p.color` — IDE autocomplete + compile-time ошибка при ошибке.
|
|
||||||
- `run()` нельзя передать лишний/чужой ключ.
|
|
||||||
- Схема — единый источник для дефолтов, валидации и рендера.
|
|
||||||
- Схема одного инструмента может задавать layout, которого нет в общем рендере.
|
|
||||||
|
|
||||||
## Типовая система (набросок)
|
|
||||||
|
|
||||||
```ts
|
|
||||||
// ─── Поле схемы ───
|
|
||||||
interface NumberField { kind: "number"; default: number; min?: number; max?: number; step?: number }
|
|
||||||
interface SliderField { kind: "slider"; default: number; min: number; max: number; step?: number }
|
|
||||||
interface ColorField { kind: "color"; default: string }
|
|
||||||
interface SelectField { kind: "select"; default: string; options: { value: string; label: string }[] }
|
|
||||||
interface TextField { kind: "text"; default: string; placeholder?: string }
|
|
||||||
interface CheckboxField { kind: "checkbox"; default: boolean }
|
|
||||||
|
|
||||||
type FieldDef =
|
|
||||||
| NumberField | SliderField | ColorField | SelectField | TextField | CheckboxField;
|
|
||||||
|
|
||||||
// ─── «поле» описывает тип значения + схему вместе, чтобы не рассинхронизировать ───
|
|
||||||
interface Field<T> {
|
|
||||||
schema: FieldDef;
|
|
||||||
// T — ожидаемый runtime-тип этого поля (number|string|boolean)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Builder: field.slider({...}) возвращает Field<number> с типизированной схемой ───
|
|
||||||
const field = {
|
|
||||||
slider: (s: SliderField): Field<number> => ({ schema: s }),
|
|
||||||
number: (s: NumberField): Field<number> => ({ schema: s }),
|
|
||||||
color: (s: ColorField): Field<string> => ({ schema: s }),
|
|
||||||
select: <V extends string>(s: SelectField & { options: {value: V}[] }): Field<V> => ({ schema: s }),
|
|
||||||
text: (s: TextField): Field<string> => ({ schema: s }),
|
|
||||||
checkbox: (s: CheckboxField): Field<boolean> => ({ schema: s }),
|
|
||||||
};
|
|
||||||
|
|
||||||
// ─── toolSchema<P>: проверяет, что ключи схемы === полям P и типы сходятся ───
|
|
||||||
type FieldsOf<T> = { [K in keyof T]: Field<T[K]> } & {
|
|
||||||
layout?: { group?: string; cols?: number };
|
|
||||||
};
|
|
||||||
function toolSchema<P>(fields: FieldsOf<P>): ToolSchema<P> { ... }
|
|
||||||
// TS выдаст ошибку, если schema содержит ключ, отсутствующий в Params, или
|
|
||||||
// если type поля не совпадает с типом Params[K].
|
|
||||||
```
|
|
||||||
|
|
||||||
Ключевая идея связки: **builder `field.x<T>()` связывает runtime-тип поля с его
|
|
||||||
схемой на этапе компиляции**, а `toolSchema<P>` заставляет ключи схемы совпадать
|
|
||||||
с ключами `P`. Это и есть защита от рассинхрона.
|
|
||||||
|
|
||||||
## Составные типы (для переиспользования и группировки)
|
|
||||||
|
|
||||||
| Тип | Под-поля | Инструментов |
|
|
||||||
| ------------ | -------------------------- | ------------ |
|
|
||||||
| `dimension` | width + height | 12 |
|
|
||||||
| `color-pair` | from + to | 4 |
|
|
||||||
| `offset` | x + y | 4 |
|
|
||||||
| `gradient` | from + to + dir (+ type?) | 1 |
|
|
||||||
| `position9` | position (3×3 grid) | 3 |
|
|
||||||
| `font-style` | font + size + bold + color | 3-4 |
|
|
||||||
| `plate` | enabled + color + opacity | 2-3 |
|
|
||||||
|
|
||||||
Составной тип = группы полей, которые в интерфейсе `Params` могут
|
|
||||||
разворачиваться во вложенный объект или плоские ключи — решается на этапе
|
|
||||||
реализации. Пример для `gradient`:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
interface GradientParams {
|
|
||||||
from: string;
|
|
||||||
to: string;
|
|
||||||
dir: string;
|
|
||||||
}
|
|
||||||
// схема: field.color(from), field.color(to), field.select(dir)
|
|
||||||
// плюс annotation в схеме: { group: "gradient" } → UI рисует как цветовой переход
|
|
||||||
```
|
|
||||||
|
|
||||||
## Инвентарь: какие инструменты мигрировать
|
|
||||||
|
|
||||||
### dimension (12)
|
|
||||||
|
|
||||||
resize-png*, crop-png*, fit-on-background-png, create-empty-png,
|
|
||||||
single-color-png, random-noise-png, linear-gradient-png, color-spectrum-png,
|
|
||||||
random-colors-png, draw-grid-png, placeholder-png, change-canvas-size-png
|
|
||||||
|
|
||||||
(\* у resize/crop — не просто dimension, есть специфика: keepAspect, x/y и т.д.)
|
|
||||||
|
|
||||||
### color-pair (4)
|
|
||||||
|
|
||||||
blend-two-png, step-colors-png, linear-gradient-png, two-colors-png
|
|
||||||
|
|
||||||
### offset (4)
|
|
||||||
|
|
||||||
circle-mask-png, square-mask-png, star-mask-png, wavy-mask-png
|
|
||||||
|
|
||||||
### position9 (3)
|
|
||||||
|
|
||||||
add-text-png, date-stamp-png, watermark-image-png
|
|
||||||
|
|
||||||
### font-style (3-4)
|
|
||||||
|
|
||||||
text-to-png, add-text-png, date-stamp-png
|
|
||||||
|
|
||||||
### plate (2-3)
|
|
||||||
|
|
||||||
add-text-png, date-stamp-png
|
|
||||||
|
|
||||||
## Пошаговый план перевода инструментов
|
|
||||||
|
|
||||||
> Каждый шаг — **атомарное, маленькое изменение** (в идеале 1 инструмент = 1
|
|
||||||
> diff, укладывается в правило «коммиты < ~500 строк»). После каждого шага —
|
|
||||||
> ревью; параллельно можно делать UI нового инструмента.
|
|
||||||
>
|
|
||||||
> **Принцип малых шагов:** перевод инструмента не меняет поведение старой схемы
|
|
||||||
> (`params` остаётся), не ломает старый UI. Инструмент получает ДОПОЛНИТЕЛЬНО
|
|
||||||
> новое поле `schema` + `interface Params`, старое `run`/`params` не трогаем.
|
|
||||||
> Это гарантирует, что старый UI продолжает работать без регресса.
|
|
||||||
>
|
|
||||||
> **Новый подход (решение пользователя):** сначала делаем **один полноценный
|
|
||||||
> рабочий инструмент в preview** — не эталон «на бумаге», а реально юзабельный,
|
|
||||||
> чтобы руками оценить, насколько это рабочее решение и какие возникнут
|
|
||||||
> сложности. Оцениваем, при необходимости правим инфраструктуру, и только потом
|
|
||||||
> переводим остальные инструменты тем же проверенным паттерном.
|
|
||||||
|
|
||||||
### Фаза 0 — фундамент ✔ (сделано)
|
|
||||||
|
|
||||||
Typed field builders + `Field<T>` + `toolSchema<P>()` + `ToolSchema<P>` — в
|
|
||||||
`web/src/lib/registry-schema.ts`. `ToolEntry<P>` generic + опциональное поле
|
|
||||||
`schema`. Новые `defaultSchemaParams`/`sanitizeSchemaParams`. Старый
|
|
||||||
`ParamDef[]`/pipeline не тронуты. Проверено: compile-time-защита
|
|
||||||
(лишний/неверный/отсутствующий ключ ловит TS), тесты, svelte-check без новых
|
|
||||||
ошибок.
|
|
||||||
|
|
||||||
### Фаза 1 — ОДИН полноценный рабочий инструмент в preview (срез) ✔ (сделано)
|
|
||||||
|
|
||||||
Инструмент переехал в `registry-new` и рендерится через `SchemaToolView`
|
|
||||||
(`SchemaFields`/`SchemaPreview`); применение реально работает end-to-end до
|
|
||||||
картинки. Паттерн подтверждён. `find-contour-png` — следующий в этой фазе-2.
|
|
||||||
|
|
||||||
5. **Инструмент «добавить рамку» (`add-border-png`)** — сделать полностью
|
|
||||||
рабочим в preview:
|
|
||||||
- `interface AddBorderParams { thickness: number; color: string }`
|
|
||||||
- `addBorderSchema = toolSchema<AddBorderParams>(...)` + `schema: ...` в
|
|
||||||
entry (старый `params`/`run` остаются — старый UI не регрессит).
|
|
||||||
- **Новый рендер в preview** (`kit/ToolView` → `ParamControl` или новый
|
|
||||||
компонент схемы): читает `ToolSchema<P>`, рисует поля рамки, применение
|
|
||||||
реально работает (end-to-end до картинки).
|
|
||||||
- Дефолты/валидация/значения — из схемы (`defaultSchemaParams`,
|
|
||||||
`sanitizeSchemaParams`).
|
|
||||||
- **Ревью-оценка:** удобно ли это, что сломалось/усложнилось, что поправить в
|
|
||||||
инфраструктуре до масштабирования. Принимается макет/решение.
|
|
||||||
|
|
||||||
Результат: один реально рабочий инструмент на новой системе = доказываем
|
|
||||||
паттерн, прежде чем переводить остальных.
|
|
||||||
|
|
||||||
### Фаза 2 — простые инструменты без составных типов (1 инструмент = 1 шаг)
|
|
||||||
|
|
||||||
Одиночные/небольшие инструменты, где `interface Params` + `schema` не требуют
|
|
||||||
составных типов. Каждый — отдельный маленький diff (~15-30 строк), тем самым
|
|
||||||
проверенным в Фазе 1 паттерном. **Переезжают в `registry-new`.**
|
|
||||||
|
|
||||||
Переведено: `add-border-png` (Фаза 1), `add-stroke-png`, `find-contour-png`,
|
|
||||||
плюс весь блок простых инструментов ниже (пункты 8-22). Следующее — Фаза 3.
|
|
||||||
|
|
||||||
6. ~~add-stroke-png (color + slider)~~ → переведён в `registry-new` ✔
|
|
||||||
7. ~~find-contour-png (color + slider)~~ → переведён в `registry-new` ✔
|
|
||||||
8. ~~convert-png-to-jpg (color + slider)~~ → переведён в `registry-new` ✔
|
|
||||||
9. ~~convert-png-to-webp (slider)~~ → переведён в `registry-new` ✔
|
|
||||||
10. ~~remove-color-from-png (color + slider)~~ → переведён в `registry-new` ✔
|
|
||||||
11. ~~extract-color-from-png (color + slider)~~ → переведён в `registry-new` ✔
|
|
||||||
12. ~~add-noise-png (slider + select + number)~~ → переведён в `registry-new` ✔
|
|
||||||
13. ~~randomize-pixels-png (slider + number)~~ → переведён в `registry-new` ✔
|
|
||||||
14. ~~pixelate-png (slider)~~ → переведён в `registry-new` ✔
|
|
||||||
15. ~~vignette-png (slider)~~ → переведён в `registry-new` ✔
|
|
||||||
16. ~~gamma-png (slider)~~ → переведён в `registry-new` ✔
|
|
||||||
17. ~~temperature-png (slider)~~ → переведён в `registry-new` ✔
|
|
||||||
18. ~~tint-png (color + slider)~~ → переведён в `registry-new` ✔
|
|
||||||
19. ~~quantize-png (slider)~~ → переведён в `registry-new` ✔
|
|
||||||
20. ~~custom-palette-png (text)~~ → переведён в `registry-new` ✔
|
|
||||||
21. ~~dithering-png (slider + select)~~ → переведён в `registry-new` ✔
|
|
||||||
22. ~~jpeg-artifacts-png (slider)~~ → переведён в `registry-new` ✔
|
|
||||||
|
|
||||||
> **Замечание (конвертеры):** старые `convert-png-to-jpg`/`convert-png-to-webp`
|
|
||||||
> несли `output`-метаданные (`mime`/`ext`/`qualityParamId`) для выбора формата
|
|
||||||
> вывода. В новом `ToolEntry<P>` поля `output` пока нет, поэтому при переводе
|
|
||||||
> эти метаданные не перенесены — preview пока отдаёт результат как PNG. Механика
|
|
||||||
> выбора формата/качества в новом UI — отдельный шаг (не блокирует Фазу 2).
|
|
||||||
|
|
||||||
### Фаза 3 — инструменты с составными типами (по типу, затем по инструментам)
|
|
||||||
|
|
||||||
24. `dimension` — **переведены все 12 инструментов** ✔ (`create-empty-png`,
|
|
||||||
`single-color-png`, `random-noise-png`, `linear-gradient-png`,
|
|
||||||
`color-spectrum-png`, `random-colors-png`, `draw-grid-png`,
|
|
||||||
`placeholder-png` — генераторы в `registry-new/generate.ts`;
|
|
||||||
`fit-on-background-png`, `change-canvas-size-png`, `resize-png`, `crop-png`
|
|
||||||
— в `registry-new/geometry.ts`). Составной тип во всех видах: вложенный
|
|
||||||
объект `size: { width, height }` в Params + `field.dimension`, виджет
|
|
||||||
`kit/fields/DimensionField.svelte`. Preview: генераторы применяются через
|
|
||||||
`executeGenerate` (кнопка Generate), `SchemaFields` рендерит все kinds схемы
|
|
||||||
(slider/number/color/select/ checkbox/dimension). Тесты: 592 passed.
|
|
||||||
25. `color-pair` — **все 4 инструмента переведены** ✔ (`blend-two-png`,
|
|
||||||
`step-colors-png`, `linear-gradient-png` — генераторы в
|
|
||||||
`registry-new/generate.ts`, `two-colors-png` — run в
|
|
||||||
`registry-new/color.ts`). Составной тип во всех видах: вложенный объект
|
|
||||||
`pair: { from, to }` + `field.colorPair`, виджет
|
|
||||||
`kit/fields/schema/ColorPairControl.svelte`, kind `color-pair` в схеме
|
|
||||||
(default/sanitize). Тесты: +4 (596 passed).
|
|
||||||
26. `offset` — **все 4 инструмента переведены** ✔ (`circle-mask-png`,
|
|
||||||
`square-mask-png`, `star-mask-png`, `wavy-mask-png` — run в
|
|
||||||
`registry-new/alpha.ts`). Составной тип во всех видах: вложенный объект
|
|
||||||
`offset: { x, y }` + `field.offset`, виджет
|
|
||||||
`kit/fields/schema/OffsetControl.svelte`, kind `offset` в схеме
|
|
||||||
(default/sanitize). Тесты: +4 (600 passed).
|
|
||||||
27. `position9` — **переведены `add-text-png` и `date-stamp-png`** ✔ (run в
|
|
||||||
`registry-new/text.ts`, оба `domOnly`). Составной тип во всех видах:
|
|
||||||
значение — строка `Position9`, kind `position9` в схеме (default/sanitize по
|
|
||||||
`POSITION9_VALUES`), виджет 3×3 `kit/fields/schema/PositionControl.svelte`.
|
|
||||||
В `ToolEntry` добавлен флаг `domOnly` (превью-executor держит такие
|
|
||||||
инструменты вне worker). `watermark-image-png` — отдельный под-шаг: ему
|
|
||||||
нужен overlay-source (`getOverlay`/store), которого в новом превью пока нет.
|
|
||||||
Тесты: +2 (602 passed). `font-style` и `plate` на этих инструментах сводятся
|
|
||||||
в шаги 28-29.
|
|
||||||
28. `font-style` — **все 3 инструмента переведены** ✔ (`text-to-png` — генератор
|
|
||||||
в `registry-new/generate.ts` (domOnly), `add-text-png`/ `date-stamp-png` —
|
|
||||||
рефакторинг в `registry-new/text.ts`). Составной тип во всех видах:
|
|
||||||
вложенный объект `style: { font, size, bold, color }` + `field.fontStyle`,
|
|
||||||
виджет `kit/fields/schema/FontStyleControl.svelte`, kind `font-style` в
|
|
||||||
схеме (default/sanitize, clamp размера к min/max). Тесты: +1 (603 passed).
|
|
||||||
29. `plate` — **все 2 инструмента переведены** ✔ (`add-text-png`,
|
|
||||||
`date-stamp-png` — рефакторинг в `registry-new/text.ts`). Составной тип во
|
|
||||||
всех видах: вложенный объект `plate: { enabled, color, opacity }` +
|
|
||||||
`field.plate`, виджет `kit/fields/schema/PlateControl.svelte` (чекбокс +
|
|
||||||
цвет + слайдер непрозрачности, деактивируется при выключенной плашке), kind
|
|
||||||
`plate` в схеме (default/sanitize, clamp opacity к 0..100). Покрытие тестов
|
|
||||||
расширено (дефолты/sanitize plate в существующих тестах).
|
|
||||||
30. `gradient` — **переведён** ✔ (linear-gradient-png). Направление градиента —
|
|
||||||
не горизонталь/вертикаль, а **произвольный угол**: новый переиспользуемый
|
|
||||||
виджет `kit/fields/schema/AngleControl.svelte` (слайдер 0..360° + кнопки
|
|
||||||
стандартных углов 0°/90°/180°/270° с активным состоянием). Составной тип
|
|
||||||
`gradient: { from, to, angle }` во всех видах (`field.gradient`, kind
|
|
||||||
`gradient` в схеме — default/sanitize, clamp угла к 0..360, валидация
|
|
||||||
цветов; виджет `kit/fields/schema/GradientControl.svelte` — пара цветов +
|
|
||||||
AngleControl). Рендер по углу — локальный `angleGradient` в
|
|
||||||
`registry-new/generate.ts` (core-`gradientImage` не трогали: он умеет только
|
|
||||||
horizontal/vertical и используется старым UI). Семантика: 0° слева направо,
|
|
||||||
90° сверху вниз, рост угла по часовой (ось Y вниз); нормализация по полному
|
|
||||||
диапазону проекции на ось (180°/270° разворачивают градиент). Тесты:
|
|
||||||
дефолты, направление по углу (0°/90°/180°), sanitize/кламп.
|
|
||||||
|
|
||||||
### Фаза 4 — масштаб UI на остальные инструменты
|
|
||||||
|
|
||||||
31. По мере перевода инструментов в Фазе 2-3 — рендер схемы (из Фазы 1)
|
|
||||||
покрывает их автоматически; составные виджеты (dimension, color-pair,
|
|
||||||
offset, position9, font-style, plate, gradient) — по одному, каждый с ревью.
|
|
||||||
32. Пер-инструмент layout (`schema.layout`) — **реализован** ✔.
|
|
||||||
`ToolSchemaLayout.groups: { title?, cols?, fields[] }` в registry-schema;
|
|
||||||
`SchemaFields.svelte` рендерит группы (подпись + сетка `1..n` колонок,
|
|
||||||
неупомянутые поля — в общей группе в конце, без заголовка). Пилот —
|
|
||||||
`add-text-png` (Text: text+style; Placement: position+margin; Plate). Тесты:
|
|
||||||
schema-layout в registry-schema.test.ts; полные запуски чисты.
|
|
||||||
33. Для каждого переведённого инструмента — UI-макет **расставлен** ✔.
|
|
||||||
Layout-группы заданы там, где группировка содержательна (канвас отдельно от
|
|
||||||
параметров эффекта, фигура отдельно от позиции; составные виджеты — внутри
|
|
||||||
групп целиком):
|
|
||||||
- geometry: fit-on-background (Canvas/Background), change-canvas-size
|
|
||||||
(Canvas/Anchor), resize (Canvas/Scaling), crop (Offset/Crop area);
|
|
||||||
- alpha-маски: circle/square/star/wavy (Shape — с 2 колонками где плотно,
|
|
||||||
Position — offset);
|
|
||||||
- generate: create-empty (Canvas/Fill), linear-gradient (Canvas/Colors),
|
|
||||||
color-spectrum (Canvas/Spectrum), random-colors (Canvas/Random), draw-grid
|
|
||||||
(Canvas/Grid), step-colors (Colors/Output), placeholder
|
|
||||||
(Canvas/Colors/Text), text-to-png (Text/Background/Padding);
|
|
||||||
- text: add-text, date-stamp (Text/Placement/Plate);
|
|
||||||
- filters: randomize-pixels (Blocks), add-noise (Noise/Seed). Инструменты с
|
|
||||||
1–3 простыми полями остались без layout (одна общая группа).
|
|
||||||
|
|
||||||
### Фаза 5 — изоляция старого UI
|
|
||||||
|
|
||||||
34. Поглощение `tool-views.ts` (preview/lede/layout → meta инструмента) ✔.
|
|
||||||
Отдельного `tool-views.ts` в репо нет: preview сразу строился на
|
|
||||||
`registry-new`. Meta инструмента живёт в `ToolEntry` (`title`,
|
|
||||||
`description`, `category`, `run`/`generate`), layout — в `schema.layout`
|
|
||||||
(шаг 32), рендер — `SchemaToolView`/`SchemaFields`/`SchemaPreview`.
|
|
||||||
Дублирующей системы нет.
|
|
||||||
35. Старый UI **не удаляется**, а выносится в `(old)/`-маршруты и остаётся там
|
|
||||||
временно (посмотреть, как работает, сравнить с новым; старые тесты
|
|
||||||
продолжают проходить):
|
|
||||||
- маршруты `(old)/{+page,demo,list-tools,tools/[id]}` — старый дизайн, тянет
|
|
||||||
`old.css` (не `design2.css`), старый header/footer;
|
|
||||||
- старый `registry.ts`/`registry/` + `ParamDef[]` обслуживают только
|
|
||||||
`(old)/`-инструменты — не удаляются, не рефакторятся;
|
|
||||||
- удаление происходит позже, отдельным решением (когда новый UI покроет все
|
|
||||||
инструменты и ревью завершено).
|
|
||||||
36. **Линтер-изоляция веток** (гарантия, что old и preview не смешиваются) ✔.
|
|
||||||
Кастомный ESLint-плагин `web/eslint-plugins/isolation/no-mixed-imports`
|
|
||||||
Резолвит каждый импорт (и `$lib/...`, и относительные `./`/`../`) до
|
|
||||||
реального файла, классифицирует источник и цель по фактическому пути и
|
|
||||||
ругается на old→new и new→old. Конфигурация (`old`/`new` glob-паттерны,
|
|
||||||
`root`, `alias`) вынесена в настройки правила — единая точка правды:
|
|
||||||
- старое: `routes/(old)/**`, `lib/registry.ts`, `lib/registry/**`,
|
|
||||||
`lib/registry-helpers.ts`, `lib/categories.ts`, `lib/tools/**`,
|
|
||||||
`lib/components/**` (кроме `kit/`);
|
|
||||||
- новое: `routes/preview/**`, `lib/registry-new/**`, `lib/preview/**`,
|
|
||||||
`lib/registry-schema.ts`, `lib/registry-schema.test.ts`,
|
|
||||||
`lib/components/kit/**`;
|
|
||||||
- общее (разрешено обоим): всё прочее — `core/`, `i18n/`, `theme`,
|
|
||||||
`assets/`, корневой `lib` (`index.ts`, тесты). Достигнутая полная изоляция
|
|
||||||
(одиночные пересечения устранены):
|
|
||||||
- старые пилоты `registry/geometry.ts` (`add-border`) и `registry/alpha.ts`
|
|
||||||
(`add-stroke`) получали `schema` из нового `registry-schema` — убрано; оба
|
|
||||||
инструмента работают в старом UI через `params: ParamDef[]`, в preview —
|
|
||||||
через свои schema-версии в `registry-new/`;
|
|
||||||
- `registry.ts` больше не импортирует `ToolSchema` из `registry-schema`;
|
|
||||||
- preview `list-tools` тянул `TOOL_ICONS` из старого `lib/tools/tool-icons`
|
|
||||||
→ создана копия `lib/preview/tool-icons.ts` (правило копий).
|
|
||||||
37. Перенос всех старых компонентов/библиотек в папки `old/` (**следующее**):
|
|
||||||
переезд не трогает плагин — достаточно дописать один glob-паттерн в
|
|
||||||
настройку правила (например `lib/old/**`, `lib/components/old/**`), а сама
|
|
||||||
проверка работает по фактическим путям автоматически.
|
|
||||||
|
|
||||||
### Как ревьюить каждый шаг
|
|
||||||
|
|
||||||
- Diff **только один инструмент** (или фундамент без инструментов).
|
|
||||||
- Старый `params`/`run`/`defaultParams`/`sanitizeParams` — **не изменены**
|
|
||||||
(кроме добаление `schema`, если это пилот).
|
|
||||||
- Старый UI и старые тесты продолжают работать — запустить `pnpm --dir web test`
|
|
||||||
и `pnpm --dir web exec svelte-check`.
|
|
||||||
- Порядок полей в `interface Params` и в `schema` совпадает; дефолты в сeme
|
|
||||||
равны старым `default` из `params`.
|
|
||||||
- Пилот из Фазы 1 проверяется **руками в preview**: применяется рамка,
|
|
||||||
дефолты/валидация/смена значений работают end-to-end.
|
|
||||||
|
|
||||||
## Поток данных (runtime)
|
|
||||||
|
|
||||||
### Старый pipeline (не трогаем)
|
|
||||||
|
|
||||||
Всё как сейчас: старое `defaultParams`/`sanitizeParams`/executor/chain работают
|
|
||||||
на `ParamDef[]` и `Record<string, unknown>`. Никаких изменений.
|
|
||||||
|
|
||||||
### Новый pipeline (строим рядом)
|
|
||||||
|
|
||||||
- **Хранилище значений** — типизировано на схему; у нового UI значения полей
|
|
||||||
собираются в `Params` по `ToolSchema<P>`.
|
|
||||||
- **defaultParams / sanitizeParams (новые)** — выводятся из `ToolSchema<P>`,
|
|
||||||
дефолты лежат в схеме. Старые функции не трогаем, добавляем новые рядом.
|
|
||||||
- **Executor/worker (новые)** — получает `Record<string, unknown>`, собирает
|
|
||||||
`Params` по схеме, вызывает `run`. Остаётся serializable.
|
|
||||||
- **Pipeline/chain (новый)** — шаги хранят значения, типизируются по схеме.
|
|
||||||
|
|
||||||
## Изменения в UI-компонентах
|
|
||||||
|
|
||||||
- **Старый UI** (`ParamForm.svelte`, группа `(old)/`) — **не трогаем.** Работает
|
|
||||||
на `ParamDef[]` как раньше.
|
|
||||||
- **Новый UI** (kit/`SchemaToolView` + `SchemaFields` + `SchemaPreview`, читает
|
|
||||||
`ToolSchema<P>`) — сейчас рендерит поля по схеме (number/slider/color).
|
|
||||||
- `tool-views.ts` отсутствует — preview/lede/layout уже живут в meta инструмента
|
|
||||||
(`ToolEntry` + `schema.layout`), дублирования нет.
|
|
||||||
|
|
||||||
## Оценка трудозатрат
|
|
||||||
|
|
||||||
| Часть | Сложность | Время |
|
|
||||||
| ----------------------------------------------------- | ------------------------ | ---------------------- |
|
|
||||||
| Typed field builders + toolSchema\<P\> | Средняя | ~2ч |
|
|
||||||
| Новые defaultParams/sanitizeParams под схему | Средняя | ~2-3ч |
|
|
||||||
| ToolEntry\<P\> generic + поле schema (рядом с params) | Низкая-Средняя | ~1-2ч |
|
|
||||||
| Новый executor/worker/pipeline под схему | Средняя | ~2-3ч |
|
|
||||||
| Миграция инструментов на interface Params + схемы | Механическая, но крупная | ~8-12ч |
|
|
||||||
| Новый рендер (kit/ParamControl) + составные виджеты | Средняя | ~4-5ч |
|
|
||||||
| Поглощение tool-views.ts | Средняя | ~1-2ч |
|
|
||||||
| Вынос старого UI в (old)/ + адаптация маршрутов | Средняя-Низкая | ~2-3ч |
|
|
||||||
| **Итого** | | **~22-32ч** (поэтапно) |
|
|
||||||
|
|
||||||
> Оценка выросла по сравнению с ранней версией плана, потому что принят путь
|
|
||||||
> «явный interface Params + схема + общий рендер с пер-инструмент layout» — это
|
|
||||||
> полный рефакторинг pipeline, а не только добавление составных типов.
|
|
||||||
>
|
|
||||||
> Старый UI/`ParamDef[]`/старый pipeline в смету **не входят** — они не
|
|
||||||
> рефакторятся, а продолжают работать на `(old)/`-маршрутах до перехода (затем
|
|
||||||
> убираются отдельным решением — см. шаг 35).
|
|
||||||
|
|
||||||
## Порядок реализации (кратко)
|
|
||||||
|
|
||||||
Детальный пошаговый план с атомарными изменениями и порядком ревью — в разделе
|
|
||||||
«Пошаговый план перевода инструментов» выше. Здесь краткая сводка фаз:
|
|
||||||
|
|
||||||
1. **Фаза 0** — фундамент: builders + `toolSchema<P>` + `ToolEntry<P>.schema` +
|
|
||||||
новые default/sanitize. Ни одного инструмента не изменено. ✔
|
|
||||||
2. **Фаза 1** — **один полноценный рабочий инструмент в preview** («добавить
|
|
||||||
рамку»): schema + рендер + end-to-end применение. Ручная оценка
|
|
||||||
работоспособности решения/сложностей.
|
|
||||||
3. **Фаза 2** — простые/одиночные инструменты (проверенный паттерн Фазы 1), 1
|
|
||||||
инструмент = 1 диф.
|
|
||||||
4. **Фаза 3** — инструменты с составными типами (dimension → color-pair → offset
|
|
||||||
→ position9 → font-style → plate → gradient), пилот каждого типа отдельно.
|
|
||||||
5. **Фаза 4** — масштаб UI на остальные: составные виджеты, пер-инструмент
|
|
||||||
layout (для каждого инструмента — UI-макет).
|
|
||||||
6. **Фаза 5** — изоляция: старый UI на `(old)/`-маршруты (не удаляется, остаётся
|
|
||||||
для ревью); preview на `registry-new` полностью; линтер-изоляция веток
|
|
||||||
(плагин `isolation`), затем перенос старых компонентов/библиотек в папки
|
|
||||||
`old/`.
|
|
||||||
|
|
||||||
## Зависимости
|
|
||||||
|
|
||||||
- **UI сперва доделать** — текущая активная работа; типизация params ожидает
|
|
||||||
готовности нового дизайна.
|
|
||||||
- Ветка redesign (`plan-redesign.md`) — параллельная, учесть при миграции
|
|
||||||
kit/`ParamControl.svelte` и `overlay-store`.
|
|
||||||
- Старый pipeline остаётся рабочим и шippable на всём протяжении перехода
|
|
||||||
(аналогично стратегии `plan-redesign.md`).
|
|
||||||
@@ -1,139 +0,0 @@
|
|||||||
# План: доводка дизайна до верности (фикс по refs)
|
|
||||||
|
|
||||||
> **СТАТУС: ЗАКРЫТ (2026-09-07).** Актуальный дизайн (`preview/*`) ушёл вперёд
|
|
||||||
> от референсов `refs-html/` — сверка с ними больше не информативна (новые
|
|
||||||
> компоненты `SchemaToolView`/`SchemaPreview`/каталог не имеют рефовой копии).
|
|
||||||
> Аудит против рефа прекращается; качество дизайна дальше гоняется ревью руками
|
|
||||||
> и специализированными проверками (`lint:all` —
|
|
||||||
> stylelint/design-tokens/токен-аудит). Порядок удаления старого и перехода — по
|
|
||||||
> `plan-redesign.md` (C17–C21).
|
|
||||||
|
|
||||||
> Статус: к выполнению. Ветка `feat/redesign`. Источники:
|
|
||||||
> `docs/archive/demo-fidelity.md` (архивная копия старого отчёта по верности,
|
|
||||||
> неактуальна), `docs/plan-redesign.md` (общая стратегия, фазы 1–4 выполнены до
|
|
||||||
> C16). Актуальный аудит верности — `pnpm refs-cdp-audit` /
|
|
||||||
> `pnpm refs-css-audit` / `pnpm refs-dom-audit` → `web/audit/audit-report.md`
|
|
||||||
> (текущий список скриптов — в `web/package.json`, см. `refs-*audit`).
|
|
||||||
|
|
||||||
## Контекст
|
|
||||||
|
|
||||||
- Новый дизайн собран параллельно под `preview/*` (см. `plan-redesign.md`),
|
|
||||||
старый сайт `(old)/` не трогаем до C17.
|
|
||||||
- `refs/` и `refs-html/` приведены к консистентному виду — текущая «правда по
|
|
||||||
пикселям». Механизм синхронизации ref→код не создаётся (см. §9 plan-redesign).
|
|
||||||
- Аудит верности автоматизирован: `pnpm refs-cdp-audit` (1440×900,
|
|
||||||
`scripts/audit-cdp.mjs`) пишет `web/audit/audit-report.md` — light/dark
|
|
||||||
токены + поэлементный дифф стилей; пиксель-дифф не используется. Палитры
|
|
||||||
токенов уже сверены с рефом (light совпадает, dark собран). Темы в `refs-html`
|
|
||||||
переключаются — скрипт инжектится в `scripts/extract-static.mjs`.
|
|
||||||
- Исходный отчёт `demo-fidelity.md` перенесён в `docs/archive/` как утративший
|
|
||||||
актуальность; его данные актуализированы ниже (Этап 1).
|
|
||||||
- Зафиксированные решения (подтверждены автором):
|
|
||||||
- **F1 — плоский фон холста**, без blueprint-сетки (как в рефе).
|
|
||||||
- **F4 — preview-stack в одну колонку** (как в рефе).
|
|
||||||
|
|
||||||
## Принципы
|
|
||||||
|
|
||||||
- Никаких правок старого дизайна `(old)/` и старого `ui/`.
|
|
||||||
- Компоненты пишем в `kit/` (переименование в `ui/` — на C19 по plan-redesign).
|
|
||||||
- Коммиты атомарные (< ~500 строк), по 1–2 компонента; после каждого —
|
|
||||||
`svelte-check` + `lint` + `build` + `refs-cdp-audit` (diff% не должен расти
|
|
||||||
без причины).
|
|
||||||
- Коммиты делает автор (агент не коммитит).
|
|
||||||
|
|
||||||
## Этап 0 — Рефакторинг (SRP / дизайн-обёртки), без изменения визуала
|
|
||||||
|
|
||||||
Гигантские страницы (`demo` ~513, `tools/[id]` ~451 строк) дробим на компоненты
|
|
||||||
единой ответственности; убираем дублирующиеся inline-стили.
|
|
||||||
|
|
||||||
Новые обёртки в `src/lib/components/kit/`:
|
|
||||||
|
|
||||||
- `FieldGrid` — сетка полей (заменяет `.controls` / `.controls.compact`).
|
|
||||||
- `ToggleRow` — лейбл + `Toggle` (дублировался в `demo` и `tools/[id]`).
|
|
||||||
- `PreviewStack` — контейнер превью-плиток, **одна колонка** (реализует F4 в
|
|
||||||
одном месте).
|
|
||||||
- `SectionLabel` — label + strong + actions (вместо `.pipeline-head` /
|
|
||||||
`.preview-top`).
|
|
||||||
- `FileChip` — пилюля файла.
|
|
||||||
- `PipelineFooter` — reset + auto-note.
|
|
||||||
- `WorkspaceLayout` — двухпанельная сетка (settings | preview) со sticky +
|
|
||||||
брейкпоинт 800px.
|
|
||||||
|
|
||||||
Декомпозиция страниц:
|
|
||||||
|
|
||||||
- `tools/[id]`: вынести `ParamControl` (диспетчер полей по `ParamDef`),
|
|
||||||
`SettingsPanel` (левая панель), `PreviewPanel` (правая панель) — колоцированно
|
|
||||||
в `routes/preview/tools/[id]/`. Страница сжимается до состояния + глёв.
|
|
||||||
- `demo`: переписать на те же обёртки + `WorkspaceHeader` (eyebrow + h1 + lede +
|
|
||||||
file-chip).
|
|
||||||
|
|
||||||
Побочный баг, устраняемый тут же: в `demo` поля сейчас обёрнуты в
|
|
||||||
`<div class="control-block"><MonoLabel>…</MonoLabel><SliderField label>` —
|
|
||||||
**двойной лейбл**, т.к. `ColorField`/`SliderField`/`SelectField`/`NumberField`
|
|
||||||
уже содержат `Field` (MonoLabel) изнутри. При рефакторе убираем внешний
|
|
||||||
`MonoLabel`, оставляем само поле.
|
|
||||||
|
|
||||||
Критерий Этапа 0: визуал идентичен (audit diff% без регрессии), старые тесты
|
|
||||||
зелёные.
|
|
||||||
|
|
||||||
## Этап 1 — Верность (по refs-html; статус на момент плана)
|
|
||||||
|
|
||||||
Аудит: `pnpm refs-cdp-audit` (1440×900) пишет `web/audit/audit-report.md` —
|
|
||||||
light/dark токены + поэлементный дифф стилей. Пиксель-дифф не используется.
|
|
||||||
Палитры токенов уже сверены с рефом: light совпадает (`--background`, `--panel`,
|
|
||||||
`--foreground`, `--muted`, `--line`, `--blue`), dark собран (16 токенов). Темы в
|
|
||||||
`refs-html` переключаются (скрипт инжектится в `extract-static.mjs`).
|
|
||||||
|
|
||||||
Открытые / решённые пункты (исходник — `docs/archive/demo-fidelity.md`):
|
|
||||||
|
|
||||||
- **B1** (открыт, P1): `button, input, select, textarea` рендерятся Arial —
|
|
||||||
добавить в reset `design2.css` `font-family: inherit`. Влияет на все страницы.
|
|
||||||
- **B2** (решён — проверить): двойного топбара нет. `preview/+layout.svelte`
|
|
||||||
рендерит ровно один `TopBar`; `demo` свой `.topbar` не рендерит. Подтвердить
|
|
||||||
аудитом/визуально при Этапе 1.
|
|
||||||
- **F1** (решено — плоский фон): наш `--background` уже `#EEF1F4` (как реф);
|
|
||||||
видимое отличие было в blueprint-сетке поверх фона. Фикс: убрать
|
|
||||||
`BlueprintGrid` из `AppShell`.
|
|
||||||
- **F2** (сделано C16): тип шага (`StepCard`) — приглушённый mono-текст, не
|
|
||||||
синий бейдж; индекс шага — синий `#1769D2`.
|
|
||||||
- **F3** (сделано C16): подпись превью-тайла (`PreviewTile`) — синяя `#1769D2`.
|
|
||||||
- **F4** (решено — одна колонка): реализуется в `PreviewStack` (Этап 0), колонка
|
|
||||||
= `1fr`.
|
|
||||||
- **F5** (открыт): вес `MonoLabel` 500 → 400 (как в рефе).
|
|
||||||
- **F6** (открыт): мета-подписи (MetaRow/MetaList, DIMENSIONS/FORMAT/SIZE) fs
|
|
||||||
10px → 9px.
|
|
||||||
- **F7** (низкий): копирайт/стиль — у нас `+ Add tool` (реф `Add tool`); кнопка
|
|
||||||
`Download result` — сверить с рефом (синяя, ~478×42). Проверить визуально
|
|
||||||
(замеры NOT FOUND — артефакт вложенного чипа размера).
|
|
||||||
- **F8** (низкий): `h1` line-height чуть выше рефа (150px vs 115px).
|
|
||||||
- **F9** (открыт): вертикальный ритм — подровнять паддинги секций под реф
|
|
||||||
(pipeline-head и пр.).
|
|
||||||
- **F10** (открыт): внутренние отступы preview-панели (паддинг панели).
|
|
||||||
|
|
||||||
После Этапа 1 — повторный `refs-cdp-audit`, зафиксировать остаточный diff
|
|
||||||
(sub-pixel / структурный) и обновить статус пунктов выше прямо в этом файле.
|
|
||||||
|
|
||||||
## Этап 2 — plan-redesign.md (C17–C21), ОТЛОЖЕНО
|
|
||||||
|
|
||||||
Выполняется **только после** того, как новый дизайн доведён до приемлемого
|
|
||||||
уровня верности (Этапы 0–1). Сами пункты — в `plan-redesign.md` §10 Шаг 5–6:
|
|
||||||
|
|
||||||
- **C17**: `preview/*` → реальные маршруты, удалить `(old)/`, корневой
|
|
||||||
`+layout.svelte` ← `design2.css`, поправить `newCode` в `eslint.config.js`
|
|
||||||
(`src/routes/preview/**` → `src/routes/**`).
|
|
||||||
- **C18**: правки импортов/редиректов, проверка билда.
|
|
||||||
- **C19**: удалить старое `ui/` + `app.css`, `kit/` → `ui/` (или оставить),
|
|
||||||
раскрыть ESLint `recommended` на весь код.
|
|
||||||
- **C20**: брейкпоинты 1200/1100/800/480; build без предупреждений; `grep` по
|
|
||||||
старым токенам/классам пуст.
|
|
||||||
- **C21**: опц. удалить `refs/` и `refs-html/` из репо.
|
|
||||||
|
|
||||||
## Порядок коммитов (Этап 0)
|
|
||||||
|
|
||||||
1. `kit/`: `FieldGrid`, `ToggleRow`, `PreviewStack`, `SectionLabel`, `FileChip`,
|
|
||||||
`PipelineFooter`, `WorkspaceLayout` — по 1–2 на коммит.
|
|
||||||
2. `tools/[id]`: `ParamControl` → `SettingsPanel` + `PreviewPanel` → тонкая
|
|
||||||
страница.
|
|
||||||
3. `demo`: переписать на обёртки.
|
|
||||||
|
|
||||||
После каждого коммита: `svelte-check` + `lint` + `build` + `refs-cdp-audit`.
|
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
# План: вторая волна EASY-инструментов
|
|
||||||
|
|
||||||
> **СТАТУС: ВЫПОЛНЕН 2026-08-23.**
|
|
||||||
|
|
||||||
## 1. Принципы отбора
|
|
||||||
|
|
||||||
- Максимум переиспользования: готовые контракты записей (`run`, `preview`,
|
|
||||||
формат вывода, info-режим) и существующие операции ядра.
|
|
||||||
- Новые механизмы ядра — минимальные, чистые, покрытые тестами до подключения
|
|
||||||
инструментов.
|
|
||||||
- Canvas/DOM-зависимые утилиты (SVG-вход, надписи и водяные знаки, эффекты через
|
|
||||||
фильтры контекста) — вне этого пакета, отдельная волна.
|
|
||||||
- Целевой объём волны: около 37 новых инструментов, каталог вырастает с 11 до
|
|
||||||
~48.
|
|
||||||
|
|
||||||
## 2. Новые механизмы
|
|
||||||
|
|
||||||
- **Генераторы без входного файла**: у записи появляется признак «источник не
|
|
||||||
нужен»; страница такого инструмента не показывает левую панель и зону
|
|
||||||
перетаскивания — только параметры и результат.
|
|
||||||
- **Текстовый ввод и вывод**: часть утилит принимает вставленный текст вместо
|
|
||||||
файла и выдаёт текстовый результат (панель с полем только для чтения, кнопки
|
|
||||||
«копировать» и «скачать как текст»). Распространяется на base64, data-uri,
|
|
||||||
hex.
|
|
||||||
- **BMP-энкодер в ядре**: 24-битный, чистая функция из пикселей в байты, с
|
|
||||||
тестами на маленьких матрицах (заголовок, порядок каналов BGR, паддинг
|
|
||||||
строки).
|
|
||||||
|
|
||||||
## 3. Состав пакета
|
|
||||||
|
|
||||||
**Конвертация — 6** `jpg-to-png`, `webp-to-png`, `gif-to-png` (первый кадр),
|
|
||||||
`bmp-to-png`, `ico-to-png` — всё через уже умеющий декодировать ввод, сохранение
|
|
||||||
в PNG; плюс `png-to-bmp` на новом энкодере.
|
|
||||||
|
|
||||||
**Текстовые представления — 6** `png-to-base64`, `base64-to-png`,
|
|
||||||
`png-to-data-uri`, `data-uri-to-png`, `png-to-hex`, `hex-to-png`.
|
|
||||||
|
|
||||||
**Цвет — 9** `change-opacity` (множитель прозрачности), `sepia`, `change-hue`
|
|
||||||
(сдвиг тона), `extract-channel` (выбор R/G/B в оттенках серого), `swap-channels`
|
|
||||||
(перестановка парой), `threshold-black-white` (порог яркости), `posterize`
|
|
||||||
(число уровней), `two-colors` (два цвета + порог), `invert-alpha`.
|
|
||||||
|
|
||||||
**Прозрачность и холст — 7** `remove-alpha-channel` (подложка белым),
|
|
||||||
`set-alpha-channel` (константная прозрачность), `extract-alpha-mask` (альфа в
|
|
||||||
чёрно-белую маску), `add-padding` (отступ + цвет/прозрачность), `add-border`
|
|
||||||
(толщина + цвет), `round-corners` (радиус), `fit-on-background` (вписать на
|
|
||||||
полотно цвета с полями).
|
|
||||||
|
|
||||||
**Композиция — 2** `tile` (повтор по сетке), `center-by-alpha` (центрирование по
|
|
||||||
непрозрачному содержимому).
|
|
||||||
|
|
||||||
**Генераторы — 4** `create-empty` (размер + цвет или прозрачность),
|
|
||||||
`single-color`, `random-noise` (с фиксированным зерном для воспроизводимости),
|
|
||||||
`linear-gradient` (два цвета, направление).
|
|
||||||
|
|
||||||
**Анализ — 3** `is-grayscale`, `is-transparent`, `orientation`
|
|
||||||
(портрет/ландшафт/квадрат) — текстовые сводки в info-режиме.
|
|
||||||
|
|
||||||
## 4. Новые операции ядра (все — чистые функции с тестами)
|
|
||||||
|
|
||||||
- Холст: расширение полотна (общая база для padding/border/fit), тайлинг, центр
|
|
||||||
по bounding-box альфы, скругление углов маской радиуса.
|
|
||||||
- Цвет: сепия, сдвиг тона через HSL, множитель альфы, извлечение/перестановка
|
|
||||||
каналов, пороги (яркость и двухцветный), постеризация уровней, инверсия альфы.
|
|
||||||
- Генерация: заливка цветом, шум с зерном, линейный градиент.
|
|
||||||
- Форматы: BMP-энкодер; кодеки текстовых представлений (base64/data-uri/hex в
|
|
||||||
обе стороны).
|
|
||||||
- Анализ: проверка монохромности, полной непрозрачности, ориентации.
|
|
||||||
|
|
||||||
## 5. Этапы и проверки
|
|
||||||
|
|
||||||
- **A. Механизмы** — генераторный режим страницы, текстовый ввод/вывод,
|
|
||||||
BMP-энкодер. Проверка: `test/check/build`.
|
|
||||||
- **B. Конвертация и текстовые представления** — 12 инструментов. Проверка:
|
|
||||||
ручной прогон каждой конвертации туда-обратно.
|
|
||||||
- **C. Цвет** — 9 инструментов. Проверка: юнит-тесты операций + визуально.
|
|
||||||
- **D. Прозрачность, холст, композиция** — 9 инструментов. Проверка:
|
|
||||||
юнит-тесты + визуально.
|
|
||||||
- **E. Генераторы и анализ** — 7 инструментов. Проверка: детерминизм шума (одно
|
|
||||||
зерно — одно изображение), своды анализа.
|
|
||||||
- **F. Итог** — полный прогон, смоук всех новых инструментов «настроить →
|
|
||||||
скачать», регресс старых одиннадцати.
|
|
||||||
|
|
||||||
## 6. Критерии готовности
|
|
||||||
|
|
||||||
- Каталог содержит ~48 рабочих инструментов, каждый доступен страницей с формы
|
|
||||||
из реестра.
|
|
||||||
- Все новые операции ядра покрыты тестами; обратимые конвертации проверены
|
|
||||||
круговым прогоном (например PNG → base64 → PNG даёт те же пиксели).
|
|
||||||
- Генераторы работают без загрузки файла; текстовые представления копируются и
|
|
||||||
скачиваются.
|
|
||||||
- `test/check/build` зелёные.
|
|
||||||
|
|
||||||
## 7. Что сознательно не делаем
|
|
||||||
|
|
||||||
- SVG-вход, рисование текста и водяных знаков, эффекты через фильтры контекста,
|
|
||||||
GIF/APNG-энкодинг, пакетная обработка — последующие волны.
|
|
||||||
- Семейства logo/icon/signature/stamp как отдельные страницы — они будут
|
|
||||||
конфигурациями тех же операций после этой волны.
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
# План: третья волна EASY — трансформации, свет, SVG
|
|
||||||
|
|
||||||
> **СТАТУС: ВЫПОЛНЕН 24.08.2026.**
|
|
||||||
|
|
||||||
## 1. Что получается
|
|
||||||
|
|
||||||
Одиннадцать новых инструментов: свободные трансформации формы (наклон, поворот
|
|
||||||
на произвольный угол, зум, сдвиг), светокоррекция (гамма, автоконтраст,
|
|
||||||
температура, тонирование), виньетка, эффект «артефактов сжатия» и вход SVG.
|
|
||||||
Каталог вырастает с 58 до ~69.
|
|
||||||
|
|
||||||
Текстовые инструменты и водяные знаки в волну не входят — им нужен рендер текста
|
|
||||||
на канвасе, это отдельная четвёртая волна.
|
|
||||||
|
|
||||||
## 2. Новое ядро
|
|
||||||
|
|
||||||
- **Аффинные трансформации** (`core/affine.ts`): единая функция обратного
|
|
||||||
отображения — для каждого пикселя результата вычисляется исходная координата
|
|
||||||
матрицей и семплируется билинейно (общий семплер, вынесенный из `resize`).
|
|
||||||
Поверх неё: наклон по осям, поворот на произвольный угол (с расширением холста
|
|
||||||
под новые габариты), зум к центру, сдвиг.
|
|
||||||
- **Свет и цвет** (`core/color.ts` дополнения): гамма-коррекция через LUT,
|
|
||||||
автоконтраст (растяжка по гистограмме яркости с отсечкой хвостов), температура
|
|
||||||
(множители тёплых/холодных каналов), тонирование (умножение RGB на цвет с
|
|
||||||
сохранением яркостного баланса).
|
|
||||||
- **Виньетка** (`core/effects.ts`): радиальное затемнение от центра, сила
|
|
||||||
слайдером; чистая математика без canvas.
|
|
||||||
- **IO-расширения** (`core/io.ts`): декодирование SVG из текста/файла через
|
|
||||||
blob-URL и `Image`; раундтрип «закодировать JPEG с качеством Q → декодировать
|
|
||||||
обратно» для эффекта артефактов сжатия.
|
|
||||||
|
|
||||||
## 3. Инструменты — 11
|
|
||||||
|
|
||||||
- **Геометрия:** `skew-png` (углы X/Y), `rotate-free-png` (любой угол, холст
|
|
||||||
расширяется), `zoom-png` (масштаб ≥100 % к центру), `shift-png` (сдвиг со
|
|
||||||
смещением содержимого).
|
|
||||||
- **Фильтры:** `vignette-png` (сила затемнения), `jpeg-artifacts-png` (имитация
|
|
||||||
пережатия).
|
|
||||||
- **Цвет:** `gamma-png` (0.1–3.0), `auto-contrast-png` (без параметров),
|
|
||||||
`temperature-png` (−100 тёплый…+100 холодный или наоборот — подпись честная),
|
|
||||||
`tint-png` (цвет + сила).
|
|
||||||
- **Конвертация:** `svg-to-png` (текст SVG слева, как base64-режим).
|
|
||||||
|
|
||||||
## 4. Этапы и проверки
|
|
||||||
|
|
||||||
- **A. Ядро** — семплер, аффинный модуль, свет/цвет, эффекты + точные тесты:
|
|
||||||
поворот квадрата на 90° совпадает с существующим `rotate90`, зум ×2 удваивает
|
|
||||||
размеры и центр остаётся центром, гамма 1 — идентичность, автоконтраст
|
|
||||||
растягивает известный диапазон, виньетка симметрична и не трогает центр при
|
|
||||||
нулевой силе. Проверка: `pnpm test/check`.
|
|
||||||
- **B. IO** — SVG-декод и JPEG-раундтрип. Проверка: сборка; живой прогон на
|
|
||||||
этапе D.
|
|
||||||
- **C. Реестр** — одиннадцать записей, иконки, тесты целостности. Проверка:
|
|
||||||
`test/build`, страницы в экспорте.
|
|
||||||
- **D. Итог** — полный прогон, смоук §7.
|
|
||||||
|
|
||||||
## 5. Смоук-чеклист
|
|
||||||
|
|
||||||
1. Наклон X/Y по отдельности и вместе — форма искажается предсказуемо, без
|
|
||||||
чёрных полос при малых углах.
|
|
||||||
2. Поворот на 30° — холст вырос, углы прозрачны, содержимое повернуто.
|
|
||||||
3. Зум 200 % — содержимое увеличено к центру, холст прежнего размера, края
|
|
||||||
обрезаны.
|
|
||||||
4. Сдвиг уводит содержимое за край, противоположный край заполняется фоном.
|
|
||||||
5. Гамма 0.5 темнее, 2.0 светлее, 1.0 — без изменений.
|
|
||||||
6. Автоконтраст на тусклом фото вытягивает диапазон.
|
|
||||||
7. Температура ±50 заметно теплеет/холодеет.
|
|
||||||
8. Тонирование красным даёт равномерный оттенок, белые области окрашиваются
|
|
||||||
сильнее всего.
|
|
||||||
9. Виньетка затемняет только края, центр не трогает.
|
|
||||||
10. Артефакты: качество 10 даёт видимые квадраты JPEG.
|
|
||||||
11. SVG вставленный слева декодируется в результат справа; битый SVG — баннер.
|
|
||||||
12. Все новые инструменты работают в цепочке и скачиваются из своих секций.
|
|
||||||
|
|
||||||
## 6. Критерии готовности
|
|
||||||
|
|
||||||
- Одиннадцать инструментов работают и покрыты юнит-тестами ядра (кроме
|
|
||||||
IO-дом-части).
|
|
||||||
- Аффинные операции опираются на общий билинейный семплер; дублирования
|
|
||||||
сэмплинга нет.
|
|
||||||
- Каталог ~69 инструментов; `test/check/build` зелёные, смоук §5 пройден.
|
|
||||||
|
|
||||||
## 7. Что сознательно не делаем
|
|
||||||
|
|
||||||
- Рендер текста, водяные знаки, штампы — четвёртая волна (DOM-текст).
|
|
||||||
- Субпиксельная точность аффинных трансформаций сверх билинейного семплинга.
|
|
||||||
- Пакетное применение и пресеты.
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
# План: закрытие EASY/MEDIUM-пробелов из сравнения с onlinepngtools
|
|
||||||
|
|
||||||
> **СТАТУС: ВЫПОЛНЕН (2026-09-07) — устарел.** Все волны W1–W9 закрыты. Идеи из
|
|
||||||
> раздела «Вне очереди» (region-инструменты, мультифайловый вывод, анимационные,
|
|
||||||
> HARD-серия) перенесены в `backlog.md`.
|
|
||||||
|
|
||||||
> Статус: черновик на ревью
|
|
||||||
|
|
||||||
## Принципы
|
|
||||||
|
|
||||||
- Волны собираются вокруг общего математического ядра: одна волна = один
|
|
||||||
движок + N инструментов на нём.
|
|
||||||
- Каждая волна оформляется отдельным планом при старте; здесь — очередь и
|
|
||||||
состав.
|
|
||||||
- Порядок: сначала кластеры, дающие много инструментов малой кровью, затем
|
|
||||||
одиночные эффекты, MEDIUM-пакеты в конце.
|
|
||||||
- Полный список параметров каждого инструмента живёт в карте инструментов
|
|
||||||
(tools-map.md); здесь только состав и ядро.
|
|
||||||
|
|
||||||
## Очередь волн
|
|
||||||
|
|
||||||
### W1. Палитры — ВЫПОЛНЕНА (11 инструментов)
|
|
||||||
|
|
||||||
Ядро: RGB↔HSL + гармонии цветового круга. Вывод: свотч-полотно (генератор).
|
|
||||||
Состав: color-wheel, complementary, monochromatic, analogous, triadic, tetradic,
|
|
||||||
similar-shades, sort-colors. Плюс утилиты смешения тем же ядром: mix-colors,
|
|
||||||
average-color, blend-two, step-between (+4, итого 12).
|
|
||||||
|
|
||||||
### W2. Каналы и пространства — ВЫПОЛНЕНА (6 инструментов)
|
|
||||||
|
|
||||||
Ядро: матрицы преобразования RGB→(HSL/HSV/HSI/CMYK/YCbCr/LAB) + визуализация
|
|
||||||
выбранного компонента серым или окрашенно. Общий select «компонент» + select
|
|
||||||
«режим отображения».
|
|
||||||
|
|
||||||
### W3. Маски по свойствам пикселей — ВЫПОЛНЕНА (7 инструментов)
|
|
||||||
|
|
||||||
Ядро: предикат над пикселем → бинарная маска (с инверсией и подсветкой цветом).
|
|
||||||
Состав: show-transparent, show-grayscale, show-color, light-mask, dark-mask,
|
|
||||||
unique-color-mask, extract-by-color.
|
|
||||||
|
|
||||||
### W4. Фигурные маски — ВЫПОЛНЕНА (4 инструмента)
|
|
||||||
|
|
||||||
Ядро: SDF фигуры (круг/квадрат/звезда/волна) → альфа-маска с fit-режимами.
|
|
||||||
Состав: circle-mask, square-mask, star-mask, wavy-mask.
|
|
||||||
|
|
||||||
### W5. Геометрия-добивки — ВЫПОЛНЕНА (5 инструментов)
|
|
||||||
|
|
||||||
Ядро: bbox по альфе (trim) переиспользуется тремя инструментами источника.
|
|
||||||
Состав: trim-empty-space (закрывает remove border/space), change-canvas-size,
|
|
||||||
change-aspect-ratio, landscape↔portrait, symmetric-copy.
|
|
||||||
|
|
||||||
### W6. Эффекты лёгкие — ВЫПОЛНЕНА (6 инструментов; color-blocks покрыт pixelate)
|
|
||||||
|
|
||||||
Состав: pixelate, color-blocks, randomize-pixels (seed), add-noise,
|
|
||||||
feather-edges, clean-edges, silhouette. Shadow/glow — сюда же, если потянет
|
|
||||||
этап: оба = размытая альфа + смещение + цвет (ядро blur уже есть).
|
|
||||||
|
|
||||||
### W7. Конвертеры и генераторы — ВЫПОЛНЕНА (11 инструментов)
|
|
||||||
|
|
||||||
PNG↔bytes, PNG↔rgb-values (текстовые результаты), verify-is-png (анализ
|
|
||||||
сигнатуры), text-to-png (domText без входной картинки), emoji-to-png,
|
|
||||||
placeholder-png, color-spectrum, colorful-random (seed), draw-grid.
|
|
||||||
multi-color-gradient — если успеем новый тип параметра «список цветов», иначе
|
|
||||||
перенос.
|
|
||||||
|
|
||||||
### W8. Цветовые MEDIUM — ВЫПОЛНЕНА (4 инструмента; median-cut + Floyd–Steinberg/Bayer, custom-palette через text-параметр)
|
|
||||||
|
|
||||||
Ядро: квантование (median-cut или k-means). Состав: quantize (k),
|
|
||||||
decrease-color-count (=quantize с пресетами), custom-palette (маппинг на список
|
|
||||||
цветов — нужен тип параметра «список»), dithering (Floyd–Steinberg/Bayer поверх
|
|
||||||
квантования).
|
|
||||||
|
|
||||||
### W9. Сжатие — ВЫПОЛНЕНА (3 инструмента; optimize/change-quality осознанно не выделены — покрыты пресетами compress и W8)
|
|
||||||
|
|
||||||
Без wasm честное управление размером PNG ограничено: реальный рычаг —
|
|
||||||
квантование (W8) + обрезка метаданных + итеративный подбор под целевой KB.
|
|
||||||
Состав: compress (пресеты усилий), reduce-to-size (целевой KB, бинарный поиск по
|
|
||||||
k), optimize (re-encode), change-quality/low-quality — переосмыслить как пресеты
|
|
||||||
W8; jpeg-artifacts уже покрывает «испортить».
|
|
||||||
|
|
||||||
## Вне очереди
|
|
||||||
|
|
||||||
- Region-инструменты
|
|
||||||
(censor/erase/pixelate-area/blur-area/sharpen-area/reverse-area) — ждут UI
|
|
||||||
выделения области на превью; отдельное решение.
|
|
||||||
- Мультифайловый вывод (split-parts, gif-frames, separate-colors) — ждут
|
|
||||||
механизм «результат = набор файлов».
|
|
||||||
- Анимационные (slow-reveal/fade/scrolling) — выход не PNG; отдельное решение о
|
|
||||||
формате.
|
|
||||||
- HARD: glitch, barcode, signature-extract, handwritten-digital.
|
|
||||||
- Нишевые серии logo/icon/stamp/signature — не копируем.
|
|
||||||
|
|
||||||
## Оценка масштаба
|
|
||||||
|
|
||||||
W1–W7 дают **~47 новых инструментов**, почти все EASY. W8 добавляет ядро
|
|
||||||
квантования (открывает custom-palette/dithering и улучшает W9). Суммарно каталог
|
|
||||||
достигает ~120+ при текущих 71.
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
# План: герой-поиск v2 — иконки, популярность, карточки
|
|
||||||
|
|
||||||
> **СТАТУС: ВЫПОЛНЕН 2026-08-23.**
|
|
||||||
|
|
||||||
## 1. Что получается
|
|
||||||
|
|
||||||
Выдача поиска на главной превращается из списка строк в сетку карточек: слева
|
|
||||||
иконка инструмента, справа название и описание. Инструменты получают два новых
|
|
||||||
поля реестра — `popularity` (приоритет в выдаче) и `icon` (идентификатор
|
|
||||||
иконки). Иконки переиспользуются в каталоге `/list-tools` и в легендах шагов
|
|
||||||
цепочки. Поиск работает по-прежнему: нечёткий скоринг, клавиатура ↑↓ Enter Esc.
|
|
||||||
|
|
||||||
## 2. Данные реестра
|
|
||||||
|
|
||||||
- **`popularity?: number`** (0–100, по умолчанию 50) — приоритет внутри равного
|
|
||||||
скоринга запроса. Значения проставляются вручную осознанно: базовые операции
|
|
||||||
(обрезка, размер, удаление фона, конвертации JPG/PNG) — 80–95; цветовые и
|
|
||||||
геометрия — 40–70; узкие и генераторы — 10–30.
|
|
||||||
- **`icon?: string`** — имя иконки из набора Lucide. Явный словарь-маппинг для
|
|
||||||
всех инструментов в одном месте; отсутствие записи даёт нейтральную заглушку
|
|
||||||
(Wrench).
|
|
||||||
- Тест целостности расширяется: популярность в диапазоне, иконка — непустая
|
|
||||||
строка из разрешённого набора.
|
|
||||||
|
|
||||||
## 3. Иконки
|
|
||||||
|
|
||||||
Зависимость `lucide-svelte` (MIT) — один пакет, иконки рендерятся компонентами,
|
|
||||||
дерево не раздувается за счёт выборочного импорта.
|
|
||||||
|
|
||||||
Маппинг ведётся одним объектом `tool-icons.ts` рядом с реестром: 51 запись вида
|
|
||||||
`'crop-png': Crop`. Подбор по смыслу: обрезка — Crop, размер — Scaling, поворот
|
|
||||||
— RotateCw, отражение — FlipHorizontal2, оттенок — Palette, размытие — Droplets,
|
|
||||||
резкость — Focus, удаление фона — Scissors, анализ — Info и так далее; для
|
|
||||||
семейств конвертаций — FileInput/FileOutput варианты.
|
|
||||||
|
|
||||||
Иконка появляется в трёх местах: карточка выдачи, карточка каталога
|
|
||||||
`/list-tools`, легенда шага цепочки («Шаг N: [icon] Название»).
|
|
||||||
|
|
||||||
## 4. Карточки выдачи
|
|
||||||
|
|
||||||
- Сетка: одна колонка на узком экране, две — начиная со среднего героя.
|
|
||||||
- Композиция карточки: квадрат иконки слева, справа название (полужирное) и
|
|
||||||
однострочное описание с обрезкой.
|
|
||||||
- Состояния hover и активной с клавиатуры — тот же акцент подсветки, что сейчас.
|
|
||||||
- Сортировка выдачи: скоринг запроса ↓, затем популярность ↓, затем название.
|
|
||||||
|
|
||||||
## 5. Этапы и проверки
|
|
||||||
|
|
||||||
- **A. Реестр** — поля `popularity` и `icon`, словарь иконок на все 51,
|
|
||||||
расширение тестов целостности. Проверка: `pnpm test/check`.
|
|
||||||
- **B. Карточки поиска** — сетка выдачи с иконками и учётом популярности,
|
|
||||||
клавиатура без изменений. Проверка: визуально, `check/build`.
|
|
||||||
- **C. Распространение** — иконки в каталоге `/list-tools` и легендах звеньев.
|
|
||||||
Проверка: визуально, `build`.
|
|
||||||
- **D. Итог** — полный прогон и смоук §8.
|
|
||||||
|
|
||||||
## 6. Смоук-чеклист
|
|
||||||
|
|
||||||
1. Пустой запрос на герое показывает популярные первыми после первого символа.
|
|
||||||
2. Запрос «обрез» поднимает карточку обрезки выше прочих совпадений.
|
|
||||||
3. У каждой карточки есть иконка; у большинства — разные, соответствующие
|
|
||||||
смыслу.
|
|
||||||
4. Наведение и стрелки подсвечивают карточку; Enter открывает инструмент.
|
|
||||||
5. Открытый инструмент работает как раньше: параметры в колонках, результат,
|
|
||||||
скачивание.
|
|
||||||
6. Цепочка: легенда каждого шага показывает иконку выбранного инструмента.
|
|
||||||
7. `/list-tools`: карточки категорий с иконками, ссылки работают.
|
|
||||||
8. Регресс: старые сценарии (загрузка файла, цепочка, скачивание) не изменились.
|
|
||||||
|
|
||||||
## 7. Критерии готовности
|
|
||||||
|
|
||||||
- Все инструменты имеют иконку и ненулевую популярность; тесты это гарантируют.
|
|
||||||
- Выдача поиска — сетка карточек с корректной сортировкой и живой клавиатурой.
|
|
||||||
- Иконки присутствуют в поиске, каталоге и легендах шагов.
|
|
||||||
- `test/check/build` зелёные, смоук §6 пройден.
|
|
||||||
|
|
||||||
## 8. Что сознательно не делаем
|
|
||||||
|
|
||||||
- Автоматический расчёт популярности из статистики использования — значения
|
|
||||||
ручные.
|
|
||||||
- Собственный набор SVG-иконок — берём готовый открытый.
|
|
||||||
- Переводы названий и поиск на других языках — отдельная i18n-волна.
|
|
||||||
- Изображения-превью в карточках поиска — только иконка и текст.
|
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
# План: i18n — русский и английский
|
|
||||||
|
|
||||||
> **СТАТУС: ВЫПОЛНЕН 25.08.2026.** Базовая локаль — английский (реестр), русский
|
|
||||||
> — перевод в словаре; строки инструментов в реестре переведены на английский в
|
|
||||||
> рамках этапа C.
|
|
||||||
|
|
||||||
## 1. Что получается
|
|
||||||
|
|
||||||
Сайт работает на двух языках: русский (базовый) и английский. Переключатель в
|
|
||||||
шапке, выбор сохраняется в localStorage, тег `html lang` обновляется на клиенте.
|
|
||||||
Переведено всё: каркас интерфейса, карточки инструментов, цепочки, поиск, тексты
|
|
||||||
ошибок. Поиск находит инструменты по запросу на любом из двух языков независимо
|
|
||||||
от активной локали.
|
|
||||||
|
|
||||||
Каталог и поведение не меняются: ни новых инструментов, ни изменений логики
|
|
||||||
существующих.
|
|
||||||
|
|
||||||
## 2. Решения
|
|
||||||
|
|
||||||
- Без сегментов языка в URL. Статика пререндерится на русском; переключение на
|
|
||||||
клиенте, выбор живёт в localStorage. hreflang/canonical и вторая копия страниц
|
|
||||||
для поисковиков — не в этой волне.
|
|
||||||
- Ядро не знает о локалях. Ошибки в core выбрасываются стабильными кодами
|
|
||||||
(например ERR_BAD_HEX); перевод кода в человекочитаемый текст происходит в
|
|
||||||
слое исполнителя/UI.
|
|
||||||
- Английский — базовая локаль и фолбэк: строки инструментов в реестре записаны
|
|
||||||
по-английски. Переводы живут в секции tools словарей по ключу id инструмента
|
|
||||||
(title, description, подписи параметров, подписи опций select,
|
|
||||||
тексты-результаты анализаторов); сейчас перевод — русский в ru.ts. Доступ из
|
|
||||||
компонентов только через хелперы
|
|
||||||
toolTitle/toolDescription/paramLabel/optionLabel. Так каждая строка имеет
|
|
||||||
единственный источник: en — реестр, переводы — словари.
|
|
||||||
|
|
||||||
## 3. Структура i18n-модуля
|
|
||||||
|
|
||||||
- web/src/lib/i18n/locale.svelte.ts — состояние активной локали (runes), персист
|
|
||||||
в localStorage (ключ locale, guard на SSR), обновление
|
|
||||||
document.documentElement.lang.
|
|
||||||
- web/src/lib/i18n/t.ts — функция перевода с фолбэком на базовый язык и
|
|
||||||
интерполяцией переменных вида {n}.
|
|
||||||
- web/src/lib/i18n/ru.ts и en.ts — словари одного типа Dict; тип экспортируется
|
|
||||||
из ru.ts, en.ts обязан ему соответствовать — пропущенные ключи ловятся на
|
|
||||||
компиляции.
|
|
||||||
- Секции словаря: header, home, catalog, toolPage, chain, sourceCard,
|
|
||||||
resultCard, paramsCard, textInput, textResult, download, infoPanel, dropZone,
|
|
||||||
search, ui (aria кнопок слайдера и пипетки), categories, errors, tools.
|
|
||||||
|
|
||||||
## 4. Этапы и проверки
|
|
||||||
|
|
||||||
Каждый этап заканчивается зелёными test/check/build и ревью.
|
|
||||||
|
|
||||||
- A. Инфраструктура — модуль состояния, функция перевода с фолбэком и
|
|
||||||
интерполяцией, словари-скелеты со всеми секциями (заполнены header и
|
|
||||||
categories как пилот), переключатель RU/EN в шапке, реактивный lang. Тесты:
|
|
||||||
фолбэк на ru при отсутствии ключа в en, интерполяция, персист локали.
|
|
||||||
- B. Каркас интерфейса — перенос всех хардкод-строк компонентов и маршрутов в
|
|
||||||
словари: layout (шапка, футер), главная (герой, «вернуть последний», title),
|
|
||||||
каталог, карточки источника/результата/параметров, текстовый вход и результат,
|
|
||||||
«Показать маску», DropZone/DropOverlay, кнопка скачивания (busyText),
|
|
||||||
InfoPanel плюс форматирование чисел через тег локали вместо жёсткого ru-RU,
|
|
||||||
ToolPage (легенды, «Шаг n», ошибки шага), ChainToolBlock, placeholder и пустой
|
|
||||||
результат поиска, страницы инструмента (title, 404). Обёрточные сообщения
|
|
||||||
исполнителя («Ошибка исполнения в воркере») — тоже здесь.
|
|
||||||
- C. Контент реестра — переводы 69 инструментов
|
|
||||||
(title/description/labels/options) в tools-секции словаря en; компоненты
|
|
||||||
получают строки через хелперы toolTitle/toolDescription/paramLabel/optionLabel
|
|
||||||
с фолбэком на реестр. Тест целостности: у каждого инструмента есть непустые
|
|
||||||
перевод title/description, полный набор param-ключей и подписи всех опций
|
|
||||||
select; лишние ключи словарей отсутствуют.
|
|
||||||
- D. Поиск по любому языку — нормализация запроса и полей: нижний регистр, ё→е,
|
|
||||||
снятие диакритики через NFD; скоринг против активной и базовой локали сразу,
|
|
||||||
лучший балл побеждает; сортировка совпадений через Intl.Collator активной
|
|
||||||
локали. Тесты нормализации: ё/е, диакритика, английский запрос при русской
|
|
||||||
локали и наоборот.
|
|
||||||
- E. Ошибки на кодах — около 40 мест throw в core, registry, pipeline заменяются
|
|
||||||
на коды; исполнитель переводит код в текст активной локали, неизвестные
|
|
||||||
сообщения показываются как есть. Пять тестов, матчащих русские подстроки
|
|
||||||
ошибок (executor, pipeline, registry, io, alpha), переходят на коды — это
|
|
||||||
упрощает ассерты. Русские describe/it в тестах не трогаем.
|
|
||||||
- F. Итог — полный прогон, смоук чеклист §6, архивация плана.
|
|
||||||
|
|
||||||
## 5. Смоук-чеклист
|
|
||||||
|
|
||||||
1. Переключение RU/EN в шапке мгновенно меняет язык без перезагрузки; после
|
|
||||||
перезагрузки выбор сохранён.
|
|
||||||
2. Тег html lang соответствует активной локали.
|
|
||||||
3. Главная, каталог, страница инструмента, цепочка из трёх звеньев — везде
|
|
||||||
переведено, без смеси языков.
|
|
||||||
4. Форма параметров: подписи полей и опции select переведены у произвольных пяти
|
|
||||||
инструментов из разных категорий.
|
|
||||||
5. Битый вход (некорректный HEX в тонировании, нулевой размер) показывает ошибку
|
|
||||||
на активном языке.
|
|
||||||
6. Поиск: «повер» находит повороты при русской локали; «rotate» находит их же;
|
|
||||||
при английской локали наоборот. Диакритика и ё не мешают совпадению.
|
|
||||||
7. Числа в панели информации отформатированы по локали.
|
|
||||||
8. Скачивание, пипетка, маски, слайдеры работают одинаково на обеих локалях —
|
|
||||||
функциональность не задета.
|
|
||||||
|
|
||||||
## 6. Критерии готовности
|
|
||||||
|
|
||||||
- Оба словаря типобезопасны и полны; тест целостности секции tools зелёный.
|
|
||||||
- Ни одного пользовательского русского строки вне словарей (кроме тестовых
|
|
||||||
describe/it).
|
|
||||||
- test/check/build зелёные, смоук пройден.
|
|
||||||
|
|
||||||
## 7. Что сознательно не делаем
|
|
||||||
|
|
||||||
- Сегменты языка в URL, prerender обеих локалей, hreflang/canonical.
|
|
||||||
- Транслитерация поисковых запросов ru-en.
|
|
||||||
- Третьи и дальнейшие языки (структура позволяет, добавление — отдельная волна).
|
|
||||||
- Перевод описаний коммитов, README и документации docs/.
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
# План: параметры между исходником и результатом
|
|
||||||
|
|
||||||
> **СТАТУС: УСТАРЕЛ (2026-09-07).** Раскладка описывалась для старого
|
|
||||||
> `ToolPage`; перекрыта редизайном (`plan-redesign.md`) — страницы инструментов
|
|
||||||
> теперь `SchemaToolView`/`SchemaPreview` в новом дизайне. Сама идея «инлайн:
|
|
||||||
> исходник → параметры → результат» живёт в `backlog.md` (№2).
|
|
||||||
|
|
||||||
> Статус: план к выполнению.
|
|
||||||
|
|
||||||
## 1. Что получается
|
|
||||||
|
|
||||||
Альтернативная раскладка этапа инструмента: три колонки «Исходник | Параметры |
|
|
||||||
Результат» на широких экранах, вертикальный стек на узких. Цель — отдать вариант
|
|
||||||
дизайнеру как отправную точку. Откат к текущей раскладке — замена одной строки в
|
|
||||||
`ToolPage`.
|
|
||||||
|
|
||||||
## 2. Решения
|
|
||||||
|
|
||||||
- Два взаимозаменяемых компонента этапа с одинаковым набором пропсов; `ToolPage`
|
|
||||||
рендерит один тег. Попробовать новое — поменять компонент в этом теге; вернуть
|
|
||||||
старое — поменять обратно. Никаких флагов и конфигов.
|
|
||||||
- Общая типизация пропсов в одном файле рядом с компонентами; сами карточки
|
|
||||||
(`SourceCard`, `ResultCard`, `TextInputCard`, `ParamsCard`) переиспользуются
|
|
||||||
как есть — вся логика исполнения, маски, пипетки и автозапуска остаётся в
|
|
||||||
`ToolPage` и приходит сверху.
|
|
||||||
- Chain-звенья используют ту же панельную систему «Вход | Параметры | Результат»
|
|
||||||
— общие классы `.pane` / `.pane-legend` / `.pane-params` вынесены в app.css,
|
|
||||||
вертикальные поля в узкой колонке заданы один раз глобально.
|
|
||||||
- Один порог отзывчивости: три колонки от ~75rem, ниже — стек «исходник →
|
|
||||||
параметры → результат». Промежуточные перестроения не придумываем до фидбека
|
|
||||||
дизайнера.
|
|
||||||
|
|
||||||
## 3. Компоненты
|
|
||||||
|
|
||||||
```txt
|
|
||||||
src/lib/components/stage/
|
|
||||||
stage-props.ts # интерфейс StageProps + сборщик дефолтов
|
|
||||||
ToolStageClassic.svelte # текущая разметка .tool-stage, перенесена 1:1 из ToolPage
|
|
||||||
ToolStageInline.svelte # новая сетка [source | params | result]
|
|
||||||
```
|
|
||||||
|
|
||||||
- `ToolStageClassic` — вырезка существующего шаблона со стилями; после
|
|
||||||
извлечения страница должна быть пиксель-в-пиксель прежней.
|
|
||||||
- `ToolStageInline` — grid-колонки
|
|
||||||
`minmax(0, 1fr) | фикс-ширина параметров | minmax(0, 1fr)`; легенда
|
|
||||||
«Параметры» над средней колонкой в том же стиле edge-legend.
|
|
||||||
- Модификаторы состояний через классы на корне компонента: `no-params`
|
|
||||||
(инструменты без параметров → две колонки), `no-source` (генераторы →
|
|
||||||
параметры и результат), текстовый вход до декодирования занимает левую ячейку
|
|
||||||
как сейчас.
|
|
||||||
|
|
||||||
## 4. Этапы и проверки
|
|
||||||
|
|
||||||
Каждый этап заканчивается зелёными test/check/build и ревью.
|
|
||||||
|
|
||||||
- **A. Извлечение классики** — перенос разметки и стилей этапа из `ToolPage` в
|
|
||||||
`ToolStageClassic` без изменений поведения; пропсы собираются в одном объекте.
|
|
||||||
Критерий: страница неотличима, diff по логике `ToolPage` нулевой.
|
|
||||||
- **B. Инлайн-раскладка** — `ToolStageInline`, переключение в `ToolPage` на
|
|
||||||
новый компонент, прогон смоука §5. Ревью в браузере; решение дизайнера —
|
|
||||||
потом, код уже позволяет жить в обоих вариантах.
|
|
||||||
|
|
||||||
## 5. Смоук
|
|
||||||
|
|
||||||
1. Замена компонента этапа — одна строка; обе версии собираются без
|
|
||||||
предупреждений.
|
|
||||||
2. Инструмент без параметров (например Автоконтраст): две колонки, пустой
|
|
||||||
середины нет.
|
|
||||||
3. Генератор без исходника: параметры и результат в две колонки.
|
|
||||||
4. Текстовый инструмент до декодирования: вход слева, параметры в центре, пустое
|
|
||||||
состояние результата справа.
|
|
||||||
5. Маска, пипетка, слайдеры в средней колонке; изменение параметра перезапускает
|
|
||||||
инструмент как раньше.
|
|
||||||
6. Узкий экран: стек без горизонтального скролла, порядок исходник → параметры →
|
|
||||||
результат.
|
|
||||||
7. Легенды присутствуют у всех трёх зон в обеих раскладках.
|
|
||||||
|
|
||||||
## 6. Что сознательно не делаем
|
|
||||||
|
|
||||||
- Редизайн визуального языка (цвета, типографика, формы) — материал для
|
|
||||||
дизайнера, не для этого плана.
|
|
||||||
- Новая логика: исполнение, автосохранение, пресеты — раскладка ничего не знает
|
|
||||||
про исполнение.
|
|
||||||
@@ -1,103 +0,0 @@
|
|||||||
# Plan: `/preview/list-tools` → design2 (catalog)
|
|
||||||
|
|
||||||
> **СТАТУС: ВЫПОЛНЕН (2026-09-07) — устарел.** Каталог `/preview/list-tools`
|
|
||||||
> переписан на `CatalogHeader` / `CatalogToolbar` / `CatalogGroup` / `ToolCard`
|
|
||||||
> (kit), поиск и фильтры функциональны. Доводка верности против рефа остаётся в
|
|
||||||
> рамках `plan-design-fix.md`.
|
|
||||||
|
|
||||||
Ветка `feat/redesign`. Цель — привести каталог инструментов к рефу
|
|
||||||
`refs-html/list-tools.html`. Сейчас страница вообще не на design2: по
|
|
||||||
мульти-аудиту `onlyOurs=257, onlyRef=54` (демо-страница — `18/11`).
|
|
||||||
|
|
||||||
## Текущее состояние
|
|
||||||
|
|
||||||
- `src/routes/preview/list-tools/+page.svelte` использует собственную вёрстку
|
|
||||||
(`.catalog` / `.cat-head` / `.cat-grid`) + `Panel` / `PanelHeading` /
|
|
||||||
`ToolCard` (kit). Нет поиска, фильтров, счётчика, навигация по категориям
|
|
||||||
сделана через `Panel`-блоки, а не через `.catalog-groups`.
|
|
||||||
- Реф (`list-tools.html`) строит:
|
|
||||||
- `.catalog-page` → `.catalog-head` (`.eyebrow`, `h1` «Tool catalog», `p`
|
|
||||||
lede, `.catalog-total` «NN TOOLS AVAILABLE» с большим числом),
|
|
||||||
- `.catalog-toolbar` (`.catalog-search` + `.catalog-filters` с кнопками),
|
|
||||||
- `.catalog-groups` (grid `repeat(2,minmax(0,1fr))`, gap `56px 28px`) →
|
|
||||||
`.catalog-group` (`.group-title` «CAT · NN TOOLS») → `.tool-cards` →
|
|
||||||
`.tool-card`.
|
|
||||||
- `.tool-card` (реф) — `display:grid`, колонки `42px minmax(0,1fr) 24px 18px`,
|
|
||||||
`min-height:106px`, `gap:14px`, `padding:16px`: `.tool-icon` (42×42 синий
|
|
||||||
размытый квадрат), `.tool-copy` (`strong` 13px mono + `span` 12px mono muted),
|
|
||||||
`.tool-index` (10px mono, сверху), `.tool-arrow` (появляется при hover).
|
|
||||||
- Токены `design2.css` уже совпадают с рефом (`--background #eef1f4`,
|
|
||||||
`--panel #f8fafb`, `--foreground #17212b`, `--muted #6d7883`,
|
|
||||||
`--line #cbd3da`, `--blue #1769d2`, `--radius 4px`) — править токены не нужно.
|
|
||||||
|
|
||||||
## Шаги
|
|
||||||
|
|
||||||
1. **Restyle `kit/ToolCard.svelte`** под реф (меняется «на месте» — используется
|
|
||||||
ещё в `preview/+page` и `preview/kit`, стиль унифицируется, ок):
|
|
||||||
- grid-колонки `42px minmax(0,1fr) 24px 18px`, `align-items:center`,
|
|
||||||
`gap:14px`, `min-height:106px`, `padding:16px`, `border:1px solid line`,
|
|
||||||
`background:panel`, `border-radius:radius`.
|
|
||||||
- `.tool-icon`: 42×42 `place-items:center`,
|
|
||||||
`background:color-mix(in srgb, var(--blue) 12%, transparent)`,
|
|
||||||
`color:var(--blue)` (реф использует `--panel-strong`; вместо нового токена
|
|
||||||
берём этот `color-mix`).
|
|
||||||
- `.tool-copy strong`: `600 13px var(--font-mono)`; `span`: `12px/1.5 mono`
|
|
||||||
`muted`.
|
|
||||||
- `.tool-index`: `10px mono muted`, `align-self:start`.
|
|
||||||
- `.tool-arrow`: `color:var(--blue)`, `opacity:0` → `1` +
|
|
||||||
`translate(2px,-2px)` на `.tool-card:hover`.
|
|
||||||
- DOM-порядок как в рефе: `icon, copy, index, arrow`.
|
|
||||||
- `href` через `resolve` из `$app/paths` (убрать `svelte-ignore`).
|
|
||||||
|
|
||||||
2. **`kit/CatalogHeader.svelte`**: `.eyebrow` «EASY-PNG-TOOLS / CATALOG», `h1`
|
|
||||||
«Tool catalog», lede; `.catalog-total` с динамическим `TOOLS.length` (реф
|
|
||||||
показывает `32`).
|
|
||||||
|
|
||||||
3. **`kit/CatalogToolbar.svelte`**: `.catalog-search` (иконка + `<input>`) и
|
|
||||||
`.catalog-filters` (кнопки по категориям). Состояние `query` / `category`
|
|
||||||
поднимается наружу через `bind:` (или callback-пропы), чтобы страница
|
|
||||||
фильтровала список. Фильтры функциональные (клиентский фильтр по названию +
|
|
||||||
категории), не декоративные.
|
|
||||||
|
|
||||||
4. **`kit/CatalogGroup.svelte`**: `.group-title` (uppercase label категории +
|
|
||||||
«NN TOOLS») + слот со списком карточек; сетка `.tool-cards` — `gap:10px`
|
|
||||||
(реф), на мобиле (`@media max-width:800px`) всё схлопывается в 1 колонку.
|
|
||||||
|
|
||||||
5. **Переписать `preview/list-tools/+page.svelte`** как тонкую композицию:
|
|
||||||
- обёртка `.catalog-page` (`padding:60px clamp(24px,4vw,72px) 72px`);
|
|
||||||
- `<CatalogHeader />`;
|
|
||||||
- `<CatalogToolbar bind:query bind:category />`;
|
|
||||||
- `.catalog-groups` из отфильтрованных групп (по `CATEGORIES`, пересчитав
|
|
||||||
`tools` с учётом `query`/`category`);
|
|
||||||
- внутри `<CatalogGroup>` — `<ToolCard>` на каждый tool
|
|
||||||
(`href= "/preview/tools/{id}"`, `icon` из `tool-icons`, `index` по порядку,
|
|
||||||
`title`/`description` из registry);
|
|
||||||
- убрать `Panel`/`PanelHeading` из этого маршрута.
|
|
||||||
|
|
||||||
6. **Категории/локализация**: labels привести к рефу (реф: ALL / CONVERT /
|
|
||||||
TRANSPARENCY / COLOR / GEOMETRY / FILTERS / ANALYZE). Наши `CATEGORIES`
|
|
||||||
(`convert/alpha/color/geometry/filters/text/analyze/generate`) — сопоставить
|
|
||||||
(`alpha`→TRANSPARENCY и т.п.); точное сопоставление уточнить по рефу и
|
|
||||||
`i18n/ru.ts`. Фильтры = ALL + те же label'ы. Тексты заголовков/lede — по рефу
|
|
||||||
(англ. «Tool catalog» / «Focused utilities for working with PNG…»), либо
|
|
||||||
оставить RU-вариант, если это сознательное отклонение (зафиксировать).
|
|
||||||
|
|
||||||
7. **Адаптив**: перенести правила рефа (`@media max-width:800px`) —
|
|
||||||
`.catalog-groups` → 1fr, `.tool-card` → `38px minmax(0,1fr) 18px`, скрыть
|
|
||||||
`.tool-index`/показать `.tool-arrow`, `.catalog-head` в колонку и т.д.
|
|
||||||
|
|
||||||
## Верификация
|
|
||||||
|
|
||||||
- `pnpm lint`, `pnpm exec svelte-check --tsconfig ./tsconfig.json`, `pnpm build`
|
|
||||||
— без ошибок.
|
|
||||||
- `pnpm refs-audit` (теперь мульти-маршрутный) → глянуть секцию
|
|
||||||
`/preview/list-tools`: `onlyOurs`/`onlyRef` должны упасть с `257/54` примерно
|
|
||||||
до уровня demo (`18/11`). Остаточный diff (если будет) зафиксировать в этом
|
|
||||||
файле.
|
|
||||||
|
|
||||||
## Примечания
|
|
||||||
|
|
||||||
- `ToolCard` правится на месте — влияет на `preview/+page` и `preview/kit` (там
|
|
||||||
тоже карточки каталога, унификация уместна).
|
|
||||||
- Старый дизайн (`routes/(old)/**`, `components/search/**`) не трогать.
|
|
||||||
- Коммиты делает автор (агент не коммитит).
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
# План: MEDIUM-волна №1 — удаление фона, размытие, резкость + Web Worker
|
|
||||||
|
|
||||||
> **СТАТУС: ВЫПОЛНЕН 2026-08-23.**
|
|
||||||
|
|
||||||
## 1. Зачем
|
|
||||||
|
|
||||||
- Первые операции настоящего алгоритмического уровня: свёртки и заливка связной
|
|
||||||
области.
|
|
||||||
- Витринная фича каталога — удаление однотонного фона без нейросетей: порог по
|
|
||||||
цвету + заливка от краёв изображения + сглаживание границы маски.
|
|
||||||
- Эти операции реально блокируют UI-поток на больших картинках — повод включить
|
|
||||||
давно подготовленный путь: исполнитель `executeStep` переезжает в Web Worker,
|
|
||||||
страницы не меняются вовсе.
|
|
||||||
|
|
||||||
## 2. Инструменты — 3
|
|
||||||
|
|
||||||
- **Удалить фон PNG** (`remove-background-png`, категория «Прозрачность») —
|
|
||||||
параметры: цвет фона, допуск похожести %, режим «только внешние области»
|
|
||||||
(заливка от краёв вместо глобального совпадения), сглаживание границы (проходы
|
|
||||||
мажоритарного фильтра маски). Даёт превью маски через существующий контракт
|
|
||||||
`preview`.
|
|
||||||
- **Размытие PNG** (`blur-png`, новая категория «Фильтры») — гауссово
|
|
||||||
приближение тремя проходами разделяемого бокса; радиус слайдером 1–32 px.
|
|
||||||
- **Резкость PNG** (`sharpen-png`, «Фильтры») — свёртка ядром резкости с
|
|
||||||
регулируемой силой (смесь исходного и обработанного), сила слайдером 0–100 %.
|
|
||||||
|
|
||||||
## 3. Новое ядро (чистые функции, детерминированные, покрытые тестами)
|
|
||||||
|
|
||||||
- `core/convolution.ts`
|
|
||||||
- универсальная свёртка с клампом краёв;
|
|
||||||
- разделяемый бокс-блюр и сборка гауссова приближения из трёх проходов;
|
|
||||||
- ядро резкости и смешивание с оригиналом по силе.
|
|
||||||
- `core/background.ts`
|
|
||||||
- построение маски удаления по цветовому расстоянию (та же метрика, что у
|
|
||||||
удаления цвета);
|
|
||||||
- заливка связной области от всех граничных пикселей (очередь, без рекурсии);
|
|
||||||
- мажоритарное сглаживание маски заданным числом проходов;
|
|
||||||
- сборка результата: альфа 0 у удалённых пикселей; плюс функция маски для
|
|
||||||
превью.
|
|
||||||
|
|
||||||
## 4. Web Worker
|
|
||||||
|
|
||||||
- Точка исполнения одна — `tools/executor.ts`; меняется только её внутренность.
|
|
||||||
- Воркер-файл импортирует реестр и исполняет операцию по `toolId`; картинка
|
|
||||||
приходит копией (структурный клон), результат возвращается передаваемым
|
|
||||||
буфером.
|
|
||||||
- Ленивая инициализация воркера при первом вызове; если воркер недоступен
|
|
||||||
(старый браузер/тестовая среда) — автоматический откат на прямое исполнение в
|
|
||||||
главном потоке. Обе ветви обязаны давать побайтово одинаковый результат.
|
|
||||||
- Генераторы и текстовые представления остаются в главном потоке — они
|
|
||||||
мгновенные.
|
|
||||||
- Пипетка и лупа работают с уже готовыми картинками и не зависят от переноса.
|
|
||||||
|
|
||||||
## 5. Этапы и проверки
|
|
||||||
|
|
||||||
- **A. Ядро** — свёртки и фон с тестами на малых матрицах (точные ожидания
|
|
||||||
руками). Проверка: `pnpm test/check`.
|
|
||||||
- **B. Реестр** — категория «Фильтры», три записи, превью маски. Проверка: тесты
|
|
||||||
целостности, визуально.
|
|
||||||
- **C. Web Worker** — перенос исполнителя, отккат-ветвь. Проверка: результаты
|
|
||||||
до/после переключения идентичны байт-в-байт на фиксированной картинке и
|
|
||||||
параметрах; `test/build` зелёные.
|
|
||||||
- **D. Итог** — полный прогон и смоук §8.
|
|
||||||
|
|
||||||
## 6. Смоук-чеклист
|
|
||||||
|
|
||||||
1. Размытие: радиус 1 и 32 — плавно усиливается, прозрачные края не темнеют.
|
|
||||||
2. Резкость: сила 0 не меняет картинку; сила 100 заметно подчёркивает края без
|
|
||||||
цветового мусора.
|
|
||||||
3. Удаление фона на логотипе с однотонным фоном: фон исчез, содержимое цело.
|
|
||||||
4. Режим «только внешние области»: дырка внутри объекта того же цвета остаётся
|
|
||||||
непрозрачной.
|
|
||||||
5. Сглаживание 0/2/4 прохода — граница от жёсткой к мягкой, результат
|
|
||||||
воспроизводим.
|
|
||||||
6. Превью маски показывает белым удаляемую область; скачивание всегда отдаёт
|
|
||||||
результат.
|
|
||||||
7. Во время размытия большой картинки интерфейс остаётся отзывчивым (чип
|
|
||||||
«Пересчёт…», скролл живой).
|
|
||||||
8. Цепочка: размытие → резкость → удаление фона работает; каждый блок скачивает
|
|
||||||
свой промежуток.
|
|
||||||
9. Регресс старых инструментов и главной-поиска без изменений поведения.
|
|
||||||
|
|
||||||
## 7. Критерии готовности
|
|
||||||
|
|
||||||
- Три новых инструмента проходят смоук §6; операции ядра покрыты точными
|
|
||||||
юнит-тестами.
|
|
||||||
- Исполнение изображений идёт через воркер там, где он доступен, с
|
|
||||||
гарантированно одинаковым результатом обеих ветвей.
|
|
||||||
- `test/check/build` зелёные.
|
|
||||||
|
|
||||||
## 8. Что сознательно не делаем
|
|
||||||
|
|
||||||
- Многопроходное гауссово ядро «по-настоящему» (O(r²)) — трёхпроходного
|
|
||||||
боксового приближения достаточно визуально и оно быстрее.
|
|
||||||
- Хромакей по нескольким цветам, ИИ-сегментация — следующие волны.
|
|
||||||
- Отмена выполняющегося прогона воркера — устаревшие результаты отбрасываются
|
|
||||||
токеном, сама работа завершается фоном.
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
# План: MEDIUM-волна №2 — морфология прозрачности
|
|
||||||
|
|
||||||
> **СТАТУС: ВЫПОЛНЕН 2026-08-23.**
|
|
||||||
|
|
||||||
## 1. Зачем
|
|
||||||
|
|
||||||
Одна пара функций — расширение и сужение непрозрачных областей — открывает целое
|
|
||||||
семейство пользовательских операций: обводка логотипов и иконок, контуры,
|
|
||||||
утолщение и утончение подписей, удаление мелкого мусора и закрытие дырок. Всё
|
|
||||||
это частые запросы в наборах PNG-утилит, а ядро получается компактным и легко
|
|
||||||
покрывается тестами.
|
|
||||||
|
|
||||||
## 2. Инструменты — 7 (все в категории «Прозрачность»)
|
|
||||||
|
|
||||||
- **Обвести PNG** (`add-stroke-png`) — кольцо выбранного цвета вокруг
|
|
||||||
непрозрачного содержимого; параметры: цвет, толщина 1–10 px.
|
|
||||||
- **Найти контур PNG** (`find-contour-png`) — линия по краю непрозрачных
|
|
||||||
областей; параметр: толщина линии.
|
|
||||||
- **Утолщить PNG** (`make-thicker-png`) — расширение содержимого на N пикселей.
|
|
||||||
- **Утончить PNG** (`make-thinner-png`) — сужение содержимого на N пикселей.
|
|
||||||
- **Жёсткие края PNG** (`harden-alpha-png`) — бинаризация альфы по порогу:
|
|
||||||
полупрозрачность уходит, остаётся два состояния.
|
|
||||||
- **Убрать мусор PNG** (`despeckle-alpha-png`) — открытие: удаляет одиночные
|
|
||||||
полупрозрачные пиксели и мелкие крапинки.
|
|
||||||
- **Закрыть дыры PNG** (`close-holes-png`) — закрытие: заполняет одиночные
|
|
||||||
прозрачные точки внутри объекта.
|
|
||||||
|
|
||||||
Все операции применяются к альфа-каналу; RGB пикселей сохраняется.
|
|
||||||
|
|
||||||
## 3. Ядро `core/morphology.ts`
|
|
||||||
|
|
||||||
Чистые функции над бинарной маской и над альфа-каналом:
|
|
||||||
|
|
||||||
- `buildAlphaMask(img)` — 1 там, где альфа > 0.
|
|
||||||
- `dilateMask(mask, w, h, radius, shape)` — расширение; `shape`: квадрат или
|
|
||||||
диск (для обводок диск даёт ровную круглую линию).
|
|
||||||
- `erodeMask(...)` — зеркальная операция.
|
|
||||||
- `dilateImage / erodeImage(img, radiusPx)` — применяют маскирование к альфе
|
|
||||||
изображения, RGB сохраняется.
|
|
||||||
- Сборщики:
|
|
||||||
- обводка: `dilate(mask) − mask` → новое кольцо заливается цветом;
|
|
||||||
- контур: `mask − erode(mask)` → линия;
|
|
||||||
- открытие: `erode → dilate`; закрытие: `dilate → erode`.
|
|
||||||
- `hardenAlpha(img, thresholdPercent)` — отдельно от морфологии, но той же
|
|
||||||
волной.
|
|
||||||
|
|
||||||
Структурирующий элемент при радиусе r — диск радиуса r (расстояние до центра ≤
|
|
||||||
r); квадрат не используем, чтобы углы обводок не были угловатыми.
|
|
||||||
|
|
||||||
## 4. Web Worker
|
|
||||||
|
|
||||||
Ничего не требуется: новые операции попадают в реестр, исполнитель автоматически
|
|
||||||
исполняет их в воркере. Тесты ядра идут мимо воркера (чистые функции).
|
|
||||||
|
|
||||||
## 5. Этапы и проверки
|
|
||||||
|
|
||||||
- **A. Ядро** — `morphology.ts` + точные тесты на матрицах 5×5: расширение
|
|
||||||
диска, сужение, открытие убирает одиночный пиксель, закрытие закрывает
|
|
||||||
одиночную дыру, кольцо обводки нужного цвета и толщины. Проверка:
|
|
||||||
`pnpm test/check`.
|
|
||||||
- **B. Реестр** — семь записей, тест целостности обновляется (51 → 58).
|
|
||||||
Проверка: `test/build`, визуально.
|
|
||||||
- **C. Итог** — полный прогон, смоук §7.
|
|
||||||
|
|
||||||
## 6. Критерии готовности
|
|
||||||
|
|
||||||
- Семь новых инструментов работают на прозрачных PNG: обводка, контур,
|
|
||||||
толще/тоньше, жёсткие края, убрать мусор, закрыть дыры.
|
|
||||||
- Все операции детерминированы и покрыты юнит-тестами с ручными ожиданиями.
|
|
||||||
- Исполнение автоматически идёт в воркере.
|
|
||||||
- `test/check/build` зелёные, смоук §7 пройден.
|
|
||||||
|
|
||||||
## 7. Смоук-чеклист
|
|
||||||
|
|
||||||
1. Обводка толщиной 3 вокруг круглого стикера — ровное кольцо выбранного цвета,
|
|
||||||
без угловатости.
|
|
||||||
2. Толщина 1 — тонкая аккуратная линия; 10 — толстая, форма содержимого не
|
|
||||||
искажается сверх роста.
|
|
||||||
3. Контур показывает только край содержимого линией.
|
|
||||||
4. Толще/тоньше на тонкой надписи — штрихи растут/сжимаются без разрывов при
|
|
||||||
умеренных значениях.
|
|
||||||
5. Жёсткие края: полупрозрачность исчезает, силуэт сохраняется.
|
|
||||||
6. Убрать мусор убирает одиночные точки-крапинки, не трогая крупные области.
|
|
||||||
7. Закрыть дыры заполняет одиночные прозрачные точки внутри логотипа.
|
|
||||||
8. Цепочка: обводка после удаления фона работает; каждое звено скачивает свой
|
|
||||||
промежуток.
|
|
||||||
9. Регресс остальных инструментов без изменений.
|
|
||||||
|
|
||||||
## 8. Что сознательно не делаем
|
|
||||||
|
|
||||||
- Морфология по яркости для непрозрачных сканов — волна фокусируется на альфе
|
|
||||||
прозрачных PNG.
|
|
||||||
- Выбор структурирующего элемента пользователем — всегда диск.
|
|
||||||
- Feather/свечение краёв — отдельная будущая задача поверх размытия.
|
|
||||||
- Произвольные SVG-маски и градиентные обводки.
|
|
||||||
@@ -1,181 +0,0 @@
|
|||||||
# План: дожать перевод инструментов в preview и переезд ветки `old/`
|
|
||||||
|
|
||||||
> **СТАТУС: ЗАКРЫТ (2026-09-07).** Перевод каталога завершён — 121 из 125
|
|
||||||
> инструментов в `registry-new`; 4 хвоста отложены решениями и перенесены в
|
|
||||||
> `backlog.md` (`watermark-image-png` → overlay-райз, `png-info` → exif-райз,
|
|
||||||
> `reduce-to-size-png` → download-райз, `compress-png` → закрыт дубликатом).
|
|
||||||
> Дальнейшие «хвосты плана» — переезд старых компонентов в папки `old/`,
|
|
||||||
> закрытие долгов и сбор фидбека — ~уже учитываются в `plan-composite-params.md`
|
|
||||||
> (шаги 36–37) и `plan-design-fix.md`, поэтому план закрыт, а не продолжен.
|
|
||||||
|
|
||||||
> Архивная копия, ниже — состояние на момент закрытия.
|
|
||||||
|
|
||||||
> Статус: **переведено 121 из 125; остальные 4 — отложены/закрыты решениями.**
|
|
||||||
> Текущее состояние: **121 инструмент переведён в `registry-new`**, старый UI
|
|
||||||
> уже живёт на `(old)/`-маршрутах, линтер-изоляция old↔preview на месте (плагин
|
|
||||||
> `isolation/no-mixed-imports`). Здесь — что сделать, чтобы:
|
|
||||||
>
|
|
||||||
> 1. в preview был **весь** каталог инструментов (плюс закрыты известные долги);
|
|
||||||
> 2. физически перенести старую ветку в папки `old/` (шаг 37 основного плана);
|
|
||||||
> 3. после переезда провести детальную проверку в реальных условиях и собрать
|
|
||||||
> отзывы пользователей (правки поверх — отдельными задачами).
|
|
||||||
>
|
|
||||||
> Порядок: перевод остатка → починить известные долги → переезд в `old/` →
|
|
||||||
> пользовательская проверка/фидбек → (позже, отдельным решением) удаление
|
|
||||||
> старого кода.
|
|
||||||
|
|
||||||
## Текущая картина (факты)
|
|
||||||
|
|
||||||
- `registry.ts` + `registry/` — **125 инструментов**; `registry-new/` — **121**.
|
|
||||||
Не переведены только: `png-info` (отложен, хвост-фича с exif), `compress-png`
|
|
||||||
и `reduce-to-size-png` (закрыто решениями, см. ниже), `watermark-image-png`
|
|
||||||
(overlay — единственный реальный остаток).
|
|
||||||
- Preview (`/preview`) показывает переведённые; старый UI работает на
|
|
||||||
`/`-маршрутах (группа `(old)/`): `+page`, `demo`, `list-tools`, `tools/[id]`.
|
|
||||||
Переведено всё кроме четырёх хвостов: color 18, filters 3, geometry 13, alpha
|
|
||||||
13 (+watermark-image в остатке), generate 10 (mix/sort), analyze 12 (6 масок +
|
|
||||||
verify + 5 вердиктов), convert 14 (jpg/webp/bmp + 5 png→texт + 6 text→png),
|
|
||||||
text `watermark-tile-png`.
|
|
||||||
- Плагин `isolation/no-mixed-imports` (`web/eslint-plugins/isolation/`)
|
|
||||||
полностью разделяет ветки: старый `lib/registry.ts|/registry/**`,
|
|
||||||
`lib/registry-helpers.ts`, `lib/categories.ts`, `lib/tools/**`,
|
|
||||||
`lib/components/**`(кроме `kit/`) vs новый `routes/preview/**`,
|
|
||||||
`lib/registry-new/**`, `lib/preview/**`, `lib/registry-schema.ts`,
|
|
||||||
`lib/components/kit/**`. Общее: `core/`, `i18n/`, `theme`, `assets/`, корневой
|
|
||||||
`lib`.
|
|
||||||
- Правило копий (из основного плана): файл, используемый `(old)`, не трогаем —
|
|
||||||
делаем копию в `lib/preview/` (пример: `lib/preview/tool-icons.ts`).
|
|
||||||
- Известные долги (предсуществующие, не создавались нами): ошибка
|
|
||||||
`ToolCard.svelte:19:36` (svelte-check), фейл `palette.test.ts` (ENOENT
|
|
||||||
`src/app.css` — из-за него `pnpm test` красный при 607 passed), ~335
|
|
||||||
design-tokens ошибок `lint:all` в старых kit-компонентах, a11y-warning в
|
|
||||||
`ColorField.svelte`, пустой ruleset в `Toggle.svelte`, неиспользуемые токены в
|
|
||||||
`preview.css`.
|
|
||||||
|
|
||||||
## Скоуп перевода: переведено всё переводимое (отложено 4 хвоста)
|
|
||||||
|
|
||||||
Каталог дожат до предела текущих решений: переведено 121 из 125. Оставшиеся 4
|
|
||||||
инструмента **отложены осознанно** (не делаем сейчас):
|
|
||||||
|
|
||||||
- **`watermark-image-png` (alpha)** — **отложен**. Нужна overlay-механика
|
|
||||||
(второе изображение-знак через `getOverlay`/store + параметры
|
|
||||||
scale/opacity/position/margin/aspect); решается отдельно, вместе с вопросом,
|
|
||||||
как второй источник изображения вписывается в schema-driven preview.
|
|
||||||
- **`png-info`** — **отложен осознанно**: будет серьёзно дорабатываться
|
|
||||||
отдельным райзом (exif-теги, редактирование, структурированный вывод), для
|
|
||||||
него нужен свой отдельный случай в UI, не «ещё один text/verdict».
|
|
||||||
- **`compress-png`** — **закрыт как дубликат** `decrease-color-count-png`:
|
|
||||||
пресеты сжатия 192/96/44 добавлены прямо в схему `decrease-color-count`.
|
|
||||||
- **`reduce-to-size-png`** — **отложен** до достройки кнопки Download (backlog
|
|
||||||
№7, «экспорт с лимитом размера»).
|
|
||||||
|
|
||||||
### Переведено в этом проходе (text-механика)
|
|
||||||
|
|
||||||
- **Типы:** `ToolEntry.input: "file" | "text" | "none"`,
|
|
||||||
`ToolEntry.result: "image" | "text" | "verdict"`, `OutputFormat` (mime/ext/
|
|
||||||
qualityParamId) для download.
|
|
||||||
- **Executor:** `executeFromText` / `executeToText` / `executeTextToText`
|
|
||||||
(прямые, без worker), экспорт в `preview/executor/index.ts`.
|
|
||||||
- **UI:** `SchemaTextSource` (textarea + Render), `SchemaTextResult` (кол-блок с
|
|
||||||
Copy/Download .txt или бейдж-вердикт по префиксу Yes/No), интеграция в
|
|
||||||
`SchemaPreview`/`SchemaToolView` (режимы file/text/generate, рендер
|
|
||||||
image/text/verdict, скрытие image-download для text-результата).
|
|
||||||
- **Инструменты:** convert 5×png→text (base64/data-uri/hex/bytes/rgb-values),
|
|
||||||
6×text→png (base64/data-uri/hex/bytes/rgb-values/svg, +width), analyze
|
|
||||||
verify-is-png (text→text) и 5 вердиктов (grayscale/file-size/transparent/
|
|
||||||
orientation). Доработка: `base64ToBytes` сужен до `Uint8Array<ArrayBuffer>`
|
|
||||||
(совместимость с `decodeBytes`).
|
|
||||||
- **Гейты:** eslint 0, prettier чист, svelte-check — только предсуществующий
|
|
||||||
ToolCard, тесты — только предсуществующий фейл `palette.test.ts`.
|
|
||||||
|
|
||||||
## Известные долги перед переездом (завести tasks и закрыть)
|
|
||||||
|
|
||||||
По AGENTS.md техдолг «чинится только по заведённым tasks, не игнорируется»:
|
|
||||||
`lint:all` (~335 design-tokens в kit), неиспользуемые токены preview.css, a11y
|
|
||||||
`ColorField`, пустой ruleset `Toggle`. Плюс два, мешающих зелёной проверке
|
|
||||||
до/после переезда: svelte-check error `ToolCard.svelte:19:36` и фейл
|
|
||||||
`palette.test.ts` (ENOENT `src/app.css`). После их закрытия целевое состояние
|
|
||||||
проверок перед переездом: `pnpm --dir web exec svelte-check` → 0 errors,
|
|
||||||
`pnpm --dir web test` → зелёный, `pnpm --dir web lint:all` → только задокумент.
|
|
||||||
ированный остаток (или 0).
|
|
||||||
|
|
||||||
## Шаги перевода остатка (порядок)
|
|
||||||
|
|
||||||
1. **Простые пачки** — **выполнено**: color (18) → filters (3) → geometry (13) →
|
|
||||||
alpha (13/14, кроме watermark-image) → generate (10, включая mix/sort через
|
|
||||||
kind `colors`) → analyze-маски (6) → text `watermark-tile-png`.
|
|
||||||
2. **Text-механика** — **выполнено**: типы `input`/`result` + output-формат,
|
|
||||||
executor `executeFromText`/`toText`/`textToText`, UI
|
|
||||||
`SchemaTextSource`/`SchemaTextResult` + интеграция в preview, перевод
|
|
||||||
конвертеров convert (5 png→text + 6 text→png) и analyze (verify + 5
|
|
||||||
вердиктов). `png-info` отложен отдельным райзом (exif).
|
|
||||||
3. **Шаг про overlay не делается** — `watermark-image-png` **отложен решением**
|
|
||||||
(см. «Скоуп перевода» выше), отдельным райзом вместе с вопросом второго
|
|
||||||
источника изображения в schema-driven preview.
|
|
||||||
4. После категории — `schema.layout` для сгруппированных инструментов (шаг 32-33
|
|
||||||
паттерн) и ручная проверка в `/preview`.
|
|
||||||
|
|
||||||
## Переезд в `old/` (шаг 37 основного плана)
|
|
||||||
|
|
||||||
> Скоп-сдвиг: «Scoped-пути двигаются вместе с папками» — обновляется
|
|
||||||
> `eslint.config.js` (isolation-паттерны и design-tokens glob), не код правила.
|
|
||||||
|
|
||||||
1. **Что переезжает:** `lib/registry.ts`, `lib/registry/**`,
|
|
||||||
`lib/registry-helpers.ts`, `lib/categories.ts`, `lib/tools/**`,
|
|
||||||
`lib/components/**` (кроме `kit/`) → папки `old/` (например
|
|
||||||
`lib/old/registry …`, `lib/old/tools`, `lib/old/components`) — то есть всё
|
|
||||||
старое в одном поддереве. `routes/(old)/` остаётся как есть (уже отделено).
|
|
||||||
2. **Обновить конфиг плагина** `isolation/no-mixed-imports`: старые glob-ы
|
|
||||||
`lib/registry/**…` заменить на `lib/old/**` (+ `lib/old/components/**` и
|
|
||||||
т.п.), «новое» и «общее» не меняются. Проверить, что правило по-прежнему
|
|
||||||
разделяет (тест фолс-позитивов: временный импорт old↔new ловится).
|
|
||||||
3. **Импорты внутри старого кода** — относительные пути остаются рабочими (папки
|
|
||||||
двигаются целиком); поправить только то, что ссылалось на `$lib/...` в другом
|
|
||||||
стиле. Старые тесты (`registry.test.ts` и др.) переезжают вместе с
|
|
||||||
исходниками и продолжают проходить.
|
|
||||||
4. **Гейты переезда:** `svelte-check` 0 errors, `pnpm --dir web test` зелёный,
|
|
||||||
`prettier --check .` чистый, `lint:all` = задокументированный остаток, обе
|
|
||||||
ветки открываются (старая `/`, новая `/preview`).
|
|
||||||
|
|
||||||
## Проверка и сбор отзывов после переезда
|
|
||||||
|
|
||||||
1. **Регресс-чеклист по каталогу** — пройти все инструменты в `/preview`:
|
|
||||||
дефолты совпадают со старыми, валидация/клампы поведения не меняют, результат
|
|
||||||
эквивалентен старому UI (сверить на одинаковых входах).
|
|
||||||
2. **Тяжёлые кейсы руками:** генераторы, маски, font-style/plate/gradient на
|
|
||||||
всем наборе, text-source, output-формат. (`watermark-image` — позже, после
|
|
||||||
overlay-райза.)
|
|
||||||
3. **Сбор фидбека** — отдельные задачи на каждый найденный фикс; чеклист
|
|
||||||
зафиксировать в этом документе по мере находок (или ссылкой на tasks).
|
|
||||||
4. Сверка каталога с `tools-map.md` (покрытие/дедупликация
|
|
||||||
`convert-png-to-jpg`/`webp`), при необходимости — обновить cards/мета.
|
|
||||||
5. **Решение об удалении старого** — после завершения ревью и фидбека, отдельным
|
|
||||||
шагом: тогда чистятся `lib/old/**`, `routes/(old)/**`, `old.css`, `exports`,
|
|
||||||
изоляционный плагин (glob-ы больше не нужны) и открываются «правило
|
|
||||||
копий»-дубли.
|
|
||||||
|
|
||||||
## Как ревьюить каждый шаг (гайды)
|
|
||||||
|
|
||||||
- Диф на **один инструмент** (или фундамент без инструментов) < ~500 строк.
|
|
||||||
- Старый `params`/`run`/`defaultParams`/`sanitizeParams` не изменены; в
|
|
||||||
`registry-new` копия логики + `schema` (дефолты равны старым).
|
|
||||||
- Старый UI и старые тесты продолжают работать: `pnpm --dir web test`,
|
|
||||||
`pnpm --dir web exec svelte-check`.
|
|
||||||
- Новый инструмент проверяется **руками в `/preview`** end-to-end (применение +
|
|
||||||
сброс дефолтов + смена значений).
|
|
||||||
- После переезда каждый move-шаг гоняет 4 гейта (см. выше).
|
|
||||||
|
|
||||||
## Оценка трудозатрат
|
|
||||||
|
|
||||||
| Часть | Сложность | Оценка |
|
|
||||||
| ---------------------------------------------------------------------- | -------------- | ------------------------- |
|
|
||||||
| Простые инструменты (color/filters/geometry/alpha/generate/маски/text) | Низкая-Средняя | **выполнено** (62 шт) |
|
|
||||||
| Text-механика + конвертеры/вердикты | Средняя | **выполнено** (16 шт) |
|
|
||||||
| watermark-image / png-info / reduce-to-size (отложены решениями) | Высокая | отложено, отдельные райзы |
|
|
||||||
| Долги (ToolCard, palette.test.ts, lint:all остаток) | Средняя | ~2-4ч |
|
|
||||||
| Переезд в `old/` + конфиг плагина + гейты | Средняя-Низкая | ~2-4ч |
|
|
||||||
| Проверка/фидбек + правки | Зависит | ~3-6ч |
|
|
||||||
| **Итого** | | **~9-18ч** по фазам |
|
|
||||||
|
|
||||||
> Оценки ориентировочные; особые случаи — с ревью решения, объём финальных
|
|
||||||
> правок по фидбеку не предсказуем.
|
|
||||||
@@ -1,131 +0,0 @@
|
|||||||
# План: единый `run(ctx)` в registry-new (унификация 5 режимов исполнения)
|
|
||||||
|
|
||||||
> **СТАТУС: ВЫПОЛНЕН (2026-09-10) — устарел.** Убивал пару багов-классов
|
|
||||||
> «инструмент с generate показывает загрузку и не выдаёт результат»
|
|
||||||
> (linear-gradient-png и все генераторы): причина — `input` не задан, диспетч по
|
|
||||||
> факту наличия методов.
|
|
||||||
|
|
||||||
## Исходная проблема
|
|
||||||
|
|
||||||
Два бага у `linear-gradient-png` (и у всех генераторов):
|
|
||||||
|
|
||||||
1. Показывается загрузка изображения — `SchemaToolView.svelte` считает
|
|
||||||
`inputMode = tool.input ?? "file"`, а `tool.input` у генераторов не задан.
|
|
||||||
2. Результата нет — `SchemaActions` рендерит кнопку «Generate» только при
|
|
||||||
`inputMode === "none"`, а он всегда `"file"`. Ui-контур «догадывается» о
|
|
||||||
генераторе по эвристике `Boolean(tool.generate && !tool.run)`.
|
|
||||||
|
|
||||||
Корневая причина архитектурная: у инструмента 5 режимов исполнения (`run` 79
|
|
||||||
шт., `generate` 21, `runFromText` 6, `toText` 9, `textToText` 1), и диспетч
|
|
||||||
«какой метод вызывать» размазан между UI и executor. Способ исполнения
|
|
||||||
(worker/поток, было `domOnly`) просочился в метод-диспетч. Договорились:
|
|
||||||
**свести всё к одному методу `run(ctx)`**, а требование входа (`input`) — к
|
|
||||||
обязательному полю. `domOnly` остаётся только executor-хинтом (уйдёт при
|
|
||||||
переезде на WASM-ядро).
|
|
||||||
|
|
||||||
## Целевой контракт
|
|
||||||
|
|
||||||
```ts
|
|
||||||
export type InputMode = "image" | "text" | "none"; // было "file"
|
|
||||||
export type ResultKind = "image" | "text" | "verdict";
|
|
||||||
|
|
||||||
export interface ToolContext<P> {
|
|
||||||
params: P;
|
|
||||||
source?: PixelImage; // присутствует ⇔ input === "image"
|
|
||||||
text?: string; // присутствует ⇔ input === "text"
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ToolResult = PixelImage | string;
|
|
||||||
|
|
||||||
export type ToolEntry<P = Record<string, unknown>> = {
|
|
||||||
id: string;
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
category: CategoryId;
|
|
||||||
schema: ToolSchema<P>;
|
|
||||||
input: InputMode; // ОБЯЗАТЕЛЬНОЕ, дефолта нет
|
|
||||||
result?: ResultKind; // default "image"
|
|
||||||
run(ctx: ToolContext<P>): Promise<ToolResult> | ToolResult;
|
|
||||||
output?: OutputFormat;
|
|
||||||
icon?: string;
|
|
||||||
domOnly?: boolean; // executor-хинт, уйдёт с WASM
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
Удаляем: `generate`, `runFromText`, `toText`, `textToText`. Хелперы контракта
|
|
||||||
(бросают `ToolError`): `requireSource(ctx)`, `requireText(ctx)`. Обёртки для
|
|
||||||
миграции тел 1:1: `imgTool((img, p) => …)`, `genTool((p) => …)`,
|
|
||||||
`textGen((t, p) => …)`.
|
|
||||||
|
|
||||||
```ts
|
|
||||||
const imgTool =
|
|
||||||
<P>(fn: (img: PixelImage, p: P) => ToolResult) =>
|
|
||||||
(ctx: ToolContext<P>) =>
|
|
||||||
fn(requireSource(ctx), ctx.params);
|
|
||||||
const genTool =
|
|
||||||
<P>(fn: (p: P) => ToolResult) =>
|
|
||||||
(ctx: ToolContext<P>) =>
|
|
||||||
fn(ctx.params);
|
|
||||||
const textGen =
|
|
||||||
<P>(fn: (t: string, p: P) => ToolResult) =>
|
|
||||||
(ctx: ToolContext<P>) =>
|
|
||||||
fn(requireText(ctx), ctx.params);
|
|
||||||
```
|
|
||||||
|
|
||||||
## Executor (единый)
|
|
||||||
|
|
||||||
```ts
|
|
||||||
export async function execute(
|
|
||||||
tool: ToolEntry,
|
|
||||||
ctx: { params: Record<string, unknown>; source?: PixelImage; text?: string },
|
|
||||||
): Promise<ToolResult> {
|
|
||||||
const params = sanitizeSchemaParams(tool.schema, ctx.params);
|
|
||||||
if (tool.input === "image" && !ctx.source)
|
|
||||||
throw new ToolError("errors.sourceRequired");
|
|
||||||
if (tool.input === "text" && !ctx.text?.trim())
|
|
||||||
throw new ToolError("errors.textRequired");
|
|
||||||
return route(tool, { params, source: ctx.source, text: ctx.text });
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
`route()` — единственное место выбора места исполнения: `domOnly` или нет Worker
|
|
||||||
→ прямой вызов `tool.run(ctx)`, иначе worker с fallback (логика
|
|
||||||
`ensureWorker/runInWorker/disableWorker` остаётся). Worker-протокол —
|
|
||||||
`{ id, toolId, params, source?, text? }`.
|
|
||||||
|
|
||||||
## Шаги (ревью после каждого)
|
|
||||||
|
|
||||||
Все 11 шагов выполнены; гейт пройден: `svelte-check` → 0 errors,
|
|
||||||
`pnpm --dir web lint` → чисто, `pnpm --dir web test` → 618/618 зелёный. E2E не
|
|
||||||
делали.
|
|
||||||
|
|
||||||
| # | Шаг | Файлы | Что |
|
|
||||||
| --- | --------------- | ------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------- |
|
|
||||||
| 1 | Типы | `registry-new/types.ts` | новый `ToolEntry`/`ToolContext`/`ToolResult`/`InputMode "image"`, хелперы `requireSource`/`requireText` + `imgTool`/`genTool`/`textGen` |
|
|
||||||
| 2 | Executor+Worker | `preview/executor/executor.ts`, `executor.worker.ts`, `index.ts` | единый `execute`, `route` по `domOnly`, протокол `source?`/`text?` |
|
|
||||||
| 3 | UI kit | `kit/SchemaToolView.svelte`, `SchemaPreview.svelte`, `SchemaActions.svelte`, `SchemaSourceTile.svelte` | одна `run()`, без `isGenerator`-эвристики, `inputMode` → `"image"` |
|
|
||||||
| 4 | Генераторы | `registry-new/generate.ts` | 21 `generate(p)` → `run(ctx)` + `input: "none"` (чинит градиент и др.) |
|
|
||||||
| 5 | Текст | `registry-new/convert.ts`, `analyze.ts` | `runFromText`/`toText`/`textToText` → `run`; `input: "image"` на run-инструментах |
|
|
||||||
| 6 | Текст | `registry-new/text.ts` | domOnly run-инструменты → `run(ctx)` + `input: "image"` |
|
|
||||||
| 7 | Геометрия | `registry-new/geometry.ts` | run → `run(ctx)` + `input: "image"` |
|
|
||||||
| 8 | Альфа/цвет | `registry-new/alpha.ts`, `color.ts` | run → `run(ctx)` + `input: "image"` |
|
|
||||||
| 9 | Фильтры | `registry-new/filters.ts` | run → `run(ctx)` + `input: "image"` |
|
|
||||||
| 10 | Тесты | `registry-new/registry-new.test.ts` | `generate!`/`run!` → `run(ctx)`; guard-тест: `input` всегда задан, image-инструменты работают с источником, `none` — без |
|
|
||||||
| 11 | Гейт | — | `svelte-check` → 0 errors, `pnpm --dir web lint` → чисто, `pnpm --dir web test` → 618/618 |
|
|
||||||
|
|
||||||
Примечание шага 8: поля `preview:` (маска у remove-color/remove-background)
|
|
||||||
удалены — новый executor зовёт `run()` напрямую; идея вернуть отображение маски
|
|
||||||
— в `docs/backlog.md` №17. Prettier-варнинги `prettier --check` по
|
|
||||||
SchemaToolView.svelte и executor-файлам — предсуществующий долг.
|
|
||||||
|
|
||||||
По миграции: `input: "image"` добавлен всем run-инструментам (~80 шт.),
|
|
||||||
`input: "text"` стоит у 7 text-инструментов, `input: "none"` — у 21 генератора.
|
|
||||||
|
|
||||||
## Известные последствия
|
|
||||||
|
|
||||||
- **Красная сборка с шага 1 до конца шага 10** — осознанное решение, ревью идёт
|
|
||||||
по diff'у шагов.
|
|
||||||
- Старый контур (`registry/**` + `lib/tools/executor.ts` + `ToolPage.svelte`,
|
|
||||||
`sourceMode`) НЕ трогаем — он на удаление (C19).
|
|
||||||
- `input: "file"` переименован в `input: "image"` — семантика «нужно именно
|
|
||||||
изображение», а не «файл» (между инструментами может передаваться не файлом).
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
# План: range-слайдеры и пипетка цвета
|
|
||||||
|
|
||||||
> **СТАТУС: ВЫПОЛНЕН 2026-08-23.**
|
|
||||||
|
|
||||||
## 1. Целевое поведение
|
|
||||||
|
|
||||||
1. «Процентные» параметры (яркость, контраст, качество JPEG/WebP, порог
|
|
||||||
похожести) — слайдеры с числовым значением рядом; точные координаты и размеры
|
|
||||||
(resize/crop) остаются полями ввода.
|
|
||||||
2. У цветовых параметров появляется кнопка-переключатель «Пипетка»: активна →
|
|
||||||
клик по превью исходника снимает цвет пикселя и подставляет его в поле цвета;
|
|
||||||
после выбора режим гаснет.
|
|
||||||
3. В режиме пипетки возле курсора видна лупа: увеличенный блок пикселей вокруг
|
|
||||||
точки с перекрестием и подписью hex текущего пикселя; клик выбирает центр
|
|
||||||
перекрестия.
|
|
||||||
4. Активная пипетка видна на превью перекрестием курсора; повторный клик по
|
|
||||||
кнопке отменяет режим без выбора.
|
|
||||||
5. Всё работает поверх текущего процесса: авто-применение с задержкой, токен
|
|
||||||
запусков, маски — без изменений.
|
|
||||||
|
|
||||||
## 2. Слайдеры
|
|
||||||
|
|
||||||
- В реестре новый тип параметра `'slider'` с обязательными `min`/`max`;
|
|
||||||
валидируется как число (дефолт в диапазоне, минимум меньше максимума).
|
|
||||||
- Форма маппит его на готовый `SliderField` из `ui/`; санитизация параметров
|
|
||||||
обрабатывает slider так же, как number.
|
|
||||||
- Переводятся: яркость и контраст, качество JPEG, качество WebP, порог
|
|
||||||
похожести. Остальные инструменты — без изменений.
|
|
||||||
|
|
||||||
## 3. Пипетка и лупа
|
|
||||||
|
|
||||||
- В ядро добавляется чистая функция `rgbToHex(r, g, b)` → строка вида `#rrggbb`,
|
|
||||||
с юнит-тестами; чтение пикселя — из уже отрисованного canvas превью исходника
|
|
||||||
через `getImageData(x, y, 1, 1)`.
|
|
||||||
- `Preview.svelte` получает опциональный режим пипетки: пропсы «активна» и
|
|
||||||
колбэк «цвет выбран»; курсор перекрестие, клик вычисляет координату пикселя с
|
|
||||||
учётом разницы CSS-размера canvas и его внутренней ширины.
|
|
||||||
- **Лупа** — компонент `PipetteLoupe.svelte`: канвас около 110×110 px у курсора
|
|
||||||
(позиция fixed, `pointer-events: none`); рисует блок 15×15 пикселей из
|
|
||||||
исходника с увеличением ×8 без сглаживания, перекрестие в центре, подпись hex
|
|
||||||
текущего пикселя. Координаты блока клампятся у краёв изображения; лупа скрыта
|
|
||||||
вне превью и вне режима пипетки. Тач-устройства: прямой тап выбирает пиксель
|
|
||||||
без лупы.
|
|
||||||
- Проводка: кнопка у каждого цветового поля в карточке параметров → состояние
|
|
||||||
«активное цветовое поле» живёт в оркестраторе → выбор цвета записывает hex в
|
|
||||||
это поле и выключает режим.
|
|
||||||
- Пипетка работает только по левой панели (исходнику): удаляемый цвет и подложка
|
|
||||||
берутся с оригинала.
|
|
||||||
|
|
||||||
## 4. Этапы и проверки
|
|
||||||
|
|
||||||
- **A. Слайдеры** — тип в реестре, перевод четырёх инструментов, тесты реестра и
|
|
||||||
санитизации, тест `rgbToHex`. Проверка: `pnpm test/check`, визуально слайдеры.
|
|
||||||
- **B. Пипетка** — режим в Preview, проводка через карточки до полей цвета.
|
|
||||||
Проверка: смоук §6 пп. 5–7.
|
|
||||||
- **B2. Лупа** — компонент лупы поверх режима пипетки. Проверка: смоук §6 пп.
|
|
||||||
8–10.
|
|
||||||
- **C. Итог** — полный прогон `test/check/build`.
|
|
||||||
|
|
||||||
## 5. Изменения по файлам
|
|
||||||
|
|
||||||
- `registry.ts` — тип `'slider'` в `ParamDef`, перевод четырёх записей, ветка в
|
|
||||||
`sanitizeParams`.
|
|
||||||
- `ParamForm.svelte` — ветка slider → `SliderField`.
|
|
||||||
- `core/color.ts` — `rgbToHex`.
|
|
||||||
- `Preview.svelte` — режим пипетки (курсор, клик, колбэк).
|
|
||||||
- `PipetteLoupe.svelte` — лупа у курсора в режиме пипетки.
|
|
||||||
- `SourceCard.svelte` — проброс режима и колбэка наверх.
|
|
||||||
- `ParamsCard.svelte` + `ColorField.svelte` — кнопка «Пипетка» у цветового поля.
|
|
||||||
- `ToolPage.svelte` — состояние активного поля, обработчик выбора цвета.
|
|
||||||
|
|
||||||
## 6. Смоук-чеклист
|
|
||||||
|
|
||||||
1. Яркость/контраст — слайдеры со значением справа, результат обновляется сам
|
|
||||||
при перетаскивании.
|
|
||||||
2. Качество JPEG/WebP — слайдер от 1 до 100.
|
|
||||||
3. Порог похожести — слайдер от 0 до 100.
|
|
||||||
4. Resize и crop остались числовыми полями.
|
|
||||||
5. Удаление цвета: «Пипетка» → клик по красной области исходника → поле цвета
|
|
||||||
получил hex этого пикселя, режим погас, результат пересчитался.
|
|
||||||
6. Повторный клик по кнопке или выбор без подтверждения — режим снят, цвет не
|
|
||||||
изменился.
|
|
||||||
7. Подложка JPEG: пипетка проставляет цвет подложки тем же способом.
|
|
||||||
8. В режиме пипетки лупа следует за курсором, перекрестие показывает пиксель под
|
|
||||||
ним, подпись hex совпадает с выбранным при клике.
|
|
||||||
9. У края картинки блок лупи корректно клампится — за границей тянется
|
|
||||||
чёрный/прозрачный фон, выбор по-прежнему точен.
|
|
||||||
10. Курсор ушёл с превью или режим выключен — лупа скрыта; клики по кнопкам под
|
|
||||||
превью не перехватываются лупой.
|
|
||||||
11. Слайдеры и пипетка дружат с быстрыми правками: одна финальная версия
|
|
||||||
результата, чип «Пересчёт…» на месте.
|
|
||||||
|
|
||||||
## 7. Критерии готовности
|
|
||||||
|
|
||||||
- Все процентные параметры — слайдеры; числовые поля остались только там, где
|
|
||||||
нужен точный ввод.
|
|
||||||
- Цвет любого color-параметра можно взять кликом с исходника.
|
|
||||||
- `test/check/build` зелёные, чеклист §6 пройден.
|
|
||||||
|
|
||||||
## 8. Что сознательно не делаем
|
|
||||||
|
|
||||||
- Пипетка и лупа на превью результата и маски — только исходник.
|
|
||||||
- Кастомная стилизация слайдеров за пределами дизайн-системы.
|
|
||||||
- Лупа для тач-устройств — там прямой тап без лупы.
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
# План: инструмент «Разрезать PNG на части» (split-into-parts-png)
|
|
||||||
|
|
||||||
> Статус: **выполнено и в архиве** (2026-09). Инструмент и механизм «результат =
|
|
||||||
> набор файлов» (1 → many) реализованы; первый потребитель —
|
|
||||||
> `split-into-parts-png`. Общий процесс таких инструментов — задача 18 в
|
|
||||||
> `docs/backlog.md`.
|
|
||||||
|
|
||||||
## Цель
|
|
||||||
|
|
||||||
Пользователь загружает одну картинку, выбирает число столбцов и строк, картинка
|
|
||||||
разрезается на равномерные части. Результат — набор PNG-файлов, скачивается
|
|
||||||
ZIP-архивом.
|
|
||||||
|
|
||||||
## Решения
|
|
||||||
|
|
||||||
- **Неравномерное деление:** канвас дополняется прозрачным до кратного размера
|
|
||||||
(`pieceW = ceil(w / cols)`, `newW = pieceW * cols`), все части строго равные,
|
|
||||||
картинка покрывается целиком.
|
|
||||||
- **Дефолт схемы:** 2 × 2.
|
|
||||||
- **Zip-библиотека:** `fflate` (zero deps, малый размер), скачивание из UI-слоя
|
|
||||||
(`encode()` требует DOM).
|
|
||||||
- **Результат инструмента:** `result: "files"` — терминальный тип, в пайплайн
|
|
||||||
(следующий шаг) не передаётся.
|
|
||||||
|
|
||||||
## Шаги
|
|
||||||
|
|
||||||
1. **Зависимость:** `fflate` в `web/package.json`.
|
|
||||||
2. **`registry/types.ts`:** `RESULT_KINDS.files`, тип `ToolImageFile`,
|
|
||||||
`FileResult`, расширение `ToolResult = PixelImage | string | FileResult`.
|
|
||||||
3. **`core/geometry.ts`:** `splitToParts(img, columns, rows)` — padding + сетка
|
|
||||||
через `crop`.
|
|
||||||
4. **`web/src/lib/zip.ts` (новый):** `downloadZip(files, zipName)` через
|
|
||||||
`zipSync`.
|
|
||||||
5. **`core/errors.ts`:** ключ `errors.tooManyParts` (страховочный лимит
|
|
||||||
cols·rows ≤ 1000; при максимуме 6×6 недостижим).
|
|
||||||
6. **`registry/geometry.ts`:** схема (`columns`, `rows`, дефолт 2×2, min 1, max
|
|
||||||
6 — максимум 36 частей) + entry `split-into-parts-png` в `geometryEntries`.
|
|
||||||
7. **Executor:** сериализация/десериализация `FileResult` в `executor.worker.ts`
|
|
||||||
и тип ветки в `executor.ts`.
|
|
||||||
8. **UI:** `SchemaToolView.svelte` (`fileResult`, Download → zip),
|
|
||||||
`SchemaPreview.svelte` (canDownload для files), `SchemaResultTile.svelte`
|
|
||||||
(сетка-превью частей, мета «N parts / ZIP»).
|
|
||||||
9. **i18n:** переводы в `ru.ts`/`en.ts` **не добавляем** — словари `tools`
|
|
||||||
привязаны к v1-реестру (тест «нет лишних ключей»), а preview берёт заголовки
|
|
||||||
из registry (EN). Перевод придёт вместе с беклог-задачей «i18n в preview».
|
|
||||||
Поиск по id/заголовку работает из registry.
|
|
||||||
10. **Тесты:** юнит `splitToParts`, registry (result: "files"), i18n coverage.
|
|
||||||
11. **Docs:** `docs/backlog.md` (задача «процесс 1 → many и many → 1», отметить
|
|
||||||
пункт 11 «Мультифайловый вывод»), `docs/tools-map.md` (перенос из идей).
|
|
||||||
|
|
||||||
## Файлы
|
|
||||||
|
|
||||||
| Файл | Изменение |
|
|
||||||
| ------------------------------------------------ | ----------------------------------------------------------------- |
|
|
||||||
| `web/package.json` | `+ fflate` (dependencies) |
|
|
||||||
| `web/src/lib/registry/types.ts` | `RESULT_KINDS.files`, `ToolImageFile`, `FileResult`, `ToolResult` |
|
|
||||||
| `web/src/lib/core/geometry.ts` | `splitToParts()` |
|
|
||||||
| `web/src/lib/zip.ts` | **новый** — `downloadZip()` |
|
|
||||||
| `web/src/lib/core/errors.ts` | `errors.tooManyParts` |
|
|
||||||
| `web/src/lib/registry/geometry.ts` | schema + `splitPartsTool` |
|
|
||||||
| `web/src/lib/executor/executor.worker.ts` | ветка `FileResult` |
|
|
||||||
| `web/src/lib/executor/executor.ts` | тип ветки `FileResult` |
|
|
||||||
| `web/src/lib/components/SchemaToolView.svelte` | `fileResult`, download → zip |
|
|
||||||
| `web/src/lib/components/SchemaPreview.svelte` | props `fileResult`, `canDownload` |
|
|
||||||
| `web/src/lib/components/SchemaResultTile.svelte` | сетка частей, «N parts» |
|
|
||||||
| `web/src/lib/i18n/ru.ts`, `en.ts` | записи инструмента |
|
|
||||||
|
|
||||||
## Нейминг файлов в ZIP
|
|
||||||
|
|
||||||
`part-<row>-<col>.png` (row, col нумеруются с 1, при 2×2 — `part-1-1.png`).
|
|
||||||
|
|
||||||
## Верификация
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pnpm --dir web exec svelte-check --tsconfig ./tsconfig.json
|
|
||||||
pnpm --dir web test
|
|
||||||
pnpm --dir web lint
|
|
||||||
```
|
|
||||||
|
|
||||||
Ручная проверка: PNG не кратного размера (например 101×77), 3×2 → 6 частей
|
|
||||||
одинакового размера, скачивается ZIP; на кратном размере (100×80, 3×2) padding
|
|
||||||
= 0.
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
# План: закрытие техдолга перед переездом `old/` (C17)
|
|
||||||
|
|
||||||
> **Статус: ЗАКРЫТ.** Все гейты пройдены, C17 разблокирован.
|
|
||||||
|
|
||||||
## Состав техдолга
|
|
||||||
|
|
||||||
- **svelte-check error** — `ToolCard.svelte:19:36`: `resolve(normalizedHref)`
|
|
||||||
принимает только типизированный роут, а `normalizedHref: string`. Красный
|
|
||||||
svelte-check, висит на каждом прогоне.
|
|
||||||
- **фейл теста** — `src/lib/palette.test.ts`: `ENOENT src/app.css` (файл
|
|
||||||
разъехался на `old.css` + `design2.css` при редизайне). Тест проверял
|
|
||||||
контрасты палитры по токенам старого `app.css`
|
|
||||||
(`text/surface/bg/link/ accent/danger/border`). При гейтах `pnpm test`
|
|
||||||
красный, хотя 607 тестов проходят.
|
|
||||||
- **design-tokens в `kit/**`** — ~335 ошибок `lint:all` от локального плагина
|
|
||||||
`eslint-plugins/design-tokens` (захардкоженные цвета/размеры/длительности/
|
|
||||||
z-index/`color-mix()` в `<style>`-блоках svelte), плюс stylelint по CSS.
|
|
||||||
- **a11y-предупреждение** — `ColorField.svelte`.
|
|
||||||
- **пустой ruleset** — `Toggle.svelte`.
|
|
||||||
|
|
||||||
## Этапы
|
|
||||||
|
|
||||||
### Этап 1 — «зелёные» гейты (мелкие, по ~10–40 строк каждый)
|
|
||||||
|
|
||||||
1. **ToolCard** — убрать ошибку типов: либо валидировать `href` против набора
|
|
||||||
известных маршрутов, либо явно протащить типизированный роут из каталога.
|
|
||||||
Проверка: svelte-check 0 errors.
|
|
||||||
2. **palette.test.ts** — переписать чтение на новый словарь: тест контрастов
|
|
||||||
гоняется по токенам `preview.css` (`:root` + `[data-theme="dark"]`, цвета
|
|
||||||
`--color-*`/`--brand-*`). Отдельно: взять из нового дизайна, какие пары
|
|
||||||
реально проверить (текст/фон, muted, акцент/бренд, бордер). Если часть
|
|
||||||
проверок потеряла смысл в новом дизайне — сократить, не растягивать.
|
|
||||||
3. **a11y `ColorField` + пустой ruleset `Toggle`** — пофиксить прямо в этих
|
|
||||||
компонентах.
|
|
||||||
|
|
||||||
Гейт этапа: `svelte-check` 0 errors, `pnpm test` зелёный.
|
|
||||||
|
|
||||||
### Этап 2 — миграция `kit/**` на design-токены (main объём)
|
|
||||||
|
|
||||||
Цель — `lint:all` без ошибок design-tokens (все примитивы/`color-mix()` уходят в
|
|
||||||
токены `preview.css`).
|
|
||||||
|
|
||||||
**Вариант А (основной): авто-фикс одноразовым скриптом.** Писать скрипт ТОЛЬКО
|
|
||||||
если он гарантированно ничего не испортит. Гарантии:
|
|
||||||
|
|
||||||
- Скрипт — одноразовый (`web/scripts/fix-design-tokens.mjs`), после прогона
|
|
||||||
удаляется либо помечается неактуальным.
|
|
||||||
- Строгая карта замен: примитив → токен **1:1, только если значение точно
|
|
||||||
совпадает с уже существующим токеном словаря** `preview.css` (цвет →
|
|
||||||
`--color-*`/`--brand-*`, радиус → `--radius-*`, размер →
|
|
||||||
`--space-*`/`--size-*`, длительность → `--duration-*`, z-index → `--z-*`).
|
|
||||||
Никаких новых токенов скрипт не генерирует.
|
|
||||||
- Замены — только примитивы, не содержащие `var()`/`calc()`/`color-mix()`;
|
|
||||||
исключения по правилам плагина (0, 0px, 1px, проценты, unitless) не трогаются.
|
|
||||||
`color-mix()` → выносится в `preview.css` как производный `--color-*` токен
|
|
||||||
(может требовать ручного решения имени).
|
|
||||||
- Скрипт обрабатывает только `src/lib/components/kit/**` и `src/preview.css`
|
|
||||||
(scoped-пути плагина) и только `<style>`-блоки/`.css` — логику/разметку не
|
|
||||||
меняет, только строки стилей.
|
|
||||||
- После прогона: обязательный дифф-ревью (все изменения — замена значений на
|
|
||||||
`var(--x)`, ничего удалено/переставлено), затем полный `lint:all` +
|
|
||||||
`svelte-check` + `pnpm test`. Если хоть что-то вне «замена примитива на токен»
|
|
||||||
— скрипт считается проваленным, откатываем и переходим на вариант Б.
|
|
||||||
|
|
||||||
**Вариант Б (страховка): руками, пакетами по компонентам.** Если скрипт не
|
|
||||||
удался — чистим/заводим токены вручную, пакетами по компонентам (1–2 на задачу,
|
|
||||||
дифф < ~500 строк). Отсутствующие токены (например
|
|
||||||
`--space-2xl`/`--space-panel`, уже дважды обходились в SchemaTextSource)
|
|
||||||
добавляем в словарь `preview.css` осознанно.
|
|
||||||
|
|
||||||
Гейт этапа: `pnpm --dir web lint:all` — только допустимый остаток
|
|
||||||
(неиспользуемые токены — варнинг), плюс регресс-прогон тестов и svelte-check.
|
|
||||||
|
|
||||||
### Этап 3 — чистота словаря `preview.css`
|
|
||||||
|
|
||||||
- Разобрать варнинги неиспользуемых токенов (check-tokens): либо использовать,
|
|
||||||
либо удалить из словаря.
|
|
||||||
- После этапа 2 — повторный аудит токенов
|
|
||||||
(`pnpm --dir web exec node scripts/check-tokens.mjs`): parity light/dark,
|
|
||||||
hct-only, без мёртвых токенов.
|
|
||||||
|
|
||||||
### Этап 4 — финальные гейты (гейт переезда `old/`)
|
|
||||||
|
|
||||||
1. `pnpm --dir web exec svelte-check --tsconfig ./tsconfig.json` → 0 errors
|
|
||||||
2. `pnpm --dir web test` → зелёный
|
|
||||||
3. `pnpm --dir web exec prettier --check .` → чистый
|
|
||||||
4. `pnpm --dir web lint:all` → 0 (или только задокументированный остаток)
|
|
||||||
5. Обе ветки открываются: старый UI `/`, новый `/preview`
|
|
||||||
|
|
||||||
## Порядок и коммиты
|
|
||||||
|
|
||||||
- Коммиты атомарные, < ~500 строк (AGENTS). По завершении каждой мини-задачи —
|
|
||||||
ревью (агент не коммитит).
|
|
||||||
- Техдолг чинится по заведённым задачам, правила линтеров не отключаются и не
|
|
||||||
игнорируются.
|
|
||||||
@@ -1,194 +0,0 @@
|
|||||||
# План: тестирование preview перед промоушеном в главную
|
|
||||||
|
|
||||||
> Статус: **реализация завершена**. Playwright-набор в `web/e2e/` (108 тестов:
|
|
||||||
> 104 прошли, 4 `test.fixme` — известные баги №1–4 ниже), ручной чек-лист —
|
|
||||||
> `docs/checklist-manual-testing.md`. План ветки preview
|
|
||||||
> (`web/src/routes/preview/**`, регистр `lib/registry-new/`, 121 инструмент)
|
|
||||||
> перед шагом C17 из `docs/plan-redesign.md` (переезд preview → реальные
|
|
||||||
> маршруты).
|
|
||||||
>
|
|
||||||
> Охват: **документация плана** — реализация тестов (Playwright-раннер, сами
|
|
||||||
> тесты) делается отдельными задачами после ревью этого документа.
|
|
||||||
>
|
|
||||||
> Приоритет: **отсутствие падений/ошибок** > пиксельная корректность. Дизайн
|
|
||||||
> доделываем по ходу, визуальная сверка — ручная и вторичная.
|
|
||||||
>
|
|
||||||
> Команды для проверки в процессе:
|
|
||||||
>
|
|
||||||
> - `pnpm --dir web test` — существующий Vitest (42 файла, 121 инструмент
|
|
||||||
> частично покрыт юнит-тестами).
|
|
||||||
> - `pnpm --dir web test:e2e` — Playwright (`web/e2e/`), сборка +
|
|
||||||
> `scripts/serve-static.mjs` на 4173.
|
|
||||||
> - `pnpm --dir web exec svelte-check --tsconfig ./tsconfig.json` — проверка
|
|
||||||
> типов.
|
|
||||||
> - `pnpm --dir web lint:all` — ESLint + stylelint + токен-аудит.
|
|
||||||
> - `pnpm --dir web build` — статическая сборка (adapter-static).
|
|
||||||
|
|
||||||
## 0. Что тестируем
|
|
||||||
|
|
||||||
Preview-ветка — клиентское SPA на SvelteKit (adapter-static, всё в браузере,
|
|
||||||
никакого API). Структура:
|
|
||||||
|
|
||||||
| Маршрут | Что это |
|
|
||||||
| ------------------------------------- | -------------------------------------------------------------------------------------------- |
|
|
||||||
| `/preview` | Workspace: поиск + сетка `ToolCard` (топ-24), «Open last project» (stub), футер |
|
|
||||||
| `/preview/kit` | Витрина всех kit-компонентов (light/dark) |
|
|
||||||
| `/preview/list-tools` | Каталог: 8 групп, 121 инструмент, фильтр по категории, поиск |
|
|
||||||
| `/preview/tools/[id]` | Страница инструмента: `SchemaToolView` (настройки + live preview), prerendered на все 121 id |
|
|
||||||
| `/preview/tools/xxx` (несуществующий) | 404 → `EmptyState` «Tool not found» |
|
|
||||||
|
|
||||||
Инструменты: **121**, категории CONVERT / TRANSPARENCY (alpha) / COLOR /
|
|
||||||
GEOMETRY / FILTERS / TEXT / ANALYZE / GENERATE. Режимы инструмента:
|
|
||||||
|
|
||||||
- **file** (transform): upload → `run` → картинка → download;
|
|
||||||
- **text** (convert-source): ввод текста → `runFromText` → картинка, либо
|
|
||||||
`textToText` → текстовый результат;
|
|
||||||
- **verdict** (`result: "verdict"`, file): `toText` → текстовый вердикт;
|
|
||||||
- **text-output** (`result: "text"`, file): `toText` → текстовый результат;
|
|
||||||
- **generate**: без входа, `executeGenerate` → картинка.
|
|
||||||
|
|
||||||
Исполнение: `preview/executor` — worker с fallback на main-thread (domOnly / нет
|
|
||||||
Worker / ошибка worker). Auto-run с debounce 200 мс.
|
|
||||||
|
|
||||||
## 1. Функциональные блоки для проверки
|
|
||||||
|
|
||||||
### A. Инфраструктура и навигация
|
|
||||||
|
|
||||||
1. Все 4 маршрута + 404 открываются без console errors, uncaught exceptions и
|
|
||||||
failed requests.
|
|
||||||
2. Поиск на `/preview` — фильтрация по названию/описанию, пустой результат →
|
|
||||||
аккуратное empty-state.
|
|
||||||
3. Каталог `/preview/list-tools` — 8 групп, счётчик 121, фильтр категории,
|
|
||||||
иконки не падают.
|
|
||||||
4. Тема light/dark и язык RU/EN переключаются на превью-страницах без регресса
|
|
||||||
рендера.
|
|
||||||
5. 404 на несуществующий id инструмента → EmptyState, без краша.
|
|
||||||
6. Prerender: `pnpm --dir web build` проходит; все 121 страницы
|
|
||||||
`/preview/tools/[id]` генерируются (проверка `entries()`/`getTool`).
|
|
||||||
|
|
||||||
### B. Конвейер инструмента (file-инструменты)
|
|
||||||
|
|
||||||
7. Upload → авто-run → результатный тайл появился, поле ошибки пустое.
|
|
||||||
8. Download — событие download, имя `<toolId>.<ext>`, MIME соответствует
|
|
||||||
`output`.
|
|
||||||
9. Reset — возвращает дефолты, сбрасывает результат и ошибку.
|
|
||||||
10. Смена параметра → авто-перезапуск (debounce 200 мс) без «залипания»
|
|
||||||
running-стейта.
|
|
||||||
11. Worker-исполнение и fallback-исполнение оба дают результат.
|
|
||||||
|
|
||||||
### C. Текстовые инструменты и вердикты
|
|
||||||
|
|
||||||
12. Text-source: ввод → текстовый результат или картинка; copy/download .txt.
|
|
||||||
13. Вердикты (analyze): корректный вердикт на PNG с альфой, на PNG без альфы, на
|
|
||||||
не-PNG входе → без падений.
|
|
||||||
14. Пустой text-source → run пропускается без ошибки.
|
|
||||||
|
|
||||||
### D. Генераторы
|
|
||||||
|
|
||||||
15. Каждый генератор по дефолтам создаёт картинку; смена размеров/цвета
|
|
||||||
перегенерирует; канва ожидаемого размера.
|
|
||||||
|
|
||||||
## 2. Репрезентативная выборка по категориям (для «нет падений»)
|
|
||||||
|
|
||||||
Полный прогон всех 121 — опционально (C/all); минимальный чек — по группам:
|
|
||||||
|
|
||||||
| Категория | Кол-во | Охват для «нет падений» |
|
|
||||||
| ------------ | ------ | --------------------------------------------------------------------------------- |
|
|
||||||
| CONVERT | 14 | convert-png-to-jpg, convert-png-to-webp, png-to-base64, base64-to-png, svg-to-png |
|
|
||||||
| TRANSPARENCY | 20 | remove-background, round-corners, add-stroke, remove-color-from, circle-mask |
|
|
||||||
| COLOR | 25 | grayscale, invert, black-and-white, quantize, dithering, png-to-hsl |
|
|
||||||
| GEOMETRY | 18 | resize, crop, rotate, flip, add-border, tile |
|
|
||||||
| FILTERS | 8 | blur, sharpen, pixelate, add-noise |
|
|
||||||
| TEXT | 3 | add-text, date-stamp, watermark-tile |
|
|
||||||
| ANALYZE | 12 | extract-color-from, show-transparent-pixels, verify-is-png, png-is-transparent |
|
|
||||||
| GENERATE | 21 | create-empty, linear-gradient, two-colors, placeholder, text-to-png |
|
|
||||||
|
|
||||||
## 3. Playwright (авто) против ручного — разбиение
|
|
||||||
|
|
||||||
### ✅ Playwright — детерминированные сквозные потоки и «нет падений»
|
|
||||||
|
|
||||||
- Все маршруты + 404: открытие, отсутствие console errors / uncaught exceptions
|
|
||||||
/ failed requests (слушатели `page.on('console'/'pageerror')`).
|
|
||||||
- Поиск и каталог: фильтрация, счётчики, переходы.
|
|
||||||
- Upload (`setInputFiles` с фикстурой PNG) → авто-результат → Download (ожидание
|
|
||||||
`download` события, проверка имени/расширения/suggestedFilename).
|
|
||||||
- Reset / смена параметра → перегенерация, running не «залипает».
|
|
||||||
- Тема light/dark и язык RU/EN.
|
|
||||||
- Текстовые инструменты: ввод → текстовый результат → копия/скачивание.
|
|
||||||
- Вердикты на подготовленных фикстурах (PNG с альфой, PNG без альфы,
|
|
||||||
JPG/некартинка).
|
|
||||||
- Генераторы: дефолты → результат появился, размер канвы ожидаемый.
|
|
||||||
- 404 и empty-state.
|
|
||||||
- Worker и fallback: сравнить оба пути рендера (инжект отключения Worker).
|
|
||||||
|
|
||||||
### 🔄 Playwright + ручная сверка
|
|
||||||
|
|
||||||
- Пиксельная корректность результата (сравнение `<canvas>` через
|
|
||||||
`toDataURL`/тест-per-pixel) — по приоритету вторично; эталоны задаются
|
|
||||||
вручную, делать отдельной задачей.
|
|
||||||
- Реальные пользовательские картинки (не фикстуры) — вручную.
|
|
||||||
|
|
||||||
### ✋ Ручное тестирование
|
|
||||||
|
|
||||||
- **Краевые PNG:** большие (мегапиксели), 1×1, анимация, палитровые,
|
|
||||||
чекерборд/полупрозрачность, повреждённые файлы → нет зависаний, понятные
|
|
||||||
сообщения об ошибке.
|
|
||||||
- **Края параметров:** экстремальные значения, NaN/пустые поля, инвалидные цвета
|
|
||||||
в плашках/градиентах → корректная санитизация (silent repair).
|
|
||||||
- **Производительность/отзывчивость:** задержка авто-реранов на слайдерах, нет
|
|
||||||
«залипания» UI, скролл при длинных настройках.
|
|
||||||
- **Визуальный слой дизайна** (доделываем по ходу): сетки, выравнивание, отступы
|
|
||||||
на брейкпоинтах mobile/tablet/desktop (640/800/1100).
|
|
||||||
- **Буфер обмена** (copy) в реальном браузере.
|
|
||||||
- **Drag-n-drop** файла на dropzone (UX-ощущения).
|
|
||||||
- **Разные браузеры:** Firefox/Safari-нюансы (webkit в Playwright опционален).
|
|
||||||
- **Субъективная корректность:** dithering, two-colors, watermark, текст
|
|
||||||
не-ASCII/кириллица в add-text/date-stamp/watermark-tile.
|
|
||||||
|
|
||||||
## 4. Критерии приёмки (доступ в главную)
|
|
||||||
|
|
||||||
1. Все 4 маршрута + 404 открываются без console errors и исключений.
|
|
||||||
2. Репрезентативный прогон по 8 категориям: ни один инструмент не падает и не
|
|
||||||
показывает поле ошибки.
|
|
||||||
3. Upload → результат → download завершается для file-инструментов;
|
|
||||||
текст/вердикты/генераторы дают корректный output-тип.
|
|
||||||
4. `pnpm --dir web test`, `pnpm --dir web lint:all`, `pnpm --dir web build`
|
|
||||||
зелёные.
|
|
||||||
5. Тема и язык переключаются без регрессов.
|
|
||||||
|
|
||||||
## 5. Оценка покрытия
|
|
||||||
|
|
||||||
- **~60–70%** «нет падений/ошибок» закрывается Playwright детерминированно
|
|
||||||
(маршруты, сквозные потоки, отсутствие ошибок).
|
|
||||||
- **~30–40%** — руками: краевые PNG/параметры, реальные пользовательские файлы,
|
|
||||||
буфер обмена, кросс-браузеры, визуальный/производительный слой, субъективная
|
|
||||||
корректность.
|
|
||||||
|
|
||||||
## 6. Шаги реализации (после ревью)
|
|
||||||
|
|
||||||
1. ✅ Playwright-раннер: `web/playwright.config.ts` (webServer на
|
|
||||||
`pnpm build + `scripts/serve-static.mjs` `--port
|
|
||||||
4173`), папка `web/e2e/`, npm-скрипт `test:e2e`.
|
|
||||||
2. ✅ Фикстуры PNG (генерируются в рантайме, `web/e2e/helpers/fixtures.ts`): с
|
|
||||||
альфой, без альфы, 1×1, большой, повреждённый, ландшафт.
|
|
||||||
3. ✅ E2E по блокам A–D и секции 3 (авто-часть): `navigation.spec.ts`,
|
|
||||||
`catalog.spec.ts`, `pipeline.spec.ts`, `text-and-verdicts.spec.ts`,
|
|
||||||
`tools-smoke.spec.ts` (матрица ~71 инструмента), `generators.spec.ts`,
|
|
||||||
`known-issues.spec.ts` (fixme).
|
|
||||||
4. ✅ Ручной чек-лист из секции 3 (ручная часть) —
|
|
||||||
`docs/checklist-manual-testing.md` (разделы A–G, включая известные баги).
|
|
||||||
5. ✅ Найденные баги (зафиксированы как `test.fixme` + чек-лист §G):
|
|
||||||
1. генераторы (21/121) не имеют кнопки «Generate» и полей схемы — результат
|
|
||||||
через UI недостижим;
|
|
||||||
2. `resize-png` с дефолтом 0×0 → `errors.resizeSize` при любом входе;
|
|
||||||
3. `crop-png` с дефолтом 0×0 → `errors.cropSize` при любом входе;
|
|
||||||
4. ключи ошибок показываются сырыми (`errors.*`) вместо переведённого текста
|
|
||||||
из `en.ts`/`ru.ts`. Исправления — отдельными атомарными задачами по
|
|
||||||
конвенции AGENTS.md (после правок: `lint:all`, `test:e2e`).
|
|
||||||
|
|
||||||
## 7. Итоговое состояние на момент сдачи
|
|
||||||
|
|
||||||
- `pnpm --dir web test:e2e` — **104 passed, 4 skipped (fixme), 0 failed**.
|
|
||||||
- `pnpm --dir web lint:all` и `svelte-check` — зелёные.
|
|
||||||
- Визуальный слой, краевые PNG/параметры, кросс-браузеры — остаются на ручной
|
|
||||||
прогон по `docs/checklist-manual-testing.md`.
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
# План: волна 4 — текст, штампы, водяные знаки
|
|
||||||
|
|
||||||
> **СТАТУС: ВЫПОЛНЕН 25.08.2026.**
|
|
||||||
|
|
||||||
## 1. Что получается
|
|
||||||
|
|
||||||
Четыре инструмента новой категории «Текст»: надпись на изображении, дата-штамп,
|
|
||||||
защитная плитка (повторяющийся полупрозрачный текст под углом), водяной знак
|
|
||||||
картинкой. Каталог вырастает с 69 до 73. Это первые операции, рисующие через
|
|
||||||
canvas 2D — класс perceptual по роадмапу: точные пиксельные тесты на них не
|
|
||||||
пишутся, юнит-тестами покрывается чистая математика раскладки.
|
|
||||||
|
|
||||||
## 2. Решения
|
|
||||||
|
|
||||||
- Вся геометрия — чистые функции без DOM: якорь из 9-позиционной сетки →
|
|
||||||
координаты, перенос текста по ширине с инъекцией измерителя, раскладка плитки
|
|
||||||
с углом и шагом. Canvas-вызовы — тонкий слой рядом (`core/textdraw.ts`), в
|
|
||||||
юнит-тесты не попадает.
|
|
||||||
- Один движок рендера текста на все три текстовых инструмента: шрифт из
|
|
||||||
системного набора (sans/serif/mono), размер, цвет, жирность, прозрачность,
|
|
||||||
поворот. Различия инструментов — только в пресетах параметров и количестве
|
|
||||||
слоёв (одна надпись vs плитка).
|
|
||||||
- Дата-штамп — надпись с автотекстом: строка формата (`YYYY-MM-DD hh:mm`)
|
|
||||||
разворачивается в текущую дату в момент запуска; свой мини-форматтер без
|
|
||||||
зависимостей.
|
|
||||||
- Водяной знак картинкой — первый двухисточниковый инструмент: вторая картинка
|
|
||||||
грузится отдельной зоной в этапе, живёт только в состоянии страницы и
|
|
||||||
сознательно не сохраняется ни в workspace-pipeline, ни в экспорте JSON; после
|
|
||||||
восстановления цепочки знак нужно выбрать заново. Ограничение фиксируем в
|
|
||||||
описании инструмента.
|
|
||||||
- Новая категория `text` встаёт после «Фильтров»; названия в обоих словарях,
|
|
||||||
порядок в CATEGORIES один.
|
|
||||||
|
|
||||||
## 3. Инструменты
|
|
||||||
|
|
||||||
- `add-text-png` — параметры: текст (строка), размер (слайдер 8–200), цвет,
|
|
||||||
шрифт (select), жирность (чекбокс), позиция (select 3×3: углы/стороны/центр),
|
|
||||||
отступ (слайдер). Опциональная подложка: цвет + прозрачность плашки за
|
|
||||||
текстом.
|
|
||||||
- `date-stamp-png` — тот же набор минус текст, плюс формат даты; дефолт — нижний
|
|
||||||
правый угол.
|
|
||||||
- `watermark-tile-png` — текст, размер, цвет, прозрачность (0–100), угол
|
|
||||||
(-90…90), шаг по X/Y; плитка заполняет весь холст.
|
|
||||||
- `watermark-image-png` — второй источник (загрузка PNG с альфой), масштаб знака
|
|
||||||
(% от ширины холста), прозрачность, позиция 3×3, отступ.
|
|
||||||
|
|
||||||
## 4. Этапы и проверки
|
|
||||||
|
|
||||||
Каждый этап заканчивается зелёными test/check/build и ревью.
|
|
||||||
|
|
||||||
- **A. Фундамент** — категория `text` (categories + словари + порядок),
|
|
||||||
`core/textdraw.ts`: anchor9, wrap, tileLayout с юнит-тестами на математике
|
|
||||||
(координаты углов/центра, переносы слов, плотность плитки), тонкий
|
|
||||||
canvas-модуль `domText.ts` (без тестов). Реестр пока пуст в категории.
|
|
||||||
- **B. Надпись и дата** — `add-text-png`, `date-stamp-png` на общем движке;
|
|
||||||
иконки, переводы, записи реестра. Смоук: обе локали, тёмная тема (плашка
|
|
||||||
читаема), длинный текст переносится и не вылезает за край.
|
|
||||||
- **C. Плитка** — `watermark-tile-png`; смоук: отрицательные и нулевой угол,
|
|
||||||
прозрачность 0/50/100, мелкий шаг не подвешивает страницу на больших холстах
|
|
||||||
(ограничение количества плиток капом).
|
|
||||||
- **D. Знак картинкой** — двухисточниковый UX: вторая drop-зона в этапе
|
|
||||||
(компонент рядом с SourceCard, виден только у этого инструмента),
|
|
||||||
масштаб/позиция/отступ; ограничение про сохранение — в описании и подсказке.
|
|
||||||
Этап режется без ущерба остальным, если решим отложить.
|
|
||||||
- **E. Итог** — полный прогон, смоук §6, архивация плана.
|
|
||||||
|
|
||||||
## 5. Критерии готовности
|
|
||||||
|
|
||||||
- 73 инструмента; вся математика раскладки покрыта юнит-тестами; canvas-слой
|
|
||||||
изолирован в одном модуле.
|
|
||||||
- test/check/build зелёные; новые инструменты работают в цепочке как обычные
|
|
||||||
шаги (кроме источника знака — см. ограничение).
|
|
||||||
|
|
||||||
## 6. Что сознательно не делаем
|
|
||||||
|
|
||||||
- Выбор произвольных пользовательских шрифтов (файлами) и веб-шрифтов — только
|
|
||||||
системный набор.
|
|
||||||
- Кернинг/линии/многострочное редактирование с абзацами — один абзац с
|
|
||||||
автопереносом.
|
|
||||||
- Сохранение картинки-знака в пайплайне и её передача между сессиями.
|
|
||||||
- Текст по контуру, градиентные заливки текста.
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
# План: светлая и тёмная тема
|
|
||||||
|
|
||||||
> **СТАТУС: ВЫПОЛНЕН 25.08.2026.**
|
|
||||||
|
|
||||||
## 1. Что получается
|
|
||||||
|
|
||||||
Переключатель light/dark в шапке рядом с языковым. Выбор в localStorage
|
|
||||||
(`theme`), первый визит — по `prefers-color-scheme`. Тёмная палитра —
|
|
||||||
переопределение токенов селектором `[data-theme='dark']`; компоненты не
|
|
||||||
правятся.
|
|
||||||
|
|
||||||
## 2. Решения
|
|
||||||
|
|
||||||
- Два состояния, без «авто»: ключ хранит только явный выбор, отсутствие ключа =
|
|
||||||
системная тема.
|
|
||||||
- Анти-вспышка: инлайн-скрипт в `app.html` до гидрации ставит `data-theme` из
|
|
||||||
localStorage или `matchMedia`; пререндеренная разметка остаётся светлой.
|
|
||||||
- `color-scheme: dark` на корне — нативные контролы (input[type=color], range,
|
|
||||||
select, скроллбары) темнеют без правок.
|
|
||||||
- Модуль `theme.svelte.ts` по образцу locale: состояние, персист, применение
|
|
||||||
атрибута. Стартовую установку делает скрипт из `app.html`, модуль
|
|
||||||
синхронизирует runes-состояние после гидрации.
|
|
||||||
|
|
||||||
## 3. Палитра
|
|
||||||
|
|
||||||
- Переопределяем: `--bg`, `--surface`, `--border`, `--text`, `--text-muted`.
|
|
||||||
- Акцент `#2563eb` не меняется; `--accent-hover` — светлее для тёмного фона.
|
|
||||||
- `--danger` осветлить для читаемости.
|
|
||||||
- `--check-a`/`--check-b` шахматки — приглушённые серые: паттерн прозрачности
|
|
||||||
остаётся узнаваемым, но не слепит.
|
|
||||||
- `--shadow-card` — почти чёрная с низкой альфой вместо синей.
|
|
||||||
- Все `color-mix(… var(--surface))` пересчитаются сами — проверить визуально
|
|
||||||
легенды, recalc-бейдж, drag-оверлеи.
|
|
||||||
|
|
||||||
## 4. Этапы и проверки
|
|
||||||
|
|
||||||
Каждый этап заканчивается зелёными test/check/build и ревью.
|
|
||||||
|
|
||||||
- **A. Инфраструктура** — `theme.svelte.ts`, скрипт в `app.html`, переключатель
|
|
||||||
в шапке, черновая тёмная палитра. Тесты: персист выбора, игнор мусора в
|
|
||||||
localStorage, применение атрибута (стаб document).
|
|
||||||
- **B. Палитра** — выверка значений: герой, каталог, страница инструмента,
|
|
||||||
цепочка, баннер ошибки, маска, текстовые инструменты; фокус-ринги и hover; обе
|
|
||||||
локали.
|
|
||||||
|
|
||||||
## 5. Смоук
|
|
||||||
|
|
||||||
1. Переключение мгновенное, без перезагрузки; выбор переживает её же.
|
|
||||||
2. При системной тёмной теме первый визит открывается сразу тёмным.
|
|
||||||
3. Нативные контролы выглядят тёмными.
|
|
||||||
4. Шахматка различима, легенды и бейджи читаемы.
|
|
||||||
5. Баннер ошибки и пипетка заметны на тёмных панелях.
|
|
||||||
|
|
||||||
## 6. Критерий готовности
|
|
||||||
|
|
||||||
Grep: ни одного цвета вне токенов в компонентах и маршрутах.
|
|
||||||
|
|
||||||
## 7. Не делаем
|
|
||||||
|
|
||||||
Три позиции в переключателе; пользовательские цветовые схемы; автосмену по
|
|
||||||
времени суток; отдельную тему для превью изображений.
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
# План: декомпозиция страницы инструмента, реактивные параметры, предпросмотр маски
|
|
||||||
|
|
||||||
> **СТАТУС: ВЫПОЛНЕН 2026-08-23.**
|
|
||||||
|
|
||||||
## 1. Зачем
|
|
||||||
|
|
||||||
- Страница инструмента (`ToolPage.svelte`) разрослась: состояние процесса,
|
|
||||||
загрузка файлов, вставка из буфера и разметка трёх зон живут в одном файле.
|
|
||||||
- Назревают два требования к процессу: параметры применяются автоматически без
|
|
||||||
кнопки; у части утилит на экране показывается одно — визуализация
|
|
||||||
фильтра/маски, а скачивается (и в будущем передаётся следующему инструменту)
|
|
||||||
другое — результат.
|
|
||||||
|
|
||||||
## 2. Контракт «результат и маска» в реестре
|
|
||||||
|
|
||||||
- `run(img, params)` — как сейчас: возвращает итоговое изображение. Его
|
|
||||||
скачивают и будут передавать дальше по цепочке.
|
|
||||||
- Запись реестра получает опциональное поле `preview?(img, params)` — возвращает
|
|
||||||
картинку для показа на экране (маску/фильтр).
|
|
||||||
- Если у записи есть `preview`, в карточке результата появляется переключатель
|
|
||||||
«Показать маску». Включён — на превью маска, выключен (по умолчанию) —
|
|
||||||
результат. Скачивание всегда отдаёт результат `run`, независимо от положения
|
|
||||||
переключателя.
|
|
||||||
- Первая реализация — удаление цвета: в ядро добавляется чистая функция
|
|
||||||
`colorMask(img, hex, tolerance)` — чёрно-белая маска, где удаляемые пиксели
|
|
||||||
белые; с юнит-тестами; `preview` записи вызывает её.
|
|
||||||
|
|
||||||
## 3. Декомпозиция страницы
|
|
||||||
|
|
||||||
Новая папка `components/tool/`; карточки — чисто отображательные, состояние
|
|
||||||
живёт только в оркестраторе:
|
|
||||||
|
|
||||||
- **`SourceCard.svelte`** — левая половина сцены: пусто → зона перетаскивания;
|
|
||||||
загружено → превью исходника под оверлеем замены с кнопкой «Заменить
|
|
||||||
изображение».
|
|
||||||
- **`ResultCard.svelte`** — правая половина: пустые состояния, превью результата
|
|
||||||
или сводка, чип «Пересчёт…», кнопка скачивания, при наличии `preview` —
|
|
||||||
переключатель маски.
|
|
||||||
- **`ParamsCard.svelte`** — форма параметров и подпись «Изменения применяются
|
|
||||||
автоматически»; для инструментов без параметров не выводится.
|
|
||||||
- **`ToolPage.svelte`** остаётся оркестратором: состояние (исходник, результат,
|
|
||||||
сводка, статус, ошибка, значения), декодирование файлов, вставка из буфера,
|
|
||||||
сброс, токен запусков и эффект с задержкой. Ожидаемо худеет примерно вдвое.
|
|
||||||
|
|
||||||
Карточки переиспользуются будущим многошаговым режимом.
|
|
||||||
|
|
||||||
## 4. Реактивные параметры (в этом же проходе)
|
|
||||||
|
|
||||||
- Кнопки «Применить» больше нет ни у одного инструмента.
|
|
||||||
- Изменение параметра запускает вычисление через 300 мс после последнего
|
|
||||||
изменения; каждое новое изменение перезапускает таймер.
|
|
||||||
- Токен запусков: если во время вычисления параметры поменялись, устаревший
|
|
||||||
ответ отбрасывается.
|
|
||||||
- Загрузка и замена файла считают сразу и подавляют ровно один авто-запуск
|
|
||||||
эффекта.
|
|
||||||
- Ошибка пересчёта — баннер сверху, прежний результат не сбрасывается.
|
|
||||||
|
|
||||||
## 5. Этапы и проверки
|
|
||||||
|
|
||||||
- **A. Декомпозиция** — перенос разметки в три карточки один к одному, поведение
|
|
||||||
не меняется. Проверка: `check/build`, визуально идентично.
|
|
||||||
- **B. Реактивность** — токен, задержка, чип пересчёта, минус кнопка. Проверка:
|
|
||||||
смоук §6 пп. 1–6.
|
|
||||||
- **C. Маска** — типы реестра, `colorMask` с тестами, переключатель в карточке
|
|
||||||
результата, пример на удалении цвета. Проверка: тесты + смоук §6 пп. 7–9.
|
|
||||||
- **D. Итог** — полный прогон `test/check/build`.
|
|
||||||
|
|
||||||
## 6. Смоук-чеклист
|
|
||||||
|
|
||||||
1. Загрузка файла → результат появляется сразу.
|
|
||||||
2. Изменить параметр → около 0.3 с и результат обновился сам.
|
|
||||||
3. Быстро изменить яркость несколько раз → одна финальная версия, промежуточные
|
|
||||||
не мелькают.
|
|
||||||
4. Во время пересчёта чип виден, предыдущий результат не исчезает.
|
|
||||||
5. Инструмент без параметров и информационная страница ведут себя как раньше.
|
|
||||||
6. Некорректные параметры (обрезка вне картинки) → баннер ошибки, прежний
|
|
||||||
результат цел.
|
|
||||||
7. Переключатель «Показать маску» у удаления цвета: включён — белым по чёрному
|
|
||||||
видно, что удалится; выключен — обычный полупрозрачный результат.
|
|
||||||
8. С включённой маской скачивание всё равно сохраняет прозрачный результат, а не
|
|
||||||
маску.
|
|
||||||
9. Замена исходника возвращает переключатель маски в выключенное состояние.
|
|
||||||
|
|
||||||
## 7. Критерии готовности
|
|
||||||
|
|
||||||
- `ToolPage` содержит только оркестрацию состояния и эффектов; вся разметка
|
|
||||||
панелей — в `components/tool/*`.
|
|
||||||
- Кнопки «Применить» нет ни на одной странице.
|
|
||||||
- Скачивание всегда соответствует `run`; экран может показывать `preview`.
|
|
||||||
- `test/check/build` зелёные, смоук §6 пройден.
|
|
||||||
|
|
||||||
## 8. Что сознательно не делаем
|
|
||||||
|
|
||||||
- Вынос вычислений в веб-воркер; настраиваемая задержка — фиксированные 300 мс.
|
|
||||||
- Редактор масок (кисть, слои, история) — только автогенерация из параметров
|
|
||||||
инструмента.
|
|
||||||
- Передача результата между инструментами — придёт вместе с многошаговым режимом
|
|
||||||
позже.
|
|
||||||
@@ -1,118 +0,0 @@
|
|||||||
# План: раскладка «исходник слева — результат справа» + вставка изображения
|
|
||||||
|
|
||||||
> **СТАТУС: ВЫПОЛНЕН 2026-08-22.**
|
|
||||||
>
|
|
||||||
> **Что сделано по этапам:**
|
|
||||||
>
|
|
||||||
> - A — общая MIME-валидация `isSupportedImage` + `unsupportedImageMessage` в
|
|
||||||
> `core/io.ts`; `DropZone` переведён на них ✔
|
|
||||||
> - B — постоянная двухпанельная раскладка: одна карточка-«сцена» с равными
|
|
||||||
> половинами «Исходник | Результат», кнопки внутри своих половин, параметры —
|
|
||||||
> отдельной карточкой ниже; контент сайта растянут на всю ширину окна ✔
|
|
||||||
> - C — Ctrl+V: `<svelte:window onpaste>`, поиск image-item в буфере, общая
|
|
||||||
> валидация; текстовые вставки не затрагиваются ✔
|
|
||||||
> - D — drag-замена: компонент `DropOverlay` с вуалью-подсказкой и счётчиком
|
|
||||||
> `dragenter/dragleave` против мерцания; тот же счётчик в `DropZone` ✔
|
|
||||||
> - E — верификация: тесты 85/85, svelte-check 0 ошибок, статический билд с 11
|
|
||||||
> страницами; ручной смоук-чеклист §5 пройден ✔
|
|
||||||
>
|
|
||||||
> **Отступления от исходного плана в ходе реализации:** вместо пропа `overlay` у
|
|
||||||
> `DropZone` сделан отдельный компонент-обёртка `DropOverlay` (drag-события
|
|
||||||
> ловит контент под курсором); раскладка доработана по ревью — одна карточка
|
|
||||||
> равных половин вместо двух независимых колонок, сайт без ограничения ширины.
|
|
||||||
|
|
||||||
## 1. Целевое поведение
|
|
||||||
|
|
||||||
1. **Постоянная двухпанельная раскладка** страницы инструмента — отдельного
|
|
||||||
состояния «страница загрузки» больше нет:
|
|
||||||
- **Слева — исходник**, панель есть всегда. Не загружено → пустое дроп-поле
|
|
||||||
(drag&drop или клик). Загружено → превью исходника.
|
|
||||||
- **Справа — результат**: до загрузки — `EmptyState` «Результат появится
|
|
||||||
здесь»; во время обработки — `EmptyState` «Обработка…»; после — превью
|
|
||||||
результата (для info-инструмента — `InfoPanel`).
|
|
||||||
2. **Ctrl+V** — картинка из буфера обмена вставляется как исходник с любого
|
|
||||||
места страницы.
|
|
||||||
3. **Замена перетаскиванием** — новый файл, брошенный на левую панель с
|
|
||||||
загруженной картинкой, заменяет исходник (с оверлеем-подсказкой во время
|
|
||||||
перетаскивания).
|
|
||||||
4. **Кнопка «Заменить»** под превью исходника (переносится туда текущая
|
|
||||||
«Загрузить другое изображение»).
|
|
||||||
|
|
||||||
На мобильной ширине панели складываются в столбец: исходник сверху, результат
|
|
||||||
снизу.
|
|
||||||
|
|
||||||
## 2. Изменения по файлам
|
|
||||||
|
|
||||||
Новые компоненты не вводим — `EmptyState`, `Preview`, `DropZone` покрывают всё.
|
|
||||||
Меняем три файла:
|
|
||||||
|
|
||||||
- **`core/io.ts`** — экспорт `isSupportedImage(file): boolean`: общая
|
|
||||||
MIME-проверка на базе `ACCEPTED_IMAGE_TYPES`.
|
|
||||||
- **`DropZone.svelte`** — переходит на `isSupportedImage`; новый опциональный
|
|
||||||
проп `overlay` — режим «невидимая зона приёма поверх контента» для замены
|
|
||||||
перетаскиванием.
|
|
||||||
- **`ToolPage.svelte`** — постоянная сетка из двух панелей;
|
|
||||||
`<svelte:window onpaste>`; drag-оверлей левой панели; кнопки замены/сброса
|
|
||||||
переезжают в левую панель; правая панель — три состояния через `EmptyState`.
|
|
||||||
|
|
||||||
## 3. Этапы и проверки
|
|
||||||
|
|
||||||
- **A. Общая валидация** — `isSupportedImage` в `io.ts`, `DropZone` переведён на
|
|
||||||
неё. Проверка: `pnpm check`.
|
|
||||||
- **B. Раскладка** — две постоянные колонки в ToolPage; правая панель через
|
|
||||||
EmptyState; кнопки уходят в левую панель; обёртка `{#if !source}` на всю
|
|
||||||
страницу убирается. Проверка: визуальный осмотр всех инструментов,
|
|
||||||
`check/build`.
|
|
||||||
- **C. Ctrl+V** — `onpaste` на окне → поиск image-item в `clipboardData` → тот
|
|
||||||
же `handleFile`; не-изображения игнорируются молча. Проверка: ручной смоук со
|
|
||||||
скриншотом из буфера.
|
|
||||||
- **D. Drag-замена** — оверлей DropZone(overlay) при dragover на левой панели;
|
|
||||||
drop → `handleFile` нового файла. Проверка: ручной смоук «бросить файл на
|
|
||||||
загруженный исходник».
|
|
||||||
- **E. Итог** — полный ручной смоук-чеклист (§5). Проверка: `test/check/build`
|
|
||||||
зелёные.
|
|
||||||
|
|
||||||
## 4. Нюансы и решения
|
|
||||||
|
|
||||||
- **Мерцание dragleave** — классическая проблема оверлеев: используем счётчик
|
|
||||||
`dragenter/dragleave`, оверлей скрывается только когда счётчик обнулился и
|
|
||||||
произошёл `drop`/`dragend`.
|
|
||||||
- **Paste при фокусе в поле параметров** — обрабатываем всегда: если в буфере
|
|
||||||
именно изображение, пользователь очевидно хочет заменить исходник; текстовые
|
|
||||||
вставки обработчик не трогает.
|
|
||||||
- **Форматы буфера** — скриншоты приходят как PNG, часть приложений отдаёт
|
|
||||||
JPEG/WebP; все проходят общую `isSupportedImage`. GIF/BMP из буфера редки, но
|
|
||||||
тоже допустимы.
|
|
||||||
- **Info-инструменты** — ведут себя так же: слева исходник, справа после
|
|
||||||
загрузки появляется `InfoPanel`.
|
|
||||||
- **Состояние ошибки** — баннер остаётся сверху страницы; неудачная замена не
|
|
||||||
сбрасывает старый исходник.
|
|
||||||
|
|
||||||
## 5. Чеклист ручного смоука
|
|
||||||
|
|
||||||
1. Пустые обе панели → drag&drop файла слева → появился исходник и результат.
|
|
||||||
2. Клик по пустой левой панели → выбор файла.
|
|
||||||
3. Ctrl+V со скриншотом → исходник заменился, результат пересчитался.
|
|
||||||
4. Drag файла на загруженную левую панель → оверлей виден, после drop — замена.
|
|
||||||
5. Отмена перетаскивания (Esc / мимо) → оверлей скрылся, исходник не изменился.
|
|
||||||
6. Бросок неподдерживаемого файла (например .txt) → баннер ошибки, старый
|
|
||||||
исходник цел.
|
|
||||||
7. Кнопка «Заменить» открывает выбор файла.
|
|
||||||
8. png-info: работает та же схема, справа сводка.
|
|
||||||
9. Мобильная ширина: панели в столбец, порядок исходник → результат.
|
|
||||||
10. Все 11 инструментов отвечают на замену исходника корректным пересчётом.
|
|
||||||
|
|
||||||
## 6. Критерии готовности
|
|
||||||
|
|
||||||
- На странице инструмента нет состояния «только дропзона на всю страницу» —
|
|
||||||
раскладка двухпанельная всегда.
|
|
||||||
- Загрузка работает тремя способами: клик, drag&drop (включая замену), Ctrl+V.
|
|
||||||
- Ошибочные файлы и отмена перетаскивания не портят текущий исходник.
|
|
||||||
- `test/check/build` зелёные; чеклист §5 пройден.
|
|
||||||
|
|
||||||
## 7. Что сознательно не делаем (граница скоупа этого плана)
|
|
||||||
|
|
||||||
- Множественная загрузка / пакетная обработка — вне скоупа этого плана.
|
|
||||||
- Камера/скриншот-инструменты захвата — только буфер обмена.
|
|
||||||
- Прогресс-бары декодирования — файлы читаются быстро, достаточно индикации
|
|
||||||
«Обработка…».
|
|
||||||
@@ -1,130 +0,0 @@
|
|||||||
# План: главная страница как рабочая область с цепочкой
|
|
||||||
|
|
||||||
> **СТАТУС: ВЫПОЛНЕН 2026-08-23.** Заменяет идею отдельной страницы рабочего
|
|
||||||
> пространства.
|
|
||||||
|
|
||||||
## 1. Модель
|
|
||||||
|
|
||||||
1. **Главная страница — это рабочая область.** Без выбранного инструмента на ней
|
|
||||||
большой поиск: начните вводить название — список отфильтровывается; выбор
|
|
||||||
разворачивает инструмент прямо здесь, без перехода.
|
|
||||||
2. Развернутый инструмент выглядит как привычная страница: слева исходник,
|
|
||||||
справа результат со своей кнопкой скачивания, параметры — в отдельной
|
|
||||||
карточке под сценой.
|
|
||||||
3. Под результатом рядом с кнопкой скачивания — кнопка **«⛓ Chain with…»**:
|
|
||||||
добавляет пустую секцию, внутри которой такой же поиск; выбор заполняет
|
|
||||||
секцию следующим инструментом. Так цепочка растёт сколько угодно.
|
|
||||||
4. Каждая заполненная секция цепи — полноценная: свои параметры, своё превью,
|
|
||||||
своя кнопка скачивания промежуточного результата. Крестик убирает секцию;
|
|
||||||
кнопка «✂ Remove chain» снимает всю цепь разом (без подтверждения).
|
|
||||||
5. Чекбокс «Показать маску» живёт в карточке параметров базового инструмента и
|
|
||||||
влияет только на его превью: в цепь всегда передаётся сам результат.
|
|
||||||
6. Сетка-каталог инструментов переезжает на отдельную страницу `/list-tools` —
|
|
||||||
для SEO и визуального поиска. Основная работа происходит на главной; в шапке
|
|
||||||
появляются ссылки на обе.
|
|
||||||
7. Прямые адреса `/tools/<id>` сохраняются: тот же развернутый инструмент для
|
|
||||||
глубоких ссылок.
|
|
||||||
8. Цепочка переживает перезагрузку (автосохранение в localStorage).
|
|
||||||
Экспорт/импорт файлом отложен до появления CLI/wasm-части.
|
|
||||||
|
|
||||||
## 2. Архитектура
|
|
||||||
|
|
||||||
- Страница инструмента уже собрана как самодостаточный компонент, принимающий
|
|
||||||
запись реестра, — он и становится встраиваемым блоком. Маршруты `/tools/[id]`
|
|
||||||
и главная используют один и тот же компонент: первая — по адресу из URL,
|
|
||||||
вторая — по выбору из поиска.
|
|
||||||
- Состояние главной: выбран ли инструмент; если да — рендерится блок
|
|
||||||
инструмента, целиком владеющий своим исходником, параметрами, результатом и
|
|
||||||
цепочкой.
|
|
||||||
- Смена инструмента на главной (через повторный поиск) сбрасывает блок целиком.
|
|
||||||
- Цепочка хранится массивом шагов; у пустого звена инструмент ещё не выбран —
|
|
||||||
такие звенья не сохраняются в localStorage и не участвуют в прогоне.
|
|
||||||
- Исполнение — последовательная свёртка: выход каждого звена — вход следующего;
|
|
||||||
результаты всех звеньев доступны их секциям. Общий авто-раннер даёт токен
|
|
||||||
отмены и задержку ~300 мс.
|
|
||||||
|
|
||||||
## 3. Переиспользование и доработки существующего
|
|
||||||
|
|
||||||
Готовое берётся как есть: `SourceCard`, `Preview`, `ParamForm`,
|
|
||||||
`DownloadButton`, `EmptyState`, модуль пайплайна, авто-раннер, `PNG_OUTPUT`.
|
|
||||||
|
|
||||||
Доработки:
|
|
||||||
|
|
||||||
- **Блок страницы инструмента** — расширяется секцией цепочки: список
|
|
||||||
заполненных звеньев, каждое со своими параметрами, превью и скачиванием, плюс
|
|
||||||
пустые звенья с поиском. Кнопки «⛓» и «✂ Remove chain» — рядом со скачиванием
|
|
||||||
базового результата.
|
|
||||||
- **`ParamsCard`** — опциональный блок «Показать маску» (флаг preview у
|
|
||||||
инструмента + двусторонний флаг).
|
|
||||||
- **Карточка результата** — упрощается: получает готовую картинку, маска уходит
|
|
||||||
в карточку параметров.
|
|
||||||
- **Существующая главная-каталог** — переезжает на маршрут `/list-tools` почти
|
|
||||||
без изменений.
|
|
||||||
|
|
||||||
Удаляется: черновой маршрут рабочего пространства и его компоненты
|
|
||||||
списка/селектора — вместо них новые, под вертикальную модель.
|
|
||||||
|
|
||||||
## 4. Новые компоненты
|
|
||||||
|
|
||||||
- **`ToolSearch.svelte`** — поиск с нечётким совпадением: поле ввода,
|
|
||||||
отфильтрованный список (регистронезависимая подпоследовательность по названию
|
|
||||||
и описанию, приоритет совпадению с начала), клавиатура ↑↓ Enter Esc; два
|
|
||||||
размера — герой на главной и компактный внутри пустого звена. Предлагает
|
|
||||||
только инструменты-трансформеры.
|
|
||||||
- **`ChainStep.svelte`** — секция одного звена: пустое звено показывает
|
|
||||||
компактный поиск; заполненное — заголовок с номером и крестиком, форму
|
|
||||||
параметров, превью результата, кнопку скачивания.
|
|
||||||
- **`ChainBar.svelte`** — строка под результатом базового инструмента: «⛓ Chain
|
|
||||||
with…» и «✂ Remove chain» (видна только при непустой цепи).
|
|
||||||
|
|
||||||
## 5. Этапы и проверки
|
|
||||||
|
|
||||||
- **A. Поиск и главная** — `ToolSearch`, герой поиска на главной, встраивание
|
|
||||||
блока инструмента, перенос каталога на `/list-tools`, ссылки в шапке.
|
|
||||||
Проверка: `check/build`, визуально.
|
|
||||||
- **B. Цепочка** — состояние цепи в блоке инструмента, пустые звенья с поиском,
|
|
||||||
прогон, промежуточные превью и скачивания, ошибки звеньев. Проверка: смоук §8
|
|
||||||
пп. 1–6.
|
|
||||||
- **C. Маска и упрощение** — блок маски в `ParamsCard`, упрощение карточки
|
|
||||||
результата. Проверка: смоук §8 п. 7.
|
|
||||||
- **D. Хранение и итог** — автосохранение и восстановление цепочки, удаление
|
|
||||||
чернового маршрута рабочего пространства, полный регресс каталога,
|
|
||||||
`test/check/build`.
|
|
||||||
|
|
||||||
## 6. Смоук-чеклист
|
|
||||||
|
|
||||||
1. На главной — большое поле поиска; ввод фильтрует список по мере набора; Enter
|
|
||||||
выбирает подсвеченный.
|
|
||||||
2. Выбранный инструмент разворачивается на той же странице: исходник, параметры,
|
|
||||||
результат.
|
|
||||||
3. «⛓ Chain with…» добавляет пустую секцию с компактным поиском; выбор заполняет
|
|
||||||
её.
|
|
||||||
4. У каждой секции своё превью и своё скачивание промежуточного результата.
|
|
||||||
5. Крестик убирает секцию; «✂ Remove chain» снимает все звенья разом.
|
|
||||||
6. Ошибка звена — баннер «Шаг N (имя): …»; последние успешные результаты секций
|
|
||||||
не исчезают.
|
|
||||||
7. «Показать маску» переключает превью базового инструмента; в цепь идёт
|
|
||||||
результат, скачивание базового отдаёт результат, не маску.
|
|
||||||
8. Перезагрузка восстанавливает цепочку заполненных звеньев; пустые звенья при
|
|
||||||
этом исчезают.
|
|
||||||
9. Быстрые правки подряд — один финальный пересчёт всей вертикали.
|
|
||||||
10. `/list-tools` открывает старый каталог по категориям; прямые адреса
|
|
||||||
`/tools/<id>` работают.
|
|
||||||
|
|
||||||
## 7. Критерии готовности
|
|
||||||
|
|
||||||
- Работа начинается с главной: поиск → инструмент → цепочка звеньями, каждый со
|
|
||||||
своим превью и скачиванием.
|
|
||||||
- Отдельной страницы рабочего пространства нет; каталог доступен на
|
|
||||||
`/list-tools`; прямые ссылки инструментов работают.
|
|
||||||
- Пустые звенья не попадают в сохранение и не ломают прогон.
|
|
||||||
- `test/check/build` зелёные, смоук §6 пройден.
|
|
||||||
|
|
||||||
## 8. Что сознательно не делаем
|
|
||||||
|
|
||||||
- Ветвления и прицепывание от середины цепи — новое звено всегда добавляется в
|
|
||||||
конец.
|
|
||||||
- Пипетка и маски внутри звеньев цепи — они остаются фичей базового инструмента.
|
|
||||||
- Экспорт и импорт цепочки файлом — отложено до появления CLI/wasm-части.
|
|
||||||
- Несколько параллельных инструментов на главной одновременно — один активный
|
|
||||||
блок за раз.
|
|
||||||
+77
-99
@@ -1,22 +1,30 @@
|
|||||||
# Backlog
|
# Backlog
|
||||||
|
|
||||||
|
> Статус: **active**. Продуктовые, UX- и инфраструктурные задачи. Здесь только
|
||||||
|
> **открытые** задачи: выполненные удаляются, а критичные для проекта факты из
|
||||||
|
> них переносятся в тематический документ (архитектура, quality gates, lint,
|
||||||
|
> e2e) по мере закрытия.
|
||||||
|
>
|
||||||
|
> «Что сознательно не вводим и почему» живёт в `docs/decisions.md`, архитектура
|
||||||
|
> — в `docs/architecture.md`. Raw-находки UX-аудита — `docs/tools-audit.md`, их
|
||||||
|
> разбивка на волны — `docs/plan-fields-audit-fixes.md`.
|
||||||
|
>
|
||||||
|
> Ids инструментов сверять с `web/src/lib/registry/`; полный список —
|
||||||
|
> `docs/tools-map.md`.
|
||||||
|
|
||||||
## lint и прочие правила
|
## lint и прочие правила
|
||||||
|
|
||||||
- [x] Кросс-языковой линтер словарей (warn-only: паритет ключей, пустые
|
- [ ] расширить `i18n/no-hardcoded-user-text` на `web/src/routes/**`: сейчас
|
||||||
значения, совпадение `{placeholder}`-переменных между всеми локалями) —
|
правило навешено только на `components/**` (без `/kit`), поэтому
|
||||||
правило `i18n/dict-consistency`, детали в `web/eslint-plugins/README.md`.
|
захардкоженный текст в `routes/**` проходит молча (`web/eslint.config.js`,
|
||||||
Правило реализовано; Q2 перевёл i18n baseline к zero-warning через
|
`web/eslint-plugins/README.md`)
|
||||||
policy-aware fallback.
|
|
||||||
- [x] Проверка текстов в svelte/ts/html — реализовано правилом
|
|
||||||
`i18n/no-hardcoded-user-text` (warn, baseline 0): ловит текст в шаблоне и
|
|
||||||
в пользовательских атрибутах, исключения — бренд, форматы, имена файлов,
|
|
||||||
registry-строки и `/kit`. Детали в `web/eslint-plugins/README.md`.
|
|
||||||
- [ ] `tseslint.config(...)` deprecated: сигнатура
|
- [ ] `tseslint.config(...)` deprecated: сигнатура
|
||||||
`(...configs: InfiniteDepthConfigWithExtends[]): ConfigArray` помечена
|
`(...configs: InfiniteDepthConfigWithExtends[]): ConfigArray` помечена
|
||||||
устаревшей, нужен переход на `defineConfig` из `typescript-eslint` —
|
устаревшей, нужен переход на `defineConfig` из `typescript-eslint` —
|
||||||
`web/eslint.config.js`.
|
`web/eslint.config.js`.
|
||||||
- [ ] удалить мёртвые `tools.*.params` из root i18n после проверки потребителей;
|
- [ ] удалить мёртвые `tools.*.params` из root i18n после проверки потребителей;
|
||||||
до этого Q2 держит pattern-based policy-исключение
|
до этого действует pattern-based policy-исключение
|
||||||
|
(`web/eslint-plugins/README.md`, раздел `dict-consistency`)
|
||||||
|
|
||||||
## Мелочи всякие
|
## Мелочи всякие
|
||||||
|
|
||||||
@@ -90,14 +98,13 @@
|
|||||||
|
|
||||||
## Переезд
|
## Переезд
|
||||||
|
|
||||||
- [x] добавить i18n в preview
|
|
||||||
- [ ] множественные числа (часть/части/частей; «N параметров», счётчики) —
|
- [ ] множественные числа (часть/части/частей; «N параметров», счётчики) —
|
||||||
`tPlural()` и формат `PluralForms`; детали и критерии — в этом backlog
|
`tPlural()` и формат `PluralForms`; детали и критерии — в этом backlog
|
||||||
|
|
||||||
## SEO/GEO
|
## SEO/GEO
|
||||||
|
|
||||||
Общий план: `docs/plan-seo.md` (S0–S4). C17 выполнен; старт возможен — пока
|
Общий план: `docs/plan-seo.md` (S0–S4). Переезд preview на реальные маршруты
|
||||||
только domain-free часть S1.
|
выполнен; старт возможен — пока только domain-free часть S1.
|
||||||
|
|
||||||
- [ ] переименование бренда + домен: чек-лист в локальном плане
|
- [ ] переименование бренда + домен: чек-лист в локальном плане
|
||||||
`docs/plan-domain.local.md` (вне git, имя домена в репо не публикуется;
|
`docs/plan-domain.local.md` (вне git, имя домена в репо не публикуется;
|
||||||
@@ -125,38 +132,12 @@
|
|||||||
|
|
||||||
## Качество production UI
|
## Качество production UI
|
||||||
|
|
||||||
Q5–Q6 и Q7–Q8 закрывают известные preview-проблемы, special fixtures, smoke,
|
Политика проверок — `docs/quality-gates.md`, e2e — `docs/testing-strategy.md`.
|
||||||
layout/view-model инварианты, browser matrix, coverage-гейт и линтеры; активных
|
|
||||||
`test.fixme` в e2e больше нет.
|
|
||||||
|
|
||||||
- [x] Генераторы auto-run без кнопки Generate — `web/e2e/generators.spec.ts`.
|
|
||||||
- [x] `resize-png` и `crop-png` используют размеры текущего source через общий
|
|
||||||
source-aware schema resolver — `web/e2e/pipeline.spec.ts`.
|
|
||||||
- [x] Reset сохраняет source/text, восстанавливает defaults и запускает deferred
|
|
||||||
auto-run — `web/e2e/pipeline.spec.ts`.
|
|
||||||
- [x] Ошибки хранят i18n key/vars и переводятся при выводе; изменение языка
|
|
||||||
обновляет alert — `web/e2e/i18n.spec.ts`.
|
|
||||||
- [x] Malformed/special PNG fixtures: CRC, truncated, palette и 16-bit проходят
|
|
||||||
через upload без краша — `web/e2e/png-fixtures.spec.ts`.
|
|
||||||
- [x] Registry-wide smoke покрывает все 122 инструмента по input/result-типу —
|
|
||||||
`web/e2e/tools-smoke.spec.ts`, `generators.spec.ts`,
|
|
||||||
`text-and-verdicts.spec.ts`.
|
|
||||||
- [x] Layout-группировка вынесена в `resolveLayoutGroups` и покрыта
|
|
||||||
unit-тестами; `SchemaFields.svelte` использует общий seam.
|
|
||||||
- [x] `SchemaPreview` view-model вынесена в `buildSchemaPreviewModel` и покрыта
|
|
||||||
unit-тестами для всех input/result-типов.
|
|
||||||
- [x] Browser matrix: полный Chromium, Firefox/WebKit для browser-critical
|
|
||||||
спецификаций и mobile Chromium 390×844 — `web/playwright.config.ts`.
|
|
||||||
- [x] Расхождение декодеров зафиксировано как ожидаемое поведение: Firefox и
|
|
||||||
WebKit декодируют `truncated-no-iend` и `idat-cut`, а WebKit ещё и
|
|
||||||
`crc-bad-idat`. Таблица движков живёт в `web/e2e/png-fixtures.spec.ts`,
|
|
||||||
продукт не ужесточается, и любая выгрузка битого файла обязана давать
|
|
||||||
alert, а не падение или «пустой» результат.
|
|
||||||
- [ ] Component-тесты `SchemaToolView`/`SchemaPreview` требуют отдельного
|
- [ ] Component-тесты `SchemaToolView`/`SchemaPreview` требуют отдельного
|
||||||
решения: node Vitest не имеет DOM/canvas, а e2e уже покрывает поведение.
|
решения: node Vitest не имеет DOM/canvas, а e2e уже покрывает поведение.
|
||||||
- [ ] Ошибка декодирования изображения из `createImageBitmap` пока показывается
|
- [ ] Ошибка декодирования изображения из `createImageBitmap` пока показывается
|
||||||
сырым сообщением браузера; нужен `ToolError`-маппер и i18n-ключ. Это
|
сырым сообщением браузера; нужен `ToolError`-маппер и i18n-ключ.
|
||||||
продуктовая задача, не Q6d.
|
|
||||||
|
|
||||||
## Идеи
|
## Идеи
|
||||||
|
|
||||||
@@ -178,7 +159,7 @@ layout/view-model инварианты, browser matrix, coverage-гейт и л
|
|||||||
4. **Batch-обработка архивов выбранным chain** — оценка L. Вход — zip-архив
|
4. **Batch-обработка архивов выбранным chain** — оценка L. Вход — zip-архив
|
||||||
картинок (распаковка в браузере), прогон текущего или выбранного сохранённого
|
картинок (распаковка в браузере), прогон текущего или выбранного сохранённого
|
||||||
chain по каждому файлу, сборка результата обратно в zip для скачивания.
|
chain по каждому файлу, сборка результата обратно в zip для скачивания.
|
||||||
Зависит от п.4 (выбор цепочки) и от экспортного формата pipeline JSON;
|
Зависит от идеи 3 (выбор цепочки) и от экспортного формата pipeline JSON;
|
||||||
прогресс и ошибки — пофайлово.
|
прогресс и ошибки — пофайлово.
|
||||||
|
|
||||||
5. **Избранные инструменты (fav tools)** — оценка S/M. Тоггл-звёздочка на
|
5. **Избранные инструменты (fav tools)** — оценка S/M. Тоггл-звёздочка на
|
||||||
@@ -188,59 +169,48 @@ layout/view-model инварианты, browser matrix, coverage-гейт и л
|
|||||||
|
|
||||||
6. **Конвертация формата — НЕ отдельный инструмент, а выбор в кнопке Download**
|
6. **Конвертация формата — НЕ отдельный инструмент, а выбор в кнопке Download**
|
||||||
— оценка M/L. Инструменты `convert-png-to-jpg` / `convert-png-to-webp` (и др.
|
— оценка M/L. Инструменты `convert-png-to-jpg` / `convert-png-to-webp` (и др.
|
||||||
форматы) не должны жить как самостоятельные инструменты цепочки. Вместо этого
|
форматы) не должны жить как самостоятельные инструменты цепочки. Механическая
|
||||||
— бесшовное внедрение выбора формата/качества прямо в кнопку/диалог
|
часть уже есть: `ToolEntry.output` (`OutputFormat`: mime/ext/qualityParamId)
|
||||||
**Download**: юзер скачивает результат в нужном формате (mime/ext/quality), а
|
и кодирование по нему в `SchemaToolView.download()` — `docs/architecture.md`,
|
||||||
pipeline при этом не усложняется лишним звеном. **Сделано (2026-09):**
|
раздел 3. Осталось (UX-райз «Download»):
|
||||||
механическая часть реализована в preview — `ToolEntry.output`
|
|
||||||
(`OutputFormat`: mime/ext/qualityParamId) в `web/src/lib/registry/types.ts`;
|
|
||||||
`SchemaToolView.download()` кодирует по `output` (jpg/webp/bmp уже настроены;
|
|
||||||
`png-to-bmp` переведён сюда же). Q6c покрывает этот фиксированный контракт
|
|
||||||
unit/E2E-проверками; переключатель форматов и quality остаётся отдельным
|
|
||||||
UX-райзом. **Осталось (UX-райз «Download»):**
|
|
||||||
- выбор формата/качества прямо в кнопке/диалоге Download для любого
|
- выбор формата/качества прямо в кнопке/диалоге Download для любого
|
||||||
результата (не отдельными конвертерами);
|
результата (не отдельными конвертерами);
|
||||||
- экспорт с лимитом размера → закрывает отложенный `reduce-to-size-png`
|
- экспорт с лимитом размера → закрывает отложенный `reduce-to-size-png`
|
||||||
(целевой KB, бинарный поиск по k из квантования). Проработать вместе с
|
(целевой KB, бинарный поиск по k из квантования). Проработать вместе с
|
||||||
достройкой download-флоу нового UI.
|
достройкой download-флоу нового UI.
|
||||||
|
|
||||||
7. **`watermark-image-png` (водяной знак картинкой)** — оценка M. Отложен из
|
7. **`watermark-image-png` (водяной знак картинкой)** — оценка M. Нужна
|
||||||
миграции (`archive/plan-migrate-remaining.md`). Нужна overlay-механика:
|
overlay-механика: второй источник изображения (getOverlay/store), параметры
|
||||||
второй источник изображения (getOverlay/store), параметры scale/opacity/
|
scale/opacity/position/margin/aspect. Решается вместе с вопросом, как второй
|
||||||
position/margin/aspect. Решается вместе с вопросом, как второй источник
|
источник картинки вписывается в schema-driven preview (`SchemaToolView`).
|
||||||
картинки вписывается в schema-driven preview (`SchemaToolView`).
|
|
||||||
|
|
||||||
8. **`png-info` (детальная информация о PNG)** — оценка M/L. Отложен из
|
8. **`png-info` (детальная информация о PNG)** — оценка M/L. Отдельный райз:
|
||||||
миграции. Отдельный райз: exif-теги, редактирование, структурированный вывод;
|
exif-теги, редактирование, структурированный вывод; нужен свой отдельный
|
||||||
нужен свой отдельный случай в UI, не «ещё один text/verdict». возможно нужна
|
случай в UI, не «ещё один text/verdict». Возможно нужна вообще отдельная
|
||||||
вообще отдельная страница, несовместимая с другими инструментами: 1) она
|
страница, несовместимая с другими инструментами: 1) она работает не с
|
||||||
работает не с изображением, а с мета данными из файла, 2) она не может быть
|
изображением, а с метаданными из файла, 2) она не может быть частью цепочки,
|
||||||
частью цепочки, т.к. результат не является изображением, 3) если надо
|
т.к. результат не является изображением, 3) если надо изменить теги, никакого
|
||||||
изменить теги, никакого форматирования и изменения в изображение вносить не
|
форматирования и изменения в изображение вносить не надо, только в
|
||||||
надо, только в мета информацияю
|
метаинформацию.
|
||||||
|
|
||||||
9. **Region-инструменты** (censor/erase/pixelate-area/blur-area/sharpen-area/
|
9. **Region-инструменты** (censor/erase/pixelate-area/blur-area/sharpen-area/
|
||||||
reverse-area) — ждут UI выделения области на превью. Из
|
reverse-area) — ждут UI выделения области на превью. Вне очереди.
|
||||||
`archive/plan-gap-waves.md` («вне очереди»).
|
|
||||||
|
|
||||||
10. **Мультифайловые инструменты-потребители** (gif-to-frames, separate-colors)
|
10. **Мультифайловые инструменты-потребители** (gif-to-frames, separate-colors)
|
||||||
— механизм «результат = набор файлов» (1 → many) реализован: `FileResult` +
|
— механизм «результат = набор файлов» (1 → many) уже есть
|
||||||
`result: "files"` + скачивание zip-архивом + грид-превью частей. Первый
|
(`docs/architecture.md`, раздел 7), первый потребитель —
|
||||||
потребитель — `split-into-parts-png`. Остальные подключаются по мере нужды.
|
`split-into-parts-png`. Остальные подключаются по мере нужды.
|
||||||
|
|
||||||
11. **Анимационные** (slow-reveal/fade/scrolling) — выход не PNG; отдельное
|
11. **Анимационные** (slow-reveal/fade/scrolling) — выход не PNG; отдельное
|
||||||
решение о формате. Из `archive/plan-gap-waves.md`.
|
решение о формате.
|
||||||
|
|
||||||
12. **HARD-серия** (glitch, barcode, signature-extract, handwritten-digital). Из
|
12. **HARD-серия** (glitch, barcode, signature-extract, handwritten-digital).
|
||||||
`archive/plan-gap-waves.md`.
|
|
||||||
|
|
||||||
13. **Контраст текста на акцентных кнопках — частично закрыт**. Для dark-темы
|
13. **Контраст текста на акцентных кнопках — частично закрыт**. Тона
|
||||||
`--color-main`/`--color-accent` подняты до tone 56, чтобы пройти текущий
|
`--color-main`/`--color-accent` подняты под текущий порог `palette.test.ts`;
|
||||||
порог `palette.test.ts` 4.0; targeted-тест проходит. Light-тема сохраняет
|
targeted-тест проходит. Light-тема всё ещё ниже AA 4.5 для обычного текста,
|
||||||
контраст ~4.1–4.3, что ниже AA 4.5 для обычного текста, поэтому после
|
а `--color-danger` в light-теме на границе: после завершения редизайна
|
||||||
завершения редизайна нужно повторно поднять тона и вернуть порог 4.5. Также
|
вернуть порог 4.5 и перепроверить оба токена.
|
||||||
проверить `--color-danger` (dark, тон 55): текст на нём 4.93 — ок, но
|
|
||||||
light-danger tone 48 = 4.25 на границе.
|
|
||||||
|
|
||||||
14. **Отображение маски для инструментов (runMask/preview-mask)** — придумать и
|
14. **Отображение маски для инструментов (runMask/preview-mask)** — придумать и
|
||||||
реализовать процесс показа маски результата. **Контекст:** в старом
|
реализовать процесс показа маски результата. **Контекст:** в старом
|
||||||
@@ -252,25 +222,32 @@ layout/view-model инварианты, browser matrix, coverage-гейт и л
|
|||||||
возможность вернуть (или инструмент — объявить) `runMask` — ч/б изображение
|
возможность вернуть (или инструмент — объявить) `runMask` — ч/б изображение
|
||||||
маски изменений либо иной подход (отдельное поле `mask` + функция маски,
|
маски изменений либо иной подход (отдельное поле `mask` + функция маски,
|
||||||
второй результат, overlay). Оценка — на усмотрение при проработке (S/M).
|
второй результат, overlay). Оценка — на усмотрение при проработке (S/M).
|
||||||
Пока живём без неё. **Идея 2:** каждый `run` возвращает два изображения -
|
Пока живём без неё. **Идея 2:** каждый `run` возвращает два изображения —
|
||||||
для отображения (показывает маску или сам результат), и для передачи в
|
для отображения (показывает маску или сам результат) и для передачи в
|
||||||
следующий инструмент (всегда результат. Оценка — на усмотрение при
|
следующий инструмент (всегда результат). Оценка — на усмотрение при
|
||||||
проработке (S/M). Пока живём без неё.
|
проработке (S/M). Пока живём без неё.
|
||||||
|
|
||||||
15. **Процесс работы с инструментами 1 → many и many → 1** — абстракция
|
15. **Набор файлов в пайплайне: 1 → many не должен быть терминальным** — оценка
|
||||||
результата «набор файлов» оформлена и работает: тип `FileResult`,
|
L. **Сейчас** `execute` принимает один `source: PixelImage | undefined` и
|
||||||
`result: "files"` в реестре, сериализация через executor, скачивание
|
возвращает один `ToolResult`, а `SchemaToolView` раскладывает вывод в
|
||||||
zip-архивом, грид-превью частей. Реализовано на первом потребителе
|
`result` / `fileResult` / `textResult` по `resultKind` — поэтому шаг с
|
||||||
`split-into-parts-png` (2026-09, `docs/archive/plan-split-into-parts.md`).
|
`result: "files"` упирается в конец цепочки. **Направление:** следующий
|
||||||
Q6b закрывает контракт ZIP/download: unit-тесты `web/src/lib/zip.test.ts` и
|
инструмент применяется **ко всем картинкам** предыдущего результата
|
||||||
browser-сценарий `web/e2e/pipeline.spec.ts` проверяют имена частей и
|
(fan-out), а many → 1 (вход = архив или несколько файлов) — обратная сторона
|
||||||
содержимое архива. **1 → many:** подключение остальных потребителей
|
того же контракта. Терминальными остаются только результаты, которые не
|
||||||
(gif-to-frames, separate-colors) при необходимости; формат/качество файлов
|
картинка (text, verdict). Что обдумать:
|
||||||
внутри zip — отложено до UX-райза «Download» (п.7). **many → 1** (вход =
|
- где проходит граница: один вызов `run(ctx)` на файл (имя части склеивается
|
||||||
архив/несколько картинок → единый результат): не реализовано. Зависит от
|
в `FileResult.files`) или батч целиком на стороне исполнителя;
|
||||||
batch-обработки архивов (п.5 идей, roadmap фаза 8 «batch-страница»). Нужна
|
`executor/protocol.ts` `files` уже сериализуются, но при батче нужен
|
||||||
отдельная проработка входа (распаковка zip в браузере, порядок файлов,
|
прогресс и отмена;
|
||||||
поведение в пайплайне — такие инструменты терминальны, как и 1 → many).
|
- частичные ошибки: один битый файл не должен ронять весь шаг — нужен
|
||||||
|
per-file отчёт, а не один `ToolError` на шаг;
|
||||||
|
- имена частей на каждом шаге и Download для набора (сейчас zip по
|
||||||
|
`tool.id`, `SchemaToolView.download()`);
|
||||||
|
- обратная сторона: распаковка zip в браузере, порядок файлов, поведение
|
||||||
|
many → 1 → many в одной цепочке. Зависит от идеи 10 (остальные потребители
|
||||||
|
1 → many) и идеи 4 (batch по архиву), `docs/roadmap.md` фаза 8
|
||||||
|
«batch-страница».
|
||||||
|
|
||||||
16. **Главная должна отличаться от каталога** — оценка S. Сейчас
|
16. **Главная должна отличаться от каталога** — оценка S. Сейчас
|
||||||
`src/routes/+page.svelte` и `src/routes/list-tools/+page.svelte` побайтово
|
`src/routes/+page.svelte` и `src/routes/list-tools/+page.svelte` побайтово
|
||||||
@@ -278,5 +255,6 @@ layout/view-model инварианты, browser matrix, coverage-гейт и л
|
|||||||
рабочий маршрут: на него ссылается крошка в `+layout.svelte` и он открыт
|
рабочий маршрут: на него ссылается крошка в `+layout.svelte` и он открыт
|
||||||
e2e-сценариями `catalog.spec.ts`, `i18n.spec.ts`, `navigation.spec.ts`.
|
e2e-сценариями `catalog.spec.ts`, `i18n.spec.ts`, `navigation.spec.ts`.
|
||||||
Замысел: главная — крупнее и с более простым поиском, без категорий; каталог
|
Замысел: главная — крупнее и с более простым поиском, без категорий; каталог
|
||||||
остаётся группировкой с фильтрами. Найдено измерением дублирования разметки
|
остаётся группировкой с фильтрами. Детектор дублирования разметки
|
||||||
по 65 `.svelte`-файлам: файлы побайтово идентичны.
|
сознательно не вводим — `docs/decisions.md`, раздел 1, поэтому расхождение
|
||||||
|
ловится на ревью.
|
||||||
|
|||||||
@@ -96,41 +96,10 @@
|
|||||||
корректный (не «кракозябры»).
|
корректный (не «кракозябры»).
|
||||||
- [ ] remove-background на сложной полупрозрачности — края не «звенят».
|
- [ ] remove-background на сложной полупрозрачности — края не «звенят».
|
||||||
|
|
||||||
## G. Закрытые Q5/Q6b/Q6c/Q6d/Q6e/Q6f.1/Q6f.2/Q6f.3-проблемы
|
|
||||||
|
|
||||||
> Q5/Q6b/Q6c/Q6d/Q6e/Q6f.1/Q6f.2/Q6f.3-проблемы закрыты автоматическими тестами;
|
|
||||||
> для ручной проверки остаются субъективные сценарии разделов A–F.
|
|
||||||
|
|
||||||
- [x] **Генераторы:** auto-run по дефолтам, результат и изменение параметра без
|
|
||||||
кнопки Generate. `web/e2e/generators.spec.ts`.
|
|
||||||
- [x] **resize-png/crop-png:** размеры текущего source используются по
|
|
||||||
умолчанию, alert не появляется. `web/e2e/pipeline.spec.ts`.
|
|
||||||
- [x] **Reset:** defaults восстанавливаются, source сохраняется, deferred
|
|
||||||
auto-run завершается результатом. `web/e2e/pipeline.spec.ts`.
|
|
||||||
- [x] **Локализация ошибок:** alert хранит key/vars, переводится при выводе и
|
|
||||||
обновляется при смене языка. `web/e2e/i18n.spec.ts`.
|
|
||||||
- [x] **FileResult → ZIP:** `split-into-parts-png` скачивает
|
|
||||||
`split-into-parts-png.zip` с именованными PNG-частями; содержимое и
|
|
||||||
сигнатуры проверяются `web/src/lib/zip.test.ts` и
|
|
||||||
`web/e2e/pipeline.spec.ts`.
|
|
||||||
- [x] **Output MIME/quality:** JPG/WebP/BMP имеют ожидаемые расширения и
|
|
||||||
сигнатуры, quality 10/90 меняет размер файла; `web/e2e/pipeline.spec.ts`.
|
|
||||||
- [x] **Malformed/special PNG:** CRC, truncated, palette и 16-bit fixtures
|
|
||||||
проходят через upload без краша; `web/e2e/png-fixtures.spec.ts`.
|
|
||||||
- [x] **Registry smoke:** все 122 production tools имеют специализированный
|
|
||||||
browser smoke по input/result-типу; `web/e2e/tools-smoke.spec.ts`,
|
|
||||||
`generators.spec.ts`, `text-and-verdicts.spec.ts`.
|
|
||||||
- [x] **Layout resolver:** `resolveLayoutGroups` покрыт unit-тестами; UI
|
|
||||||
группировка полей использует тот же seam.
|
|
||||||
- [x] **Preview model:** `buildSchemaPreviewModel` покрывает source/result
|
|
||||||
values, формат и `hasResult` для image/text/verdict/files.
|
|
||||||
- [x] **Browser matrix:** pipeline, verdicts и PNG-fixtures проходят в Firefox и
|
|
||||||
WebKit, mobile viewport 390×844 — в Chromium; отказ декодера битого PNG
|
|
||||||
зафиксирован per-engine. См. раздел 7 `docs/testing-strategy.md`.
|
|
||||||
|
|
||||||
## Как долго
|
## Как долго
|
||||||
|
|
||||||
- Полный прогон: ~40 минут (все разделы, включая мегапиксели и 4К) в двух
|
- Полный прогон: все разделы A–F, включая мегапиксели и 4К, в двух браузерах.
|
||||||
браузерах.
|
- Быстрый смоук: A (кроме мегапикселей), B (выборочно), C, F выборочно.
|
||||||
- Быстрый смоук (< 15 мин): A (кроме мегапикселей), B (выборочно), C, F
|
|
||||||
выборочно, G обязательно.
|
> Сценарии, закрытые e2e-тестами, в этот чек-лист не попадают: их покрытие видно
|
||||||
|
> по спецификациям в `web/e2e/`, политика e2e — `docs/testing-strategy.md`.
|
||||||
|
|||||||
@@ -0,0 +1,155 @@
|
|||||||
|
# Решения по гейтам и тестам
|
||||||
|
|
||||||
|
> Статус: **active**. Журнал решений «что сознательно НЕ вводим» и «как читаем
|
||||||
|
> результат CI». Только про гейты, линтеры, coverage, e2e и CI — **для обычных
|
||||||
|
> правок production-кода не нужен**. Читать, когда проектируется или меняется
|
||||||
|
> сам гейт, либо когда спрашивают «почему так, а не иначе». Оперативные команды
|
||||||
|
> и пороги — в `docs/quality-gates.md`, правила нагрузки на машину — в
|
||||||
|
> `docs/agent-workflow.md`, раздел 3.
|
||||||
|
|
||||||
|
Здесь лежат решения, чтобы не пересматривать их заново. Для каждого: причина и
|
||||||
|
то, что решение пересмотрело бы. В `docs/backlog.md` такие пункты не
|
||||||
|
дублируются: backlog живёт задачами, а не историей решений.
|
||||||
|
|
||||||
|
Числа ниже — разовые замеры, а не текущие значения: помечены датой. Актуальные
|
||||||
|
цифры даёт прогон (`docs/quality-gates.md`), по правилу `AGENTS.md` в документах
|
||||||
|
нет величин, которые меняются вместе с кодом.
|
||||||
|
|
||||||
|
## 1. Детектор дублирования разметки — не вводим
|
||||||
|
|
||||||
|
Ни свой AST-правило, ни готовая зависимость (jscpd, `eslint-plugin-sonarjs`).
|
||||||
|
|
||||||
|
- Замер по корпусу `.svelte` в `components/**` и `routes/**` (сентябрь 2026):
|
||||||
|
при пороге 5 нормализованных строк — одна осмысленная пара, при пороге 3 — 17
|
||||||
|
пар, и все они шум (`</label>`, `{/each}`, `{#if children}`). Семейство
|
||||||
|
schema-контролов делит максимум 4 строки — законное сходство by design.
|
||||||
|
- Причина: построчное сравнение Svelte-разметки принципиально ненадёжно —
|
||||||
|
одинаковый контейнер с разным содержимым даёт ложное срабатывание, а намеренно
|
||||||
|
общий класс — ложную тишину. Чтобы не ловить шум, порог пришлось бы поднять до
|
||||||
|
12+ строк, и тогда правило срабатывает ровно один раз.
|
||||||
|
- Контроль остаётся на ревью (расхождение видно в диффе) и на поведенческих
|
||||||
|
e2e-проверках; извлечение общей разметки закреплено конвенцией в `AGENTS.md`.
|
||||||
|
- Пересмотреть, если появится реально «уехавшая» копипаста в компонентах или
|
||||||
|
библиотека разрастётся на уровень композиций страниц.
|
||||||
|
|
||||||
|
## 2. Линтер согласованности комментариев — не вводим
|
||||||
|
|
||||||
|
Описания инструментов различаются осознанно, и ловить это текстовым правилом
|
||||||
|
дороже, чем смотреть на ревью. Действенная часть выполнена вручную: все
|
||||||
|
`TODO`/`FIXME` перенесены в backlog, незарегистрированных пометок в коде нет;
|
||||||
|
новые запрещены правилом в `AGENTS.md`.
|
||||||
|
|
||||||
|
## 3. Линтер «изменяемых чисел» в документации — не вводим
|
||||||
|
|
||||||
|
Правило «не фиксировать числа, которые меняются вместе с кодом» живёт в
|
||||||
|
`AGENTS.md` и держится на ревью. Автоматизация отклонена по замеру (сентябрь
|
||||||
|
2026): regex «число + существительное» на корпусе Markdown дал 15 совпадений, из
|
||||||
|
которых 14 — ложные («30 дней» политики архива, «9 опций» описания поля, «4
|
||||||
|
проекта» факт из `playwright.config.ts», цитата самого правила).
|
||||||
|
|
||||||
|
- Правило всё равно неполно: ловит «122 инструмента», но не «инструментов —
|
||||||
|
122», не «более 100», не «≈120»; на каждое обходное движение нужна правка
|
||||||
|
правила, а allowlist легитимных чисел становится вторым источником долга.
|
||||||
|
Худший исход — не «шумно», а «вроде гейт, а толку нет»: срабатывания на пустом
|
||||||
|
месте забейзелят, и через месяц правило перестанет ловить настоящие нарушения.
|
||||||
|
Видимое блокирующее правило, которое на деле ничего не проверяет, хуже
|
||||||
|
отсутствия правила.
|
||||||
|
- Соблюдение правила дешевле обхода: «список — в `docs/tools-map.md`» стоит
|
||||||
|
столько же символов, сколько цифра, но не требует обновления.
|
||||||
|
- Пересмотреть, если появится источник истины, из которого число можно
|
||||||
|
**вычислить** и сравнить автоматически, без разбора прозы. Такой случай есть и
|
||||||
|
решён отдельно — следующий пункт.
|
||||||
|
|
||||||
|
## 4. Сверка `docs/tools-map.md` с реестром — автоматизирована
|
||||||
|
|
||||||
|
Правило про числа не закрывает более опасный класс расхождений: документ не
|
||||||
|
устарел числом, а **перечисляет то, чего нет в коде**. В разделе «Реализовано»
|
||||||
|
карты лежали несуществующие id (`png-info`, `color-wheel-generator`,
|
||||||
|
`jpg-to-png`, `watermark-image-png`), которые агент принял бы за готовые
|
||||||
|
инструменты. Счёт при этом был верный — число такой класс не ловит.
|
||||||
|
|
||||||
|
Сверка множеств отвечает на вопрос однозначно и без allowlist, поэтому
|
||||||
|
автоматизирована: `web/src/lib/registry/tools-map-doc.test.ts` парсит раздел
|
||||||
|
«Реализовано» и сравнивает id с `TOOLS` в обе стороны (id без инструмента и
|
||||||
|
инструмент без записи), плюс ловит повтор id между буллетами. Формат строки
|
||||||
|
карты — всё до первого тире, id через `/` или `,` — закреплён в самом документе
|
||||||
|
и в тесте.
|
||||||
|
|
||||||
|
Чтобы сломанный парсер не выглядел как успех, у теста есть отдельная проверка
|
||||||
|
«разбор не пустой»: если формат карты снова поменяется, тест упадёт с сообщением
|
||||||
|
про парсер, а не молча перестанет что-либо проверять. Входит в обычный
|
||||||
|
`pnpm --dir web test`; новых гейтов и конфигов не добавляет.
|
||||||
|
|
||||||
|
## 5. Visual snapshots — не вводим
|
||||||
|
|
||||||
|
`toHaveScreenshot` сравнивает страницу с PNG-baseline из прошлого коммита, а не
|
||||||
|
с замыслом дизайна. При активно меняющемся визуальном языке это превращает
|
||||||
|
каждый дизайн-PR в регенерацию и ревью картинок, а baseline платформенные
|
||||||
|
(снимок с Windows не совпадёт на `ubuntu-latest`). Потребность закрывают
|
||||||
|
e2e-поведение, `lint:tokens` с token audit, i18n-гейт и ручной чеклист
|
||||||
|
`docs/checklist-manual-testing.md`.
|
||||||
|
|
||||||
|
Вернуться стоит, когда визуальный язык стабилизируется; тогда первым идёт `/kit`
|
||||||
|
в двух темах, а baseline рождаются на Linux.
|
||||||
|
|
||||||
|
## 6. Fail-режим для unused-token аудита — не вводим
|
||||||
|
|
||||||
|
Остаётся предупреждением: `verify` входит в deploy-workflow, поэтому стилевой
|
||||||
|
долг блокировал бы деплой сайта. Кроме того, аудит видит только ссылки
|
||||||
|
`var(--x)`, и чтение токена из JS дало бы ложное срабатывание. Правило «новый
|
||||||
|
токен — вместе с потребителем» живёт в `AGENTS.md`.
|
||||||
|
|
||||||
|
## 7. Per-project `expect.timeout` — не задаём
|
||||||
|
|
||||||
|
Firefox и WebKit отдают результат так же быстро, как Chromium: расхождения
|
||||||
|
объясняются памятью, а не скоростью движка. Таймаут под конкретную машину —
|
||||||
|
магическое число, которое придётся подстраивать на каждом раннере, поэтому
|
||||||
|
Playwright-конфиг остаётся с дефолтами, а проблема решается лимитом воркеров.
|
||||||
|
Практические правила — `docs/agent-workflow.md`, раздел 3.
|
||||||
|
|
||||||
|
## 8. Дублирование `verify` между workflow — сознательно
|
||||||
|
|
||||||
|
`Deploy to GitHub Pages` и `Quality` оба зовут `verify`: deploy проверяет
|
||||||
|
быстрый gate и собирает сайт, `Quality` прогоняет тот же `verify` и browser
|
||||||
|
matrix. По замеру (сентябрь 2026) `verify` — единицы минут, matrix — около трёх,
|
||||||
|
поэтому дублирование дешевле условных job-и, а matrix дёшево держать на каждом
|
||||||
|
PR. Актуальные длительности — в логе прогона. Ручной
|
||||||
|
`gh workflow run quality.yml` нужен только для перепрогона старого коммита.
|
||||||
|
|
||||||
|
## 9. Политика e2e в CI
|
||||||
|
|
||||||
|
Текущее состояние: `e2e` — non-blocking baseline, запускается на каждом пуше в
|
||||||
|
`main` и на каждом PR целиком (все проекты matrix). Ночного разделения нет: при
|
||||||
|
стоимости job около трёх минут оно не окупается, поэтому новые browser-critical
|
||||||
|
спецификации просто добавляются в `browserContractTests`.
|
||||||
|
|
||||||
|
`e2e` станет blocking, когда выполнятся три условия:
|
||||||
|
|
||||||
|
1. Условия matrix пересмотрены: решено, какие спецификации обязаны идти в
|
||||||
|
`browserContractTests` и `mobileTests`, нужен ли `png-fixtures` вне Chromium
|
||||||
|
и что делать с engine-специфичными расхождениями декодеров.
|
||||||
|
2. Накопились стабильные прогоны на раннере без падений по таймингу.
|
||||||
|
3. Вместе с `continue-on-error` снимается и сам trick с общим conclusion: иначе
|
||||||
|
зелёный прогон при красном `e2e` останется возможен.
|
||||||
|
|
||||||
|
Пока условия не выполнены, `e2e` остаётся non-blocking, а его статус читается по
|
||||||
|
отдельному job — см. следующий раздел.
|
||||||
|
|
||||||
|
## 10. Чтение результатов CI после пуша
|
||||||
|
|
||||||
|
Первый реальный прогон browser matrix происходит на runner, а не на локальной
|
||||||
|
машине: локально Firefox и WebKit проходят на 1 воркере, но состав, скорость и
|
||||||
|
память Linux-раннера отличаются. Поэтому после пуша разработчика:
|
||||||
|
|
||||||
|
1. дождаться завершения workflow и посмотреть, какие job вообще запустились;
|
||||||
|
2. смотреть статусы **отдельных job**, а не общий conclusion прогона: у `e2e`
|
||||||
|
стоит `continue-on-error: true`, поэтому прогон с упавшим e2e целиком
|
||||||
|
помечается как `success` — проверено на run `36225579363` (`E2E: failure` при
|
||||||
|
`conclusion: success`);
|
||||||
|
3. если `e2e` запускался и зелёный — сообщить фактические числа passed, failed,
|
||||||
|
skipped;
|
||||||
|
4. если `e2e` не запускался, сказать это прямо: matrix на раннере не проверена,
|
||||||
|
и её можно прогнать через `gh workflow run quality.yml`;
|
||||||
|
5. красный `e2e` только из-за ресурсов раннера фиксируется как ограничение
|
||||||
|
окружения: сначала проверяются один проект и один spec
|
||||||
|
(`docs/agent-workflow.md`, раздел 3), а не увеличиваются таймауты.
|
||||||
@@ -3,7 +3,13 @@
|
|||||||
> Статус: **draft** — на ревью.
|
> Статус: **draft** — на ревью.
|
||||||
>
|
>
|
||||||
> Источник: `docs/tools-audit.md`. Этот документ разбивает аудит на конкретные
|
> Источник: `docs/tools-audit.md`. Этот документ разбивает аудит на конкретные
|
||||||
> шаги, группирует по типу работы и фиксирует решения.
|
> шаги, группирует по типу работы и фиксирует решения. Порядок и правила
|
||||||
|
> проверки — `docs/quality-gates.md`; точки касания при добавлении вида поля —
|
||||||
|
> `docs/architecture.md`, раздел 4.
|
||||||
|
>
|
||||||
|
> **Ids инструментов в таблицах волны B сверены с реестром.** Названия из
|
||||||
|
> `tools-audit.md` вида `png-to-hsl` или `color-wheel-generator` в реестре нет
|
||||||
|
> (`hsl`, `color-wheel-png`).
|
||||||
|
|
||||||
## 0. Ключевое решение: select → buttons
|
## 0. Ключевое решение: select → buttons
|
||||||
|
|
||||||
@@ -116,7 +122,7 @@ buttons: <V extends string>(s: Omit<ButtonsSpec<V>, "kind">): Field<V> => ({
|
|||||||
Каждый шаг — один коммит, < 500 строк. Меняем `field.select()` →
|
Каждый шаг — один коммит, < 500 строк. Меняем `field.select()` →
|
||||||
`field.buttons()` в перечисленных ниже схемах.
|
`field.buttons()` в перечисленных ниже схемах.
|
||||||
|
|
||||||
### B1. geometry.ts (8 инструментов)
|
### B1. geometry.ts
|
||||||
|
|
||||||
| Инструмент | Поле | Опции |
|
| Инструмент | Поле | Опции |
|
||||||
| ----------------------- | -------- | ----------------------------------------- |
|
| ----------------------- | -------- | ----------------------------------------- |
|
||||||
@@ -132,7 +138,7 @@ buttons: <V extends string>(s: Omit<ButtonsSpec<V>, "kind">): Field<V> => ({
|
|||||||
`field.select()`, т.к. 9 кнопок в ряд не поместятся. Аналогично `position9` (уже
|
`field.select()`, т.к. 9 кнопок в ряд не поместятся. Аналогично `position9` (уже
|
||||||
отдельный компонент `PositionControl`).
|
отдельный компонент `PositionControl`).
|
||||||
|
|
||||||
### B2. color.ts (4 инструмента)
|
### B2. color.ts
|
||||||
|
|
||||||
| Инструмент | Поле | Опции |
|
| Инструмент | Поле | Опции |
|
||||||
| ------------------------ | --------- | ------------------------------------ |
|
| ------------------------ | --------- | ------------------------------------ |
|
||||||
@@ -144,7 +150,7 @@ buttons: <V extends string>(s: Omit<ButtonsSpec<V>, "kind">): Field<V> => ({
|
|||||||
**Исключение:** channel spaces (hsl/hsv/... component) — 3-6 опций, но это
|
**Исключение:** channel spaces (hsl/hsv/... component) — 3-6 опций, но это
|
||||||
динамические инструменты, оставить `select`.
|
динамические инструменты, оставить `select`.
|
||||||
|
|
||||||
### B3. analyze.ts (5 инструментов)
|
### B3. analyze.ts
|
||||||
|
|
||||||
| Инструмент | Поле | Опции |
|
| Инструмент | Поле | Опции |
|
||||||
| ------------------------- | ---- | ------------------ |
|
| ------------------------- | ---- | ------------------ |
|
||||||
@@ -156,13 +162,13 @@ buttons: <V extends string>(s: Omit<ButtonsSpec<V>, "kind">): Field<V> => ({
|
|||||||
|
|
||||||
Все используют общий `maskBaseFields` — правка в одном месте.
|
Все используют общий `maskBaseFields` — правка в одном месте.
|
||||||
|
|
||||||
### B4. filters.ts (1 инструмент)
|
### B4. filters.ts
|
||||||
|
|
||||||
| Инструмент | Поле | Опции |
|
| Инструмент | Поле | Опции |
|
||||||
| ------------- | ---- | ------------ |
|
| ------------- | ---- | ------------ |
|
||||||
| add-noise-png | mode | mono / color |
|
| add-noise-png | mode | mono / color |
|
||||||
|
|
||||||
### B5. generate.ts (12 инструментов)
|
### B5. generate.ts
|
||||||
|
|
||||||
| Инструмент | Поле | Опции |
|
| Инструмент | Поле | Опции |
|
||||||
| ------------------ | ------------ | --------------------- |
|
| ------------------ | ------------ | --------------------- |
|
||||||
@@ -183,10 +189,10 @@ buttons: <V extends string>(s: Omit<ButtonsSpec<V>, "kind">): Field<V> => ({
|
|||||||
|
|
||||||
### C1. Source-aware defaults для dimension-полей
|
### C1. Source-aware defaults для dimension-полей
|
||||||
|
|
||||||
Q5 добавил общий `defaultFromSource` resolver в `registry-schema`. Для
|
Source-aware defaults реализованы: общий resolver в `registry-schema`
|
||||||
`resize-png` и `crop-png` дефолт после загрузки равен размерам текущего source;
|
(`applySourceDefaults`). Для `resize-png` и `crop-png` дефолт после загрузки
|
||||||
до загрузки используется положительный fallback `1×1`. Один-sided `0` у resize
|
равен размерам текущего source; до загрузки используется положительный fallback
|
||||||
сохраняет режим auto.
|
`1×1`. Один-sided `0` у resize сохраняет режим auto.
|
||||||
|
|
||||||
### C2. symmetric-copy-png: "keep side" не работает
|
### C2. symmetric-copy-png: "keep side" не работает
|
||||||
|
|
||||||
@@ -201,9 +207,9 @@ Q5 добавил общий `defaultFromSource` resolver в `registry-schema`.
|
|||||||
|
|
||||||
### C3. Source-aware bounds (отдельный следующий шаг)
|
### C3. Source-aware bounds (отдельный следующий шаг)
|
||||||
|
|
||||||
Q5 реализовал только defaults dimension-полей через `SchemaContext` и
|
Реализованы только defaults dimension-полей через `SchemaContext` и
|
||||||
`defaultFromSource`. Динамические min/max для crop x/y, shift и resize остаются
|
`applySourceDefaults`. Динамические min/max для crop x/y, shift и resize
|
||||||
отдельной задачей: bounds не следует смешивать с default resolver.
|
остаются отдельной задачей: bounds не следует смешивать с default resolver.
|
||||||
|
|
||||||
**Что нужно спроектировать и завести:**
|
**Что нужно спроектировать и завести:**
|
||||||
|
|
||||||
@@ -232,8 +238,7 @@ Q5 реализовал только defaults dimension-полей через `S
|
|||||||
4. **Первые потребители:** crop (x/y → ±sourceWidth/sourceHeight), shift
|
4. **Первые потребители:** crop (x/y → ±sourceWidth/sourceHeight), shift
|
||||||
(offsetX/offsetY), resize (max → maxSide).
|
(offsetX/offsetY), resize (max → maxSide).
|
||||||
|
|
||||||
Это отдельный refactor после Q6/Q7; source-aware defaults из Q5 не заменяют
|
Это отдельный рефакторинг; source-aware defaults не заменяют bounds.
|
||||||
bounds.
|
|
||||||
|
|
||||||
### C4. verify-is-png: неверное название
|
### C4. verify-is-png: неверное название
|
||||||
|
|
||||||
|
|||||||
+72
-54
@@ -1,10 +1,12 @@
|
|||||||
# План: SEO + GEO — индексация и конкуренция в image tools
|
# План: SEO + GEO — индексация и конкуренция в image tools
|
||||||
|
|
||||||
> **Статус (2026-09-11):** план к выполнению. Переезд `preview/*` на реальные
|
> **Статус (2026-09-11; уточнено 2026-09-27):** план к выполнению. Переезд
|
||||||
> маршруты выполнен (в `noindex` сохраняются до запуска S1). Сейчас применять
|
> `preview/*` на реальные маршруты выполнен. **`noindex` на страницах
|
||||||
> можно domain-free часть S1. **Уточнено:** домен не был блокером C17 (см. §6).
|
> инструментов не выставляется** — снимать нечего, каталог открыт для
|
||||||
> Задачи S1 разбиты на domain-free (можно делать параллельно с покупкой домена)
|
> индексации; запрет индексации держится только на служебных путях
|
||||||
> и domain-required (после покупки и настройки домена на Vercel).
|
> (`robots.txt`). Домен не был блокером переезда (см. §6). Задачи S1 разбиты на
|
||||||
|
> domain-free (можно делать параллельно с покупкой домена) и domain-required
|
||||||
|
> (после покупки и настройки домена).
|
||||||
>
|
>
|
||||||
> **Цель:** каждая страница инструмента — индексируемый лендинг под конкретный
|
> **Цель:** каждая страница инструмента — индексируемый лендинг под конкретный
|
||||||
> интент («crop png», «make png transparent», «jpg to png»…), конкуренция с
|
> интент («crop png», «make png transparent», «jpg to png»…), конкуренция с
|
||||||
@@ -37,18 +39,21 @@
|
|||||||
каждого инструмента — с селектором PNG/JPG/WebP/BMP (+quality для lossy). Райз
|
каждого инструмента — с селектором PNG/JPG/WebP/BMP (+quality для lossy). Райз
|
||||||
«выбор формата/качества в Download» вытащен из backlog №7 в S1f: механика уже
|
«выбор формата/качества в Download» вытащен из backlog №7 в S1f: механика уже
|
||||||
есть (`ToolEntry.output`), нужна как продуктовая основа «image
|
есть (`ToolEntry.output`), нужна как продуктовая основа «image
|
||||||
tools»-позиционирования. Отдельные страницы-конвертеры — только для
|
tools»-позиционирования. В направлении «из формата в PNG» отдельных
|
||||||
подтверждённых вордстатом интентов.
|
инструментов нет (загрузка принимает любой формат) — см. §5.
|
||||||
- **Слаги инструментов `-png` сохраняются.** Они держат низкоконкурентные
|
- **Слаги инструментов `-png` сохраняются.** Они держат низкоконкурентные
|
||||||
png-интенты; широкие интенты («crop image online») переносятся теми же
|
png-интенты; широкие интенты («crop image online») переносятся теми же
|
||||||
страницами контентом: H1 «Crop image», текст «works with PNG, JPG, WebP».
|
страницами контентом: H1 «Crop image», текст «works with PNG, JPG, WebP».
|
||||||
URL-алиасы без `-png` не создаются (канонический беспорядок).
|
URL-алиасы без `-png` не создаются (канонический беспорядок).
|
||||||
- **Индексируемая локаль — EN** (базовая; конкурент англоязычный). RU-версия —
|
- **Индексируемая локаль — EN** (базовая; конкурент англоязычный). RU-версия —
|
||||||
отдельно и позже: префикс `/ru/`, prerender обеих локалей, hreflang
|
отдельно и позже: префикс `/ru/`, prerender обеих локалей, hreflang
|
||||||
(сознательно отложено в `archive/plan-i18n.md`). Пока язык переключается на
|
(сознательно отложено). Пока язык переключается на клиенте (localStorage) —
|
||||||
клиенте (localStorage) — для SEO не учитываем.
|
для SEO не учитываем.
|
||||||
- **Прод-домен без `BASE_PATH`.** Сборка с базой (GitHub Pages `/repo/`) ломает
|
- **Прод-домен без `BASE_PATH`.** Сборка с базой (GitHub Pages `/repo/`, см.
|
||||||
абсолютные URL, canonical и «корневость» страниц. Свой домен + корень.
|
`BASE_PATH` в `web/vite.config.ts` и `deploy.yml`) ломает абсолютные URL,
|
||||||
|
canonical и «корневость» страниц. Свой домен + корень. Текущий деплой — GitHub
|
||||||
|
Pages (`.github/workflows/deploy.yml`); перенос на свой домен выполняется
|
||||||
|
сменой origin в CI и `PUBLIC_SITE_ORIGIN`.
|
||||||
- **URL-схема инструментов:** короткие плоские адреса. После C17 страницы
|
- **URL-схема инструментов:** короткие плоские адреса. После C17 страницы
|
||||||
инструментов находятся в корне на `/tools/[id]`, каталог — на `/list-tools`;
|
инструментов находятся в корне на `/tools/[id]`, каталог — на `/list-tools`;
|
||||||
текущую схему не менять без отдельного SEO-плана: после индексации
|
текущую схему не менять без отдельного SEO-плана: после индексации
|
||||||
@@ -66,28 +71,28 @@
|
|||||||
- SvelteKit + `adapter-static`, `prerender = true` на весь сайт
|
- SvelteKit + `adapter-static`, `prerender = true` на весь сайт
|
||||||
(`web/src/routes/+layout.ts`) — весь контент в статичном HTML, ботам не нужен
|
(`web/src/routes/+layout.ts`) — весь контент в статичном HTML, ботам не нужен
|
||||||
JS. Это главный технический козырь.
|
JS. Это главный технический козырь.
|
||||||
- 122 инструмента в `web/src/lib/registry` с «SEO-шными» id (`crop-png`,
|
- Инструменты в `web/src/lib/registry` с «SEO-шными» id (`crop-png`,
|
||||||
`add-border-png`, …) — URL уже повторяют паттерн конкурента.
|
`add-border-png`, …) — URL уже повторяют паттерн конкурента. Актуальный
|
||||||
|
состав: `docs/tools-map.md`.
|
||||||
- На корневых страницах инструментов есть базовые `<title>` и `robots`; полный
|
- На корневых страницах инструментов есть базовые `<title>` и `robots`; полный
|
||||||
SEO-слой (canonical, OG, content и JSON-LD) остаётся S1/S2.
|
SEO-слой (canonical, OG, content и JSON-LD) остаётся S1/S2.
|
||||||
- Каталог со ссылками на все инструменты (внутренняя перелинковка) и карта
|
- Каталог со ссылками на все инструменты (внутренняя перелинковка) и карта
|
||||||
инструментов против конкурента: `docs/tools-map.md` (у них 311, из них 107 —
|
инструментов против конкурента: `docs/tools-map.md`.
|
||||||
нишевые клоны-серии Logo*/Icon*/Stamp* поверх обычных операций).
|
|
||||||
- EN-тексты в реестре + i18n-словари ru/en.
|
- EN-тексты в реестре + i18n-словари ru/en.
|
||||||
|
|
||||||
Чего нет (пробелы закрываются этапами ниже):
|
Чего нет (пробелы закрываются этапами ниже):
|
||||||
|
|
||||||
| Пробел | Где сейчас | Этап |
|
| Пробел | Где сейчас | Этап |
|
||||||
| ----------------------------------------------------- | ---------------------------------------- | ---- |
|
| -------------------------------------------------- | ----------------------------------- | ---- |
|
||||||
| `sitemap.xml` | нет | S1 |
|
| `sitemap.xml` | нет | S1 |
|
||||||
| `robots.txt` без `Sitemap:` | allow-all (`web/static/robots.txt`) | S1 |
|
| `robots.txt` без `Sitemap:` | allow-all (`web/static/robots.txt`) | S1 |
|
||||||
| `rel=canonical` | нет | S1 |
|
| `rel=canonical` | нет | S1 |
|
||||||
| OG/Twitter-теги, OG-картинки | нет | S1 |
|
| OG/Twitter-теги, OG-картинки | нет | S1 |
|
||||||
| `noindex, nofollow` на страницах инструментов preview | `web/src/routes/tools/[id]/+page.svelte` | S1 |
|
| Канонический домен в абсолютных URL | нет, деплой на GitHub Pages с базой | S1 |
|
||||||
| Контент страницы (H1-текст, how-to, FAQ, related) | только title/description | S2 |
|
| Контент страницы (H1-текст, how-to, FAQ, related) | только title/description | S2 |
|
||||||
| JSON-LD (SoftwareApplication, FAQPage, Breadcrumb) | нет | S3 |
|
| JSON-LD (SoftwareApplication, FAQPage, Breadcrumb) | нет | S3 |
|
||||||
| `llms.txt`, хабы категорий с текстом | нет | S3 |
|
| `llms.txt`, хабы категорий с текстом | нет | S3 |
|
||||||
| GSC + приватная аналитика | нет | S4 |
|
| GSC + приватная аналитика | нет | S4 |
|
||||||
|
|
||||||
## 2. Конкурент (onlinepngtools.com)
|
## 2. Конкурент (onlinepngtools.com)
|
||||||
|
|
||||||
@@ -109,8 +114,8 @@
|
|||||||
## 3. Стратегия
|
## 3. Стратегия
|
||||||
|
|
||||||
1. **Не лобовая, а ядро интентов.** 80% объёма поиска приносят ~30–50 запросов.
|
1. **Не лобовая, а ядро интентов.** 80% объёма поиска приносят ~30–50 запросов.
|
||||||
Покрытие инструментов у нас почти полное (122); приоритет — контент и
|
Покрытие инструментов у нас почти полное (список — `docs/tools-map.md`);
|
||||||
качество страниц ядра, затем хвосты.
|
приоритет — контент и качество страниц ядра, затем хвосты.
|
||||||
2. **Страница = лендинг, а не «пустая коробка».** Шаблон контента: H1 → 1–2
|
2. **Страница = лендинг, а не «пустая коробка».** Шаблон контента: H1 → 1–2
|
||||||
абзаца «что делает» → как использовать (шаги) → параметры → FAQ (3–5 вопросов
|
абзаца «что делает» → как использовать (шаги) → параметры → FAQ (3–5 вопросов
|
||||||
уровня People-Also-Ask) → related tools по категории.
|
уровня People-Also-Ask) → related tools по категории.
|
||||||
@@ -132,34 +137,35 @@
|
|||||||
|
|
||||||
## 4. Этапы
|
## 4. Этапы
|
||||||
|
|
||||||
### S1. Технический минимум (сразу после C17)
|
### S1. Технический минимум (закрывает блокер индексации)
|
||||||
|
|
||||||
Разбито на две группы: **domain-free** (делаем параллельно с покупкой домена) и
|
Разбито на две группы: **domain-free** (делаем параллельно с покупкой домена) и
|
||||||
**domain-required** (после покупки и DNS-настройки).
|
**domain-required** (после покупки и DNS-настройки).
|
||||||
|
|
||||||
#### S1-free: без домена (параллельно с quality-планом)
|
#### S1-free: без домена
|
||||||
|
|
||||||
- [ ] **[S1e]** OG-картинки per-tool: генерация PNG-карточек скриптом на
|
- [ ] **[S1e]** OG-картинки per-tool: генерация PNG-карточек скриптом на
|
||||||
существующем ядре (мы сами умеем рисовать PNG) — имя инструмента + бренд,
|
существующем ядре (мы сами умеем рисовать PNG) — имя инструмента + бренд,
|
||||||
файл `static/og/[id].png`, ссылка из `Seo.svelte`. Относительные пути
|
файл `static/og/[id].png`, ссылка из `Seo.svelte`. Относительные пути
|
||||||
(`/og/crop-png.png`) работают без домена; при деплое на Vercel с
|
(`/og/crop-png.png`) работают без домена; при деплое на собственном домене
|
||||||
собственным доменом OG-картинки будут корректными.
|
OG-картинки будут абсолютными и корректными.
|
||||||
- [ ] **[S1f]** Download-кнопка с селектором формата PNG/JPG/WebP/BMP (+quality
|
- [ ] **[S1f]** Download-кнопка с селектором формата PNG/JPG/WebP/BMP (+quality
|
||||||
для lossy) для всех инструментов — механика `ToolEntry.output` уже есть
|
для lossy) для всех инструментов — механика `ToolEntry.output` уже есть
|
||||||
(backlog №7, райз «выбор формата/качества»). Продуктовая основа «image
|
(backlog №7, райз «выбор формата/качества»). Продуктовая основа «image
|
||||||
tools»-позиционирования: без неё сайт остаётся PNG-сайтом. Вести
|
tools»-позиционирования: без неё сайт остаётся PNG-сайтом. Вести
|
||||||
параллельно с текущими quality-задачами.
|
параллельно с текущими quality-задачами.
|
||||||
- [ ] **[S1g]** Поведение конвертеров после S1f: не предлагать в меню форматов
|
- [ ] **[S1g]** Поведение конвертеров после S1f: не предлагать в меню форматов
|
||||||
формат исходника (disable) — чтобы «convert png to jpg» не мог сохранить в
|
формат исходника (disable) — чтобы `convert-png-to-jpg` не мог сохранить в
|
||||||
png. Существующие страницы не трогать: `jpg-to-png`, `png-to-jpg`,
|
png. Существующие страницы не трогать: `jpg-to-png`, `convert-png-to-jpg`,
|
||||||
`png-to-webp`, `png-to-bmp` остаются самостоятельными лендингами со своим
|
`convert-png-to-webp`, `png-to-bmp` остаются самостоятельными лендингами
|
||||||
контентом (§5).
|
со своим контентом (§5).
|
||||||
|
|
||||||
#### S1-domain: с доменом (после покупки + DNS на Vercel)
|
#### S1-domain: с доменом (после покупки + DNS)
|
||||||
|
|
||||||
- [ ] **[S1a]** Снять `noindex, no_follow` со страницы инструмента
|
- [ ] **[S1a]** Перевести прод на собственный домен: убрать `BASE_PATH` из
|
||||||
(`tools/[id]/+page.svelte`) — делается одновременно с S1d (canonical)
|
`deploy.yml`, задать `PUBLIC_SITE_ORIGIN`, перенаправить
|
||||||
чтобы Google сразу проиндексировал с правильным доменом.
|
`https://<юзер>/<repo>/…` на корневые URL (301). Делается одновременно с
|
||||||
|
S1d (canonical), чтобы Google проиндексировал сразу с правильным доменом.
|
||||||
- [ ] **[S1b]** Скрипт `web/scripts/gen-sitemap.mjs`: генерирует `sitemap.xml` в
|
- [ ] **[S1b]** Скрипт `web/scripts/gen-sitemap.mjs`: генерирует `sitemap.xml` в
|
||||||
`build/` из `TOOLS` реестра (`web/src/lib/registry`) + статических
|
`build/` из `TOOLS` реестра (`web/src/lib/registry`) + статических
|
||||||
страниц. Вызов из `pnpm build` после `vite build` (adapter-static пишет в
|
страниц. Вызов из `pnpm build` после `vite build` (adapter-static пишет в
|
||||||
@@ -181,8 +187,8 @@
|
|||||||
- [ ] **[S2a]** Схема данных: `seo`-блок per-tool (`longDescription`,
|
- [ ] **[S2a]** Схема данных: `seo`-блок per-tool (`longDescription`,
|
||||||
`howTo: string[]`, `faq: {q, a}[]`) — в i18n-словарях (ru/en), не в коде
|
`howTo: string[]`, `faq: {q, a}[]`) — в i18n-словарях (ru/en), не в коде
|
||||||
реестра. Черновики — генерацией из схемы параметров инструмента, вычитка
|
реестра. Черновики — генерацией из схемы параметров инструмента, вычитка
|
||||||
вручную пачками по категориям (самый трудоёмкий этап: ~122 × 2 локали; RU
|
вручную пачками по категориям (самый трудоёмкий этап: объём растёт вместе
|
||||||
— отложить до решения о `/ru/`).
|
с числом инструментов × 2 локали; RU — отложить до решения о `/ru/`).
|
||||||
- [ ] **[S2b]** Компонент контента страницы инструмента (kit): H1, lead, how-to
|
- [ ] **[S2b]** Компонент контента страницы инструмента (kit): H1, lead, how-to
|
||||||
(`ol`), описание параметров (из схемы), FAQ, related tools (перелинковка
|
(`ol`), описание параметров (из схемы), FAQ, related tools (перелинковка
|
||||||
внутри категории + 2–3 «соседних» интента).
|
внутри категории + 2–3 «соседних» интента).
|
||||||
@@ -237,15 +243,28 @@
|
|||||||
|
|
||||||
## 5. Приоритизация инструментов (первая волна контента)
|
## 5. Приоритизация инструментов (первая волна контента)
|
||||||
|
|
||||||
Источник — `docs/tools-map.md` (122 реализовано; у конкурента 311, из них 107 —
|
Источник — `docs/tools-map.md` (наш состав и замер по конкуренту — §2). Ids ниже
|
||||||
клоны-серии). Первая волна — топ-интенты, где конкуренты ранжируются, а у нас
|
— **фактические id реестра**; первая волна — топ-интенты, где конкуренты
|
||||||
уже есть инструмент и он не хуже:
|
ранжируются, а у нас уже есть инструмент и он не хуже:
|
||||||
|
|
||||||
- прозрачность/фон: create-transparent-png, remove-background, change-opacity
|
- прозрачность/фон: create-empty-png, remove-background-png, change-png-opacity,
|
||||||
- конвертация: jpg-to-png, png-to-jpg, webp-to-png, svg-to-png, png-to-base64
|
set-alpha-channel-png
|
||||||
- геометрия: crop, resize, rotate, circle-crop, round-corners, flip
|
- конвертация: convert-png-to-jpg, convert-png-to-webp, png-to-bmp, svg-to-png,
|
||||||
- цвет/фильтры: grayscale, invert, pixelate, add-text, watermark
|
png-to-base64
|
||||||
- генерация: gradient, empty-canvas
|
- геометрия: crop-png, resize-png, rotate-png, round-corners-png, flip-png,
|
||||||
|
change-aspect-ratio-png
|
||||||
|
- цвет/фильтры: grayscale-png, invert-colors-png, pixelate-png, add-text-png,
|
||||||
|
watermark-tile-png
|
||||||
|
- генерация: linear-gradient-png, create-empty-png
|
||||||
|
|
||||||
|
> **Открытый вопрос по интентам «из формата в PNG».** У конкурента есть
|
||||||
|
> `jpg-to-png`, `webp-to-png`, `gif-to-png` как отдельные страницы; у нас таких
|
||||||
|
> инструментов нет и создавать их не хочется — загрузка файла уже принимает
|
||||||
|
> любой формат, а конвертер «во что-то» закрывает обратное направление. Это
|
||||||
|
> high-volume интент без посадочной страницы. Решение (страница-обёртка на
|
||||||
|
> существующий инструмент / приём в H1 и тексте / ничего) требует вордстата и
|
||||||
|
> должно быть принято до старта S2, иначе первая волна контента закроет не ядро
|
||||||
|
> запросов.
|
||||||
|
|
||||||
Новая волна форматов (после S1f): jpg↔webp и другие конвертации «формат →
|
Новая волна форматов (после S1f): jpg↔webp и другие конвертации «формат →
|
||||||
формат» — только по подтверждённому вордстату; базовые кейсы закрывает селектор
|
формат» — только по подтверждённому вордстату; базовые кейсы закрывает селектор
|
||||||
@@ -325,7 +344,7 @@ with PNG, JPG, WebP» в тексте + селектор формата в Downl
|
|||||||
ключи localStorage (`easy-png-tools:*` — или мигрировать отдельной
|
ключи localStorage (`easy-png-tools:*` — или мигрировать отдельной
|
||||||
мелочью), историю docs/archive.
|
мелочью), историю docs/archive.
|
||||||
- [ ] **[R2]** Купить выбранный домен (кандидаты и пробы — в локальном плане) и
|
- [ ] **[R2]** Купить выбранный домен (кандидаты и пробы — в локальном плане) и
|
||||||
настроить деплой на Vercel без `BASE_PATH` (§0). Домен нужен до S1-domain:
|
перевести прод на него без `BASE_PATH` (§0). Домен нужен до S1-domain:
|
||||||
canonical и sitemap генерятся от `PUBLIC_SITE_ORIGIN`.
|
canonical и sitemap генерятся от `PUBLIC_SITE_ORIGIN`.
|
||||||
- **Порядок:** R1+R2 можно делать параллельно с текущими quality-задачами (не
|
- **Порядок:** R1+R2 можно делать параллельно с текущими quality-задачами (не
|
||||||
блокируют). R2 (домен) — блокер только для S1-domain (canonical + sitemap +
|
блокируют). R2 (домен) — блокер только для S1-domain (canonical + sitemap +
|
||||||
@@ -334,8 +353,8 @@ with PNG, JPG, WebP» в тексте + селектор формата в Downl
|
|||||||
|
|
||||||
## 7. Что осознанно НЕ делаем
|
## 7. Что осознанно НЕ делаем
|
||||||
|
|
||||||
- Клонирование 311 URL конкурента (низкокачественные дубли + нет ресурсов на
|
- Клонирование всего каталога конкурента (низкокачественные дубли + нет ресурсов
|
||||||
контент) — вместо этого ядро + обоснованные хвосты.
|
на контент) — вместо этого ядро + обоснованные хвосты.
|
||||||
- RU-локаль в индексе до решения о структуре `/ru/` + hreflang.
|
- RU-локаль в индексе до решения о структуре `/ru/` + hreflang.
|
||||||
- GA/реклама/трекинг — противоречит позиционированию.
|
- GA/реклама/трекинг — противоречит позиционированию.
|
||||||
- Дорвеи, программные зеркала, покупные ссылки.
|
- Дорвеи, программные зеркала, покупные ссылки.
|
||||||
@@ -352,6 +371,5 @@ with PNG, JPG, WebP» в тексте + селектор формата в Downl
|
|||||||
- `docs/backlog.md` — пункты SEO/GEO в общем бэклоге.
|
- `docs/backlog.md` — пункты SEO/GEO в общем бэклоге.
|
||||||
- `docs/roadmap.md` — фазы 2–8 (ядро TS→Rust/wasm, CLI); фазы 9–10 — API и PWA.
|
- `docs/roadmap.md` — фазы 2–8 (ядро TS→Rust/wasm, CLI); фазы 9–10 — API и PWA.
|
||||||
- `docs/plan-platform.md` — планы API и PWA (общее ядро, оффлайн-режим).
|
- `docs/plan-platform.md` — планы API и PWA (общее ядро, оффлайн-режим).
|
||||||
- `docs/archive/plan-i18n.md` — отложенная многоязычная схема (будущий `/ru/`).
|
|
||||||
- `docs/plan-domain.local.md` — план переименования/домена; **в git не входит**
|
- `docs/plan-domain.local.md` — план переименования/домена; **в git не входит**
|
||||||
(`.gitignore`), лежит только локально.
|
(`.gitignore`), лежит только локально.
|
||||||
|
|||||||
+55
-169
@@ -1,12 +1,12 @@
|
|||||||
# Quality gates
|
# Quality gates
|
||||||
|
|
||||||
> Статус: **active**. Каноническое описание быстрых проверок репозитория.
|
> Статус: **active**. Каноническое описание быстрых проверок: команды, coverage,
|
||||||
> Краткие обязательные команды остаются в `AGENTS.md`.
|
> warnings, CI. Почему гейт такой, а не иначе, — `docs/decisions.md`.
|
||||||
|
|
||||||
|
Команды выполняются из корня репозитория.
|
||||||
|
|
||||||
## Каноническая команда
|
## Каноническая команда
|
||||||
|
|
||||||
Запускать из корня репозитория:
|
|
||||||
|
|
||||||
```text
|
```text
|
||||||
pnpm verify
|
pnpm verify
|
||||||
```
|
```
|
||||||
@@ -28,25 +28,40 @@ pnpm verify
|
|||||||
## Coverage
|
## Coverage
|
||||||
|
|
||||||
`pnpm --dir web test:coverage` считает покрытие только по `src/lib` и падает
|
`pnpm --dir web test:coverage` считает покрытие только по `src/lib` и падает
|
||||||
ниже порога. Scope и список исключений — в `web/vitest.config.ts`, обоснование —
|
ниже порога. Scope и список исключений — в `web/vitest.config.ts`.
|
||||||
в `docs/testing-strategy.md`.
|
|
||||||
|
|
||||||
| Метрика | Порог | Baseline |
|
| Метрика | Порог |
|
||||||
| ---------- | ----- | -------- |
|
| ---------- | ----- |
|
||||||
| statements | 90 | 91.77% |
|
| statements | 90 |
|
||||||
| branches | 82 | 83.3% |
|
| branches | 82 |
|
||||||
| functions | 72 | 74.31% |
|
| functions | 72 |
|
||||||
| lines | 91 | 92.53% |
|
| lines | 91 |
|
||||||
|
|
||||||
Порог — нижняя граница от текущего baseline, а не оценка качества: он блокирует
|
Порог — нижняя граница, а не оценка качества: он блокирует регрессию и не
|
||||||
регрессию и не требует сначала закрывать весь долг. Локальный замер занимает ~30
|
требует сначала закрывать весь долг. Актуальные проценты даёт
|
||||||
с, поэтому отдельного прогона в `verify` нет — `test:coverage` заменяет `test`.
|
`pnpm --dir web test:coverage`; здесь они не дублируются, чтобы число не
|
||||||
|
устарело молча (`AGENTS.md`). Локальный замер занимает единицы секунд, поэтому
|
||||||
|
отдельного прогона в `verify` нет — `test:coverage` заменяет `test`.
|
||||||
|
|
||||||
|
Исключения из scope и их причины:
|
||||||
|
|
||||||
|
| Исключение | Причина |
|
||||||
|
| ------------------------------------------- | ------------------------------------------- |
|
||||||
|
| `**/*.test.ts`, `**/test-helpers.ts` | сами тесты |
|
||||||
|
| `**/*.svelte.ts` | runes, не работают в node |
|
||||||
|
| `executor/executor.worker.ts` | entry worker, исполняется только в браузере |
|
||||||
|
| `components/define.ts` | type-only, нет исполняемого кода |
|
||||||
|
| `core/domText.ts`, `core/io.ts` | canvas, `createImageBitmap`, нужен браузер |
|
||||||
|
| `i18n/en.ts`, `i18n/ru.ts`, `tool-icons.ts` | данные, а не логика |
|
||||||
|
|
||||||
|
`registry/*` из scope НЕ исключены: низкое покрытие там означает непокрытую
|
||||||
|
логику, а не ограничение окружения. Включение браузерных модулей в scope —
|
||||||
|
отдельное решение, а не молчаливое расширение списка.
|
||||||
|
|
||||||
## E2E
|
## E2E
|
||||||
|
|
||||||
E2E не входит в `verify`, потому что Playwright поднимает build и статический
|
E2E не входит в `verify`, потому что Playwright поднимает build и статический
|
||||||
сервер. Locator, fixture, i18n и `test.fixme`-политика описаны в
|
сервер:
|
||||||
`docs/testing-strategy.md`:
|
|
||||||
|
|
||||||
```text
|
```text
|
||||||
pnpm --dir web test:e2e
|
pnpm --dir web test:e2e
|
||||||
@@ -55,104 +70,41 @@ pnpm --dir web test:e2e
|
|||||||
Запускать его обязательно для изменений маршрутов, загрузки файла, результата,
|
Запускать его обязательно для изменений маршрутов, загрузки файла, результата,
|
||||||
download, темы или локализации. Изменения runtime fixtures/загрузки файла
|
download, темы или локализации. Изменения runtime fixtures/загрузки файла
|
||||||
проверяются отдельным `web/e2e/png-fixtures.spec.ts`; ручные бинарники из
|
проверяются отдельным `web/e2e/png-fixtures.spec.ts`; ручные бинарники из
|
||||||
`tests/fixtures/` не входят в `verify`. В текущем CI это отдельный non-blocking
|
`tests/fixtures/` не входят в `verify`.
|
||||||
baseline; после закрытия известных ошибок его можно сделать blocking.
|
|
||||||
|
|
||||||
Локально прогон идёт с `workers: 1` и заметно медленнее: это осознанный размен,
|
Локально прогон идёт с `workers: 1` и заметно медленнее. Правила нагрузки на
|
||||||
экономит память и оставляет компьютер отзывчивым. Playwright не умеет
|
машину пользователя и диагностика флаков — `docs/agent-workflow.md`, раздел 3;
|
||||||
per-project лимит воркеров, а browser matrix упирается в память, а не CPU.
|
матрица проектов и ожидаемое поведение декодеров — `docs/testing-strategy.md`,
|
||||||
Тяжёлые прогоны запускаются по команде пользователя — по одному, без фонового
|
раздел 7; статус e2e в CI и порядок чтения прогона — `docs/decisions.md`,
|
||||||
запуска и циклов повторов; детали в `docs/agent-workflow.md`.
|
разделы 9–10.
|
||||||
|
|
||||||
## Быстрые подмножества
|
## Быстрые подмножества
|
||||||
|
|
||||||
| Изменение | Команда |
|
Матрица «изменение → минимальный набор проверок»:
|
||||||
| ---------------------------- | ------------------------------------------------------- |
|
|
||||||
| Только Markdown | `pnpm check:docs` |
|
| Изменение | Команда |
|
||||||
| TypeScript/Svelte | `pnpm --dir web check` и `pnpm --dir web test` |
|
| ---------------------------------------------------- | ------------------------------------------------------- |
|
||||||
| Lint-правило или lint-конфиг | `pnpm --dir web test:rules` и `pnpm --dir web lint:all` |
|
| Только Markdown | `pnpm check:docs` |
|
||||||
| Пользовательский поток | `pnpm --dir web test:e2e` |
|
| TypeScript/Svelte | `pnpm --dir web check` и `pnpm --dir web test` |
|
||||||
| Fixtures/загрузка файла | `pnpm --dir web test:e2e` |
|
| Lint-правило или lint-конфиг | `pnpm --dir web test:rules` и `pnpm --dir web lint:all` |
|
||||||
| Полный локальный gate | `pnpm verify` |
|
| Маршрут, загрузка, результат, download, тема, локаль | `pnpm --dir web test:e2e` |
|
||||||
|
| Перед ревью или merge | `pnpm verify` |
|
||||||
|
|
||||||
## Errors и warnings
|
## Errors и warnings
|
||||||
|
|
||||||
- `errors` завершают соответствующий инструмент ненулевым кодом.
|
- `errors` завершают соответствующий инструмент ненулевым кодом.
|
||||||
- После Q2 ESLint запускается с `--max-warnings=0`: новые ESLint warnings
|
- ESLint запускается с `--max-warnings=0`: новые ESLint warnings блокируют gate.
|
||||||
блокируют gate.
|
- Token audit оставляет unused-token сообщение неблокирующим: новый токен
|
||||||
- Token audit оставляет unused-token сообщение неблокирующим: после Q7a пыль
|
появляется вместе с потребителем, но стиль не блокирует деплой (решение —
|
||||||
вычищена, решение о fail-режиме зафиксировано в Q7d — новый токен появляется
|
`docs/decisions.md`, раздел 6).
|
||||||
вместе с потребителем, но стиль не блокирует деплой.
|
|
||||||
- Резолв `var()` в `app.css` — ошибка: несуществующий токен молча роняет
|
- Резолв `var()` в `app.css` — ошибка: несуществующий токен молча роняет
|
||||||
объявление, поэтому этот check failing, в отличие от unused.
|
объявление, поэтому этот check failing, в отличие от unused.
|
||||||
- `lint:all` сейчас чист: 0 errors, 0 i18n warnings, 0 unused tokens.
|
|
||||||
- `dict-consistency` использует base-locale fallback для registry metadata и
|
- `dict-consistency` использует base-locale fallback для registry metadata и
|
||||||
pattern-based policy для временно неиспользуемых `tools.*.params`; это не
|
pattern-based policy для временно неиспользуемых `tools.*.params`; это не
|
||||||
молчаливый `allowPaths`-список.
|
молчаливый `allowPaths`-список.
|
||||||
|
|
||||||
## Решения, принятые без автоматизации
|
`lint:all` может завершиться успешно при warnings — это не означает чистый
|
||||||
|
baseline. Агент показывает число errors, warnings и token warnings отдельно.
|
||||||
Здесь лежат решения «что сознательно НЕ вводим», чтобы не пересматривать их
|
|
||||||
заново через полгода. Для каждого — причина и то, что пересмотрело бы решение. В
|
|
||||||
`docs/backlog.md` такие пункты не дублируются: backlog живёт задачами, а не
|
|
||||||
историей решений.
|
|
||||||
|
|
||||||
### Детектор дублирования разметки — не вводим
|
|
||||||
|
|
||||||
Ни свой AST-правило, ни готовая зависимость (jscpd, `eslint-plugin-sonarjs`).
|
|
||||||
|
|
||||||
- Измерение по 65 `.svelte` в `components/**` и `routes/**`: при пороге 5
|
|
||||||
нормализованных строк — одна осмысленная пара, при пороге 3 строки — 17 пар и
|
|
||||||
все они шум (`</label>`, `{/each}`, `{#if children}`). Семейство
|
|
||||||
schema-контролов делит максимум 4 строки — это законное сходство by design.
|
|
||||||
- Причина: построчное сравнение Svelte-разметки принципиально ненадёжно —
|
|
||||||
одинаковый контейнер с разным содержимым даёт ложное срабатывание, а намеренно
|
|
||||||
общий класс — ложную тишину. Чтобы не ловить шум, порог пришлось бы поднять до
|
|
||||||
12+ строк, и тогда правило срабатывает ровно один раз.
|
|
||||||
- Контроль остаётся на ревью (расхождение видно в диффе) и на поведенческих
|
|
||||||
e2e-проверках; извлечение общей разметки закреплено конвенцией в `AGENTS.md`.
|
|
||||||
- Пересмотреть, если появится реально «уехавшая» копипаста в компонентах или
|
|
||||||
библиотека разрастётся на уровень композиций страниц.
|
|
||||||
|
|
||||||
### Линтер согласованности комментариев — не вводим
|
|
||||||
|
|
||||||
Описания инструментов различаются осознанно, и ловить это текстовым правилом
|
|
||||||
дороже, чем смотреть на ревью. Действенная часть выполнена вручную: все
|
|
||||||
`TODO`/`FIXME` перенесены в backlog, незарегистрированных пометок в коде нет;
|
|
||||||
новые запрещены правилом в `AGENTS.md`.
|
|
||||||
|
|
||||||
### Visual snapshots — не вводим
|
|
||||||
|
|
||||||
`toHaveScreenshot` сравнивает страницу с PNG-baseline из прошлого коммита. При
|
|
||||||
активно меняющемся визуальном языке это превращает каждый дизайн-PR в
|
|
||||||
перегенерацию и ревью картинок, а baseline платформенные (снимок с Windows не
|
|
||||||
совпадёт на `ubuntu-latest`). Потребность закрывают e2e-поведение, `lint:tokens`
|
|
||||||
с token audit, i18n-гейт и ручной чеклист. Вернуться стоит, когда визуальный
|
|
||||||
язык стабилизируется; тогда первым идёт `/kit` в двух темах, а baseline
|
|
||||||
рождаются на Linux.
|
|
||||||
|
|
||||||
### Fail-режим для unused-token аудита — не вводим
|
|
||||||
|
|
||||||
Остаётся предупреждением: `verify` входит в deploy-workflow, поэтому стилевой
|
|
||||||
долг блокировал бы деплой сайта. Кроме того, аудит видит только ссылки
|
|
||||||
`var(--x)`, и чтение токена из JS дало бы ложное срабатывание. Правило «новый
|
|
||||||
токен — вместе с потребителем» живёт в `AGENTS.md`.
|
|
||||||
|
|
||||||
### Per-project `expect.timeout` — не задаём
|
|
||||||
|
|
||||||
Firefox и WebKit отдают результат так же быстро, как Chromium: расхождения
|
|
||||||
объясняются памятью, а не скоростью движка. Таймаут под конкретную машину —
|
|
||||||
магическое число, которое придётся подстраивать на каждом раннере, поэтому
|
|
||||||
`lint`/Playwright-конфиг остаётся с дефолтами, а проблема решается лимитом
|
|
||||||
воркеров.
|
|
||||||
|
|
||||||
### Coverage: браузерные модули вне scope
|
|
||||||
|
|
||||||
`src/lib/core/domText.ts` и `src/lib/core/io.ts` (canvas, `createImageBitmap`)
|
|
||||||
исключены из coverage-гейта: в node-Vitest они не исполнимы, и их включение
|
|
||||||
превращало бы метрику в шум. `registry/*` из scope НЕ исключены — низкое
|
|
||||||
покрытие там означает непокрытую логику, а не ограничение окружения. Список
|
|
||||||
исключений с причинами — в `docs/testing-strategy.md`.
|
|
||||||
|
|
||||||
## CI
|
## CI
|
||||||
|
|
||||||
@@ -165,76 +117,10 @@ Firefox и WebKit отдают результат так же быстро, ка
|
|||||||
`.github/workflows/deploy.yml` перед production build запускает тот же
|
`.github/workflows/deploy.yml` перед production build запускает тот же
|
||||||
`pnpm verify`, поэтому deploy не должен проходить при ошибке быстрых проверок.
|
`pnpm verify`, поэтому deploy не должен проходить при ошибке быстрых проверок.
|
||||||
|
|
||||||
### Что и когда запускается
|
|
||||||
|
|
||||||
| Workflow | Триггер | Jobs |
|
| Workflow | Триггер | Jobs |
|
||||||
| ------------------------ | --------------------------------------- | --------------------------- |
|
| ------------------------ | --------------------------------------- | --------------------------- |
|
||||||
| `Deploy to GitHub Pages` | push в `main`, dispatch | `verify`, `build`, `deploy` |
|
| `Deploy to GitHub Pages` | push в `main`, dispatch | `verify`, `build`, `deploy` |
|
||||||
| `Quality` | push в `main`, `pull_request`, dispatch | `verify`, `e2e` |
|
| `Quality` | push в `main`, `pull_request`, dispatch | `verify`, `e2e` |
|
||||||
|
|
||||||
Пуш в `main` запускает оба workflow: deploy проверяет быстрый gate и собирает
|
Результат после пуша читается **по отдельным job** — процедура в
|
||||||
сайт, `Quality` прогоняет тот же `verify` и browser matrix. Дублирование
|
`docs/decisions.md`, раздел 10.
|
||||||
`verify` (~1 мин) сознательное: matrix на каждый пуш стоит ~3 мин, и условные
|
|
||||||
job-и ради минуты не окупаются. Browser matrix проверяется на каждом пуше, а
|
|
||||||
ручной `gh workflow run quality.yml` нужен только для перепрогона старого
|
|
||||||
коммита.
|
|
||||||
|
|
||||||
### Проверка результатов CI после пуша
|
|
||||||
|
|
||||||
Первый реальный прогон browser matrix происходит на runner, а не на локальной
|
|
||||||
машине: локально Firefox и WebKit проходят на 1 воркере, но состав, скорость и
|
|
||||||
память Linux-раннера отличаются. Поэтому после пуша разработчика:
|
|
||||||
|
|
||||||
1. дождаться завершения workflow и посмотреть, какие job вообще запустились;
|
|
||||||
2. смотреть статусы **отдельных job**, а не общий conclusion прогона: у `e2e`
|
|
||||||
стоит `continue-on-error: true`, поэтому прогон с упавшим e2e целиком
|
|
||||||
помечается как `success` — проверено на run `36225579363` (`E2E: failure` при
|
|
||||||
`conclusion: success`);
|
|
||||||
3. если `e2e` запускался и зелёный — сообщить фактические числа passed, failed,
|
|
||||||
skipped;
|
|
||||||
4. если `e2e` не запускался, сказать это прямо: matrix на раннере не проверена,
|
|
||||||
и её можно прогнать через `gh workflow run quality.yml`;
|
|
||||||
5. красный `e2e` только из-за ресурсов раннера фиксируется как ограничение
|
|
||||||
окружения: сначала проверяется один проект и один spec, а не увеличиваются
|
|
||||||
таймауты под конкретную машину.
|
|
||||||
|
|
||||||
### Измеренная стоимость прогона
|
|
||||||
|
|
||||||
Замер на ручном запуске `quality.yml` (276 e2e-тестов, 4 проекта,
|
|
||||||
ubuntu-latest):
|
|
||||||
|
|
||||||
| Этап | Время |
|
|
||||||
| ----------------- | -------- |
|
|
||||||
| `verify` job | ~1 мин |
|
|
||||||
| `e2e` job целиком | ~3.1 мин |
|
|
||||||
| из них сами тесты | ~2.2 мин |
|
|
||||||
| весь run | ~4.2 мин |
|
|
||||||
|
|
||||||
Browser matrix стоит около трёх минут, поэтому её дешёво держать на каждом PR.
|
|
||||||
Полный прогон локально на 1 воркере — заметно дольше (~7 мин), и именно поэтому
|
|
||||||
локальные правила нагрузки описаны в `docs/agent-workflow.md`.
|
|
||||||
|
|
||||||
## Политика e2e в CI
|
|
||||||
|
|
||||||
Текущее состояние: `e2e` — non-blocking baseline, запускается на каждом пуше в
|
|
||||||
`main` и на каждом PR целиком (все четыре проекта matrix). Ночного разделения
|
|
||||||
нет: при стоимости job около трёх минут оно не окупается, поэтому новые
|
|
||||||
browser-critical спецификации просто добавляются в `browserContractTests`.
|
|
||||||
|
|
||||||
`e2e` станет blocking, когда выполнятся три условия:
|
|
||||||
|
|
||||||
1. Условия matrix пересмотрены: решено, какие спецификации обязаны идти в
|
|
||||||
`browserContractTests` и `mobileTests`, нужен ли `png-fixtures` вне Chromium
|
|
||||||
и что делать с engine-специфичными расхождениями декодеров.
|
|
||||||
2. Накопились стабильные прогоны на раннере без падений по таймингу.
|
|
||||||
3. Вместе с `continue-on-error` снимается и сам trick с общим conclusion: иначе
|
|
||||||
зелёный прогон при красном `e2e` останется возможен.
|
|
||||||
|
|
||||||
Пока условия не выполнены, `e2e` остаётся non-blocking, а его статус читается по
|
|
||||||
отдельному job.
|
|
||||||
|
|
||||||
## Локальная диагностика
|
|
||||||
|
|
||||||
При медленном полном прогоне сначала запускай минимальную команду из таблицы, а
|
|
||||||
перед ревью или merge — `pnpm verify`. После каждого шага плана результат команд
|
|
||||||
и текущий baseline фиксируются в отчёте агента.
|
|
||||||
|
|||||||
+52
-59
@@ -1,58 +1,53 @@
|
|||||||
# План разработки: easy-png-tools
|
# План развития: easy-png-tools
|
||||||
|
|
||||||
> **Статус:** Фаза 1 (полноценный TS-сайт) в основном выполнена — сайт живёт,
|
> **Статус (2026-09):** Фаза 1 (полноценный TS-сайт) в основном выполнена — сайт
|
||||||
> каталог переведён в типизированный `registry` (122 текущих инструмента), новый
|
> живёт, каталог переведён в типизированный `registry`, новый дизайн стал
|
||||||
> дизайн стал единственным production UI на корневых маршрутах; UI-v1 удалён в
|
> единственным production UI на корневых маршрутах, UI-v1 удалён. Tech debt не
|
||||||
> Q3. Tech debt не закрыт полностью; текущий baseline и состав quality-проверок
|
> закрыт полностью; состав quality-проверок — `docs/quality-gates.md`,
|
||||||
> — в `docs/README.md` и `docs/quality-gates.md`. Следующий этап: SEO S1-free
|
> архитектура кода — `docs/architecture.md`, список инструментов —
|
||||||
> (`plan-seo.md`) + баги и продуктовые задачи из `docs/backlog.md`. Домен нужен
|
> `docs/tools-map.md`. Текущий фокус: SEO S1-free (`docs/plan-seo.md`) +
|
||||||
> для SEO (`plan-seo.md`, S1-domain). Фазы 2–8 — будущие. Фазы 9–10 спланированы
|
> продуктовые задачи из `docs/backlog.md`. Домен нужен для SEO (`plan-seo.md`,
|
||||||
> в `docs/plan-platform.md`.
|
> S1-domain). Фазы 2–8 — будущие; фазы 9–10 спланированы в
|
||||||
|
> `docs/plan-platform.md`.
|
||||||
|
>
|
||||||
|
> **Нумерация фаз этого документа не совпадает с волнами разработки** (`Q*`/`C*`
|
||||||
|
> в других документах) и не используется как идентификатор задач: ссылаться на
|
||||||
|
> фазу, а не на номер волны.
|
||||||
|
|
||||||
## 0. Решения
|
## 0. Решения
|
||||||
|
|
||||||
- **Набор PNG-утилит, всё в браузере** (SvelteKit + `adapter-static`), данные не
|
- **Набор утилит для изображений, всё в браузере** (SvelteKit +
|
||||||
покидают машину.
|
`adapter-static`), данные не покидают машину.
|
||||||
- **Первый и главный приоритет — полностью рабочий TS-сайт**: максимум простых
|
- **Первый и главный приоритет — полностью рабочий сайт**: максимум простых
|
||||||
инструментов уровня EASY, UX/UI, пайплайны (несколько инструментов подряд),
|
инструментов, UX/UI, пайплайны (несколько инструментов подряд), описания
|
||||||
описания инструментов. Сайтом можно пользоваться уже после этой фазы.
|
инструментов.
|
||||||
- **Никакого wasm/Rust на этом этапе.** TS-реализация на canvas + `ImageData` —
|
- **Никакого wasm/Rust на этом этапе.** TS-реализация на canvas + `ImageData` —
|
||||||
эталон алгоритмов, из которого позже генерируются эталонные файлы для проверки
|
эталон алгоритмов, из которого позже генерируются эталонные файлы для проверки
|
||||||
Rust-порта. Rust-ядро, WASM и CLI — отдельные фазы позже, когда сайт живёт.
|
Rust-порта. Rust-ядро, WASM и CLI — отдельные фазы позже, когда сайт живёт.
|
||||||
- **Пайплайн = JSON-список шагов** `{tool, params}`. Формат закладывается сейчас
|
- **Пайплайн = JSON-список шагов** `{tool, params}`. Формат закладывается сейчас
|
||||||
и потом станет общим для браузера, wasm и CLI.
|
и потом станет общим для браузера, wasm и CLI.
|
||||||
- **Реестр инструментов** (id, title, description, схема параметров, `run`) —
|
- **Реестр инструментов** (`web/src/lib/registry/`: id, title, description,
|
||||||
источник истины: из него генерируются страницы, формы, пайплайны. Позже —
|
category, schema, `run`) — источник истины: из него генерируются страницы,
|
||||||
общий формат для Rust-парсера.
|
формы, пайплайны. Позже — общий формат для Rust-парсера.
|
||||||
- Валидация будущего Rust-порта — эталонные файлы, сгенерированные из
|
- Валидация будущего Rust-порта — эталонные файлы, сгенерированные из
|
||||||
TS-реализации. Сравнение: **бит-в-бит** для чистых пиксельных операций,
|
TS-реализации. Сравнение: **бит-в-бит** для чистых пиксельных операций,
|
||||||
**perceptual diff** для canvas-зависимых.
|
**perceptual diff** для canvas-зависимых.
|
||||||
- **Единая точка исполнения инструментов.** Страницы вызывают операции только
|
- **Единая точка исполнения инструментов.** Страницы вызывают операции только
|
||||||
через `web/src/lib/tools/executor.ts::executeStep` — нигде напрямую
|
через `web/src/lib/executor/` — нигде напрямую `tool.run`. Контракт уже
|
||||||
`tool.run`. Контракт уже асинхронный (`Promise<PixelImage>`), поэтому:
|
асинхронный, поэтому:
|
||||||
- когда появятся тяжёлые MEDIUM-операции (свёртки, квантование), внутренности
|
- когда появятся тяжёлые MEDIUM-операции (свёртки, квантование), внутренности
|
||||||
`executeStep` переезжают в Web Worker — воркер импортирует тот же чистый
|
исполнителя переезжают в Web Worker — воркер импортирует тот же чистый core
|
||||||
core как TS-фолбэк, алгоритмы не дублируются, вызывающий код не меняется;
|
как TS-фолбэк, алгоритмы не дублируются, вызывающий код не меняется;
|
||||||
- в WASM-фазе тот же воркер принимает wasm-модуль и диспетчеризует по `toolId`
|
- в WASM-фазе тот же воркер принимает wasm-модуль и диспетчеризует по `toolId`
|
||||||
— свитч A/B из фазы 5 сводится к замене реализации внутри исполнителя;
|
— свитч A/B из фазы 5 сводится к замене реализации внутри исполнителя;
|
||||||
- до появления MEDIUM-операций воркер не вводится: EASY-операции быстры и не
|
- до появления MEDIUM-операций воркер не вводится: EASY-операции быстры и не
|
||||||
блокируют UI.
|
блокируют UI.
|
||||||
|
|
||||||
## 1. Структура репозитория (этап TS)
|
## 1. Где что лежит
|
||||||
|
|
||||||
```txt
|
Актуальная карта модулей, поток данных и рецепт добавления инструмента —
|
||||||
easy-png-tools/
|
`docs/architecture.md`. Список реализованных инструментов — `docs/tools-map.md`,
|
||||||
web/ # SvelteKit (adapter-static)
|
открытые продуктовые задачи — `docs/backlog.md`.
|
||||||
src/lib/core/ # TS-ядро: Image (ImageData) + операции (без DOM)
|
|
||||||
color.ts alpha.ts geometry.ts format.ts text.ts analyze.ts
|
|
||||||
src/lib/registry.ts # реестр инструментов: id, title, description, params, run
|
|
||||||
src/lib/pipeline.ts # пайплайн: шаги, применение, localStorage, экспорт/импорт JSON
|
|
||||||
src/lib/components/ # DropZone, Preview, ParamForm (из схемы), Download, PipelineSteps
|
|
||||||
src/routes/ # маршруты-утилиты (генерируются из реестра) + /workspace
|
|
||||||
docs/
|
|
||||||
core/ # (позже) Rust-крейт
|
|
||||||
tests/ # (позже) эталонные файлы и harness
|
|
||||||
```
|
|
||||||
|
|
||||||
## 2. Фазы
|
## 2. Фазы
|
||||||
|
|
||||||
@@ -64,22 +59,21 @@ clamp-семантики (в TS это делает `Uint8ClampedArray` сам).
|
|||||||
1.1 **Каркас и UX/UI.** SvelteKit + static adapter, дизайн-система (цвета,
|
1.1 **Каркас и UX/UI.** SvelteKit + static adapter, дизайн-система (цвета,
|
||||||
типографика, компоненты), общий лейаут, шапка с навигацией по категориям.
|
типографика, компоненты), общий лейаут, шапка с навигацией по категориям.
|
||||||
|
|
||||||
1.2 **Ядро.** Тип `Image` (обёртка над `ImageData`), загрузка/декодирование
|
1.2 **Ядро.** Операции над `ImageData` (загрузка/декодирование файла,
|
||||||
файла, кодирование и скачивание, библиотека операций
|
кодирование и скачивание, библиотека операций
|
||||||
(color/alpha/geometry/format/text/analyze). Всё без DOM-зависимостей внутри
|
color/alpha/geometry/format/text/analyze). Всё без DOM-зависимостей внутри
|
||||||
`core`.
|
`core`.
|
||||||
|
|
||||||
1.3 **Реестр инструментов.** Одна запись =
|
1.3 **Реестр инструментов.** Одна запись =
|
||||||
`{ id, title, description, category, params: ParamDef[], run }`. Универсальный
|
`{ id, title, description, category, schema, run }`. Универсальный рендерер:
|
||||||
рендерер: страница инструмента и форма параметров строятся из записи реестра
|
страница инструмента и форма параметров строятся из записи реестра
|
||||||
автоматически; маршруты-утилиты генерируются из реестра на билде. Новый
|
автоматически; маршруты-утилиты генерируются из реестра на билде. Новый
|
||||||
инструмент = новая запись + функция `run`.
|
инструмент = новая запись + функция `run` (рецепт — `docs/architecture.md` §8).
|
||||||
|
|
||||||
1.4 **Пайплайн-workspace** (`/workspace`): загрузил изображение → список шагов
|
1.4 **Пайплайн-workspace**: загрузил изображение → список шагов (инструмент +
|
||||||
(инструмент + его параметры) → последовательное применение с превью каждого шага
|
его параметры) → последовательное применение с превью каждого шага → скачивание
|
||||||
→ скачивание финального результата. Добавление/удаление/перестановка шагов.
|
финального результата. Добавление/удаление шагов. Сохранение пайплайнов в
|
||||||
Сохранение пайплайнов в localStorage, экспорт/импорт JSON. Общие компоненты:
|
localStorage, экспорт/импорт JSON.
|
||||||
DropZone, Preview, ParamForm, Download, PipelineSteps.
|
|
||||||
|
|
||||||
1.5 **Массовая реализация EASY-инструментов** по категориям:
|
1.5 **Массовая реализация EASY-инструментов** по категориям:
|
||||||
|
|
||||||
@@ -105,17 +99,19 @@ wasm.
|
|||||||
- **Checkpoint:** в `tests/reference/<op>/` лежат `input-*.png` +
|
- **Checkpoint:** в `tests/reference/<op>/` лежат `input-*.png` +
|
||||||
`expected-*.png`.
|
`expected-*.png`.
|
||||||
|
|
||||||
### Фаза 3 — Playwright-тесты
|
### Фаза 3 — Эталонные файлы и UI-регресс
|
||||||
|
|
||||||
- `tools/gen-references.ts`: грузит страницу, гоняет TS-эталон, сохраняет
|
> UI-регресс на Playwright уже существует и закрывает фазу 3 частично
|
||||||
|
> (`web/e2e/`, browser matrix). Недостающая часть фазы — именно эталоны.
|
||||||
|
|
||||||
|
- Скрипт генерации эталонов: грузит страницу, гоняет TS-реализацию, сохраняет
|
||||||
`expected-*.png`.
|
`expected-*.png`.
|
||||||
- Регресс: изменение TS-ядра ловится тестами.
|
|
||||||
- **Checkpoint:** регресс-тесты падают при изменении выхода эталона.
|
- **Checkpoint:** регресс-тесты падают при изменении выхода эталона.
|
||||||
|
|
||||||
### Фаза 4 — Rust-ядро
|
### Фаза 4 — Rust-ядро
|
||||||
|
|
||||||
- Порт 1:1 чистых операций (первым делом те, что покрыты эталонными файлами) с
|
- Порт 1:1 чистых операций (первым делом те, что покрыты эталонными файлами) с
|
||||||
`Image` ↔ TS `ImageData`.
|
`PixelImage` ↔ TS `ImageData`.
|
||||||
- `clamp_to_u8` с семантикой `ToUint8Clamp` (round-half-to-even, ≥255 → 255) +
|
- `clamp_to_u8` с семантикой `ToUint8Clamp` (round-half-to-even, ≥255 → 255) +
|
||||||
юнит-тесты на краях.
|
юнит-тесты на краях.
|
||||||
- `cargo test` читает эталонные файлы напрямую, **без браузера**.
|
- `cargo test` читает эталонные файлы напрямую, **без браузера**.
|
||||||
@@ -179,8 +175,8 @@ Manifest + service worker, оффлайн-режим («работает без
|
|||||||
|
|
||||||
- **Реестр разрастается, страницы дублируются** — один универсальный рендерер
|
- **Реестр разрастается, страницы дублируются** — один универсальный рендерер
|
||||||
страниц/форм из записей реестра; инструмент = данные + `run`.
|
страниц/форм из записей реестра; инструмент = данные + `run`.
|
||||||
- **Семантика пайплайна** — зафиксировать: каждый шаг применяется к результату
|
- **Семантика пайплайна** — зафиксирована: каждый шаг применяется к результату
|
||||||
предыдущего; единый тип `Image` на всём пути; схема параметров на шаге.
|
предыдущего; единый тип `PixelImage` на всём пути; схема параметров на шаге.
|
||||||
- **Canvas-операции не бит-в-бит** — класс perceptual закладывается на Фазе 2–3,
|
- **Canvas-операции не бит-в-бит** — класс perceptual закладывается на Фазе 2–3,
|
||||||
а не в конце.
|
а не в конце.
|
||||||
- **WASM + Vite** — спайк на Фазе 5 до массовой интеграции (риск отложен
|
- **WASM + Vite** — спайк на Фазе 5 до массовой интеграции (риск отложен
|
||||||
@@ -192,10 +188,7 @@ Manifest + service worker, оффлайн-режим («работает без
|
|||||||
|
|
||||||
## 5. Команды верификации
|
## 5. Команды верификации
|
||||||
|
|
||||||
```bash
|
Текущие команды и матрица «изменение → проверки» — `docs/quality-gates.md`
|
||||||
pnpm dev # разработка (Фаза 1+, обёртка `pnpm --dir web dev`)
|
(`pnpm verify` — быстрый gate текущего этапа). Для будущих фаз добавляются
|
||||||
pnpm build # статический экспорт (обёртка `pnpm --dir web build`)
|
`cargo test` (Rust-ядро против эталонных файлов, фаза 4+) и
|
||||||
pnpm --dir web exec playwright test # эталоны + UI-регресс (Фаза 3+)
|
`harness compare tests/reference` (сквозная сверка native + wasm, фаза 7).
|
||||||
cargo test # Rust-ядро против эталонных файлов (Фаза 4+)
|
|
||||||
harness compare tests/reference # сквозная сверка native + wasm (Фаза 7)
|
|
||||||
```
|
|
||||||
|
|||||||
+14
-60
@@ -68,24 +68,8 @@ browser-контракт: critical CRC и truncated отклоняются дв
|
|||||||
`src/**/*.test.ts`; DOM, hydration и пользовательские потоки — в Playwright.
|
`src/**/*.test.ts`; DOM, hydration и пользовательские потоки — в Playwright.
|
||||||
Component-тесты не дублируют e2e-контракт.
|
Component-тесты не дублируют e2e-контракт.
|
||||||
|
|
||||||
### Что попадает в coverage
|
Состав и причины исключений из coverage-гейта, а также пороги — в
|
||||||
|
`docs/quality-gates.md`; здесь они не дублируются.
|
||||||
Coverage-гейт считает только `src/lib` и только то, что node-Vitest способен
|
|
||||||
исполнить. Исключения в `web/vitest.config.ts` и их причины:
|
|
||||||
|
|
||||||
| Исключение | Причина |
|
|
||||||
| ------------------------------------------- | ------------------------------------------- |
|
|
||||||
| `**/*.test.ts`, `**/test-helpers.ts` | сами тесты |
|
|
||||||
| `**/*.svelte.ts` | runes, не работают в node |
|
|
||||||
| `executor/executor.worker.ts` | entry worker, исполняется только в браузере |
|
|
||||||
| `components/define.ts` | type-only, нет исполняемого кода |
|
|
||||||
| `core/domText.ts`, `core/io.ts` | canvas, `createImageBitmap`, нужен браузер |
|
|
||||||
| `i18n/en.ts`, `i18n/ru.ts`, `tool-icons.ts` | данные, а не логика |
|
|
||||||
|
|
||||||
`registry/*` в scope остаётся: низкое покрытие там — это непокрытая логика, а не
|
|
||||||
следствие окружения. Исключение браузерных модулей означает, что переход
|
|
||||||
браузерного кода в scope — отдельное решение, связанное с DOM-тестами или visual
|
|
||||||
snapshots, а не молчаливое расширение списка исключений.
|
|
||||||
|
|
||||||
## 5. Helpers и `test.fixme`
|
## 5. Helpers и `test.fixme`
|
||||||
|
|
||||||
@@ -99,7 +83,7 @@ text — `renderText` + результат, generator — auto-run без upload
|
|||||||
|
|
||||||
`test.fixme` допускается только для зарегистрированной проблемы в backlog или
|
`test.fixme` допускается только для зарегистрированной проблемы в backlog или
|
||||||
checklist. В комментарии указывается ссылка на неё. После исправления тест и
|
checklist. В комментарии указывается ссылка на неё. После исправления тест и
|
||||||
ссылка обновляются вместе; в текущем Q6e baseline активных fixme нет.
|
ссылка обновляются вместе.
|
||||||
|
|
||||||
Если тест сравнивает два состояния одного элемента, он ждёт изменения значения
|
Если тест сравнивает два состояния одного элемента, он ждёт изменения значения
|
||||||
(`not.toHaveText`, `not.toHaveAttribute`), а не его видимости. Пересчёт идёт
|
(`not.toHaveText`, `not.toHaveAttribute`), а не его видимости. Пересчёт идёт
|
||||||
@@ -108,21 +92,14 @@ checklist. В комментарии указывается ссылка на н
|
|||||||
|
|
||||||
## 6. Проверки
|
## 6. Проверки
|
||||||
|
|
||||||
Для изменений e2e выполняются:
|
Матрица «изменение → минимальный набор проверок» и состав `pnpm verify` — в
|
||||||
|
`docs/quality-gates.md`. Для изменений e2e дополнительно уместны
|
||||||
```text
|
`pnpm --dir web check` и `pnpm --dir web format:check`.
|
||||||
pnpm --dir web check
|
|
||||||
pnpm --dir web test:rules
|
|
||||||
pnpm --dir web lint:all
|
|
||||||
pnpm --dir web format:check
|
|
||||||
pnpm check:docs
|
|
||||||
pnpm --dir web test:e2e
|
|
||||||
pnpm verify
|
|
||||||
```
|
|
||||||
|
|
||||||
`verify` не включает Playwright: E2E остаётся отдельным тяжёлым gate. В CI
|
`verify` не включает Playwright: E2E остаётся отдельным тяжёлым gate. В CI
|
||||||
non-blocking E2E не считается заменой локальному smoke; перед ревью фиксируются
|
non-blocking E2E не считается заменой локальному smoke; перед ревью фиксируются
|
||||||
число passed, skipped и известные fixme.
|
число passed, skipped и известные fixme. Статус e2e в CI — `docs/decisions.md`,
|
||||||
|
раздел 9.
|
||||||
|
|
||||||
## 7. Browser matrix
|
## 7. Browser matrix
|
||||||
|
|
||||||
@@ -148,32 +125,9 @@ non-blocking E2E не считается заменой локальному smo
|
|||||||
| `idat-cut.png` | Chromium |
|
| `idat-cut.png` | Chromium |
|
||||||
| ancillary CRC, tail, palette, 16-bit | ни один движок |
|
| ancillary CRC, tail, palette, 16-bit | ни один движок |
|
||||||
|
|
||||||
- Firefox и WebKit работают с теми же таймаутами, что и Chromium: движок не
|
- `workers` — 1 локально и 2 в CI: Playwright не умеет per-project лимит
|
||||||
причина расхождений, поэтому per-project `expect.timeout` не задаётся.
|
воркеров, а browser matrix ограничена памятью, а не CPU. Правила нагрузки на
|
||||||
- `workers` — 1 локально и 2 в CI. Playwright не умеет per-project лимит
|
машину и порядок диагностики флаков — `docs/agent-workflow.md`, раздел 3;
|
||||||
воркеров, а browser matrix ограничена памятью, а не CPU: при высокой
|
обоснование отсутствия per-project таймаутов — `docs/decisions.md`, раздел 7.
|
||||||
параллельности браузеры на машине с 16 GB RAM не успевают поднять worker, и
|
- Visual snapshots и per-project таймауты сознательно не используются —
|
||||||
тесты падают без ошибок. Локальный matrix поэтому медленный, но не нагружает
|
`docs/decisions.md`, разделы 5 и 7.
|
||||||
компьютер.
|
|
||||||
- Локальный baseline: 276 тестов, 276 passed, 0 skipped, 0 failed.
|
|
||||||
- Coverage ограничена `src/lib` и не включает браузерные модули — см. раздел 4.
|
|
||||||
|
|
||||||
## 8. Visual snapshots
|
|
||||||
|
|
||||||
Сознательно не используются. `toHaveScreenshot` сравнивает страницу с
|
|
||||||
PNG-baseline из прошлого коммита, а не с замыслом дизайна, поэтому:
|
|
||||||
|
|
||||||
- визуальный язык в активной работе означает перегенерацию и ревью картинок в
|
|
||||||
каждом дизайн-изменяющем PR;
|
|
||||||
- baseline привязан к платформе (ОС, шрифты, рендеринг): снимок с Windows не
|
|
||||||
совпадёт на `ubuntu-latest`, и baseline должны рождаться на Linux;
|
|
||||||
- четыре проекта matrix плюс тема и viewport дают нелинейный рост числа файлов.
|
|
||||||
|
|
||||||
Потребность закрывают e2e-поведение, `lint:tokens` с token audit, i18n-гейт и
|
|
||||||
ручной чеклист `docs/checklist-manual-testing.md`; «элемент визуально наехал»
|
|
||||||
ловится чеклистом и ревью.
|
|
||||||
|
|
||||||
Возвращаться к snapshots стоит, когда визуальный язык стабилизируется, изменения
|
|
||||||
станут редкими, а чеклист перестанет справляться. Тогда первым идёт `/kit` в
|
|
||||||
двух темах, а baseline генерируются на Linux (docker-образ Playwright или
|
|
||||||
отдельный CI-job).
|
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
# Аудит использования - UX\UI
|
# Аудит использования - UX\UI
|
||||||
|
|
||||||
|
> Статус: **active**. Сырые наблюдения по инструментам и полям — вход для
|
||||||
|
> `docs/plan-fields-audit-fixes.md`, который разбивает их на волны правок, и для
|
||||||
|
> `docs/backlog.md`. Здесь фиксируется, **что замечено**, а не порядок работы.
|
||||||
|
> Ids инструментов сверять с `web/src/lib/registry/` — исторические названия
|
||||||
|
> (`webp-to-png`, `color-wheel-generator`, `png-info`) в реестре отсутствуют.
|
||||||
|
|
||||||
## везде
|
## везде
|
||||||
|
|
||||||
- возможность переключать пиксели\проценты
|
- возможность переключать пиксели\проценты
|
||||||
|
|||||||
+80
-73
@@ -1,40 +1,53 @@
|
|||||||
# Карта инструментов: реализовано · добавить · идеи
|
# Карта инструментов: реализовано · добавить · идеи
|
||||||
|
|
||||||
> Живой документ для сверки с каталогом. Формат записи:
|
> Статус: **active**. Живой документ для сверки с каталогом.
|
||||||
> `id / название — параметры через запятую`. Источник сравнения:
|
>
|
||||||
> onlinepngtools.com (311 уникальных; 107 из них — нишевые клоны серий
|
> **Источник правды по id — реестр** `web/src/lib/registry/`. Этот список —
|
||||||
> Logo*/Icon*/Stamp*/Signature* поверх обычных операций).
|
> навигационный: сверять перед использованием. Формат записи:
|
||||||
|
> `id — параметры через запятую`. Сравнение с каталогом источника (объём,
|
||||||
|
> нишевые клоны серий Logo*/Icon*/Stamp*/Signature*) — замер в
|
||||||
|
> `docs/plan-seo.md`, §2; здесь он не дублируется.
|
||||||
|
>
|
||||||
|
> Количество инструментов здесь не фиксируется — оно выводится из раздела 1 и из
|
||||||
|
> `TOOLS` в `registry/index.ts`, и дублировать его цифрой значит заставлять себя
|
||||||
|
> обновлять документ при каждом добавлении (`AGENTS.md`).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 1. Реализовано
|
## 1. Реализовано
|
||||||
|
|
||||||
### Конвертация
|
Ids приведены к фактическим в `web/src/lib/registry/`.
|
||||||
|
|
||||||
- jpg-to-png — без параметров
|
**Формат строки (проверяется тестом
|
||||||
- webp-to-png — без параметров
|
`web/src/lib/registry/tools-map-doc.test.ts`):** в буллите всё **до первого
|
||||||
- gif-to-png — первый кадр
|
тире** `—` — это id инструментов через `/` или `,`; всё после тире — свободное
|
||||||
- bmp-to-png — без параметров
|
описание параметров и не разбирается. Второй id нельзя оставлять после тире,
|
||||||
- ico-to-png — выбор размера кадра
|
иначе он не попадёт в разбор и инструмент будет считаться неописанным.
|
||||||
|
|
||||||
|
### Конвертация (`convert.ts`)
|
||||||
|
|
||||||
|
Отдельных «из формата в PNG» инструментов **нет**: входной формат выбирается
|
||||||
|
загрузкой файла, любой инструмент принимает PNG/JPG/WebP/GIF/BMP. Все конвертеры
|
||||||
|
исходят из PNG.
|
||||||
|
|
||||||
|
- svg-to-png — width результата (текстовый вход: SVG-код)
|
||||||
- png-to-bmp — 24-бит, фон вместо альфы
|
- png-to-bmp — 24-бит, фон вместо альфы
|
||||||
- convert-png-to-jpg — background, quality
|
- convert-png-to-jpg — background, quality
|
||||||
- convert-png-to-webp — quality
|
- convert-png-to-webp — quality
|
||||||
- svg-to-png — width результата
|
|
||||||
- png-to-base64 / base64-to-png — строка
|
- png-to-base64 / base64-to-png — строка
|
||||||
- png-to-data-uri / data-uri-to-png — строка
|
- png-to-data-uri / data-uri-to-png — строка
|
||||||
- png-to-hex — rrggbbaa по строкам / hex-to-png — tokens + width
|
- png-to-hex / hex-to-png — rrggbbaa по строкам / tokens + width
|
||||||
- png-to-bytes — десятичные RGBA-байты по строкам / bytes-to-png — tokens +
|
- png-to-bytes / bytes-to-png — десятичные RGBA-байты по строкам / tokens +
|
||||||
width
|
width
|
||||||
- png-to-rgb-values — rgba(r,g,b,a) по пикселям / rgb-values-to-png — числа +
|
- png-to-rgb-values / rgb-values-to-png — rgba(r,g,b,a) по пикселям / числа +
|
||||||
width
|
width
|
||||||
- svg-to-png — width результата
|
|
||||||
- verify-is-png — текстовый источник (base64/data-uri), вердикт по сигнатуре
|
|
||||||
|
|
||||||
### Прозрачность
|
### Прозрачность (`alpha.ts`)
|
||||||
|
|
||||||
- change-png-opacity — percent
|
- change-png-opacity — percent
|
||||||
- set-alpha-channel-png — percent
|
- set-alpha-channel-png — percent
|
||||||
- remove-alpha-channel-png — без параметров
|
- remove-alpha-channel-png — без параметров (фон `#ffffff` захардкожен, см.
|
||||||
|
`docs/plan-fields-audit-fixes.md` D1)
|
||||||
- extract-alpha-mask-png — без параметров
|
- extract-alpha-mask-png — без параметров
|
||||||
- invert-alpha-png — без параметров
|
- invert-alpha-png — без параметров
|
||||||
- remove-background-png — color, tolerance, outerOnly, smooth
|
- remove-background-png — color, tolerance, outerOnly, smooth
|
||||||
@@ -45,8 +58,6 @@
|
|||||||
- harden-alpha-png — threshold
|
- harden-alpha-png — threshold
|
||||||
- feather-edges-png — radius (размытие только альфы)
|
- feather-edges-png — radius (размытие только альфы)
|
||||||
- clean-edges-png — radius (defringe: RGB от ближайшего непрозрачного)
|
- clean-edges-png — radius (defringe: RGB от ближайшего непрозрачного)
|
||||||
- watermark-image-png — вторая картинка-знак (загружается на странице), scale,
|
|
||||||
opacity, position, margin
|
|
||||||
- despeckle-alpha-png / close-holes-png — radius
|
- despeckle-alpha-png / close-holes-png — radius
|
||||||
- center-by-alpha-png — без параметров
|
- center-by-alpha-png — без параметров
|
||||||
- round-corners-png — radius
|
- round-corners-png — radius
|
||||||
@@ -55,7 +66,7 @@
|
|||||||
- star-mask-png — points, innerRadius, size, rotation, offsetX, offsetY
|
- star-mask-png — points, innerRadius, size, rotation, offsetX, offsetY
|
||||||
- wavy-mask-png — size, amplitude, waves, phase, offsetX, offsetY
|
- wavy-mask-png — size, amplitude, waves, phase, offsetX, offsetY
|
||||||
|
|
||||||
### Цвет
|
### Цвет (`color.ts`)
|
||||||
|
|
||||||
- grayscale-png / invert-colors-png / sepia-png / auto-contrast-png — без
|
- grayscale-png / invert-colors-png / sepia-png / auto-contrast-png — без
|
||||||
параметров
|
параметров
|
||||||
@@ -69,23 +80,17 @@
|
|||||||
- temperature-png — percent
|
- temperature-png — percent
|
||||||
- gamma-png — value
|
- gamma-png — value
|
||||||
- tint-png — color, strength
|
- tint-png — color, strength
|
||||||
|
|
||||||
### Квантование и палитры
|
|
||||||
|
|
||||||
- quantize-png — colors (k, median-cut)
|
- quantize-png — colors (k, median-cut)
|
||||||
- decrease-color-count-png — maxColors (пресеты 2…256)
|
- decrease-color-count-png — maxColors (пресеты 2…256)
|
||||||
- custom-palette-png — colors (hex через запятую, ближайший цвет)
|
- custom-palette-png — colors (hex через запятую, ближайший цвет)
|
||||||
- dithering-png — colors (k), pattern (Floyd–Steinberg / Bayer 4×4)
|
- dithering-png — colors (k), pattern (Floyd–Steinberg / Bayer 4×4)
|
||||||
|
|
||||||
### Разложение каналов
|
### Разложение каналов (`color.ts`)
|
||||||
|
|
||||||
- png-to-hsl / png-to-hsv / png-to-hsi — component (h/s/l и т.п.), display (gray
|
- png-to-hsl, png-to-hsv, png-to-hsi, png-to-cmyk, png-to-ycbcr, png-to-lab —
|
||||||
| space-as-rgb)
|
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
|
|
||||||
|
|
||||||
### Геометрия
|
### Геометрия (`geometry.ts`)
|
||||||
|
|
||||||
- resize-png — width/height (размеры source по умолчанию; 0=авто для одной
|
- resize-png — width/height (размеры source по умолчанию; 0=авто для одной
|
||||||
стороны), keepAspect
|
стороны), keepAspect
|
||||||
@@ -107,7 +112,7 @@
|
|||||||
- swap-orientation-png — target (portrait/landscape)
|
- swap-orientation-png — target (portrait/landscape)
|
||||||
- symmetric-copy-png — axis, keepSide
|
- symmetric-copy-png — axis, keepSide
|
||||||
|
|
||||||
### Фильтры
|
### Фильтры (`filters.ts`)
|
||||||
|
|
||||||
- blur-png — radius
|
- blur-png — radius
|
||||||
- sharpen-png — strength
|
- sharpen-png — strength
|
||||||
@@ -118,12 +123,14 @@
|
|||||||
- add-noise-png — amount, mode (mono/color), seed
|
- add-noise-png — amount, mode (mono/color), seed
|
||||||
- silhouette-png — color, threshold
|
- silhouette-png — color, threshold
|
||||||
|
|
||||||
### Анализ
|
### Анализ и вердикты (`analyze.ts`)
|
||||||
|
|
||||||
- png-info — размеры, альфа, число цветов
|
- png-file-size — без параметров (DOM-инструмент: кодирует в PNG и отдаёт
|
||||||
|
размер)
|
||||||
- png-is-transparent / png-is-grayscale / png-orientation — текстовый вердикт
|
- png-is-transparent / png-is-grayscale / png-orientation — текстовый вердикт
|
||||||
|
- verify-is-png — текстовый источник (base64/data-uri), вердикт по сигнатуре
|
||||||
|
|
||||||
### Маски по свойствам пикселей
|
### Маски по свойствам пикселей (`analyze.ts`)
|
||||||
|
|
||||||
- show-transparent-png — color, opacity (подсветка прозрачных/полупрозрачных)
|
- show-transparent-png — color, opacity (подсветка прозрачных/полупрозрачных)
|
||||||
- show-grayscale-pixels-png — tolerance, mode (binary/highlight),
|
- show-grayscale-pixels-png — tolerance, mode (binary/highlight),
|
||||||
@@ -136,21 +143,29 @@
|
|||||||
- extract-color-from-png — color, tolerance (оставить близкие, остальное
|
- extract-color-from-png — color, tolerance (оставить близкие, остальное
|
||||||
прозрачным)
|
прозрачным)
|
||||||
|
|
||||||
### Генерация
|
### Генерация (`generate.ts`)
|
||||||
|
|
||||||
- create-empty-png — width, height, transparent, color
|
- create-empty-png — width, height, transparent, color
|
||||||
- single-color-png — width, height, color
|
- single-color-png — width, height, color
|
||||||
- random-noise-png — width, height, seed
|
- random-noise-png — width, height, seed
|
||||||
- linear-gradient-png — width, height, fromColor, toColor, direction
|
- linear-gradient-png — width, height, fromColor, toColor, direction
|
||||||
- text-to-png — text, fontSize, font, bold, color, transparentBg,
|
|
||||||
backgroundColor, padding
|
|
||||||
- emoji-to-png — emoji, size
|
|
||||||
- placeholder-png — width, height, backgroundColor, color, showText
|
|
||||||
- color-spectrum-png — width, height, direction, saturation, lightness
|
- color-spectrum-png — width, height, direction, saturation, lightness
|
||||||
- random-colors-png — width, height, blockSize, seed
|
- random-colors-png — width, height, blockSize, seed
|
||||||
- draw-grid-png — width, height, cols, rows, lineWidth, color, transparentBg
|
- draw-grid-png — width, height, cols, rows, lineWidth, color, transparentBg
|
||||||
|
- placeholder-png — width, height, backgroundColor, color, showText
|
||||||
|
- blend-two-png — pair (from/to), width
|
||||||
|
- step-colors-png — pair (from/to), steps, width
|
||||||
|
- emoji-to-png — emoji, size
|
||||||
|
- text-to-png — text, fontSize, font, bold, color, transparentBg,
|
||||||
|
backgroundColor, padding
|
||||||
|
- color-wheel-png — size, lightness
|
||||||
|
- complementary-png / triadic-png / tetradic-png / analogous-png /
|
||||||
|
monochromatic-png / shades-png — общая база палитры: baseColor, width, layout;
|
||||||
|
`shades-png` дополнительно count, depth
|
||||||
|
- mix-colors-png — colors, width
|
||||||
|
- sort-colors-png — colors, порядок (hue/brightness/saturation), width
|
||||||
|
|
||||||
### Текст
|
### Текст (`text.ts`)
|
||||||
|
|
||||||
- add-text-png — text, fontSize, color, font, bold, position (3×3), margin,
|
- add-text-png — text, fontSize, color, font, bold, position (3×3), margin,
|
||||||
plate, plateColor, plateOpacity
|
plate, plateColor, plateOpacity
|
||||||
@@ -159,28 +174,16 @@
|
|||||||
- watermark-tile-png — text, fontSize, color, opacity, angle, stepX, stepY,
|
- watermark-tile-png — text, fontSize, color, opacity, angle, stepX, stepY,
|
||||||
font, bold
|
font, bold
|
||||||
|
|
||||||
### Палитры и цветовые утилиты
|
|
||||||
|
|
||||||
- color-wheel-generator — size, кольца/сектора, показ hex при клике (у нас —
|
|
||||||
статичный свотч-полотно)
|
|
||||||
- complementary-palette — baseColor
|
|
||||||
- monochromatic-palette — baseColor, count
|
|
||||||
- analogous-palette — baseColor, spread
|
|
||||||
- triadic-palette — baseColor
|
|
||||||
- tetradic-palette — baseColor
|
|
||||||
- similar-shades-palette — baseColor, count, range
|
|
||||||
- sort-colors — источник (палитра-картинка или список), порядок (hsl/luma)
|
|
||||||
- mix-colors — colors[], веса?
|
|
||||||
- average-color — colors[]; blend-two — a, b, steps; step-between — a, b, steps
|
|
||||||
(три частных случая одного движка)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. Можно добавить — из onlinepngtools
|
## 2. Можно добавить — из onlinepngtools
|
||||||
|
|
||||||
|
Всё из этого списка **в реестре отсутствует**.
|
||||||
|
|
||||||
### Разложение каналов — остаток
|
### Разложение каналов — остаток
|
||||||
|
|
||||||
- separate-colors — minShare слоя (MEDIUM, мультифайловый вывод → пока идея)
|
- separate-colors — minShare слоя (MEDIUM, мультифайловый вывод → см. идеи в
|
||||||
|
`docs/backlog.md`)
|
||||||
|
|
||||||
### Края и силуэт — остаток
|
### Края и силуэт — остаток
|
||||||
|
|
||||||
@@ -197,8 +200,6 @@
|
|||||||
- sort-pixels — blockSize, ключ (яркость/канал), направление (MEDIUM)
|
- sort-pixels — blockSize, ключ (яркость/канал), направление (MEDIUM)
|
||||||
- slow-reveal / fade-in / fade-out / disappear — анимационные (→ идеи)
|
- slow-reveal / fade-in / fade-out / disappear — анимационные (→ идеи)
|
||||||
|
|
||||||
### Сжатие и качество — реализовано (см. Конвертация: compress, reduce-to-size; jpeg-artifacts; W8 quantize)
|
|
||||||
|
|
||||||
### Генераторы — остаток
|
### Генераторы — остаток
|
||||||
|
|
||||||
- multi-color-gradient — список стопов (нужен новый тип параметра → MEDIUM-UI)
|
- multi-color-gradient — список стопов (нужен новый тип параметра → MEDIUM-UI)
|
||||||
@@ -206,31 +207,37 @@
|
|||||||
### Конвертеры — остаток
|
### Конвертеры — остаток
|
||||||
|
|
||||||
- png-to-gif — MEDIUM (однокадровый GIF-энкодер руками)
|
- png-to-gif — MEDIUM (однокадровый GIF-энкодер руками)
|
||||||
- gif-to-frames — MEDIUM (мультифайловый вывод → идея)
|
- gif-to-frames — MEDIUM (мультифайловый вывод → идеи)
|
||||||
- change-bit-depth — MEDIUM (пересборка PNG)
|
- change-bit-depth — MEDIUM (пересборка PNG)
|
||||||
|
|
||||||
### Прочее единичное
|
### Сжатие и качество
|
||||||
|
|
||||||
- pick-a-color — пипетка уже есть в превью; отдельная страница не планируется
|
- compress, reduce-to-size, pick-a-color, extract-barcode — **не реализованы**;
|
||||||
(покрыто)
|
заведены в `docs/backlog.md` (reduce-to-size зависит от UX-райза «Download»).
|
||||||
- preview/test на цветных фонах — фича превью, не инструмент (решить позже)
|
Пипетка для выбора цвета уже есть в превью, отдельная страница не планируется.
|
||||||
- extract-barcode — HARD, вне планов
|
Извлечение штрихкодов — HARD, вне планов.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. Идеи на рассмотрение (нужна архитектура или спорная ценность)
|
## 3. Идеи на рассмотрение (нужна архитектура или спорная ценность)
|
||||||
|
|
||||||
- **Region-инструменты** — требуется UI выделения прямоугольника/ластика на
|
Развёрнутые описания этих идей живут в `docs/backlog.md`, раздел «Идеи»; здесь
|
||||||
превью: censor-region, erase-region, pixelate-area, blur-area, sharpen-area,
|
только список.
|
||||||
reverse-colors-area. Один раз делаем selection-компонент — получаем сразу
|
|
||||||
шесть инструментов.
|
- **Region-инструменты** — нужен selection-компонент на превью: censor-region,
|
||||||
- **Мультифайловый вывод** — механизм «результат = набор файлов» (zip)
|
erase-region, pixelate-area, blur-area, sharpen-area, reverse-colors-area.
|
||||||
реализован Остальные 1→many: gif-to-frames, separate-colors,
|
Один раз делаем selection — получаем шесть инструментов.
|
||||||
multiply-grid-as-files — подключаются по мере нужды.
|
- **Мультифайловый выход** — механизм «результат = набор файлов» (zip)
|
||||||
|
реализован на `split-into-parts-png`. Остальные 1→many: gif-to-frames,
|
||||||
|
separate-colors, multiply-grid-as-files — подключаются по мере нужды.
|
||||||
- **Анимационные эффекты** — slow-reveal, fade-in/out, disappearing, scrolling:
|
- **Анимационные эффекты** — slow-reveal, fade-in/out, disappearing, scrolling:
|
||||||
это видео/GIF на выходе, а не PNG. Отдельное решение о формате результата.
|
это видео/GIF на выходе, а не PNG. Отдельное решение о формате результата.
|
||||||
- **HARD-хвост** — glitch-art, extract-signature, handwritten→digital,
|
- **HARD-хвост** — glitch-art, extract-signature, handwritten→digital,
|
||||||
extract-barcode.
|
extract-barcode.
|
||||||
- **Нишевые серии** (logo/icon/stamp/signature — 107 клонов у источника) —
|
- **Нишевые серии** (logo/icon/stamp/signature — замер в шапке документа) —
|
||||||
сознательно не копируем: это обычные операции над конкретным контентом, у нас
|
сознательно не копируем: это обычные операции над конкретным контентом, у нас
|
||||||
они доступны через базовые инструменты + цепочки.
|
они доступны через базовые инструменты + цепочки.
|
||||||
|
- **Входные «из формата в PNG»** (jpg-to-png, webp-to-png) сознательно не
|
||||||
|
заведены: загрузка файла уже принимает любой поддержанный формат, а
|
||||||
|
инструмент-конвертер добавил бы лишнее звено цепочки. Отдельные страницы под
|
||||||
|
эти интенты — предмет решения в `docs/plan-seo.md` §5.
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
# sv
|
|
||||||
|
|
||||||
Everything you need to build a Svelte project, powered by
|
|
||||||
[`sv`](https://github.com/sveltejs/cli).
|
|
||||||
|
|
||||||
## Creating a project
|
|
||||||
|
|
||||||
If you're seeing this, you've probably already done this step. Congrats!
|
|
||||||
|
|
||||||
```sh
|
|
||||||
# create a new project
|
|
||||||
npx sv create my-app
|
|
||||||
```
|
|
||||||
|
|
||||||
To recreate this project with the same configuration:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
# recreate this project
|
|
||||||
pnpm dlx sv@0.17.0 create --template minimal --types ts --install pnpm web
|
|
||||||
```
|
|
||||||
|
|
||||||
## Developing
|
|
||||||
|
|
||||||
Once you've created a project and installed dependencies with `npm install` (or
|
|
||||||
`pnpm install` or `yarn`), start a development server:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
npm run dev
|
|
||||||
|
|
||||||
# or start the server and open the app in a new browser tab
|
|
||||||
npm run dev -- --open
|
|
||||||
```
|
|
||||||
|
|
||||||
## Building
|
|
||||||
|
|
||||||
To create a production version of your app:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
npm run build
|
|
||||||
```
|
|
||||||
|
|
||||||
You can preview the production build with `npm run preview`.
|
|
||||||
|
|
||||||
> To deploy your app, you may need to install an
|
|
||||||
> [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.
|
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
`src/routes/**`). Scoped-пути двигаются вместе с папками: не оставлять
|
`src/routes/**`). Scoped-пути двигаются вместе с папками: не оставлять
|
||||||
устаревшие пути в `eslint.config.js`.
|
устаревшие пути в `eslint.config.js`.
|
||||||
- Базовый `js.configs.recommended` + `globals.node` навешаны на инфраструктуру
|
- Базовый `js.configs.recommended` + `globals.node` навешаны на инфраструктуру
|
||||||
линтинга: `eslint-plugins/**/*.js` и `scripts/**/*.mjs` (Q7c).
|
линтинга: `eslint-plugins/**/*.js` и `scripts/**/*.mjs`.
|
||||||
TS/svelte-рекомендации туда не подключаются.
|
TS/svelte-рекомендации туда не подключаются.
|
||||||
- Для `*.svelte` выключен `prefer-const` (пропсы в Svelte 5 пишутся через
|
- Для `*.svelte` выключен `prefer-const` (пропсы в Svelte 5 пишутся через
|
||||||
`let`).
|
`let`).
|
||||||
|
|||||||
@@ -0,0 +1,119 @@
|
|||||||
|
import { readFileSync } from "node:fs";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { TOOLS } from ".";
|
||||||
|
|
||||||
|
const MAP_PATH = fileURLToPath(
|
||||||
|
new URL("../../../../docs/tools-map.md", import.meta.url),
|
||||||
|
);
|
||||||
|
|
||||||
|
const SECTION_START = /^## 1\.\s/;
|
||||||
|
const SECTION_END = /^## 2\.\s/;
|
||||||
|
|
||||||
|
// Все id в реестре содержат дефис, поэтому формат строгий: это отсекает слова
|
||||||
|
// описания вроде `median-cut` из «colors (k, median-cut)».
|
||||||
|
const ID_SHAPE = /^[a-z][a-z0-9]*(-[a-z0-9]+)+$/;
|
||||||
|
|
||||||
|
type Mention = { id: string; line: number };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Буллиты раздела «1. Реализовано» вместе с номером первой строки.
|
||||||
|
* Перенесённые строки приклеиваются к буллету: иначе id, оказавшийся на
|
||||||
|
* следующей строке, молча выпадал бы из разбора и инструмент считался бы
|
||||||
|
* неописанным.
|
||||||
|
*/
|
||||||
|
function implementedBullets(
|
||||||
|
markdown: string,
|
||||||
|
): { text: string; line: number }[] {
|
||||||
|
const lines = markdown.split("\n");
|
||||||
|
const start = lines.findIndex((l) => SECTION_START.test(l));
|
||||||
|
const end = lines.findIndex((l) => SECTION_END.test(l));
|
||||||
|
if (start === -1 || end === -1 || end <= start) {
|
||||||
|
throw new Error(
|
||||||
|
"docs/tools-map.md: не найден раздел «1. Реализовано» или «2.» — " +
|
||||||
|
"тест разбирает только реализованные инструменты",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const bullets: { text: string; line: number }[] = [];
|
||||||
|
for (let i = start; i < end; i++) {
|
||||||
|
const line = lines[i];
|
||||||
|
const text = line.trim();
|
||||||
|
const previous = bullets[bullets.length - 1];
|
||||||
|
const isBullet = text.startsWith("- ");
|
||||||
|
const isContinuation =
|
||||||
|
previous !== undefined &&
|
||||||
|
line.startsWith(" ") &&
|
||||||
|
!/^#{1,6}\s/.test(text) &&
|
||||||
|
!/^[-*]\s/.test(text);
|
||||||
|
if (isBullet) {
|
||||||
|
bullets.push({ text: text.slice(2), line: start + i + 1 });
|
||||||
|
} else if (isContinuation) {
|
||||||
|
previous.text += ` ${text}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bullets;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Всё до первого тире `—` — id через `/` или `,`; после тире свободное
|
||||||
|
* описание параметров, оно не разбирается. Формат закреплён в самой карте.
|
||||||
|
*/
|
||||||
|
function mentionedIds(markdown: string): Mention[] {
|
||||||
|
const found: Mention[] = [];
|
||||||
|
for (const { text, line } of implementedBullets(markdown)) {
|
||||||
|
const head = text.split("—")[0];
|
||||||
|
for (const raw of head.split(/[/,]/)) {
|
||||||
|
const id = raw.trim().replace(/`/g, "");
|
||||||
|
if (ID_SHAPE.test(id)) found.push({ id, line });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("docs/tools-map.md ↔ реестр", () => {
|
||||||
|
const markdown = readFileSync(MAP_PATH, "utf8");
|
||||||
|
const mentioned = mentionedIds(markdown);
|
||||||
|
const registryIds = new Set(TOOLS.map((t) => t.id));
|
||||||
|
|
||||||
|
it("разбирает непустой список id (парсер не сломался молча)", () => {
|
||||||
|
expect(implementedBullets(markdown).length).toBeGreaterThan(0);
|
||||||
|
expect(new Set(mentioned.map((m) => m.id)).size).toBeGreaterThan(50);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("в карте нет id, которых нет в реестре", () => {
|
||||||
|
const bogus = mentioned.filter((m) => !registryIds.has(m.id));
|
||||||
|
expect(
|
||||||
|
bogus,
|
||||||
|
bogus
|
||||||
|
.map(
|
||||||
|
(m) =>
|
||||||
|
`${m.id} (docs/tools-map.md:${m.line}) — такого инструмента нет в web/src/lib/registry/`,
|
||||||
|
)
|
||||||
|
.join("\n"),
|
||||||
|
).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("каждый инструмент реестра описан в карте", () => {
|
||||||
|
const mentionedSet = new Set(mentioned.map((m) => m.id));
|
||||||
|
const missing = [...registryIds].filter((id) => !mentionedSet.has(id));
|
||||||
|
expect(
|
||||||
|
missing,
|
||||||
|
`не описаны в разделе «Реализовано»:\n${missing.join("\n")}`,
|
||||||
|
).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("id не продублированы между буллетами", () => {
|
||||||
|
const seen = new Map<string, number>();
|
||||||
|
const dupes: string[] = [];
|
||||||
|
for (const { id, line } of mentioned) {
|
||||||
|
const first = seen.get(id);
|
||||||
|
if (first !== undefined) {
|
||||||
|
dupes.push(`${id} (строки ${first} и ${line})`);
|
||||||
|
} else {
|
||||||
|
seen.set(id, line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
expect(dupes, `повторяющиеся id:\n${dupes.join("\n")}`).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user