docs: review all

This commit is contained in:
2026-10-04 07:18:50 +05:00
parent 8cb3f1a8f0
commit 1d337f065b
21 changed files with 696 additions and 495 deletions
+10 -1
View File
@@ -27,7 +27,7 @@
Читать только нужную строку, а не все документы подряд. Читать только нужную строку, а не все документы подряд.
| Задача | Документ | | Задача | Документ |
| --------------------------------------------- | ---------------------------------- | | ---------------------------------------------- | ---------------------------------- |
| Слои, поток данных, новый инструмент/поле | `docs/architecture.md` | | Слои, поток данных, новый инструмент/поле | `docs/architecture.md` |
| Какие проверки запускать, пороги, CI | `docs/quality-gates.md` | | Какие проверки запускать, пороги, CI | `docs/quality-gates.md` |
| Правка самих гейтов: «почему так, а не иначе» | `docs/decisions.md` | | Правка самих гейтов: «почему так, а не иначе» | `docs/decisions.md` |
@@ -38,8 +38,17 @@
| Актуальный список инструментов | `docs/tools-map.md` | | Актуальный список инструментов | `docs/tools-map.md` |
| Правка полей и раскладки по волнам | `docs/plan-fields-audit-fixes.md` | | Правка полей и раскладки по волнам | `docs/plan-fields-audit-fixes.md` |
| Ручная приёмка UI | `docs/checklist-manual-testing.md` | | Ручная приёмка UI | `docs/checklist-manual-testing.md` |
| Направления развития, фазы 1–11 | `docs/roadmap.md` |
| SEO/GEO-план (адреса, `noindex`, sitemap) | `docs/plan-seo.md` |
| API/PWA/шрифты | `docs/plan-platform.md` |
| Переименование бренда и покупка домена | `docs/plan-domain.local.md` † |
| Сырые UX-находки по инструментам (вход аудита) | `docs/tools-audit.md` |
| Архивирование и удаление старых документов | `docs/archive/README.md` |
| Индекс остальных документов | `docs/README.md` | | Индекс остальных документов | `docs/README.md` |
† `docs/plan-domain.local.md` вне git, в клоне отсутствует — в этом случае
задачу ищут по `docs/plan-seo.md` §6.
## Инварианты, которые линтер не проверяет ## Инварианты, которые линтер не проверяет
- **Сначала искать.** Новая константа, тип или утилита не заводится, пока не - **Сначала искать.** Новая константа, тип или утилита не заводится, пока не
+9 -8
View File
@@ -12,16 +12,16 @@ and open source.
transparency/alpha, color, channel decomposition, geometry, filters, transparency/alpha, color, channel decomposition, geometry, filters,
pixel-property masks, generation, text/watermarks, analysis & verdicts — the pixel-property masks, generation, text/watermarks, analysis & verdicts — the
current list lives in [`docs/tools-map.md`](docs/tools-map.md) current list lives in [`docs/tools-map.md`](docs/tools-map.md)
- **Pipeline system (workspace)** — load image → chain multiple tool steps →
sequential application → download result; pipelines saved to localStorage,
exportable as JSON
- **Web Worker execution** — heavy operations run off the main thread with - **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 - **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
- **Fully static** — no backend, no analytics, no tracking - **Fully static** — no backend; the image never leaves your browser. The only
third-party request is a Google Analytics page-view counter — it never sees
the file or its contents; the boundary and its limits are in
`docs/plan-seo.md` §0
## Tech Stack ## Tech Stack
@@ -65,8 +65,9 @@ pnpm verify # full fast quality gate — the pre-review gate
pnpm build # static production export → web/build/ pnpm build # static production export → web/build/
``` ```
Полный список команд и матрица «какие проверки запускать при каком изменении» — For the full command list and the "which checks to run for which change" matrix,
в `AGENTS.md` и `docs/quality-gates.md`. see [`AGENTS.md`](AGENTS.md) and
[`docs/quality-gates.md`](docs/quality-gates.md).
## Architecture ## Architecture
@@ -74,8 +75,8 @@ Everything is built around the **two-layer registry** (`web/src/lib/registry/`):
a tool is a self-contained implementation `{id, schema, input, result, run}` a tool is a self-contained implementation `{id, schema, input, result, run}`
(`registry/tools/`), and a page is `{slug, title, description, category, steps}` (`registry/tools/`), and a page is `{slug, title, description, category, steps}`
(`registry/pages/`) that names the address and the tools to run — one tool can (`registry/pages/`) that names the address and the tools to run — one tool can
back several pages. Pages, forms, and pipelines are generated from it, and all back several pages. Pages and forms are generated from it, and all execution
execution goes through a single `executor.execute` entry point. goes through a single `executor.execute` entry point.
Core image processing lives in `web/src/lib/core/` and operates directly on Core image processing lives in `web/src/lib/core/` and operates directly on
`ImageData`, with no browser API dependencies. `ImageData`, with no browser API dependencies.
+4 -3
View File
@@ -15,6 +15,7 @@
| Документ | Когда читать | Статус | | Документ | Когда читать | Статус |
| ---------------------------------- | -------------------------------------------------- | ------ | | ---------------------------------- | -------------------------------------------------- | ------ |
| `AGENTS.md` | Всегда: контракт агента | active | | `AGENTS.md` | Всегда: контракт агента | active |
| `docs/README.md` | Индекс документов, правила ведения документации | active |
| `docs/architecture.md` | Слои, поток данных, новый инструмент/поле/маршрут | active | | `docs/architecture.md` | Слои, поток данных, новый инструмент/поле/маршрут | active |
| `docs/quality-gates.md` | Какие проверки запускать, пороги, coverage, CI | active | | `docs/quality-gates.md` | Какие проверки запускать, пороги, coverage, CI | active |
| `docs/decisions.md` | Правка самих гейтов; чтение CI после пуша | active | | `docs/decisions.md` | Правка самих гейтов; чтение CI после пуша | active |
@@ -60,9 +61,9 @@
`web/src/lib/registry/tools/`, адрес и имя страницы (`slug`) — в `web/src/lib/registry/tools/`, адрес и имя страницы (`slug`) — в
`web/src/lib/registry/pages/`; исторические названия в старых планах не `web/src/lib/registry/pages/`; исторические названия в старых планах не
действуют (`docs/architecture.md`, раздел 3). действуют (`docs/architecture.md`, раздел 3).
- Поиск по `docs/` задевает `docs/archive/` (объём больше, чем у активных - Поиск по `docs/` задевает `docs/archive/`: там вынесены завершённые планы и
документов вместе). Для правок production-кода архив не нужен: grep по исследования, поэтому grep по `docs/` даёт больше шума, чем пользы. Для правок
`docs/*.md` без `archive/`. production-кода архив не нужен: grep по `docs/*.md` без `archive/`.
- Проверка ссылок в документации не автоматизирована; ожидаемо неразрешимыми - Проверка ссылок в документации не автоматизирована; ожидаемо неразрешимыми
считаются ссылки на `docs/plan-domain.local.md` (файл вне git). Всё остальное считаются ссылки на `docs/plan-domain.local.md` (файл вне git). Всё остальное
неразрешимое — ошибка. неразрешимое — ошибка.
+8 -3
View File
@@ -12,7 +12,10 @@
4. Определить границу изменения: production UI, shared-модуль или инфраструктура 4. Определить границу изменения: production UI, shared-модуль или инфраструктура
— `AGENTS.md`, раздел «Границы изменений». Изменение shared-кода требует — `AGENTS.md`, раздел «Границы изменений». Изменение shared-кода требует
проверки production-потребителей. проверки production-потребителей.
5. Не добавлять исключения линтера ради отдельных модулей без записи в baseline. 5. Не добавлять исключения линтера ради отдельных модулей: осознанные
отступления живут в конфиге правила (`allowPaths`, `allowWords`,
`ignoreMissingPatterns`, `allowDefaultProject` —
`web/eslint-plugins/README.md`), а не в самих проверяемых файлах.
## 2. Порядок изменения ## 2. Порядок изменения
@@ -27,8 +30,10 @@
warnings и чтение CI — в `docs/quality-gates.md`. Здесь только то, что относится warnings и чтение CI — в `docs/quality-gates.md`. Здесь только то, что относится
к работе агента. к работе агента.
`lint:all` может завершиться успешно при warnings. Это не означает чистый `lint:all` роняет gate на любых ESLint warnings (`--max-warnings=0`).
baseline: нужно отдельно показать число errors, warnings и token warnings. Единственное, что он пропускает, — unused-token сообщения (неблокирующие).
Поэтому «команда прошла» не равно «нечего чинить»: нужно отдельно показать число
errors, warnings и token warnings.
E2E запускается отдельно от быстрых проверок, потому что поднимает build и E2E запускается отдельно от быстрых проверок, потому что поднимает build и
статический сервер. Для UI-изменений сначала выбирается минимальный статический сервер. Для UI-изменений сначала выбирается минимальный
+55 -32
View File
@@ -16,9 +16,9 @@ web/src/lib/
catalog.ts # группировка страниц и счётчики для /list-tools catalog.ts # группировка страниц и счётчики для /list-tools
categories.ts # id/названия категорий categories.ts # id/названия категорий
tool-icons.ts # slug страницы → иконка tool-icons.ts # slug страницы → иконка
core/ # чистый TS: операции над ImageData, без DOM core/ # операции над ImageData; без DOM, кроме io.ts/domText.ts
executor/ # единственная точка исполнения инструментов executor/ # единственная точка исполнения инструментов
i18n/ # словари ru/en, t(), списки для i18n-правил линтера i18n/ # словари ru/en, t(), состояние локали
components/ # UI (fields/, layout/, ui/, schema-компоненты) components/ # UI (fields/, layout/, ui/, schema-компоненты)
zip.ts # сборка мультифайлового результата (1 → many) zip.ts # сборка мультифайлового результата (1 → many)
theme.svelte.ts # light/dark + localStorage theme.svelte.ts # light/dark + localStorage
@@ -30,9 +30,15 @@ web/src/routes/
``` ```
Правило зависимостей: `components/**` и `routes/**` знают про `registry/**`, Правило зависимостей: `components/**` и `routes/**` знают про `registry/**`,
`executor/**` и `core/**`; `core/**` не знает ни о ком и не зависит от DOM; `executor/**` и `core/**`; `core/**` не знает ни о ком; `registry/**` описывает
`registry/**` описывает данные и знает только `core/**` и `registry-schema.ts`. данные и знает только `core/**` и `registry-schema.ts`. Нарушение этого порядка
Нарушение этого порядка — повод не заводить импорт, а инвертировать зависимость. — повод не заводить импорт, а инвертировать зависимость.
Исключение из «core без DOM» — `core/io.ts` (декодирование файла, кодирование,
скачивание) и `core/domText.ts` (canvas-рендер текста и эмодзи). Они трогают
`document`, `createImageBitmap` и `HTMLCanvasElement`, поэтому не работают в
node-Vitest и исключены из coverage-гейта (`docs/quality-gates.md`, раздел
Coverage). Всё остальное в `core/**` — чистые функции над `ImageData`.
### Чего в коде нет ### Чего в коде нет
@@ -45,9 +51,11 @@ web/src/routes/
полей двух инструментов — отдельная работа, описанная как фаза 11 полей двух инструментов — отдельная работа, описанная как фаза 11
`docs/roadmap.md`. Под неё зарезервированы `components/StepCard.svelte` `docs/roadmap.md`. Под неё зарезервированы `components/StepCard.svelte`
(сейчас импортируется только витриной `/kit`) и ключи `chain.*`, (сейчас импортируется только витриной `/kit`) и ключи `chain.*`,
`errors.badPipelineShape`, `errors.noSteps`. `errors.badPipelineShape`, `errors.pipelineVersion`, `errors.noSteps`.
- **Разделения страниц, которые делят один инструмент** — сводит 14 - **Разделения страниц, которые делят один инструмент** — сводит все
convert-страниц на один инструмент; задача в `docs/backlog.md`. convert-страницы на один инструмент; задача в `docs/backlog.md`. Пока
`registry.test.ts` не требует, чтобы на инструмент ссылались две страницы, а
на практике не ссылается ни одна.
Задачи по обоим пунктам — в `docs/backlog.md`; архитектурные решения по Задачи по обоим пунктам — в `docs/backlog.md`; архитектурные решения по
пайплайну — `docs/roadmap.md`. пайплайну — `docs/roadmap.md`.
@@ -67,7 +75,7 @@ registry/{tools,pages}/*.ts
├─► SchemaFields.svelte поля из schema.fields (FIELDS: kind → контрол) ├─► SchemaFields.svelte поля из schema.fields (FIELDS: kind → контрол)
├─► SchemaSourceTile/SchemaTextSource.svelte вход ├─► SchemaSourceTile/SchemaTextSource.svelte вход
└─► SchemaPreview + schema-preview-model.ts вид превью └─► SchemaPreview + schema-preview-model.ts вид превью
│ buildSchemaPreviewModel(inputMode, resultKind, …) → source/result/hasResult │ buildSchemaPreviewModel(input) → { sourceValue, resultValue, formatValue, hasResult }
▼ ▼
executor.execute(ctx) единственная точка исполнения executor.execute(ctx) единственная точка исполнения
│ сериализация параметров → worker → worker-handler → tool.run(ctx) │ сериализация параметров → worker → worker-handler → tool.run(ctx)
@@ -104,9 +112,9 @@ registry/{tools,pages}/*.ts
| `id` | внутреннее имя инструмента, уникально; ключ `tools.<id>.*` и `toolId` в worker | | `id` | внутреннее имя инструмента, уникально; ключ `tools.<id>.*` и `toolId` в worker |
| `schema` | `toolSchema({ fields, layout?, meta? })` | | `schema` | `toolSchema({ fields, layout?, meta? })` |
| `input` | `"image" \| "text" \| "none"` — что ждёт инструмент на входе | | `input` | `"image" \| "text" \| "none"` — что ждёт инструмент на входе |
| `result` | `"image" \| "text" \| "verdict" \| "files"` — что отдаёт | | `result`? | `"image" \| "text" \| "verdict" \| "files"` — что отдаёт |
| `domOnly` | инструмент требует DOM (canvas); превью-executor запускает его напрямую | | `domOnly`? | инструмент требует DOM (canvas); превью-executor запускает его напрямую |
| `output` | mime/ext/quality для скачивания (не у всех) | | `output`? | mime/ext/quality для скачивания (не у всех) |
| `run(ctx)` | единственный метод: `ctx` → `ToolResult` | | `run(ctx)` | единственный метод: `ctx` → `ToolResult` |
`Page` (записи в `registry/pages/<group>.ts`): `Page` (записи в `registry/pages/<group>.ts`):
@@ -121,15 +129,22 @@ registry/{tools,pages}/*.ts
Кто по чему адресуется: `slug` — маршрут, имя файла, `TOOL_ICONS`, поиск по Кто по чему адресуется: `slug` — маршрут, имя файла, `TOOL_ICONS`, поиск по
каталогу; `id` — ключ `tools.<id>.*` (подписи полей, `options`, `results`), каталогу; `id` — ключ `tools.<id>.*` (подписи полей, `options`, `results`),
`toolId` worker-протокола и запись пайплайна. Один инструмент может обслуживать `toolId` worker-протокола и запись пайплайна. Модель допускает, что один
несколько страниц, одна страница — состоять из нескольких инструментов. инструмент обслуживает несколько страниц, а страница состоит из нескольких
инструментов; на практике пока каждая страница ссылается на свой инструмент, а
исполняется первый (и единственный) шаг.
Инструмент работает с любым поддержанным форматом, поэтому png-интент живёт Инструмент работает с любым поддержанным форматом, поэтому png-интент живёт
только в `slug`: `id: "crop"` → страница `slug: "crop-png"` → `crop-png.png`. только в `slug`: `id: "crop"` → страница `slug: "crop-png"` → `crop-png.png`.
Правило имени: снять суффикс `-png` и в середине имени (`png-to-base64` → Правило имени `id`: снять суффикс `-png` и в середине имени (`png-to-base64` →
`to-base64`, `X-to-png` → `from-X`, `verify-is-png` → `verify-png`); следствие — `to-base64`, `X-to-png` → `from-X`, `verify-is-png` → `verify-png`). Правило
относится только к `id`: у `slug` своя история и SEO-значение, поэтому
переименование `id` не трогает адреса, а переименование `slug` меняет адрес, имя переименование `id` не трогает адреса, а переименование `slug` меняет адрес, имя
файла, ключ `pages.*`, иконку и запись в `docs/tools-map.md`. файла, ключ `pages.*`, иконку и запись в `docs/tools-map.md`. Слаги, сложившиеся
до разделения реестра, правилу не соответствуют (`convert-png-to-jpg`,
`change-png-opacity`, `png-is-grayscale`, `verify-is-png` и другие) — это не
ошибка, менять их без SEO-решения нельзя. Форму slug проверяет
`registry.test.ts` (только форма, не смысл).
Хелперы `genTool` / `imgTool` / `textGen` из `types.ts` задают `run` для типовых Хелперы `genTool` / `imgTool` / `textGen` из `types.ts` задают `run` для типовых
случаев (приведение типа результата), чтобы запись не повторяла обвязку. `TOOLS` случаев (приведение типа результата), чтобы запись не повторяла обвязку. `TOOLS`
@@ -154,15 +169,18 @@ registry/{tools,pages}/*.ts
допустимому виду. Правка поведения полей идёт здесь, а не в компоненте. допустимому виду. Правка поведения полей идёт здесь, а не в компоненте.
Новый вид поля требует: спека + фабрика `field.*` → запись в `fieldSpecs` → Новый вид поля требует: спека + фабрика `field.*` → запись в `fieldSpecs` →
контрол в `components/fields/schema/` → ключ в `FIELDS` → дефолт и санитизация → контрол (`components/fields/schema/`, а для `dimension` —
`fields/DimensionField.svelte`) → ключ в `FIELDS` → дефолт и санитизация →
i18n-подписи → тест в `registry-schema.test.ts`. i18n-подписи → тест в `registry-schema.test.ts`.
## 5. Исполнение ## 5. Исполнение
`executor/executor.ts` — фабрика `createExecutor(factories?)` плюс готовый `executor/executor.ts` — фабрика `createExecutor({ workerFactory? })` плюс
экспорт `execute`. Контракт асинхронный (`Promise`), параметры сериализуются, готовый экспорт `execute`. Контракт асинхронный (`Promise`), параметры
исполнение уходит в `executor.worker.ts`; `worker-handler.ts` разбирает сериализуются по `executor/protocol.ts`, исполнение уходит в
сообщение и зовёт `run`. Если worker недоступен — прямой вызов. `executor.worker.ts`; `worker-handler.ts` разбирает сообщение и зовёт `run`.
Если worker недоступен — прямой вызов. UI импортирует `execute` из
`executor/index.ts`.
Отсюда следует: подключение wasm позже означает замену реализации **внутри** Отсюда следует: подключение wasm позже означает замену реализации **внутри**
исполнителя, а не правку компонентов. исполнителя, а не правку компонентов.
@@ -170,13 +188,16 @@ i18n-подписи → тест в `registry-schema.test.ts`.
## 6. i18n ## 6. i18n
Словари — `web/src/lib/i18n/` (`en.ts`, `ru.ts`, общий `dict.ts` с Словари — `web/src/lib/i18n/` (`en.ts`, `ru.ts`, общий `dict.ts` с
`LOCALES`/`BASE_LOCALE`). Пользовательский текст живёт только в словарях и `LOCALES`/`BASE_LOCALE`, `t.ts` с `t()`, `locale.svelte.ts` с состоянием
читается через `t()`. Секции разделены по слоям: `pages.<slug>.title` и локали). Пользовательский текст живёт только в словарях и читается через `t()`.
`pages.<slug>.description` — тексты страницы (в базовой локали секция пустая, EN Списки ключей и допустимых атрибутов для i18n-правил линтера лежат не здесь, а в
берётся из реестра), `tools.<id>.options/params/results` — подписи полей и `web/eslint-plugins/i18n/lists.js`. Секции разделены по слоям:
тексты вердиктов инструмента, `fields.*`/`groups.*` — общие подписи схемы. `pages.<slug>.title` и `pages.<slug>.description` — тексты страницы (в базовой
Инварианты паритета ключей, плейсхолдеров и запрет хардкода проверяются локали секция пустая, EN берётся из реестра),
линтером, детали — `web/eslint-plugins/README.md`. `tools.<id>.options/params/results` — подписи полей и тексты вердиктов
инструмента, `fields.*`/`groups.*` — общие подписи схемы. Инварианты паритета
ключей, плейсхолдеров и запрет хардкода проверяются линтером, детали —
`web/eslint-plugins/README.md`.
## 7. Точки расширения ## 7. Точки расширения
@@ -204,7 +225,7 @@ i18n-подписи → тест в `registry-schema.test.ts`.
`from-base64`, `is-grayscale`). `from-base64`, `is-grayscale`).
4. **Описать схему** через `field.*`, задать `layout`, если поля нужно 4. **Описать схему** через `field.*`, задать `layout`, если поля нужно
сгруппировать. Качество кодирования (`quality`) — не поле страницы, а сгруппировать. Качество кодирования (`quality`) — не поле страницы, а
настройка скачивания (`docs/plan-seo.md` §1, S1f). настройка скачивания (`docs/plan-seo.md` §4, S1f).
5. **Написать `run`** поверх `core/*`; пиксельные операции — чистые функции в 5. **Написать `run`** поверх `core/*`; пиксельные операции — чистые функции в
`core/`, без DOM. `core/`, без DOM.
6. **Добавить страницу** в `registry/pages/<group>.ts`: `slug` с адресным 6. **Добавить страницу** в `registry/pages/<group>.ts`: `slug` с адресным
@@ -221,8 +242,10 @@ i18n-подписи → тест в `registry-schema.test.ts`.
«Реализовано», формат строки — в шапке раздела. Карта перечисляет **`slug` «Реализовано», формат строки — в шапке раздела. Карта перечисляет **`slug`
страниц** из `registry/pages/`, то есть адреса. Проверяет страниц** из `registry/pages/`, то есть адреса. Проверяет
`web/src/lib/registry/tools-map-doc.test.ts`: slug без страницы и страница `web/src/lib/registry/tools-map-doc.test.ts`: slug без страницы и страница
без записи в карте роняют тест. Обоснование и границы автоматизации — без записи в карте роняют тест; разделы «Можно добавить» и «Идеи»
`docs/decisions.md`, раздел 4. проверяются в обратную сторону — реализованный slug в плане тоже роняет
тест, поэтому при переезде инструмента из плана в реестр его вынимают
оттуда. Обоснование и границы автоматизации — `docs/decisions.md`, раздел 4.
Ожидаемый минимум проверок: `pnpm --dir web check` и `pnpm --dir web test`; для Ожидаемый минимум проверок: `pnpm --dir web check` и `pnpm --dir web test`; для
инструмента, меняющего пользовательский поток, — e2e (см. раздел 3 инструмента, меняющего пользовательский поток, — e2e (см. раздел 3
+75 -65
View File
@@ -1,36 +1,38 @@
# Backlog # Backlog
> Статус: **active**. Продуктовые, UX- и инфраструктурные задачи. Здесь только > Статус: **active**. Открытые задачи. Подробные планы, разбор решений и сырые
> **открытые** задачи: выполненные удаляются, а критичные для проекта факты из > находки — `docs/plan-seo.md`, `docs/plan-platform.md`,
> них переносятся в тематический документ (архитектура, quality gates, lint, > `docs/plan-fields-audit-fixes.md`, `docs/tools-audit.md`, `docs/decisions.md`.
> 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 и прочие правила
- [ ] `tseslint.config(...)` deprecated: сигнатура - [ ] `tseslint.config(...)` deprecated: сигнатура
`(...configs: InfiniteDepthConfigWithExtends[]): ConfigArray` помечена `(...configs: InfiniteDepthConfigWithExtends[]): ConfigArray` помечена
устаревшей, нужен переход на `defineConfig` из `typescript-eslint` — устаревшей, нужен переход на `defineConfig` из `eslint/config` (ESLint
`web/eslint.config.js`. core, а не `typescript-eslint`) — `web/eslint.config.js`.
- [ ] удалить мёртвые `tools.*.params` из root i18n после проверки потребителей; - [ ] удалить мёртвые `tools.*.params` из root i18n после проверки потребителей;
до этого действует pattern-based policy-исключение до этого действует pattern-based policy-исключение
(`web/eslint-plugins/README.md`, раздел `dict-consistency`) (`web/eslint-plugins/README.md`, раздел `dict-consistency`)
## Мелочи всякие ## Мелочи всякие
Порядок правок полей и раскладки — `docs/plan-fields-audit-fixes.md`, сами - [ ] split-into-parts-png - увеличить максимальные значения, плохо работает для
находки («что плохо», с датой аудита) — `docs/tools-audit.md`. Здесь — как это нарезки ассетов
исправить; находка в аудите не отменяет задачу здесь. - [ ] crop, resize (но скорее всего все инструменты) - при загрузке нового
изображения сбрасывает размеры, настроенные пользователем
- [ ] Пересмотреть тесты - исправить всякие хрупкие, которые проверяют - [ ] split-into-parts-png, resize (может еще какие-то) - возможность указывать
количество инструментов, точное совпадение текстов, поиск по размеры из заранее одобренного списка размеров - 16х16, 32х32, 64х64 как
`img[alt="Result image"]`, svelte классам и тому подобные минимум, может еще какие-то размеры добавить, единый список из предыдущей
задачи - (для инди разработчиков, стандартные тайлы часто делаются)
- [ ] e2e: `i18n.spec.ts` и `text-and-verdicts.spec.ts` дублируют локаторы из
`helpers/page.ts` вместо вызова — правило «спека зовёт helpers»
(`docs/testing-strategy.md`, раздел 5)
- [ ] мёртвый i18n-ключ `noParams` («нет параметров — результат готов»): его
ничем не читают. Либо показывать подсказку для инструментов с пустой
схемой (FA-E1), либо удалить ключ из обеих локалей
- [ ] Пересмотреть тесты — убрать точные совпадения текстов и любые селекторы,
привязанные к разметке или svelte-классам (сейчас остались:
`tool-flows.spec.ts`, `schema-tool-strings.test.ts`)
- [ ] нужен инструмент для удаления фона по выбранной точке - удалять внутри - [ ] нужен инструмент для удаления фона по выбранной точке - удалять внутри
выделенной области выделенной области
- [ ] сделать конвертацию в смайлики\награды для твитча (6 размеров или выбор - [ ] сделать конвертацию в смайлики\награды для твитча (6 размеров или выбор
@@ -63,8 +65,10 @@
строку для svg - форматирование или подсветку синтаксиса??? для bin\hex - строку для svg - форматирование или подсветку синтаксиса??? для bin\hex -
форматирование по колонкам??? для hex\bytes\rgba - пример в placeholder форматирование по колонкам??? для hex\bytes\rgba - пример в placeholder
- [ ] сделать загрузку картинки исходника по ctrl+v и drag-and-drop (dropzone) - [ ] сделать загрузку картинки исходника по ctrl+v и drag-and-drop (dropzone)
- [ ] square-mask-png - проверить работу - параметры неверно считаются. width - - [ ] square-mask-png — перепроверить баг-репорт: он устарел, в схеме больше нет
100, height - 100, x - 100, y - 100, обрезает 50% на 50% картинку параметров `width`/`height`/`x`/`y` (теперь `widthPct`/`heightPct` 10..100
и один `offset` в диапазоне −50..50, значения 100 недостижимы). Либо
подтвердить, что баг исправлен, либо найти новый repro
- [ ] прикрутить кастомный color picker, добавить прозрачность (range?) - [ ] прикрутить кастомный color picker, добавить прозрачность (range?)
- [ ] проверить настройки range slider на crop-png - слишком большие значения - [ ] проверить настройки range slider на crop-png - слишком большие значения
- [ ] для range slider field - добавить поле для ввода цифр и кнопку сброса - [ ] для range slider field - добавить поле для ввода цифр и кнопку сброса
@@ -72,48 +76,45 @@
- [ ] для keep Aspect field - пересчитывать второе поле, учитывать какое было - [ ] для keep Aspect field - пересчитывать второе поле, учитывать какое было
изменено изменено
- [ ] обновить страницу kit или сделать storybook - [ ] обновить страницу kit или сделать storybook
- [ ] Свести convert-страницы на один инструмент `convert` — реализация уже - [ ] Свести convert-страницы (`convert-png-to-jpg`, `convert-png-to-webp`,
умеет «скопировать пиксели, декодировать вход, отдать в заданном формате», `png-to-bmp`) на один инструмент `convert` — зависит от S1f
а страницы различаются только форматом вывода. **Зависит от S1f** (`docs/plan-seo.md` §4). Мешает не только `quality` (он живёт в схеме
(`docs/plan-seo.md` §1): пока `quality` живёт в схеме страницы, у инструмента), а сама модель: `output` с mime/ext — свойство `Tool`, а
`convert-png-to-bmp` и `convert-png-to-jpg` разные наборы полей, и один `PageStep` умеет только `{ id, params? }`, поэтому страницы на общий `id`
инструмент на них не сойдётся. Качество — настройка кнопки Download получат одинаковый формат вывода. Нужно научить шаг переопределять формат
(«Download as JPEG»), а не поле страницы; после его переезда пресеты схемы вывода
не нужны, страницы различаются только `output` и `params` шага. Модель
страницы к этому готова: `Page.steps` — список инструментов, один
инструмент может обслуживать много страниц (`docs/architecture.md`, раздел
3).
- [ ] придумать процесс редактирования и настройки pipeline - drag-n-drop, - [ ] придумать процесс редактирования и настройки pipeline - drag-n-drop,
удаление, добавление инструментов. переход один инструмент - pipeline без удаление, добавление инструментов. переход один инструмент - pipeline без
перезагрузки перезагрузки
- [ ] `CatalogToolbar`: фильтры используют сырой `<button>`, а не `ui/Button` — - [ ] `CatalogToolbar`: фильтры используют сырой `<button>`, а не `ui/Button` —
единый контрол для всей библиотеки единый контрол для всей библиотеки
(`web/src/lib/components/CatalogToolbar.svelte`). (`web/src/lib/components/CatalogToolbar.svelte`).
- [ ] Вынести `ToneVariant`/варианты в общий модуль: `const`-объект с тонами - [ ] `SchemaTextResult` вердикт-бейдж рисует руками: свои классы
продублирован в `ui/Badge.svelte` и кнопках `badge`/`badge-tone--{tone}` и свой CSS в `<style>` вместо
(`web/src/lib/components/ui/Badge.svelte`). `ui/Badge.svelte` — тон и разметка бейджа живут в двух местах
- [ ] `PreviewTile`: canvas должен сам выбирать соотношение сторон в диапазоне (`web/src/lib/components/SchemaTextResult.svelte`).
16:9…9:16 вместо фиксированного - [ ] `PreviewTile`: высота плитки задана только `min-height: clamp(...)`,
поэтому она не следует за пропорциями результата — широкий результат
уезжает в узкую полосу, а высокий упирается в `max-height: 100vh`. Нужно,
чтобы соотношение сторон выбиралось по результату в диапазоне 16:9…9:16
(`web/src/lib/components/layout/PreviewTile.svelte`). (`web/src/lib/components/layout/PreviewTile.svelte`).
- [ ] `ThemeToggle`: свои анимации иконок и своя кнопка вместо `IconButton` - [ ] Радиусы бордеров: `--radius-m` ненулевой и применён ко всем бордерам
(`web/src/lib/components/ui/ThemeToggle.svelte`). (`app.css`, `SchemaToolView.svelte`). Решить, где скругления реально есть
- [ ] Радиусы бордеров не совпадают с референсом: в `refs` радиусы были 0, а мы в дизайне, а где должен быть 0
взяли ненулевой `--radius-m` для всех бордеров. Проверить токен против
референсов; возможно он должен быть 0, а скругления оставить только там,
где они реально есть в дизайне.
- [ ] `Footer`: версию брать из `package.json`, а не писать `v0.1.0` вручную - [ ] `Footer`: версию брать из `package.json`, а не писать `v0.1.0` вручную
(`web/src/lib/components/layout/Footer.svelte`). (`web/src/lib/components/layout/Footer.svelte`).
## Переезд ## Переезд
- [ ] множественные числа (часть/части/частей; «N параметров», счётчики) — - [ ] множественные числа (часть/части/частей; «N параметров», счётчики) —
`tPlural()` и формат `PluralForms`; детали и критерии — в этом backlog функция `tPlural(path, n)` и форма `PluralForms` (обязательное `other`,
опциональные `one`/`few`/`many`); первые потребители — `part`/`parts` в
подписи результата и счётчики параметров. **Критерий приёмки:** у ключа с
plurals есть все формы, для которых нужен перевод, `other` никогда не
пустой, число подставляется в текст без склейки
## SEO/GEO ## SEO/GEO
Общий план: `docs/plan-seo.md` (S1–S4). Старт возможен — пока только domain-free
часть S1. Полное содержание этапов не дублируется здесь, только идентификаторы.
- [ ] переименование бренда + домен: чек-лист в локальном файле - [ ] переименование бренда + домен: чек-лист в локальном файле
`plan-domain.local.md` (в git не входит, в клоне отсутствует; подробности `plan-domain.local.md` (в git не входит, в клоне отсутствует; подробности
и порядок — `docs/plan-seo.md` §6, R1/R2) и порядок — `docs/plan-seo.md` §6, R1/R2)
@@ -127,16 +128,17 @@
(`docs/plan-seo.md` §5) (`docs/plan-seo.md` §5)
- [ ] sitemap.xml из реестра + `Sitemap:` в robots.txt (S1b/S1c) - [ ] sitemap.xml из реестра + `Sitemap:` в robots.txt (S1b/S1c)
- [ ] Seo.svelte: canonical + OG-теги, per-tool OG-картинки (S1d/S1e) - [ ] Seo.svelte: canonical + OG-теги, per-tool OG-картинки (S1d/S1e)
- [ ] контент страницы инструмента: seo-блок в словарях, H1/how-to/FAQ/related - [ ] контент страницы инструмента: seo-блок в словарях, how-to/FAQ/related (S2)
(S2)
- [ ] JSON-LD (SoftwareApplication/FAQPage/Breadcrumb) + llms.txt (S3) - [ ] JSON-LD (SoftwareApplication/FAQPage/Breadcrumb) + llms.txt (S3)
- [ ] GSC + приватная аналитика (Plausible/Umami/CF), дистрибуция (S4) - [ ] дистрибуция: закрепить репо, alternative.to, Product Hunt, Reddit/HN (S4c)
- [ ] GSC: подключить домен, отправить sitemap, мониторинг индексации (S4a)
- [ ] GA4: cookieless-конфигурация, отключить рекламные audiences и signals,
`anonymize_ip` — сейчас в `web/src/app.html` голый `gtag('config')` (S4b)
- [ ] GA4: отсечь попадания с локальных и preview-сборок — тег стоит в
prerender-оболочке и грузится на каждой странице, нужен флаг окружения
## Платформа (API/PWA) ## Платформа (API/PWA)
Общий план: `docs/plan-platform.md`; WASM-ядро, эталоны и CLI —
`docs/roadmap.md` (фазы 2–8). Здесь только треки, которых нет в roadmap.
- [ ] PWA: manifest + service worker, оффлайн-режим, установка (после S1) - [ ] PWA: manifest + service worker, оффлайн-режим, установка (после S1)
- [ ] API: спайк серверного ядра (нативный Rust vs edge-wasm), ключи, free tier - [ ] API: спайк серверного ядра (нативный Rust vs edge-wasm), ключи, free tier
(после wasm-фаз 5–7) (после wasm-фаз 5–7)
@@ -145,8 +147,15 @@
## Качество production UI ## Качество production UI
Политика проверок — `docs/quality-gates.md`, e2e — `docs/testing-strategy.md`. - [ ] `/` и `/list-tools` выходят без `<title>`: `svelte:head` есть только на
странице инструмента, а готовые ключи `home.defaultTitle`,
`catalog.pageTitle`, `catalog.metaDescription` не читает ни один компонент
(S1d)
- [ ] **Баг e2e:** `tool-flows.spec.ts:179` создаёт контекст через
`browser.newContext()`, который не наследует `use.baseURL` из
`playwright.config.ts`, — относительный `goto('/tools/flip-png')` внутри
`openTool` не резолвится, и тест «работает без Web Worker (fallback)»
падает на всех движках. В CI это не видно: job `e2e` неблокирующий
- [ ] Component-тесты `SchemaToolView`/`SchemaPreview` требуют отдельного - [ ] Component-тесты `SchemaToolView`/`SchemaPreview` требуют отдельного
решения: node Vitest не имеет DOM/canvas, а e2e уже покрывает поведение. решения: node Vitest не имеет DOM/canvas, а e2e уже покрывает поведение.
- [ ] Ошибка декодирования изображения из `createImageBitmap` пока показывается - [ ] Ошибка декодирования изображения из `createImageBitmap` пока показывается
@@ -154,9 +163,8 @@
## Идеи ## Идеи
Идеи 1–5 и 15 относятся к **пайплайну (цепочкам шагов)**, которого в коде пока Идеи 1–5 и 15 — про пайплайн (фаза 11 `docs/roadmap.md`). Идеи 6–14 и 16 — про
нет: это фаза 11 `docs/roadmap.md`. Идеи 6–14 — к существующим инструментам и существующие инструменты и UI. Оценки (S/M/L) — оценка труда, не приоритет.
UI. Оценки (S/M/L) — оценка труда, не приоритет.
1. **Менять первый инструмент цепочки** — оценка M. Сейчас базовый инструмент 1. **Менять первый инструмент цепочки** — оценка M. Сейчас базовый инструмент
зафиксирован при открытии страницы; хочется заменить его на другой без зафиксирован при открытии страницы; хочется заменить его на другой без
@@ -171,8 +179,8 @@ UI. Оценки (S/M/L) — оценка труда, не приоритет.
3. **Несколько цепочек: сохранение и загрузка** — оценка M. Именованные цепочки 3. **Несколько цепочек: сохранение и загрузка** — оценка M. Именованные цепочки
в localStorage (список, создать/переименовать/удалить), быстрое переключение. в localStorage (список, создать/переименовать/удалить), быстрое переключение.
Формат экспортного JSON расширить опциональным полем имени; обратной Формат экспортного JSON расширить опциональным полем имени; обратной
совместимости со старыми выгрузками не делаем (см. задачу про `id`/`slug` совместимости со старыми выгрузками не делаем — формат не устоялся: он
выше — формат ещё не устоялся). появится вместе с фазой 11 `docs/roadmap.md` и до её старта может измениться.
4. **Batch-обработка архивов выбранным chain** — оценка L. Вход — zip-архив 4. **Batch-обработка архивов выбранным chain** — оценка L. Вход — zip-архив
картинок (распаковка в браузере), прогон текущего или выбранного сохранённого картинок (распаковка в браузере), прогон текущего или выбранного сохранённого
@@ -192,9 +200,11 @@ UI. Оценки (S/M/L) — оценка труда, не приоритет.
(`OutputFormat`: mime/ext/qualityParamId) и кодирование по нему в (`OutputFormat`: mime/ext/qualityParamId) и кодирование по нему в
`SchemaToolView.download()` — `docs/architecture.md`, раздел 3. Что осталось `SchemaToolView.download()` — `docs/architecture.md`, раздел 3. Что осталось
сверх S1f: сверх S1f:
- экспорт с лимитом размера → закрывает отложенную страницу - экспорт с лимитом размера → «сжатие до целевого КБ»: для lossy это quality
`reduce-to-size-png` (целевой KB, бинарный поиск по k из квантования). в селекторе формата, для PNG — бинарный поиск по k из квантования. Нужна ли
Проработать вместе с достройкой download-флоу. отдельная страница под этот интент — открытый вопрос с вордстатом, имя не
выбрано (`docs/plan-seo.md` §5). Проработать вместе с достройкой
download-флоу.
7. **Страница `watermark-image-png` (водяной знак картинкой)** — оценка M. Нужна 7. **Страница `watermark-image-png` (водяной знак картинкой)** — оценка M. Нужна
overlay-механика: второй источник изображения (getOverlay/store), параметры overlay-механика: второй источник изображения (getOverlay/store), параметры
@@ -259,7 +269,7 @@ UI. Оценки (S/M/L) — оценка труда, не приоритет.
- частичные ошибки: один битый файл не должен ронять весь шаг — нужен - частичные ошибки: один битый файл не должен ронять весь шаг — нужен
per-file отчёт, а не один `ToolError` на шаг; per-file отчёт, а не один `ToolError` на шаг;
- имена частей на каждом шаге и Download для набора (сейчас zip по - имена частей на каждом шаге и Download для набора (сейчас zip по
`tool.id`, `SchemaToolView.download()`); `page.slug`, `SchemaToolView.download()`);
- обратная сторона: распаковка zip в браузере, порядок файлов, поведение - обратная сторона: распаковка zip в браузере, порядок файлов, поведение
many → 1 → many в одной цепочке. Зависит от идеи 10 (остальные потребители many → 1 → many в одной цепочке. Зависит от идеи 10 (остальные потребители
1 → many) и идеи 4 (batch по архиву), `docs/roadmap.md` фаза 8 1 → many) и идеи 4 (batch по архиву), `docs/roadmap.md` фаза 8
+11 -9
View File
@@ -1,8 +1,9 @@
# Чек-лист ручного тестирования production UI # Чек-лист ручного тестирования production UI
> Вспомогательный документ к e2e-тестам (`web/e2e/`, `pnpm --dir web test:e2e`) > Статус: **active**. Вспомогательный документ к e2e-тестам (`web/e2e/`,
> и стратегии `docs/testing-strategy.md`. Автоматизированная часть закрывает > `pnpm --dir web test:e2e`) и стратегии `docs/testing-strategy.md`.
> «нет падений/ошибок»; здесь — то, что руками, и что Playwright не покрывает. > Автоматизированная часть закрывает «нет падений/ошибок»; здесь — то, что
> руками, и что Playwright не покрывает.
> >
> Перед прогоном: `pnpm --dir web build`, поднять локально `web/` (dev или build > Перед прогоном: `pnpm --dir web build`, поднять локально `web/` (dev или build
> → `node scripts/serve-static.mjs`). > → `node scripts/serve-static.mjs`).
@@ -23,7 +24,7 @@
(авто-покрытие: `web/e2e/png-fixtures.spec.ts`). (авто-покрытие: `web/e2e/png-fixtures.spec.ts`).
- [ ] Анимированный/APNG, если попадётся — не ломает конвейер. - [ ] Анимированный/APNG, если попадётся — не ломает конвейер.
- [ ] Повреждённый/не-PNG файл — понятная ошибка, нет краша (авто-чека в - [ ] Повреждённый/не-PNG файл — понятная ошибка, нет краша (авто-чека в
`pipeline.spec.ts` и `png-fixtures.spec.ts`). `tool-flows.spec.ts` и `png-fixtures.spec.ts`).
- [ ] PNG с битой CRC / 16-bit — как ведут себя анализаторы (ориентация, размер, - [ ] PNG с битой CRC / 16-bit — как ведут себя анализаторы (ориентация, размер,
прозрачность); CRC/16-bit авто-покрыты в `png-fixtures.spec.ts`. прозрачность); CRC/16-bit авто-покрыты в `png-fixtures.spec.ts`.
@@ -72,8 +73,9 @@
## D. Производительность и отзывчивость ## D. Производительность и отзывчивость
- [ ] Долгие авто-рераны на больших картинках (jpeg-artifacts, dithering, - [ ] Долгие авто-рераны на больших картинках (`jpeg-artifacts-png`,
quantize на 4К) — UI не фризит, канселяция/дебаунс срабатывает. `dithering-png`, `quantize-png` на 4К) — UI не фризит, канселяция/дебаунс
срабатывает.
- [ ] Скролл страницы при длинных настройках (например, text-инструменты) — нет - [ ] Скролл страницы при длинных настройках (например, text-инструменты) — нет
залипаний, панели не прыгают. залипаний, панели не прыгают.
- [ ] Вкладка не «съедает» память при 10+ повторах blur/sharpen на мегапикселе - [ ] Вкладка не «съедает» память при 10+ повторах blur/sharpen на мегапикселе
@@ -82,9 +84,9 @@
## E. Визуальный слой (доделываем по ходу) ## E. Визуальный слой (доделываем по ходу)
- [ ] Сетки/выравнивание на брейкпоинтах 640 / 800 / 1100 (mobile/tablet/ - [ ] Сетки/выравнивание на брейкпоинтах 640 / 800 / 1100 (mobile/tablet/
desktop) на всех 4 маршрутах. desktop) на всех маршрутах: `/`, `/list-tools`, `/kit`, любая `/tools/*`.
- [ ] Тема light/dark: контраст вердиктов (Yes/No), статус «LIVE PREVIEW», - [ ] Тема light/dark: контраст вердиктов (Yes/No), подпись панели превью
мета-инфо бликов не даёт. («Предпросмотр»), мета-инфо бликов не даёт.
- [ ] Иконки в каталоге на всех категориях — не «бьются» (missing icon). - [ ] Иконки в каталоге на всех категориях — не «бьются» (missing icon).
- [ ] Язык RU/EN на превью-страницах — переключение без регресса рендера. - [ ] Язык RU/EN на превью-страницах — переключение без регресса рендера.
+18 -5
View File
@@ -47,11 +47,11 @@
которых 14 — ложные («30 дней» политики архива, «9 опций» описания поля, «4 которых 14 — ложные («30 дней» политики архива, «9 опций» описания поля, «4
проекта» факт из `playwright.config.ts», цитата самого правила). проекта» факт из `playwright.config.ts», цитата самого правила).
- Правило всё равно неполно: ловит «122 инструмента», но не «инструментов — - Правило всё равно неполно: ловит «M инструментов», но не «инструментов — M»,
122», не «более 100», не «≈120»; на каждое обходное движение нужна правка не «более 100», не «≈120»; на каждое обходное движение нужна правка правила, а
правила, а allowlist легитимных чисел становится вторым источником долга. allowlist легитимных чисел становится вторым источником долга. Худший исход —
Худший исход — не «шумно», а «вроде гейт, а толку нет»: срабатывания на пустом не «шумно», а «вроде гейт, а толку нет»: срабатывания на пустом месте
месте забейзелят, и через месяц правило перестанет ловить настоящие нарушения. забейзелят, и через месяц правило перестанет ловить настоящие нарушения.
Видимое блокирующее правило, которое на деле ничего не проверяет, хуже Видимое блокирующее правило, которое на деле ничего не проверяет, хуже
отсутствия правила. отсутствия правила.
- Соблюдение правила дешевле обхода: «список — в `docs/tools-map.md`» стоит - Соблюдение правила дешевле обхода: «список — в `docs/tools-map.md`» стоит
@@ -82,6 +82,19 @@
про парсер, а не молча перестанет что-либо проверять. Входит в обычный про парсер, а не молча перестанет что-либо проверять. Входит в обычный
`pnpm --dir web test`; новых гейтов и конфигов не добавляет. `pnpm --dir web test`; новых гейтов и конфигов не добавляет.
Граница автоматизации: парсер читает разделы «Реализовано», «Можно добавить» и
«Идеи», но проверяет их в разных направлениях. Раздел 1 сверяется с реестром
обеими сторонами (slug без страницы и страница без записи). Разделы 2 и 3
сверяются только в одну сторону: там slug'и намеренно несуществующие, поэтому
проверяется, что **ни один из них не является реальной страницей** —
реализованный инструмент, оставшийся в плане, роняет тест. Проверять «у плана
есть запись в реестре» нельзя по построению: список плановый, а не фактический.
Плановые разделы устроены прозой, поэтому их разбор другой: берутся все токены
нужной формы из всего буллита, а не только из «головы» до тире. Следствие для
авторов карты: реализованные страницы в планах называются прозой между
буллитами, а не внутри них.
## 5. Visual snapshots — не вводим ## 5. Visual snapshots — не вводим
`toHaveScreenshot` сравнивает страницу с PNG-baseline из прошлого коммита, а не `toHaveScreenshot` сравнивает страницу с PNG-baseline из прошлого коммита, а не
+118 -97
View File
@@ -16,13 +16,20 @@
> которые в бэклоге, здесь не дублируются: этот документ задаёт **порядок**, > которые в бэклоге, здесь не дублируются: этот документ задаёт **порядок**,
> backlog — **учёт** (`docs/backlog.md`). > backlog — **учёт** (`docs/backlog.md`).
> >
> **Инструменты в этом документе названы по адресам страниц (slug).** В коде у > **В этом документе инструменты названы по `id`** (`registry/tools/`), а не по
> страницы `slug` (`registry/pages/`), у вызываемого инструмента — `id` без > адресу страницы: поля и раскладка принадлежат схеме инструмента, а страница
> png-интента (`crop-png` → `id: "crop"`, `png-to-hsl` → `to-hsl`), модель — > только ссылается на список шагов (`Page.steps`), где сейчас ровно один
> `docs/architecture.md`, раздел 3. Названия из `docs/tools-audit.md` вида > инструмент, а в будущем может быть несколько. Поэтому «у страницы `crop-png`
> `color-wheel-generator`, `webp-to-png` в реестре отсутствуют (реальная > есть поля x/y» неверно — поля есть у инструмента `crop`, а страница лишь
> страница — `color-wheel-png`): при переносе пункта аудита в шаг сверять с > показывает его. `id` не несёт png-интента: страница `crop-png` вызывает
> `PAGES`. > `id: "crop"`, страница `png-to-hsl` — `id: "to-hsl"` (модель —
> `docs/architecture.md`, раздел 3).
>
> **Как переносить пункты аудита.** `docs/tools-audit.md` назван по страницам,
> потому что это то, что видит пользователь. При переносе пункта в шаг мапить
> slug → `id` через `page.steps[0].id` в `registry/pages/`. Названия аудита вида
> `color-wheel-generator` и `webp-to-png` в реестре отсутствуют (реальная
> страница — `color-wheel-png`, инструмент — `color-wheel`).
## 0. Ключевое решение: select → buttons ## 0. Ключевое решение: select → buttons
@@ -124,9 +131,9 @@ buttons: <V extends string>(s: Omit<ButtonsSpec<V>, "kind">): Field<V> => ({
- `web/src/lib/registry-schema.ts` — `NumberSpec.variant?: "seed"` - `web/src/lib/registry-schema.ts` — `NumberSpec.variant?: "seed"`
- `web/src/lib/components/fields/schema/RangeControl.svelte` — условный рендер - `web/src/lib/components/fields/schema/RangeControl.svelte` — условный рендер
- `web/src/lib/registry/filters.ts` — `randomizePixels`, `addNoise`: поле seed → - `web/src/lib/registry/tools/filters.ts` — `randomizePixels`, `addNoiseTool`:
`{ ...field.number(...), spec: { ...spec, variant: "seed" } }` (или поле seed → `{ ...field.number(...), spec: { ...spec, variant: "seed" } }`
пересоздать через `field.seed()`) (или пересоздать через `field.seed()`)
--- ---
@@ -138,63 +145,67 @@ buttons: <V extends string>(s: Omit<ButtonsSpec<V>, "kind">): Field<V> => ({
### FA-B1. geometry.ts ### FA-B1. geometry.ts
| Инструмент | Поле | Опции | | Инструмент | Поле | Опции |
| ----------------------- | -------- | ----------------------------------------- | | ------------------- | -------- | ----------------------------------------- |
| rotate-png | angle | 90° / 180° / 270° | | rotate | angle | 90° / 180° / 270° |
| flip-png | axis | horizontal / vertical | | flip | axis | horizontal / vertical |
| swap-orientation-png | target | portrait / landscape | | swap-orientation | target | portrait / landscape |
| symmetric-copy-png | axis | vertical / horizontal | | symmetric-copy | axis | vertical / horizontal |
| symmetric-copy-png | keepSide | left / right / top / bottom | | symmetric-copy | keepSide | left / right / top / bottom |
| change-aspect-ratio-png | ratio | 1:1 / 4:3 / 3:4 / 3:2 / 2:3 / 16:9 / 9:16 | | change-aspect-ratio | ratio | 1:1 / 4:3 / 3:4 / 3:2 / 2:3 / 16:9 / 9:16 |
| change-aspect-ratio-png | mode | crop / pad | | change-aspect-ratio | mode | crop / pad |
**Исключение:** `change-canvas-size-png` (anchor, 9 опций) — оставить **Исключение:** `change-canvas-size` (anchor, 9 опций) — оставить
`field.select()`, т.к. 9 кнопок в ряд не поместятся. Аналогично `position9` (уже `field.select()`, т.к. 9 кнопок в ряд не поместятся. Аналогично `position9` (уже
отдельный компонент `PositionControl`). отдельный компонент `PositionControl`).
### FA-B2. color.ts ### FA-B2. color.ts
| Инструмент | Поле | Опции | | Инструмент | Поле | Опции |
| ------------------------ | --------- | ------------------------------------ | | -------------------- | --------- | ---------------------------------------------------- |
| dithering-png | pattern | floyd-steinberg / bayer | | dithering | pattern | floyd-steinberg / bayer |
| extract-channel-png | channel | red / green / blue | | extract-channel | channel | red / green / blue |
| swap-channels-png | pair | r-g / r-b / g-b | | swap-channels | pair | r-g / r-b / g-b |
| decrease-color-count-png | maxColors | 2 / 4 / 8 / 16 / 32 / 64 / 128 / 256 | | decrease-color-count | maxColors | 2 / 4 / 8 / 16 / 32 / 44 / 64 / 96 / 128 / 192 / 256 |
**Исключение:** channel spaces (hsl/hsv/... component) — 3-6 опций, но это **Исключение:** инструменты разложения каналов (`to-hsl`, `to-hsv`, `to-hsi`,
`to-cmyk`, `to-ycbcr`, `to-lab`, поле `component`) — 3-6 опций, но это
динамические инструменты, оставить `select`. динамические инструменты, оставить `select`.
### FA-B3. analyze.ts ### FA-B3. analyze.ts
| Инструмент | Поле | Опции | | Инструмент | Поле | Опции |
| ------------------------- | ---- | ------------------ | | --------------------- | ---- | ------------------ |
| show-transparent-png | mode | binary / highlight | | show-transparent | mode | binary / highlight |
| show-grayscale-pixels-png | mode | (тот же) | | show-grayscale-pixels | mode | (тот же) |
| show-color-pixels-png | mode | (тот же) | | show-color-pixels | mode | (тот же) |
| light-pixel-mask-png | mode | (тот же) | | light-pixel-mask | mode | (тот же) |
| dark-pixel-mask-png | mode | (тот же) | | dark-pixel-mask | mode | (тот же) |
| unique-color-mask | mode | (тот же) |
Все используют общий `maskBaseFields` — правка в одном месте. Пять инструментов из шести берут `mode` из общего `maskBaseFields` — правка в
одном месте. Исключение — `show-transparent`: он переопределяет `mode` своими
опциями и дефолтом `highlight`, поэтому его `mode` правится отдельно.
### FA-B4. filters.ts ### FA-B4. filters.ts
| Инструмент | Поле | Опции | | Инструмент | Поле | Опции |
| ------------- | ---- | ------------ | | ---------- | ---- | ------------ |
| add-noise-png | mode | mono / color | | add-noise | mode | mono / color |
### FA-B5. generate.ts ### FA-B5. generate.ts
| Инструмент | Поле | Опции | | Инструмент | Поле | Опции |
| ------------------ | ------------ | --------------------- | | -------------- | ------------ | --------------------- |
| color-spectrum-png | direction | horizontal / vertical | | color-spectrum | direction | horizontal / vertical |
| step-colors-png | layout | grid / strip | | step-colors | layout | grid / strip |
| complementary-png | layout | grid / strip | | complementary | layout | grid / strip |
| triadic-png | layout | grid / strip | | triadic | layout | grid / strip |
| tetradic-png | layout | grid / strip | | tetradic | layout | grid / strip |
| analogous-png | layout | grid / strip | | analogous | layout | grid / strip |
| monochromatic-png | layout | grid / strip | | monochromatic | layout | grid / strip |
| shades-png | layout | grid / strip | | shades | layout | grid / strip |
| sort-colors-png | layout | grid / strip | | sort-colors | layout | grid / strip |
| mix-colors-png | (нет select) | — | | mix-colors | (нет select) | — |
--- ---
@@ -203,12 +214,12 @@ buttons: <V extends string>(s: Omit<ButtonsSpec<V>, "kind">): Field<V> => ({
### FA-C1. Source-aware defaults для dimension-полей — реализовано ### FA-C1. Source-aware defaults для dimension-полей — реализовано
Source-aware defaults реализованы: общий resolver в `registry-schema` Source-aware defaults реализованы: общий resolver в `registry-schema`
(`applySourceDefaults`). Для `resize-png` и `crop-png` дефолт после загрузки (`applySourceDefaults`). Для `resize` и `crop` дефолт после загрузки равен
равен размерам текущего source; до загрузки используется положительный fallback размерам текущего source; до загрузки используется положительный fallback `1×1`.
`1×1`. Один-sided `0` у resize сохраняет режим auto. Проверено тестом Один-sided `0` у resize сохраняет режим auto. Проверено тестом
`registry.test.ts` («resize и crop получают размеры текущего source»). `registry.test.ts` («resize и crop получают размеры текущего source»).
### FA-C2. symmetric-copy-png: "keep side" не работает ### FA-C2. symmetric-copy: "keep side" не работает
Баг в `run()`: `keepSide` не учитывает `axis`. При `axis: "vertical"` (удвоение Баг в `run()`: `keepSide` не учитывает `axis`. При `axis: "vertical"` (удвоение
ширины) работают только `left`/`right`, `top`/`bottom` не имеют смысла. Нужно: ширины) работают только `left`/`right`, `top`/`bottom` не имеют смысла. Нужно:
@@ -257,55 +268,63 @@ resolver.
### FA-C4. verify-is-png: неверное название — решение принято, шаг не выполнен ### FA-C4. verify-is-png: неверное название — решение принято, шаг не выполнен
Проверяет текст, не PNG-файл. Варианты: Находка про **страницу**, а не про инструмент: страница `verify-is-png` обещает
проверку PNG-файла, а проверяет текст (base64 / data-uri). Сам инструмент назван
честно — `id: "verify-png"`, png-интент в нём означает входные данные. Адрес
страницы — `slug` `verify-is-png` (`docs/architecture.md`, раздел 3), поэтому
«неверное название» относится к `slug`, а не к `id`.
- `title` → "Verify PNG data", `description` → уточнить; `id` оставить — он же Варианты:
адрес страницы, переименование ломает ссылку
- либо полное переименование `id` → `verify-is-png-data` с редиректом со старого
URL (нужен вордстат, иначе страница выпадает из индекса)
**Решение:** поменять title и description, id не трогать. Переименование `id` - `title` → «Verify PNG data», `description` → уточнить; `slug` и `id` не
затрагивает адрес — с ним связана задача разведения `id`/`slug` трогать
(`docs/backlog.md`), поэтому в одиночку не делается. - либо переименовать `slug` → `verify-png-data` с редиректом со старого URL
(нужен вордстат, иначе страница выпадает из индекса)
**Решение:** поменять title и description, имя страницы не трогать.
Переименование `slug` меняет адрес, имя файла, ключи `pages.*` и иконку, то есть
это операция с SEO-последствиями, а не строчка в реестре — она не делается в
одиночку, вместе с решением по `docs/plan-seo.md` §5.
--- ---
## 4. Волна D — обязательные фиксы (новые параметры) ## 4. Волна D — обязательные фиксы (новые параметры)
### FA-D1. remove-alpha-channel-png: выбор цвета фона ### FA-D1. remove-alpha-channel: выбор цвета фона
Сейчас захардкожен `#ffffff`. Добавить `field.color({ default: "#ffffff" })`. Сейчас захардкожен `#ffffff`. Добавить `field.color({ default: "#ffffff" })`.
Файл: `alpha.ts`, схема `removeAlphaChannelSchema`. Файл: `alpha.ts`, схема `removeAlphaChannelSchema`.
### FA-D2. extract-alpha-mask-png: галочка «инвертировать» ### FA-D2. extract-alpha-mask: галочка «инвертировать»
Добавить `field.checkbox({ label: "fields.invertMask", default: false })`. В Добавить `field.checkbox({ label: "fields.invertMask", default: false })`. В
`run()`: если `invert`, вызвать `invertAlpha()` после `extractAlphaMask()`. `run()`: если `invert`, вызвать `invertAlpha()` после `extractAlphaMask()`.
Файл: `alpha.ts`. Файл: `alpha.ts`.
### FA-D3. change-canvas-size-png: position9 вместо select ### FA-D3. change-canvas-size: position9 вместо select
`anchor` сейчас `field.select()` с 9 опциями → `field.position9()`. `anchor` сейчас `field.select()` с 9 опциями → `field.position9()`.
Файл: `geometry.ts`. Уже есть `PositionControl`. Файл: `geometry.ts`. Уже есть `PositionControl`.
### FA-D4. Generators: добавить height ### FA-D4. Generate: добавить height
Инструменты без `size`/`height`: Инструменты без `size`/`height`:
- blend-two-png — добавить - blend-two — добавить
`height: field.slider({ min: 1, max: 1024, default: 512 })` `height: field.slider({ min: 1, max: 1024, default: 512 })`
- step-colors-png — аналогично - step-colors — аналогично
- complementary, triadic, tetradic, analogous, monochromatic, shades, - complementary, triadic, tetradic, analogous, monochromatic, shades — все
sort-colors — все используют `paletteBaseSchema` (общий объект), добавить используют `paletteBaseSchema` (общий объект), добавить `height` туда
`height` туда - `sort-colors` — своя схема, `paletteBaseSchema` не использует, `height`
- mix-colors-png — добавить height добавлять вручную
- mix-colors — добавить height
Файл: `generate.ts`. Файл: `generate.ts`.
### FA-D5. add-border-png: прозрачность цвета ### FA-D5. add-border: прозрачность цвета
Текущий `field.color()` не поддерживает alpha. Пока что: оставить как есть Текущий `field.color()` не поддерживает alpha. Пока что: оставить как есть
(прозрачность не поддерживается нативным color picker). Отметить в аудите как (прозрачность не поддерживается нативным color picker). Отметить в аудите как
@@ -322,24 +341,24 @@ blocked.
Файл: `SchemaToolView.svelte` — показать подсказку если `schema.fields` пуст. Файл: `SchemaToolView.svelte` — показать подсказку если `schema.fields` пуст.
### FA-E2. quantize-png: пресеты ### FA-E2. quantize: пресеты
`field.slider` с max 64 → добавить пресеты (8, 16, 32, 64) как кнопки под `field.slider` с max 64 → добавить пресеты (8, 16, 32, 64) как кнопки под
слайдером. Либо `field.buttons()` с 4 опциями вместо слайдера. слайдером. Либо `field.buttons()` с 4 опциями вместо слайдера.
### FA-E3. trim-empty-space-png: 0-254 → проценты ### FA-E3. trim-empty-space: 0-254 → проценты
Текущий `min: 0, max: 254` — нестандартно. Добавить переключатель px/% (аналог Текущий `min: 0, max: 254` — нестандартно. Добавить переключатель px/% (аналог
шага FA-A1 — `variant: "alpha-threshold"` в NumberSpec). шага FA-A1 — `variant: "alpha-threshold"` в NumberSpec).
### FA-E4. add-text-png: plate offset ### FA-E4. add-text: plate offset
Plate снизу имеет большой отступ — добавить галочку "compact plate" или Plate снизу имеет большой отступ — добавить галочку "compact plate" или
уменьшить дефолтный padding. уменьшить дефолтный padding.
### FA-E5. emoji-to-png: выбор эмодзи ### FA-E5. from-emoji: выбор эмодзи
Поле `text` → добавить группу часто используемых эмодзи как кнопки- пресеты над Поле `emoji` → добавить группу часто используемых эмодзи как кнопки-пресеты над
полем ввода. полем ввода.
--- ---
@@ -365,44 +384,46 @@ Plate снизу имеет большой отступ — добавить г
round-corners — px→%): нужен компонент переключателя единиц в слайдере. round-corners — px→%): нужен компонент переключателя единиц в слайдере.
Требует правки `RangeControl` + всех схем — в волны A–E не входит, нужен Требует правки `RangeControl` + всех схем — в волны A–E не входит, нужен
отдельный этап. отдельный этап.
- **Smoothing checkbox** для масок (circle, square, star, wavy, round-corners, - **Smoothing checkbox** для масок (circle-mask, square-mask, star-mask,
invert-alpha): нужен `field.checkbox("smoothing")` + правка core (shapes.ts, wavy-mask, round-corners, invert-alpha): нужен `field.checkbox("smoothing")` +
alpha). Отложен — требует визуального тестирования. правка core (shapes.ts, alpha). Отложен — требует визуального тестирования.
- **Vignette**: выбор центра + цвет + feather — 3 новых параметра. Требует - **Vignette**: выбор центра + цвет + feather — 3 новых параметра. Требует
normative решения по UI. normative решения по UI.
- **Remove-background**: выбор точки удаления — либо параметр, либо отдельный - **Remove-background**: выбор точки удаления — либо параметр, либо отдельный
инструмент. инструмент.
- **Precision/quality**: select в resize (bilinear/bicubic/nearest) — нужен ли - **Метод масштабирования в `resize`**: поля в схеме нет (только `size` и
выбор алгоритма? `keepAspect`), поэтому это уже не правка селекта, а новое поле — нужен ли
выбор алгоритма (bilinear/bicubic/nearest)?
- **Placeholders**: "нет параметров" — какой текст/компонент показывать. - **Placeholders**: "нет параметров" — какой текст/компонент показывать.
### 7.2 Операционные (тест-кейсы, описания) ### 7.2 Операционные (тест-кейсы, описания)
- find-contour-png: расширять на толщину линии (требует правки core) - find-contour: расширять на толщину линии (требует правки core)
- make-thicker-png: расширять картинку на толщину линии - make-thicker: расширять картинку на толщину линии
- make-thinner-png: уменьшать от краев, fix выступа - make-thinner: уменьшать от краев, fix выступа
- feather-edges-png: расширять картинку на толщину линии - feather-edges: расширять картинку на толщину линии
- clean-edges-png: найти тест-кейс, возможно увеличить максимум - clean-edges: найти тест-кейс, возможно увеличить максимум
- despeckle-alpha-png: найти более заметный тест-кейс - despeckle-alpha: найти более заметный тест-кейс
- close-holes-png: проверить поведение на больших полупрозрачных областях - close-holes: проверить поведение на больших полупрозрачных областях
- auto-contrast-png: найти тестовый кейс - auto-contrast: найти тестовый кейс
- sharpen-png: добавить тест-кейс - sharpen: добавить тест-кейс
- center-by-alpha-png: найти тест-кейс - center-by-alpha: найти тест-кейс
- png-is-grayscale: улучшить видимость результата - is-grayscale: улучшить видимость результата
### 7.3 Дизайн-вопросы ### 7.3 Дизайн-вопросы
- add-padding-png vs add-border-png: разница неочевидна, нужен review - add-padding vs add-border: разница неочевидна, нужен review
- change-aspect-ratio-png: не видно что меняется — рамка результата - change-aspect-ratio: не видно что меняется — рамка результата
- watermark-tile-png: заполняет не полностью - watermark-tile: заполняет не полностью
- sepia-png: нужна ли настройка силы эффекта? - sepia: нужна ли настройка силы эффекта?
- posterize-png: настройка для каждого канала отдельно? - posterize: настройка для каждого канала отдельно?
- pixelate-png: разные алгоритмы? - pixelate: разные алгоритмы?
- swap-channels-png: обдумать flow через более простые инструменты - swap-channels: обдумать flow через более простые инструменты
### 7.4 Аудит полей (компоненты) ### 7.4 Аудит полей (компоненты)
- Range slider: поле ввода + кнопки +/- + кнопка сброса - Range slider: поле ввода + кнопки +/- + кнопка сброса
- Color picker: пипетка с картинки, прозрачность - Color picker: пипетка с картинки, прозрачность
- Width/height с keep-aspect-ratio: единый компонент - Width/height с keep-aspect-ratio: единый компонент
- Position offset: отдельные слайдеры x/y (уже сделано в `OffsetControl`) - Position offset: отдельные числовые поля X/Y (уже сделано в
`fields/schema/OffsetControl.svelte`)
+23 -19
View File
@@ -3,7 +3,7 @@
> **Статус: active.** Этот план добавляет к `docs/roadmap.md` то, чего там нет: > **Статус: active.** Этот план добавляет к `docs/roadmap.md` то, чего там нет:
> **API** (фаза 9, возможная монетизация) и **PWA** (фаза 10, оффлайн + > **API** (фаза 9, возможная монетизация) и **PWA** (фаза 10, оффлайн +
> установка), плюс шрифты (раздел 4 — частично относится к фазе 8, поэтому > установка), плюс шрифты (раздел 4 — частично относится к фазе 8, поэтому
> решения-phase 8 здесь дублируются). WASM-ядро, эталоны и CLI — фазы 2–8 > решения фазы 8 здесь дублируются). WASM-ядро, эталоны и CLI — фазы 2–8
> `docs/roadmap.md`, они здесь не пересказываются. Оба трека стартуют после > `docs/roadmap.md`, они здесь не пересказываются. Оба трека стартуют после
> wasm-фаз; PWA не ждёт API и может идти раньше. > wasm-фаз; PWA не ждёт API и может идти раньше.
@@ -11,19 +11,20 @@
- **Одно ядро — три потребителя.** Rust-крейт `core/` (roadmap фаза 4) даёт: - **Одно ядро — три потребителя.** Rust-крейт `core/` (roadmap фаза 4) даёт:
wasm-пакет для сайта (фаза 5), нативный CLI (фаза 6), серверный слой API (фаза wasm-пакет для сайта (фаза 5), нативный CLI (фаза 6), серверный слой API (фаза
9). Единый контракт — JSON-пайплайн `{tool, params}` (заложен в roadmap §0, 9). Единый контракт — JSON-пайплайн `{id, params}` (заложен в roadmap §0 и
реализуется в фазе 11); эталонные файлы и harness (фазы 2, 3, 7) гарантируют совпадает с `Page.steps`, реализуется в фазе 11); эталонные файлы и harness
одинаковый результат во всех рантаймах. (фазы 2, 3, 7) гарантируют одинаковый результат во всех рантаймах.
- **API — нативный Rust на сервере, не wasm.** В браузере wasm нужен ради - **API — нативный Rust на сервере, не wasm.** В браузере wasm нужен ради
доставки и песочницы; на сервере крейт линкуется напрямую — быстрее и проще. доставки и песочницы; на сервере крейт линкуется напрямую — быстрее и проще.
Исключение — edge-воркеры (см. спайк ниже): там исполняется тот же Исключение — edge-воркеры (см. спайк ниже): там исполняется тот же
wasm-артефакт фазы 5. wasm-артефакт фазы 5.
- **PWA не трогает SEO-схему.** Сайт остаётся полностью prerendered-статикой; - **PWA не трогает SEO-схему.** Сайт остаётся полностью prerendered-статикой;
service worker добавляет оффлайн-слой поверх и не отвечает за HTML ботовам. service worker добавляет оффлайн-слой поверх и не отвечает за HTML ботовам.
- **Монетизация не ломает позиционирование.** Браузерные инструменты — бесплатны - **Монетизация не ломает позиционирование.** Браузерные инструменты —
и без трекинга; API — отдельный продукт для автоматизации/бизнеса. Файлы в API бесплатные, картинка обрабатывается в браузере и никуда не отправляется.
обрабатываются в памяти: без хранения и логирования содержимого — продолжение Счётчик посещений GA4 этому не противоречит (границы — `docs/plan-seo.md` §0).
«privacy»-истории. API — отдельный продукт для автоматизации/бизнеса. Файлы в API обрабатываются
в памяти: без хранения и логирования содержимого.
## 1. Фаза 9 — API (после фаз 5–7) ## 1. Фаза 9 — API (после фаз 5–7)
@@ -40,7 +41,7 @@
запросам/размеру файла. Только после стабильного free tier. запросам/размеру файла. Только после стабильного free tier.
- [ ] **[A5]** Документация как лендинг: `/api` — индексируемая страница - [ ] **[A5]** Документация как лендинг: `/api` — индексируемая страница
(туториал, примеры curl/JS), OpenAPI, ссылка из `llms.txt`. Dev-аудитория (туториал, примеры curl/JS), OpenAPI, ссылка из `llms.txt`. Dev-аудитория
— источник бэклинков (план-seo S4c). — источник бэклинков (`docs/plan-seo.md`, S4c).
- Гейт: один и тот же пайплайн даёт пиксельно идентичный результат в браузере, - Гейт: один и тот же пайплайн даёт пиксельно идентичный результат в браузере,
CLI и API (harness фазы 7 расширяется третьим рантаймом). CLI и API (harness фазы 7 расширяется третьим рантаймом).
@@ -68,17 +69,17 @@
фича — фаза 11 того же документа. фича — фаза 11 того же документа.
- PWA — после S1 `plan-seo.md`; API — после фаз 5–7. - PWA — после S1 `plan-seo.md`; API — после фаз 5–7.
- Синергия с дистрибуцией: «works offline, installable» и публичный API — - Синергия с дистрибуцией: «works offline, installable» и публичный API —
аргументы для Product Hunt / alternative.to / GitHub (plan-seo S4c) и для аргументы для Product Hunt / alternative.to / GitHub (`docs/plan-seo.md`, S4c)
GEO-текстов (`llms.txt`, описания категорий). и для GEO-текстов (`llms.txt`, описания категорий).
## 4. Шрифты: лицензирование и DOM-free рендер текста ## 4. Шрифты: лицензирование и DOM-free рендер текста
Текст — один из canvas-зависимых классов, которые roadmap (фаза 8) держит в TS Текст — один из canvas-зависимых классов, которые roadmap (фаза 8) держит в TS
(там их четыре: текст, градиенты, штампы, `ctx.filter`); для текста выбран курс (перечень — там же); для текста выбран курс на реимплементацию, остальные классы
на реимплементацию, остальные классы не переносятся. Для CLI/API текст всё равно не переносятся. Для CLI/API текст всё равно придётся рендерить в Rust, поэтому
придётся рендерить в Rust, поэтому решение: **перевести text-инструменты в решение: **перевести text-инструменты в ядро** (в браузере canvas-рендер
ядро** (в браузере canvas-рендер остаётся TS-фолбэком для A/B). Два вопроса — остаётся TS-фолбэком для A/B). Два вопроса — легальность шрифтов в дистрибутиве
легальность шрифтов в дистрибутиве и рендер без DOM. и рендер без DOM.
**Когда начинать:** блока F1–F4 нет — они не привязаны к фазе roadmap, но F5/F6 **Когда начинать:** блока F1–F4 нет — они не привязаны к фазе roadmap, но F5/F6
без них бессмысленны (нечего рендерить). Реализуется всё это в фазе 8, поэтому без них бессмысленны (нечего рендерить). Реализуется всё это в фазе 8, поэтому
@@ -102,15 +103,18 @@
`pnpm verify` по правилам `docs/quality-gates.md` (решение о fail-режиме — `pnpm verify` по правилам `docs/quality-gates.md` (решение о fail-режиме —
`docs/decisions.md`). Браузер грузит шрифты из бандла, не из ОС и не с `docs/decisions.md`). Браузер грузит шрифты из бандла, не из ОС и не с
`fonts.googleapis.com`-CDN (сторонний запрос = утечка IP пользователя, `fonts.googleapis.com`-CDN (сторонний запрос = утечка IP пользователя,
противоречит «no tracking»), — одинаковый результат у всех пользователей и лишняя зависимость от чужой инфраструктуры), — одинаковый результат у всех
во всех рантаймах. пользователей и во всех рантаймах.
- [ ] **[F4]** Кириллица: приоритет семействам с полным покрытием — Liberation - [ ] **[F4]** Кириллица: приоритет семействам с полным покрытием — Liberation
Sans, PT Sans/PT Serif, Noto Sans, Inter, Roboto (OFL/Apache), DejaVu Sans Sans, PT Sans/PT Serif, Noto Sans, Inter, Roboto (OFL/Apache), DejaVu Sans
(пермиссивная лицензия Bitstream Vera: редистрибуция и коммерческое (пермиссивная лицензия Bitstream Vera: редистрибуция и коммерческое
использование разрешены) — закрывает и backlog-вопрос про «только с использование разрешены) — закрывает и backlog-вопрос про «только с
кириллицей». кириллицей».
- **Стартовый набор** (все OFL/Apache, с кириллицей; состав не догма — - **Стартовый набор** (все OFL/Apache, с кириллицей; состав не догма —
расширяется по запросам, каждый проходит паспорт F3): расширяется по запросам, каждый проходит паспорт F3). Сейчас в бандле сайта
лежат `@fontsource/ibm-plex-sans` и `@fontsource/ibm-plex-mono` (тоже OFL) — в
наборе ниже их нет, и при введении паспорта F3 их судьбу нужно решить явно:
оставить или заменить.
- базовый sans: **Inter** (дефолт add-text), **PT Sans** / **Golos Text** - базовый sans: **Inter** (дефолт add-text), **PT Sans** / **Golos Text**
(профессиональная русская типографика), **Noto Sans** (фолбэк покрытия); (профессиональная русская типографика), **Noto Sans** (фолбэк покрытия);
- serif: **PT Serif**, **Lora**, **Playfair Display** (display); - serif: **PT Serif**, **Lora**, **Playfair Display** (display);
+58 -37
View File
@@ -24,8 +24,9 @@
> схему сайта они не меняют. > схему сайта они не меняют.
> >
> **Слаги в этом документе:** `jpg-to-png`, `crop-jpeg` и подобное — интенты и > **Слаги в этом документе:** `jpg-to-png`, `crop-jpeg` и подобное — интенты и
> URL конкурента, а не id наших инструментов. Наши id — в `docs/tools-map.md`; в > URL конкурента, а не наши страницы. Наши страницы — в `docs/tools-map.md`
> тексте ниже реальные id инструментов приводятся в бэктиках. > (карта перечисляет `slug`, не `id`); в тексте ниже названия инструментов
> приводятся по этим слагам, потому что для SEO важны адреса.
## 0. Решения ## 0. Решения
@@ -72,9 +73,14 @@
`registry/pages/`. Схему не менять без отдельного SEO-плана: после индексации `registry/pages/`. Схему не менять без отдельного SEO-плана: после индексации
переименование URL означает 301 и потерю позиций. Разделение инструмента и переименование URL означает 301 и потерю позиций. Разделение инструмента и
страницы (решение выше) адресов не меняет. страницы (решение выше) адресов не меняет.
- **Без GA и рекламных скриптов:** аналитика — self-hosted Plausible/Umami или - **Аналитика — GA4, в режиме «только агрегаты».** GA4 уже стоит в проде
Cloudflare Web Analytics. «No tracking» — часть позиционирования, её нельзя (`web/src/app.html`, prerender-оболочка, значит на каждой странице). Решение —
подрывать собственными скриптами. не выпиливать, а ограничить: cookieless-конфигурация, без рекламных audiences,
signals и ремаркетинга. Граница приватности проходит по данным изображения: GA
не видит ни файла, ни его содержимого, ни результата инструмента — только факт
открытия страницы. **Рекламу и ремаркетинг не ставим** — они противоречат
позиционированию, в отличие от счётчика посещений. Формулировку «без трекинга»
на публичных страницах не использовать: она теперь неточна, см. §1 и S4b.
- **Честные методы.** Никакого клонированного текста, дорвеев, PBN. Контент — по - **Честные методы.** Никакого клонированного текста, дорвеев, PBN. Контент — по
каждому инструменту, с ручной вычиткой. каждому инструменту, с ручной вычиткой.
@@ -105,10 +111,10 @@
| `rel=canonical` | нет | S1 | | `rel=canonical` | нет | S1 |
| OG/Twitter-теги, OG-картинки | нет | S1 | | OG/Twitter-теги, OG-картинки | нет | S1 |
| Канонический домен в абсолютных URL | нет, деплой на GitHub Pages с базой | S1 | | Канонический домен в абсолютных URL | нет, деплой на GitHub Pages с базой | S1 |
| Контент страницы (H1-текст, how-to, FAQ, related) | только title/description | S2 | | Контент страницы (how-to, FAQ, related) | title, robots и отрендеренный H1 | S2 |
| JSON-LD (SoftwareApplication, FAQPage, Breadcrumb) | нет | S3 | | JSON-LD (SoftwareApplication, FAQPage, Breadcrumb) | нет | S3 |
| `llms.txt`, хабы категорий с текстом | нет | S3 | | `llms.txt`, хабы категорий с текстом | нет | S3 |
| GSC + приватная аналитика | нет | S4 | | GSC + доводка GA до cookieless-конфигурации | GA4 стоит, GSC не подключён | S4 |
Строка `noindex` — первая не потому, что её сложнее всего, а потому, что она Строка `noindex` — первая не потому, что её сложнее всего, а потому, что она
блокирует всё остальное: пока тег стоит, остальные строки таблицы не дают блокирует всё остальное: пока тег стоит, остальные строки таблицы не дают
@@ -150,7 +156,7 @@
ссылок на инструменты. Но это этап S3e и только после того, как написано ядро ссылок на инструменты. Но это этап S3e и только после того, как написано ядро
S2: транзакционная страница приносит конверсию, информационная — только S2: транзакционная страница приносит конверсию, информационная — только
трафик. Тонкие FAQ-страницы «про всё» не заводить — несколько сильных трафик. Тонкие FAQ-страницы «про всё» не заводить — несколько сильных
материалов с ссылками на 3–5 инструментов каждый. материалов с ссылками на несколько связанных инструментов.
6. **Дистрибуция.** GitHub-репо (звёзды/форки = сигналы), alternative.to, 6. **Дистрибуция.** GitHub-репо (звёзды/форки = сигналы), alternative.to,
Product Hunt, Reddit по интенту — реальный путь набрать первые бэклинки без Product Hunt, Reddit по интенту — реальный путь набрать первые бэклинки без
бюджетов. бюджетов.
@@ -175,16 +181,20 @@
- [ ] **[S1e]** OG-картинки per-tool: генерация PNG-карточек скриптом на - [ ] **[S1e]** OG-картинки per-tool: генерация PNG-карточек скриптом на
существующем ядре (мы сами умеем рисовать PNG) — имя инструмента + бренд, существующем ядре (мы сами умеем рисовать PNG) — имя инструмента + бренд,
файл `static/og/[slug].png`, ссылка из `Seo.svelte`. Относительные пути файл `static/og/[slug].png`, ссылка из `Seo.svelte`. Путь к картинке
(`/og/crop-png.png`) работают без домена; при деплое на собственном домене строить через `resolve()` из `$app/paths` (как favicon в
`+layout.svelte`), а не литералом `/og/…`: под `BASE_PATH` корневой путь
ломается. Домен для этого не нужен; при деплое на собственном домене
OG-картинки будут абсолютными и корректными. OG-картинки будут абсолютными и корректными.
- [ ] **[S1f]** Download-кнопка с селектором формата PNG/JPG/WebP/BMP (+quality - [ ] **[S1f]** Download-кнопка с селектором формата PNG/JPG/WebP/BMP (+quality
для lossy) для всех инструментов — механика `Tool.output` уже есть для lossy) для всех инструментов — механика `Tool.output` уже есть
(`docs/backlog.md`, «Идеи» №6, UX-райз «Download»). Продуктовая основа (`docs/backlog.md`, «Идеи» №6, UX-райз «Download»). Продуктовая основа
«image tools»-позиционирования: без неё сайт остаётся PNG-сайтом. Вести «image tools»-позиционирования: без неё сайт остаётся PNG-сайтом. Вести
параллельно с текущими quality-задачами. Побочно снимает `quality` из схем параллельно с текущими quality-задачами. Побочно снимает `quality` из схем
страниц — после этого 14 convert-страниц сводятся на один инструмент **конвертеров** — после этого convert-страницы сводятся на один инструмент
(`docs/backlog.md`). (`docs/backlog.md`). Не из всех схем: у `jpeg-artifacts` `quality` —
функциональное поле (сила артефактов), а `output` у инструмента нет, так
что селектор формата его не покрывает.
- [ ] **[S1g]** Поведение конвертеров после S1f: не предлагать в меню форматов - [ ] **[S1g]** Поведение конвертеров после S1f: не предлагать в меню форматов
формат исходника (disable) — чтобы `convert-png-to-jpg` не мог сохранить в формат исходника (disable) — чтобы `convert-png-to-jpg` не мог сохранить в
png. Существующие страницы не трогать: `convert-png-to-jpg`, png. Существующие страницы не трогать: `convert-png-to-jpg`,
@@ -198,10 +208,12 @@
`https://<юзер>/<repo>/…` на корневые URL (301). Делается одновременно с `https://<юзер>/<repo>/…` на корневые URL (301). Делается одновременно с
S1d (canonical), чтобы Google проиндексировал сразу с правильным доменом. 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/` из `PAGES` реестра (`web/src/lib/registry`) + статических
страниц. Вызов из `pnpm build` после `vite build` (adapter-static пишет в страниц. Взять адреса у страниц, а не у инструментов: страниц может быть
`build/`, дописываем файл там же). Origin — из env (`PUBLIC_SITE_ORIGIN`), несколько на один инструмент, у инструмента адреса нет. Вызов из
учёт опционального `BASE_PATH`. `pnpm build` после `vite build` (adapter-static пишет в `build/`,
дописываем файл там же). Origin — из env (`PUBLIC_SITE_ORIGIN`), учёт
опционального `BASE_PATH`.
- [ ] **[S1c]** `robots.txt`: `Sitemap:` + закрыть служебный `/kit`; после - [ ] **[S1c]** `robots.txt`: `Sitemap:` + закрыть служебный `/kit`; после
cleanup — вернуть минимальный allow-all. cleanup — вернуть минимальный allow-all.
- [ ] **[S1d]** Компонент `Seo.svelte` (kit): canonical (абсолютный URL по - [ ] **[S1d]** Компонент `Seo.svelte` (kit): canonical (абсолютный URL по
@@ -211,9 +223,9 @@
> **Порядок S1:** S1e/S1f/S1g → покупка домена → S1a+S1b+S1c+S1d → **S1h** > **Порядок S1:** S1e/S1f/S1g → покупка домена → S1a+S1b+S1c+S1d → **S1h**
> (снятие `noindex`) → S2-контент. Гейты: `pnpm --dir web build` зелёный; в > (снятие `noindex`) → S2-контент. Гейты: `pnpm --dir web build` зелёный; в
> `build/` лежит корректный `sitemap.xml` со всеми инструментами; canonical/OG > `build/` лежит корректный `sitemap.xml` со всеми страницами инструментов;
> присутствуют на всех страницах; `noindex` отсутствует в собранном HTML > canonical/OG присутствуют на всех страницах; `noindex` отсутствует в собранном
> (`pnpm --dir web build` и проверка `build/tools/*/index.html`); > HTML (`pnpm --dir web build` и проверка `build/tools/*/index.html`);
> `pnpm --dir web test:e2e` — по правилам `docs/quality-gates.md`, в CI job > `pnpm --dir web test:e2e` — по правилам `docs/quality-gates.md`, в CI job
> `e2e` non-blocking, поэтому его статус читается по отдельному job > `e2e` non-blocking, поэтому его статус читается по отдельному job
> (`docs/decisions.md`, раздел 10). > (`docs/decisions.md`, раздел 10).
@@ -229,9 +241,9 @@
(`ol`), описание параметров (из схемы), FAQ, related tools (перелинковка (`ol`), описание параметров (из схемы), FAQ, related tools (перелинковка
внутри категории + 2–3 «соседних» интента). внутри категории + 2–3 «соседних» интента).
- [ ] **[S2c]** Хлебные крошки (Каталог → Категория → Инструмент) в UI. - [ ] **[S2c]** Хлебные крошки (Каталог → Категория → Инструмент) в UI.
- [ ] **[S2d]** Первая волна контента — топ-30 инструментов ядра (приоритет по - [ ] **[S2d]** Первая волна контента — топ приоритетных инструментов ядра
вордстату: transparent png, jpg→png, compress, resize, crop, rotate, (приоритет по вордстату: transparent png, jpg→png, compress, resize, crop,
circle crop, add text, pixelate, grayscale…). rotate, circle crop, add text, pixelate, grayscale…); отбор — §5.
- Гейты: на каждой странице инструмента — уникальный текст ≥ 300 слов, H1 - Гейты: на каждой странице инструмента — уникальный текст ≥ 300 слов, H1
совпадает с интентом, FAQ ≥ 3, related ≥ 3; текст в статике (prerender), не в совпадает с интентом, FAQ ≥ 3, related ≥ 3; текст в статике (prerender), не в
рантайме. рантайме.
@@ -266,8 +278,11 @@
- [ ] **[S4a]** Google Search Console: собственность на домен, отправка sitemap, - [ ] **[S4a]** Google Search Console: собственность на домен, отправка sitemap,
мониторинг индексации и запросов. Бесплатно и обязательно. мониторинг индексации и запросов. Бесплатно и обязательно.
- [ ] **[S4b]** Аналитика без трекинга: self-hosted Plausible/Umami либо - [ ] **[S4b]** Довести GA4 до решения §0: cookieless-конфигурация, отключённые
Cloudflare Web Analytics. GA/ЯМ не ставим (позиционирование). рекламные audiences и signals, `anonymize_ip`, запрет на передачу
параметров с данными картинки. Плюс отсечь мусорные попадания:
prerender-оболочка грузит тег и на локальных сборках, их нужно отсекать
флагом окружения. GA/ЯМ как замена счётчику не рассматриваем.
- [ ] **[S4c]** Дистрибуция: закрепить GitHub-репо (README со скриншотами), - [ ] **[S4c]** Дистрибуция: закрепить GitHub-репо (README со скриншотами),
подача на alternative.to (страница против конкурента), Product Hunt, подача на alternative.to (страница против конкурента), Product Hunt,
тематические Reddit/HN-треды. Бэклинки — главный дефицит для конкуренции. тематические Reddit/HN-треды. Бэклинки — главный дефицит для конкуренции.
@@ -279,19 +294,23 @@
## 5. Приоритизация инструментов (первая волна контента) ## 5. Приоритизация инструментов (первая волна контента)
Источник — `docs/tools-map.md` (наш состав и замер по конкуренту — §2). Ids ниже Источник — `docs/tools-map.md` (наш состав и замер по конкуренту — §2). Слаги
— **фактические id реестра**; первая волна — топ-интенты, где конкуренты ниже — **фактические `slug` страниц** из `docs/tools-map.md` §1; первая волна —
ранжируются, а у нас уже есть инструмент и он не хуже: топ-интенты, где конкуренты ранжируются, а у нас уже есть инструмент и он не
хуже:
- прозрачность/фон: create-empty-png, remove-background-png, change-png-opacity, - прозрачность/фон: create-empty-png, remove-background-png, change-png-opacity,
set-alpha-channel-png set-alpha-channel-png, round-corners-png
- конвертация: convert-png-to-jpg, convert-png-to-webp, png-to-bmp, svg-to-png, - конвертация: convert-png-to-jpg, convert-png-to-webp, png-to-bmp, svg-to-png,
png-to-base64 png-to-base64
- геометрия: crop-png, resize-png, rotate-png, round-corners-png, flip-png, - геометрия: crop-png, resize-png, rotate-png, flip-png, change-aspect-ratio-png
change-aspect-ratio-png - цвет/фильтры: grayscale-png, invert-colors-png, pixelate-png
- цвет/фильтры: grayscale-png, invert-colors-png, pixelate-png, add-text-png, - текст: add-text-png, watermark-tile-png
watermark-tile-png - генерация: linear-gradient-png
- генерация: linear-gradient-png, create-empty-png
> Группы здесь — по категории страницы в реестре (`registry/pages/`), а не по
> смыслу: `round-corners-png` лежит в `alpha`, `add-text-png` и
> `watermark-tile-png` — в `text`.
> **Открытый вопрос по интентам «из формата в PNG».** У конкурента есть > **Открытый вопрос по интентам «из формата в PNG».** У конкурента есть
> `jpg-to-png`, `webp-to-png`, `gif-to-png` как отдельные страницы; у нас таких > `jpg-to-png`, `webp-to-png`, `gif-to-png` как отдельные страницы; у нас таких
@@ -380,9 +399,10 @@ with PNG, JPG, WebP» в тексте + селектор формата в Downl
- [ ] **[R1]** Механический переезд бренда на новое имя. Список файлов и строк с - [ ] **[R1]** Механический переезд бренда на новое имя. Список файлов и строк с
хардкодом — в локальном плане (раздел «Где живёт бренд в коде»); здесь хардкодом — в локальном плане (раздел «Где живёт бренд в коде»); здесь
только SEO-ограничения. **НЕ трогать:** адреса страниц инструментов только SEO-ограничения. **НЕ трогать:** адреса страниц инструментов
(`/tools/crop-png`), ключи localStorage (`easy-png-tools:*` — или (`/tools/crop-png`) и localStorage-ключи — `theme` и `locale` лежат без
мигрировать отдельной мелочью), историю `docs/archive`. Бренд должен стать префикса бренда, поэтому переименование с ними не столкнётся (проверено в
i18n-константой, а не тремя независимыми хардкодами. `theme.svelte.ts` и `i18n/locale.svelte.ts`); историю `docs/archive`.
Бренд должен стать i18n-константой, а не тремя независимыми хардкодами.
- [ ] **[R2]** Купить выбранный домен (кандидаты и пробы — в локальном плане) и - [ ] **[R2]** Купить выбранный домен (кандидаты и пробы — в локальном плане) и
перевести прод на него без `BASE_PATH` (§0). Домен нужен до S1-domain: перевести прод на него без `BASE_PATH` (§0). Домен нужен до S1-domain:
canonical и sitemap генерятся от `PUBLIC_SITE_ORIGIN`. canonical и sitemap генерятся от `PUBLIC_SITE_ORIGIN`.
@@ -396,7 +416,8 @@ with PNG, JPG, WebP» в тексте + селектор формата в Downl
- Клонирование всего каталога конкурента (низкокачественные дубли + нет ресурсов - Клонирование всего каталога конкурента (низкокачественные дубли + нет ресурсов
на контент) — вместо этого ядро + обоснованные хвосты. на контент) — вместо этого ядро + обоснованные хвосты.
- RU-локаль в индексе до решения о структуре `/ru/` + hreflang. - RU-локаль в индексе до решения о структуре `/ru/` + hreflang.
- GA/реклама/трекинг — противоречит позиционированию. - Реклама, ремаркетинг, pixels сторонних сетей — противоречит позиционированию.
Счётчик посещений GA4 — не противоречит, условия ограничений — §0 и S4b.
- Дорвеи, программные зеркала, покупные ссылки. - Дорвеи, программные зеркала, покупные ссылки.
- Массово-сгенерированные форматные клоны (`crop-jpeg`/`crop-webp` как копии - Массово-сгенерированные форматные клоны (`crop-jpeg`/`crop-webp` как копии
`crop-png` с подстановкой формата в шаблонный текст) — doorway-паттерн, риск `crop-png` с подстановкой формата в шаблонный текст) — doorway-паттерн, риск
+4 -2
View File
@@ -98,8 +98,10 @@ download, темы или локализации. Изменения runtime fix
pattern-based policy для временно неиспользуемых `tools.*.params`; это не pattern-based policy для временно неиспользуемых `tools.*.params`; это не
молчаливый `allowPaths`-список. молчаливый `allowPaths`-список.
`lint:all` может завершиться успешно при warnings — это не означает чистый `lint:all` может завершиться успешно, напечатав unused-token сообщения: это
baseline. Агент показывает число errors, warnings и token warnings отдельно. единственный неблокирующий сигнал в нём. ESLint warnings, наоборот, роняют gate,
поэтому «прошёл» ≠ «нечего чинить». Агент показывает число errors, warnings и
token warnings раздельно.
## CI ## CI
+6 -5
View File
@@ -1,6 +1,6 @@
# План развития: easy-png-tools # План развития: easy-png-tools
> **Статус:** фаза 1 выполнена — сайт живёт на типизированном реестре, > **Статус: active.** Фаза 1 выполнена — сайт живёт на типизированном реестре,
> production UI — корневые маршруты. Текущий фокус: SEO S1-free > production UI — корневые маршруты. Текущий фокус: SEO S1-free
> (`docs/plan-seo.md`) + открытые задачи (`docs/backlog.md`). Домен нужен для > (`docs/plan-seo.md`) + открытые задачи (`docs/backlog.md`). Домен нужен для
> SEO (S1-domain). Фазы 2–8 — будущие; 9–10 спланированы в > SEO (S1-domain). Фазы 2–8 — будущие; 9–10 спланированы в
@@ -19,16 +19,17 @@
слои) — `docs/architecture.md`, разделы 2–3. слои) — `docs/architecture.md`, разделы 2–3.
- **Набор утилит для изображений, всё в браузере** (SvelteKit + - **Набор утилит для изображений, всё в браузере** (SvelteKit +
`adapter-static`), данные не покидают машину. `adapter-static`): файл картинки не покидает машину. Счётчик посещений GA4
этому не противоречит — границы `docs/plan-seo.md`, §0.
- **Первый и главный приоритет — полностью рабочий сайт**: максимум простых - **Первый и главный приоритет — полностью рабочий сайт**: максимум простых
инструментов, 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-список шагов** `{id, params}` — формат шага совпадает с
решением и станет общим для браузера, wasm и CLI; реализуется в фазе 11, не в полем `Page.steps`. Формат зафиксирован этим решением и станет общим для
фазе 1. браузера, wasm и CLI; реализуется в фазе 11, не в фазе 1.
- Валидация будущего Rust-порта — эталонные файлы, сгенерированные из - Валидация будущего Rust-порта — эталонные файлы, сгенерированные из
TS-реализации. Сравнение: **бит-в-бит** для чистых пиксельных операций, TS-реализации. Сравнение: **бит-в-бит** для чистых пиксельных операций,
**perceptual diff** для canvas-зависимых (правила сравнения — раздел 3). **perceptual diff** для canvas-зависимых (правила сравнения — раздел 3).
+16 -4
View File
@@ -106,12 +106,24 @@ non-blocking E2E не считается заменой локальному smo
`web/playwright.config.ts` запускает четыре проекта: `web/playwright.config.ts` запускает четыре проекта:
| Проект | Движок | Спецификации | | Проект | Движок | Спецификации |
| ----------------- | -------- | ---------------------------------------------------------------------- | | ----------------- | -------- | ------------------------------------------------------------------------ |
| `chromium` | Chromium | все e2e | | `chromium` | Chromium | все e2e |
| `firefox` | Firefox | `pipeline`, `png-fixtures`, `text-and-verdicts` | | `firefox` | Firefox | `tool-flows`, `png-fixtures`, `text-and-verdicts` |
| `webkit` | WebKit | `pipeline`, `png-fixtures`, `text-and-verdicts` | | `webkit` | WebKit | `tool-flows`, `png-fixtures`, `text-and-verdicts` |
| `mobile-chromium` | Chromium | `navigation`, `catalog`, `generators`, `pipeline`, 390×844, `hasTouch` | | `mobile-chromium` | Chromium | `navigation`, `catalog`, `generators`, `tool-flows`, 390×844, `hasTouch` |
- Название `tool-flows.spec.ts` описывает содержание: сквозной поток одного
инструмента (загрузка → правка параметра → результат → download) на наборе
представительных страниц. Пайплайна в коде нет (`docs/architecture.md`, раздел
1), поэтому спеку нельзя называть `pipeline` — при её переименовании поправить
`browserContractTests` и `mobileTests` в `web/playwright.config.ts` и
`allowDefaultProject` в `web/eslint.config.js`.
- Рядом живёт `tools-smoke.spec.ts` — дешёвый проход по выборке страниц («выдал
результат без ошибок»): по одной-двум на категорию, а не по каждому
инструменту. Остальные страницы закрыты точечными спеками (`generators`,
`text-and-verdicts`, `tool-flows`). `tool-flows` отвечает за более глубокие
проверки, `tools-smoke` — за дешёвый прогон подряд; дублировать их цели не
стоит.
- Новые engine-специфичные фичи сначала добавляются в `chromium`, потом - Новые engine-специфичные фичи сначала добавляются в `chromium`, потом
решается, попадает ли spec в `browserContractTests`. решается, попадает ли spec в `browserContractTests`.
- Расхождения декодеров не ужесточаются и не скрываются skip-ом: они описаны в - Расхождения декодеров не ужесточаются и не скрываются skip-ом: они описаны в
+29 -16
View File
@@ -3,19 +3,24 @@
> Статус: **active**. **Дата прохода: 2026-09-13** (инструменты и поля). Здесь > Статус: **active**. **Дата прохода: 2026-09-13** (инструменты и поля). Здесь
> только «что плохо» — наблюдения, без порядка работ и без критериев приёмки. > только «что плохо» — наблюдения, без порядка работ и без критериев приёмки.
> Новый проход — новая дата сверху; закрытая находка не удаляется, а помечается > Новый проход — новая дата сверху; закрытая находка не удаляется, а помечается
> ссылкой на задачу в `docs/backlog.md`. > ссылкой на задачу в `docs/backlog.md` или на уже сделанное в коде.
>
> **Слаги сверены с реестром** на дату прохода (`docs/tools-map.md` §1).
> Отдельно: `webp-to-png` и `color-wheel-generator` в коде отсутствуют (реальная
> страница — `color-wheel-png`), а `png-info` — ещё не заведённая страница
> (`docs/backlog.md`, «Идеи»). Такие пункты переименовываются по `PAGES`, а не
> resurrect'ятся. Перед работой по любому пункту — найти соответствующую
> страницу в `web/src/lib/registry/pages/` или `docs/tools-map.md`; ищи по
> смыслу: категория в заголовке раздела + ключевое слово в пункте.
>
> **Заголовки разделов ниже — это id категорий из `web/src/lib/categories.ts`**,
> по ним и ищи страницу в `registry/pages/<group>.ts`. Раздела `convert` в
> аудите нет: конвертация проверялась вместе с остальными инструментами,
> отдельного прохода по ней не было.
> >
> Как исправлять — `docs/backlog.md` (задачи) и > Как исправлять — `docs/backlog.md` (задачи) и
> `docs/plan-fields-audit-fixes.md` (порядок правок полей). Находка в аудите и > `docs/plan-fields-audit-fixes.md` (порядок правок полей). Находка в аудите и
> задача в бэклоге — не дубль: пока задачи нет, находка просто висит. > задача в бэклоге — не дубль: пока задачи нет, находка просто висит.
>
> Ids ниже **не сверены с реестром** и часть из них в коде отсутствует
> (`webp-to-png`, `color-wheel-generator`, `png-info`, `png-to-hsl`). Перед
> работой по любому пункту — найти соответствующий инструмент в
> `web/src/lib/registry/` или `docs/tools-map.md`; несуществующий id не
> resurrect'ить, а переименовать пункт. Чтобы не сверять вручную каждый раз, ищи
> по смыслу: категория в заголовке раздела + ключевое слово в пункте →
> `docs/tools-map.md`, где id уже фактические.
## везде ## везде
@@ -23,7 +28,7 @@
- инструменты без параметров выглядят странно, пересмотреть, может добавить - инструменты без параметров выглядят странно, пересмотреть, может добавить
какую-то надпись "нет параметров" какую-то надпись "нет параметров"
- удаление\маски и т.д. - надо добавить галочку сглаживание - удаление\маски и т.д. - надо добавить галочку сглаживание
- производительность: почему flip заметно тормозит, но нет индикации работы - производительность: почему flip-png заметно тормозит, но нет индикации работы
инструмента (также sharpen-png) инструмента (также sharpen-png)
- производительность: исследовать, есть ли смысл на инструментах геометрии - производительность: исследовать, есть ли смысл на инструментах геометрии
выводить рамку результата до полноценной работы (например на skew \ rotate выводить рамку результата до полноценной работы (например на skew \ rotate
@@ -31,7 +36,9 @@
## аудит инструментов ## аудит инструментов
### transparency ### alpha
(в UI раздел называется «Transparency» / «Прозрачность»)
- add-stroke-png: добавить галочку "расширить на толщину линии" - add-stroke-png: добавить галочку "расширить на толщину линии"
- find-contour-png: если толщина линии более 1px - круг боле с искажениями, надо - find-contour-png: если толщина линии более 1px - круг боле с искажениями, надо
@@ -64,7 +71,7 @@
полупрозрачной областью - закрывает всё, по описанию должен закрывать только полупрозрачной областью - закрывает всё, по описанию должен закрывать только
небольшие дырки небольшие дырки
### colors ### color
- two-colors-png: в range slider добавить описание что это luminance threshold - two-colors-png: в range slider добавить описание что это luminance threshold
- gamma-png: проверить правильность работы - яркое изображение выглядит не - gamma-png: проверить правильность работы - яркое изображение выглядит не
@@ -95,8 +102,10 @@
- add-border-png: прозрачность цвета (аудит полей) - add-border-png: прозрачность цвета (аудит полей)
- fit-on-background-png: цвет - fit-on-background-png: цвет
- change-canvas-size-png: использовать position9 вместо селекта - change-canvas-size-png: использовать position9 вместо селекта
- resize-png: по умолчанию размеры 0х0, надо поменять; выбор метода - resize-png: по умолчанию размеры 0х0 — сделано, дефолт берётся из source
масштабирования (`defaultFromSource`), до загрузки 1×1. Выбор метода масштабирования — поля в
схеме нет вовсе, это не правка селекта, а новое поле
(`docs/plan-fields-audit-fixes.md` §7.1)
- crop-png: offset - слишком большие значения - crop-png: offset - слишком большие значения
- rotate-png: кнопки вместо select - rotate-png: кнопки вместо select
- flip-png: кнопки вместо select - flip-png: кнопки вместо select
@@ -143,7 +152,9 @@
- png-is-grayscale: результат плохо виден, нужно ли копирование на полный - png-is-grayscale: результат плохо виден, нужно ли копирование на полный
вердикт? может отдельные поля - 0\1, yes\no? вердикт? может отдельные поля - 0\1, yes\no?
### generators ### generate
(в UI раздел называется «Generate» / «Генерация»)
- color-spectrum-png: кнопки вместо select - color-spectrum-png: кнопки вместо select
- draw-grid-png: выбор цвета фона - draw-grid-png: выбор цвета фона
@@ -166,7 +177,9 @@
## аудит полей ## аудит полей
- у полей plate, position9 и font-style вторая подпись дублирует первую - у полей plate, position9 и font-style вторая подпись дублирует первую
- position offset: должны быть отдельные range slider на x и y - position offset: сделано — отдельные числовые поля X и Y в
`fields/schema/OffsetControl.svelte` (не range slider); задача на px/% вместо
них открыта, `docs/plan-fields-audit-fixes.md` §7.1
- range slider: поле ввода, кнопки + и -, кнопка сброса на умолчание - range slider: поле ввода, кнопки + и -, кнопка сброса на умолчание
- range slider with presets: слайдер, как выше, добавочные кнопки с пресетами - range slider with presets: слайдер, как выше, добавочные кнопки с пресетами
- color picker: пикер с картинки (как исходника, так и результата), прозрачность - color picker: пикер с картинки (как исходника, так и результата), прозрачность
+167 -152
View File
@@ -1,187 +1,190 @@
# Карта инструментов: реализовано · добавить · идеи # Карта инструментов: реализовано · добавить · идеи
> Статус: **active**. Живой документ для сверки с каталогом. > Статус: **active**. Навигационный список: что уже есть по адресам
> > `/tools/<slug>`, что можно добавить, что отложено. Источник правды — реестр
> **Карта перечисляет `slug` страниц (`web/src/lib/registry/pages/`) — то есть > (`web/src/lib/registry/`), модель страницы и инструмента —
> адреса вида `/tools/crop-png`:** это `slug: "crop-png"` у страницы, которая > `docs/architecture.md`, раздел 3; сверяться с реестром перед использованием.
> вызывает инструмент `id: "crop"`. Внутренние имена инструментов png-интента не
> держат, поэтому они от карты отделены; модель — `docs/architecture.md`,
> раздел 3. Источник правды — реестр. Этот список навигационный: сверять перед
> использованием. Формат записи: `slug — параметры через запятую`. Сравнение с
> каталогом источника (объём, нишевые клоны серий Logo*/Icon*/Stamp*Signature*)
> — замер в `docs/plan-seo.md`, §2; здесь он не дублируется.
>
> Количество инструментов здесь не фиксируется — оно выводится из раздела 1 и из > Количество инструментов здесь не фиксируется — оно выводится из раздела 1 и из
> `TOOLS` в `registry/index.ts`, и дублировать его цифрой значит заставлять себя > `PAGES` (`AGENTS.md`).
> обновлять документ при каждом добавлении (`AGENTS.md`). >
> **Как читать строку.** `slug` — адрес страницы и имя скачиваемого файла;
> страница вызывает инструмент через `steps[0].id`, и у `id` нет png-интента
> (`crop-png` → `id: "crop"`, `png-to-hsl` → `id: "to-hsl"`). Имена полей — это
> id полей из `schema.fields` инструмента; составные поля (`size`, `pair`,
> `offset`, `style`, `plate`, `gradient`) в скобках раскрыты, но не выдуманы как
> отдельные поля. Сравнение с каталогом источника (объём, нишевые клоны серий
> Logo\*/Icon\*/Stamp\*) — замер в `docs/plan-seo.md` §2; здесь он не
> дублируется.
>
> **Формат строки (проверяется `web/src/lib/registry/tools-map-doc.test.ts`):**
> всё **до первого тире** `—` — это slug страниц через `/` или `,`; всё после
> тире — свободное описание параметров и не разбирается. Второй slug нельзя
> оставлять после тире, иначе он не попадёт в разбор и страница будет считаться
> неописанной.
--- ---
## 1. Реализовано ## 1. Реализовано
Ids приведены к фактическим в `web/src/lib/registry/pages/`. Slugs приведены к фактическим в `web/src/lib/registry/pages/`. Разделы идут по
файлу реестра; поля — по схеме инструмента, который страница вызывает.
**Формат строки (проверяется тестом ### Конвертация (`convert`)
`web/src/lib/registry/tools-map-doc.test.ts`):** в буллите всё **до первого
тире** `—` — это slug страниц через `/` или `,`; всё после тире — свободное
описание параметров и не разбирается. Второй slug нельзя оставлять после тире,
иначе он не попадёт в разбор и страница будет считаться неописанной.
### Конвертация (`convert.ts`)
Отдельных «из формата в PNG» инструментов **нет**: входной формат выбирается Отдельных «из формата в PNG» инструментов **нет**: входной формат выбирается
загрузкой файла, любой инструмент принимает PNG/JPG/WebP/GIF/BMP. Все конвертеры загрузкой файла, любой инструмент принимает PNG/JPG/WebP/GIF/BMP. Все конвертеры
исходят из PNG. исходят из PNG.
- svg-to-png — width результата (текстовый вход: SVG-код) - `convert-png-to-jpg` — background, quality
- png-to-bmp — 24-бит, фон вместо альфы - `convert-png-to-webp` — quality
- convert-png-to-jpg — background, quality - `png-to-bmp` — без параметров (24-бит, фон вместо альфы)
- convert-png-to-webp — quality - `svg-to-png` — width (ширина результата; вход — SVG-код)
- 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 / hex-to-png — rrggbbaa по строкам / tokens + width - `png-to-hex`, `hex-to-png` — вход `width`; rrggbbaa по строкам / tokens
- png-to-bytes / bytes-to-png — десятичные RGBA-байты по строкам / tokens + - `png-to-bytes`, `bytes-to-png` — вход `width`; десятичные RGBA-байты по
width строкам / tokens
- png-to-rgb-values / rgb-values-to-png — rgba(r,g,b,a) по пикселям / числа + - `png-to-rgb-values`, `rgb-values-to-png` — вход `width`; rgba(r,g,b,a) по
width пикселям / числа
### Прозрачность (`alpha.ts`) ### Прозрачность (`alpha`)
- change-png-opacity — percent - `add-stroke-png` — color, thickness
- set-alpha-channel-png — percent - `find-contour-png` — color, thickness
- remove-alpha-channel-png — без параметров (фон `#ffffff` захардкожен, см. - `remove-color-from-png` — targetColor, tolerance
`docs/plan-fields-audit-fixes.md` D1) - `remove-background-png` — color, tolerance, outerOnly, smooth
- extract-alpha-mask-png — без параметров - `make-thicker-png`, `make-thinner-png` — radius
- invert-alpha-png — без параметров - `harden-alpha-png` — threshold
- remove-background-png — color, tolerance, outerOnly, smooth - `feather-edges-png` — radius (размытие только альфы)
- remove-color-from-png — targetColor, tolerance - `clean-edges-png` — radius (defringe: RGB от ближайшего непрозрачного)
- add-stroke-png — color, thickness - `despeckle-alpha-png`, `close-holes-png` — radius
- find-contour-png — color, thickness - `round-corners-png` — radius (% от половины меньшей стороны)
- make-thicker-png / make-thinner-png — radius - `invert-alpha-png` — без параметров
- harden-alpha-png — threshold - `extract-alpha-mask-png` — без параметров
- feather-edges-png — radius (размытие только альфы) - `set-alpha-channel-png` — percent
- clean-edges-png — radius (defringe: RGB от ближайшего непрозрачного) - `remove-alpha-channel-png` — без параметров (фон `#ffffff` захардкожен,
- despeckle-alpha-png / close-holes-png — radius `docs/plan-fields-audit-fixes.md` FA-D1)
- center-by-alpha-png — без параметров - `circle-mask-png` — size (диаметр, % меньшей стороны), offset (x/y)
- round-corners-png — radius - `square-mask-png` — widthPct, heightPct, offset (x/y)
- circle-mask-png — size (диаметр, % меньшей стороны), offsetX, offsetY - `star-mask-png` — points, innerRadius, size, rotation, offset (x/y)
- square-mask-png — widthPct, heightPct, offsetX, offsetY - `wavy-mask-png` — size, amplitude, waves, phase, offset (x/y)
- star-mask-png — points, innerRadius, size, rotation, offsetX, offsetY
- wavy-mask-png — size, amplitude, waves, phase, offsetX, offsetY
### Цвет (`color.ts`) ### Цвет (`color`)
- grayscale-png / invert-colors-png / sepia-png / auto-contrast-png — без - `grayscale-png`, `invert-colors-png`, `sepia-png`, `auto-contrast-png` — без
параметров параметров
- adjust-brightness-contrast-png — brightness, contrast - `adjust-brightness-contrast-png` — brightness, contrast
- change-png-hue — degrees - `change-png-opacity` — percent
- extract-channel-png — channel (r/g/b) - `change-png-hue` — degrees
- swap-channels-png — pair (r-g/r-b/g-b) - `temperature-png` — percent
- black-and-white-png — threshold - `gamma-png` — value
- posterize-png — levels - `tint-png` — color, strength
- two-colors-png — lightColor, darkColor, threshold - `two-colors-png` — pair (светлый/тёмный), threshold
- temperature-png — percent - `black-and-white-png` — threshold
- gamma-png — value - `posterize-png` — levels
- 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) - `extract-channel-png` — channel (r/g/b)
- `swap-channels-png` — pair (r-g/r-b/g-b)
### Разложение каналов (`color.ts`) ### Разложение каналов (`color`)
- png-to-hsl, png-to-hsv, png-to-hsi, png-to-cmyk, png-to-ycbcr, png-to-lab — - `png-to-hsl`, `png-to-hsv`, `png-to-hsi`, `png-to-cmyk`, `png-to-ycbcr`,
component (h/s/l и т.п.), display (`gray` | `space-as-rgb`) `png-to-lab` — component (h/s/l и т.п.), display (`gray` | `space-as-rgb`)
### Геометрия (`geometry.ts`) ### Геометрия (`geometry`)
- resize-png — width/height (размеры source по умолчанию; 0=авто для одной - `resize-png` — size (по умолчанию размер source, 0 по одной стороне = авто),
стороны), keepAspect keepAspect
- crop-png — x, y, width/height (полная область source по умолчанию) - `crop-png` — x, y, size (по умолчанию вся область source)
- rotate-png — angle (90/180/270) - `rotate-png` — angle (90/180/270)
- flip-png — axis (h/v) - `rotate-free-png` — angle
- skew-png — degX, degY - `flip-png` — axis (h/v)
- rotate-free-png — angle - `skew-png` — degX, degY
- zoom-png — scale - `zoom-png` — scale
- shift-png — offsetX, offsetY, color фона - `shift-png` — offsetX, offsetY, color
- add-padding-png — padding, transparent, color - `add-padding-png` — padding, transparent, color
- add-border-png — thickness, color - `add-border-png` — thickness, color
- fit-on-background-png — width, height, transparent, color - `fit-on-background-png` — size, transparent, color
- tile-png — columns, rows - `tile-png` — columns, rows
- split-into-parts-png — columns, rows (мультифайловый вывод, zip) - `split-into-parts-png` — columns, rows (мультифайловый вывод, zip)
- trim-empty-space-png — threshold (альфа) - `trim-empty-space-png` — threshold (по альфе)
- change-canvas-size-png — width, height, anchor (3×3) - `change-canvas-size-png` — size, anchor (3×3)
- change-aspect-ratio-png — ratio (пресеты), mode (crop/pad) - `change-aspect-ratio-png` — ratio (пресеты), mode (crop/pad)
- swap-orientation-png — target (portrait/landscape) - `swap-orientation-png` — target (portrait/landscape)
- symmetric-copy-png — axis, keepSide - `symmetric-copy-png` — axis, keepSide
- `center-by-alpha-png` — без параметров
### Фильтры (`filters.ts`) ### Фильтры (`filters`)
- blur-png — radius - `blur-png` — radius
- sharpen-png — strength - `sharpen-png` — strength
- vignette-png — strength - `vignette-png` — strength
- jpeg-artifacts-png — quality (имитация пережатия jpg/webp) - `jpeg-artifacts-png` — quality (имитация пережатия jpg/webp; поле
- pixelate-png — blockSize (закрывает и их Color Blocks) функциональное, у инструмента нет `output` — `docs/plan-seo.md` S1f)
- randomize-pixels-png — blockSize, seed - `pixelate-png` — blockSize
- add-noise-png — amount, mode (mono/color), seed - `randomize-pixels-png` — blockSize, seed
- silhouette-png — color, threshold - `add-noise-png` — amount, mode (mono/color), seed
- `silhouette-png` — color, threshold
### Анализ и вердикты (`analyze.ts`) ### Анализ и вердикты (`analyze`)
- png-file-size — без параметров (DOM-инструмент: кодирует в PNG и отдаёт - `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), вердикт по сигнатуре текстовый вердикт
- `verify-is-png` — без параметров (вход — текст: base64/data-uri), вердикт по
сигнатуре
### Маски по свойствам пикселей (`analyze.ts`) ### Маски по свойствам пикселей (`analyze`)
- show-transparent-png — color, opacity (подсветка прозрачных/полупрозрачных) - `show-transparent-png` — mode, color, opacity
- show-grayscale-pixels-png — tolerance, mode (binary/highlight), - `show-grayscale-pixels-png` — mode, tolerance, color, opacity
highlightColor, highlightOpacity - `show-color-pixels-png` — mode, tolerance, color, opacity
- show-color-pixels-png — tolerance, mode, highlightColor, highlightOpacity - `light-pixel-mask-png` — mode, threshold, color, opacity
- light-pixel-mask-png — threshold, mode, highlightColor, highlightOpacity - `dark-pixel-mask-png` — mode, threshold, color, opacity
- dark-pixel-mask-png — threshold, mode, highlightColor, highlightOpacity - `unique-color-mask-png` — mode, rarity (макс. повторов), color, opacity
- unique-color-mask-png — rarity (макс. повторов), mode, highlightColor, - `extract-color-from-png` — color, tolerance
highlightOpacity
- extract-color-from-png — color, tolerance (оставить близкие, остальное
прозрачным)
### Генерация (`generate.ts`) ### Генерация (`generate`)
- create-empty-png — width, height, transparent, color - `create-empty-png` — size, transparent, color
- single-color-png — width, height, color - `single-color-png` — size, color
- random-noise-png — width, height, seed - `random-noise-png` — size, seed
- linear-gradient-png — width, height, fromColor, toColor, direction - `linear-gradient-png` — size, gradient (от/к, направление)
- color-spectrum-png — width, height, direction, saturation, lightness - `color-spectrum-png` — size, direction, saturation, lightness
- random-colors-png — width, height, blockSize, seed - `random-colors-png` — size, blockSize, seed
- draw-grid-png — width, height, cols, rows, lineWidth, color, transparentBg - `draw-grid-png` — size, cols, rows, lineWidth, color, transparentBg
- placeholder-png — width, height, backgroundColor, color, showText - `placeholder-png` — size, backgroundColor, color, showText
- blend-two-png — pair (from/to), width - `blend-two-png` — pair (от/к), width
- step-colors-png — pair (from/to), steps, width - `step-colors-png` — pair (от/к), steps, width, layout (grid/strip)
- emoji-to-png — emoji, size - `color-wheel-png` — size, lightness
- text-to-png — text, fontSize, font, bold, color, transparentBg, - `mix-colors-png` — colors, width
- `sort-colors-png` — colors, order (hue/brightness/saturation), width, layout
- `emoji-to-png` — emoji, size
- `text-to-png` — text, style (шрифт/размер/цвет/жирность), transparentBg,
backgroundColor, padding backgroundColor, padding
- color-wheel-png — size, lightness - `complementary-png`, `triadic-png`, `tetradic-png`, `analogous-png`,
- complementary-png / triadic-png / tetradic-png / analogous-png / `monochromatic-png`, `shades-png` — baseColor, width, layout; у `analogous`
monochromatic-png / shades-png — общая база палитры: baseColor, width, layout; ещё spread и count, у `monochromatic` — count и range, у `shades` — count и
`shades-png` дополнительно count, depth depth
- mix-colors-png — colors, width
- sort-colors-png — colors, порядок (hue/brightness/saturation), width
### Текст (`text.ts`) ### Текст (`text`)
- add-text-png — text, fontSize, color, font, bold, position (3×3), margin, - `add-text-png` — text, style (шрифт/размер/цвет/жирность), position (3×3),
plate, plateColor, plateOpacity margin, plate (плашка, её цвет и прозрачность)
- date-stamp-png — format, fontSize, color, font, bold, position, margin, plate, - `date-stamp-png` — format, style, position, margin, plate
plateColor, plateOpacity - `watermark-tile-png` — text, style, opacity, angle, stepX, stepY
- watermark-tile-png — text, fontSize, color, opacity, angle, stepX, stepY,
font, bold
--- ---
## 2. Можно добавить — из onlinepngtools ## 2. Можно добавить — из onlinepngtools
Всё из этого списка **в реестре отсутствует**. Всё из этого списка **в реестре отсутствует** — это план, а не факт. Список
проверяется `web/src/lib/registry/tools-map-doc.test.ts` в обратную сторону:
реализованный slug, оставшийся здесь, роняет тест.
### Разложение каналов — остаток ### Разложение каналов — остаток
@@ -216,8 +219,11 @@ Ids приведены к фактическим в `web/src/lib/registry/pages/
### Сжатие и качество ### Сжатие и качество
- compress, reduce-to-size, pick-a-color, extract-barcode — **не реализованы**; - compress, reduce-to-size, pick-a-color, extract-barcode — **не реализованы**;
заведены в `docs/backlog.md` (reduce-to-size зависит от UX-райза «Download»). заведены в `docs/backlog.md`. Сжатие до целевого размера — прежде всего
Пипетка для выбора цвета уже есть в превью, отдельная страница не планируется. селектор формата в Download (S1f, `docs/plan-seo.md` §4): для lossy это
quality-ручка, а для PNG — квантование палитры. Нужен ли отдельный инструмент
под этот интент, решается по вордстату, имя страницы пока не выбрано. Пипетка
для выбора цвета уже есть в превью, отдельная страница не планируется.
Извлечение штрихкодов — HARD, вне планов. Извлечение штрихкодов — HARD, вне планов.
--- ---
@@ -227,12 +233,21 @@ Ids приведены к фактическим в `web/src/lib/registry/pages/
Развёрнутые описания этих идей живут в `docs/backlog.md`, раздел «Идеи»; здесь Развёрнутые описания этих идей живут в `docs/backlog.md`, раздел «Идеи»; здесь
только список. только список.
Буллиты этого раздела, как и раздела 2, перечисляют **несуществующие** страницы:
уже реализованные в них не называются, иначе тест решит, что slug пора
переносить в раздел 1. Реализованные примеры пишутся прозой между буллитами, как
ниже.
- **Region-инструменты** — нужен selection-компонент на превью: censor-region, - **Region-инструменты** — нужен selection-компонент на превью: censor-region,
erase-region, pixelate-area, blur-area, sharpen-area, reverse-colors-area. erase-region, pixelate-area, blur-area, sharpen-area, reverse-colors-area.
Один раз делаем selection — получаем шесть инструментов. Один раз делаем selection — получаем шесть инструментов.
- **Мультифайловый выход** — механизм «результат = набор файлов» (zip) - **Мультифайловый выход** — остальные потребители механизма «результат = набор
реализован на `split-into-parts-png`. Остальные 1→many: gif-to-frames, файлов»: gif-to-frames, separate-colors, multiply-grid-as-files — подключаются
separate-colors, multiply-grid-as-files — подключаются по мере нужды. по мере нужды.
Механизм уже реализован на `split-into-parts-png` (`docs/architecture.md`,
раздел 7), он в разделе 1.
- **Анимационные эффекты** — 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,
@@ -240,6 +255,6 @@ Ids приведены к фактическим в `web/src/lib/registry/pages/
- **Нишевые серии** (logo/icon/stamp/signature — замер в шапке документа) — - **Нишевые серии** (logo/icon/stamp/signature — замер в шапке документа) —
сознательно не копируем: это обычные операции над конкретным контентом, у нас сознательно не копируем: это обычные операции над конкретным контентом, у нас
они доступны через базовые инструменты + цепочки. они доступны через базовые инструменты + цепочки.
- **Входные «из формата в PNG»** (jpg-to-png, webp-to-png) — решение и его SEO- - **Входные «из формата в PNG»** (jpg-to-png, webp-to-png) — решение и его
последствия описаны в разделе 1 выше; отдельные страницы под эти интенты — SEO-последствия описаны в `docs/plan-seo.md` §5; отдельные страницы под эти
предмет решения в `docs/plan-seo.md` §5. интенты — предмет решения там же.
+6 -2
View File
@@ -175,8 +175,9 @@ report-only (`warn`), baseline — 0 срабатываний.
- **Неиспользуемые токены**: выводится **варнинг** (определены, но нигде не - **Неиспользуемые токены**: выводится **варнинг** (определены, но нигде не
используются) — выход он не меняет. Проверка видит только ссылки `var(--x)`, используются) — выход он не меняет. Проверка видит только ссылки `var(--x)`,
поэтому чтение токена из JS (`getPropertyValue`) она не учтёт; решение поэтому чтение токена из JS (`getPropertyValue`) она не учтёт; решение
оставить проверку неблокирующей зафиксировано в `docs/quality-gates.md`. оставить проверку неблокирующей — `docs/decisions.md`, раздел 6 (как оно
Провалами (exit 1) считаются parity, hct-авторство и резолв `var()`. выглядит в гейте — `docs/quality-gates.md`). Провалами (exit 1) считаются
parity, hct-авторство и резолв `var()`.
## Тесты кастомных линт-правил ## Тесты кастомных линт-правил
@@ -187,6 +188,9 @@ report-only (`warn`), baseline — 0 срабатываний.
`no-token-definition-in-svelte`) через `RuleTester` со строковыми кейсами; `no-token-definition-in-svelte`) через `RuleTester` со строковыми кейсами;
`no-undefined-in-svelte` — через `Linter` API, т.к. читает словарь токенов из `no-undefined-in-svelte` — через `Linter` API, т.к. читает словарь токенов из
`__fixtures__/src/app.css` (не из реального `src/app.css`). `__fixtures__/src/app.css` (не из реального `src/app.css`).
- `__tests__/interface-props.test.ts` и
`__tests__/no-string-union-alias.test.ts` — конвенции плагина `conventions`
(интерфейс пропсов, запрет union-алиасов).
- Хелпер `__tests__/helpers.ts` собирает `Linter` с `cwd = __fixtures__` — - Хелпер `__tests__/helpers.ts` собирает `Linter` с `cwd = __fixtures__` —
`process.cwd()` не трогается, реальные `src/` не читаются. `process.cwd()` не трогается, реальные `src/` не читаются.
+1 -1
View File
@@ -63,7 +63,7 @@ export default tseslint.config(
"e2e/helpers/page.ts", "e2e/helpers/page.ts",
"e2e/navigation.spec.ts", "e2e/navigation.spec.ts",
"e2e/catalog.spec.ts", "e2e/catalog.spec.ts",
"e2e/pipeline.spec.ts", "e2e/tool-flows.spec.ts",
"e2e/text-and-verdicts.spec.ts", "e2e/text-and-verdicts.spec.ts",
"e2e/tools-smoke.spec.ts", "e2e/tools-smoke.spec.ts",
"e2e/generators.spec.ts", "e2e/generators.spec.ts",
+2 -2
View File
@@ -1,7 +1,7 @@
import { defineConfig } from "playwright/test"; import { defineConfig } from "playwright/test";
const browserContractTests = [ const browserContractTests = [
"**/pipeline.spec.ts", "**/tool-flows.spec.ts",
"**/png-fixtures.spec.ts", "**/png-fixtures.spec.ts",
"**/text-and-verdicts.spec.ts", "**/text-and-verdicts.spec.ts",
]; ];
@@ -10,7 +10,7 @@ const mobileTests = [
"**/navigation.spec.ts", "**/navigation.spec.ts",
"**/catalog.spec.ts", "**/catalog.spec.ts",
"**/generators.spec.ts", "**/generators.spec.ts",
"**/pipeline.spec.ts", "**/tool-flows.spec.ts",
]; ];
export default defineConfig({ export default defineConfig({
+57 -13
View File
@@ -8,30 +8,35 @@ const MAP_PATH = fileURLToPath(
); );
const SECTION_START = /^## 1\.\s/; const SECTION_START = /^## 1\.\s/;
const SECTION_END = /^## 2\.\s/; const SECTION_PLANNED = /^## 2\.\s/;
// Все slug страниц содержат дефис, поэтому формат строгий: это отсекает слова // Все slug страниц содержат дефис, поэтому формат строгий: это отсекает слова
// описания вроде `median-cut` из «colors (k, median-cut)». // описания вроде `median-cut` из «colors (k, median-cut)».
const ID_SHAPE = /^[a-z][a-z0-9]*(-[a-z0-9]+)+$/; const ID_SHAPE = /^[a-z][a-z0-9]*(-[a-z0-9]+)+$/;
const ID_TOKEN = /[a-z][a-z0-9]*(?:-[a-z0-9]+)+/g;
type Mention = { id: string; line: number }; type Mention = { id: string; line: number };
/** /**
* Буллиты раздела «1. Реализовано» вместе с номером первой строки. * Буллиты раздела вместе с номером первой строки. Перенесённые строки
* Перенесённые строки приклеиваются к буллету: иначе slug, оказавшийся на * приклеиваются к буллету: иначе slug, оказавшийся на следующей строке, молча
* следующей строке, молча выпадал бы из разбора и страница считалась бы * выпадал бы из разбора и страница считалась бы неописанной.
* неописанной.
*/ */
function implementedBullets( function sectionBullets(
markdown: string, markdown: string,
startPattern: RegExp,
endPattern: RegExp | null,
): { text: string; line: number }[] { ): { text: string; line: number }[] {
const lines = markdown.split("\n"); const lines = markdown.split("\n");
const start = lines.findIndex((l) => SECTION_START.test(l)); const start = lines.findIndex((l) => startPattern.test(l));
const end = lines.findIndex((l) => SECTION_END.test(l)); const end =
endPattern === null
? lines.length
: lines.findIndex((l) => endPattern.test(l));
if (start === -1 || end === -1 || end <= start) { if (start === -1 || end === -1 || end <= start) {
throw new Error( throw new Error(
"docs/tools-map.md: не найден раздел «1. Реализовано» или «2.» — " + `docs/tools-map.md: не найден раздел по ${startPattern} — тест не может ` +
"тест разбирает только реализованные инструменты", "определить границы разделов карты",
); );
} }
@@ -47,7 +52,7 @@ function implementedBullets(
!/^#{1,6}\s/.test(text) && !/^#{1,6}\s/.test(text) &&
!/^[-*]\s/.test(text); !/^[-*]\s/.test(text);
if (isBullet) { if (isBullet) {
bullets.push({ text: text.slice(2), line: start + i + 1 }); bullets.push({ text: text.slice(2), line: i + 1 });
} else if (isContinuation) { } else if (isContinuation) {
previous.text += ` ${text}`; previous.text += ` ${text}`;
} }
@@ -61,7 +66,11 @@ function implementedBullets(
*/ */
function mentionedIds(markdown: string): Mention[] { function mentionedIds(markdown: string): Mention[] {
const found: Mention[] = []; const found: Mention[] = [];
for (const { text, line } of implementedBullets(markdown)) { for (const { text, line } of sectionBullets(
markdown,
SECTION_START,
SECTION_PLANNED,
)) {
const head = text.split("—")[0]; const head = text.split("—")[0];
for (const raw of head.split(/[/,]/)) { for (const raw of head.split(/[/,]/)) {
const id = raw.trim().replace(/`/g, ""); const id = raw.trim().replace(/`/g, "");
@@ -71,14 +80,35 @@ function mentionedIds(markdown: string): Mention[] {
return found; return found;
} }
/**
* Плановые разделы «2. Можно добавить» и «3. Идеи» устроены прозой: slug стоит
* где угодно в буллите, после тире и в скобках. Поэтому берём все токены
* нужной формы из всего текста буллита, а не из «головы» до тире.
*/
function plannedIds(markdown: string): Mention[] {
const found: Mention[] = [];
for (const { text, line } of sectionBullets(
markdown,
SECTION_PLANNED,
null,
)) {
for (const id of text.replace(/`/g, "").match(ID_TOKEN) ?? []) {
found.push({ id, line });
}
}
return found;
}
describe("docs/tools-map.md ↔ страницы реестра", () => { describe("docs/tools-map.md ↔ страницы реестра", () => {
const markdown = readFileSync(MAP_PATH, "utf8"); const markdown = readFileSync(MAP_PATH, "utf8");
const mentioned = mentionedIds(markdown); const mentioned = mentionedIds(markdown);
const planned = plannedIds(markdown);
const registrySlugs = new Set(PAGES.map((page) => page.slug)); const registrySlugs = new Set(PAGES.map((page) => page.slug));
it("разбирает непустой список slug (парсер не сломался молча)", () => { it("разбирает непустой список slug (парсер не сломался молча)", () => {
expect(implementedBullets(markdown).length).toBeGreaterThan(0); expect(mentioned.length).toBeGreaterThan(0);
expect(new Set(mentioned.map((m) => m.id)).size).toBeGreaterThan(50); expect(new Set(mentioned.map((m) => m.id)).size).toBeGreaterThan(50);
expect(new Set(planned.map((m) => m.id)).size).toBeGreaterThan(10);
}); });
it("в карте нет slug, которых нет в реестре", () => { it("в карте нет slug, которых нет в реестре", () => {
@@ -116,4 +146,18 @@ describe("docs/tools-map.md ↔ страницы реестра", () => {
} }
expect(dupes, `повторяющиеся slug:\n${dupes.join("\n")}`).toEqual([]); expect(dupes, `повторяющиеся slug:\n${dupes.join("\n")}`).toEqual([]);
}); });
it("плановый slug уже не реализован (перенос из «2./3.» в «1.»)", () => {
const stale = planned.filter((m) => registrySlugs.has(m.id));
expect(
stale,
stale
.map(
(m) =>
`${m.id} (docs/tools-map.md:${m.line}) — страница уже есть в реестре, ` +
"перенеси её из планового раздела в «1. Реализовано»",
)
.join("\n"),
).toEqual([]);
});
}); });