Compare commits

...
151 Commits
Author SHA1 Message Date
Ku6epXBOCTuK 64e35f4fdf style: format docs markdown 2026-09-09 04:13:13 +05:00
Ku6epXBOCTuK 46da54140c docs: udpate backlog 2026-09-08 22:41:17 +05:00
Ku6epXBOCTuK 420d03f58c fix: css fixes 2026-09-08 18:29:56 +05:00
Ku6epXBOCTuK 3f21f1c024 test: add e2e test, add manual test checklist 2026-09-08 11:03:05 +05:00
Ku6epXBOCTuK 9a7b7c178b docs: add testing preview plan 2026-09-08 09:35:41 +05:00
Ku6epXBOCTuK e3d50f2128 refactor: update css vars lint rules and fix them 2026-09-08 07:18:57 +05:00
Ku6epXBOCTuK 2d24f51fd5 refactor: fix lint css vars rules 2026-09-08 07:02:54 +05:00
Ku6epXBOCTuK 5a4fbe7ac6 chore: update css lint rules, remove unused files 2026-09-08 06:20:25 +05:00
Ku6epXBOCTuK 6f1771cb07 fix: lint errors 2026-09-07 12:35:25 +05:00
Ku6epXBOCTuK 987f4535c8 fix: update tool card component - route resolve issue 2026-09-07 12:30:32 +05:00
Ku6epXBOCTuK 20e07fae85 chore: update pallete test - contrast lint 2026-09-07 12:30:25 +05:00
Ku6epXBOCTuK a71252dc0c docs: add new tech debt plan 2026-09-07 11:29:45 +05:00
Ku6epXBOCTuK 751e8d97a0 docs: update plans\docs - actual info 2026-09-07 11:19:15 +05:00
Ku6epXBOCTuK a1438e6f7f fix: resolve always text input issue, separate to simple components 2026-09-07 10:48:45 +05:00
Ku6epXBOCTuK 5884f4bd82 docs: update plan 2026-09-07 10:39:50 +05:00
Ku6epXBOCTuK e5b76ca3e1 feat: add text tools 2026-09-07 10:38:36 +05:00
Ku6epXBOCTuK a49785764b feat: add text core tools and views 2026-09-07 10:25:17 +05:00
Ku6epXBOCTuK c0cebdc81f feat: add output format for download 2026-09-07 10:20:01 +05:00
Ku6epXBOCTuK 37a4a0576c fix: add compress\decrease colors presets 2026-09-07 10:16:52 +05:00
Ku6epXBOCTuK b3d460a951 feat: add generate tools 2026-09-07 10:16:12 +05:00
Ku6epXBOCTuK 6be9d10c42 feat: add multiple colors field 2026-09-07 10:15:49 +05:00
Ku6epXBOCTuK cc70750727 feat: add analyze, generate, text tools and update plan 2026-09-07 09:09:29 +05:00
Ku6epXBOCTuK e2735c52ab feat: add alpha tools 2026-09-07 08:49:37 +05:00
Ku6epXBOCTuK d383cf29e3 feat: add geometry tools 2026-09-07 08:40:33 +05:00
Ku6epXBOCTuK bfad0c7d02 feat: add filters tools 2026-09-07 08:30:50 +05:00
Ku6epXBOCTuK 6750d5a373 feat: add colors tools 2026-09-07 03:07:46 +05:00
Ku6epXBOCTuK 6cbb9f95f0 docs: update plans 2026-09-07 00:17:32 +05:00
Ku6epXBOCTuK e57faddfde refactor: isolate old and preview versions, add lint rule 2026-09-05 19:41:31 +05:00
Ku6epXBOCTuK e662d1df24 feat: add layout groupt to tools 2026-09-05 17:47:41 +05:00
Ku6epXBOCTuK d48262e482 feat: add tool schema layout 2026-09-05 17:30:34 +05:00
Ku6epXBOCTuK cad9bf6cfd feat: add gradient control component 2026-09-05 17:19:03 +05:00
Ku6epXBOCTuK b99ccf5d78 feat: add plate (under text) field component 2026-09-05 16:55:00 +05:00
Ku6epXBOCTuK 0a7c4a84c9 feat: add font style field component 2026-09-05 16:46:15 +05:00
Ku6epXBOCTuK f24ccf9240 feat: add position9 field component 2026-09-05 16:36:48 +05:00
Ku6epXBOCTuK 0c28925673 feat: add offset field component 2026-09-05 16:24:55 +05:00
Ku6epXBOCTuK 743ae817ee refactor: update field schema types 2026-09-05 16:16:10 +05:00
Ku6epXBOCTuK 19f2e13b88 feat: add two colors field control 2026-09-05 16:09:39 +05:00
Ku6epXBOCTuK 4a13bfbd16 refactor: separate schema fields components 2026-09-05 15:56:21 +05:00
Ku6epXBOCTuK f286044ab9 feat: update use dimension tools 2026-09-05 15:11:26 +05:00
Ku6epXBOCTuK 2601aa5b5c feat: add dimension tools for new flow 2026-09-05 10:06:25 +05:00
Ku6epXBOCTuK 1e9b35406d feat: add registry tools for new preview flow 2026-09-04 19:09:47 +05:00
Ku6epXBOCTuK 520a1e1d37 feat: double executor to use with preview 2026-09-04 18:17:35 +05:00
Ku6epXBOCTuK a19d3db929 feat: add new registry for preview design 2026-09-04 17:37:55 +05:00
Ku6epXBOCTuK 58e75f2334 refactor: separate registry file 2026-09-04 17:07:38 +05:00
Ku6epXBOCTuK 1c746299c9 feat: add single tool schema rendering 2026-09-04 16:12:43 +05:00
Ku6epXBOCTuK d1a386f92f fix: update tools list - show all tools 2026-09-03 16:29:35 +05:00
Ku6epXBOCTuK 1813383724 docs: update plan 2026-09-03 16:29:05 +05:00
Ku6epXBOCTuK 1ae4b36c9c feat: add new registry schema for new ui 2026-09-03 15:58:20 +05:00
Ku6epXBOCTuK a57acd1ff2 docs: add plan for composite params 2026-09-03 05:13:43 +05:00
Ku6epXBOCTuK 6ec3e75df4 refactor: move form fields to separate folder 2026-09-02 21:11:43 +05:00
Ku6epXBOCTuK a24a182d18 fix: update segmented component styles 2026-09-02 20:48:09 +05:00
Ku6epXBOCTuK 1e63eaa03b fix: increase font size 2026-09-02 20:19:40 +05:00
Ku6epXBOCTuK e9de7a4bf8 refactor: move components 2026-09-02 20:08:05 +05:00
Ku6epXBOCTuK 7e94205866 refactor: update panel component 2026-09-02 19:55:52 +05:00
Ku6epXBOCTuK 2f758a88af refactor: update panel component 2026-09-02 19:33:34 +05:00
Ku6epXBOCTuK 991fe69381 refactor: separate top bar component 2026-09-02 19:31:04 +05:00
Ku6epXBOCTuK 11c4ccece4 refactor: update footer component 2026-09-02 18:49:55 +05:00
Ku6epXBOCTuK 8a6f87938d refactor: icon and download button use button component 2026-09-02 18:37:43 +05:00
Ku6epXBOCTuK 3d0f8ebb10 fix: update field component 2026-09-02 17:54:31 +05:00
Ku6epXBOCTuK 28ff9d3ee1 chore: update lint rules - css property shorthand fix 2026-09-02 17:47:13 +05:00
Ku6epXBOCTuK ef74cabf1e fix: update download button style 2026-09-02 17:37:45 +05:00
Ku6epXBOCTuK c590bdf91a fix: update button component styles 2026-09-02 17:22:47 +05:00
Ku6epXBOCTuK 4794523084 fix: update css variables, update badge component 2026-09-02 10:07:31 +05:00
Ku6epXBOCTuK 98a4bacd23 chore: update css rules, add hct plugin 2026-09-02 09:51:14 +05:00
Ku6epXBOCTuK 1310c51543 chore: add design tokens\css lint rules 2026-09-02 00:14:29 +05:00
Ku6epXBOCTuK 191ca71553 fix: update some styles 2026-09-01 21:16:54 +05:00
Ku6epXBOCTuK d38d51c7d9 docs: add readme, license 2026-09-01 15:20:28 +05:00
Ku6epXBOCTuK 749b97cb4f fix: change style, demo page visual looks as done 2026-08-31 18:07:34 +05:00
Ku6epXBOCTuK d73b9056d7 chore: update audit cdp - group margin\padding\gap 2026-08-31 13:33:25 +05:00
Ku6epXBOCTuK 1907fb9106 chore: update cdp audit script 2026-08-31 12:31:07 +05:00
Ku6epXBOCTuK 80def1f380 fix: resolve new audit styles 2026-08-31 10:38:35 +05:00
Ku6epXBOCTuK 95f1eeeecc chore: update audit cdp script - css var fixes, add grid styles 2026-08-31 10:23:46 +05:00
Ku6epXBOCTuK 9cd86104fc chore: update cdp parsing css layers 2026-08-31 09:31:47 +05:00
Ku6epXBOCTuK 0b13d4c9e8 fix: update font styles 2026-08-31 09:14:33 +05:00
Ku6epXBOCTuK ecc8efcc45 chore: update html extract script - inject fonts 2026-08-31 08:52:26 +05:00
Ku6epXBOCTuK 586c66a159 fix: update style, resolve old css leaks issue 2026-08-31 08:31:33 +05:00
Ku6epXBOCTuK b7d66f0430 chore: update cdp audit script 2026-08-31 07:07:09 +05:00
Ku6epXBOCTuK bbd5532529 fix: resolve style issues 2026-08-31 03:25:42 +05:00
Ku6epXBOCTuK 6cefd5c0a1 chore: update cdp audit script 2026-08-31 03:02:29 +05:00
Ku6epXBOCTuK 8eff9a9483 fix: step fields styles 2026-08-31 02:41:57 +05:00
Ku6epXBOCTuK 2c70c445d5 fix: update styles 2026-08-31 01:25:46 +05:00
Ku6epXBOCTuK 6c8f7b2d8b fix: update preview styles 2026-08-30 22:40:32 +05:00
Ku6epXBOCTuK 86b1c813e7 fix: update top bar styles 2026-08-30 22:40:25 +05:00
Ku6epXBOCTuK b0f0b79ee0 chore: cdp audit - fix hover\focus\active pseudo classes 2026-08-30 21:45:47 +05:00
Ku6epXBOCTuK 11b4c87486 chore: fix cdp audit css selectors specifity 2026-08-30 21:21:31 +05:00
Ku6epXBOCTuK 87dbb200d9 chore: update audit script - different resolutions 2026-08-30 20:59:16 +05:00
Ku6epXBOCTuK 2d72848e8e chore: update audit format - show really different styles 2026-08-30 19:16:18 +05:00
Ku6epXBOCTuK 453a5908e6 chore: add chrome devtools protocol audit 2026-08-30 19:16:13 +05:00
Ku6epXBOCTuK dbeffac082 feat: css audit fixes 2026-08-30 04:11:17 +05:00
Ku6epXBOCTuK 2ef4709038 feat: WIP: add css fixes 2026-08-29 11:07:02 +05:00
Ku6epXBOCTuK 1831c3d100 feat: add refs css audit 2026-08-29 10:33:32 +05:00
Ku6epXBOCTuK 9ef6ae03c6 feat: fix tools pages, dom audit fix 2026-08-29 09:08:09 +05:00
Ku6epXBOCTuK b695f5d58c feat: add preview catalog and catalog group and header 2026-08-29 07:54:45 +05:00
Ku6epXBOCTuK 7578a185b6 feat: fix list tools page to correspond refs 2026-08-29 07:44:41 +05:00
Ku6epXBOCTuK d29ec73322 feat: fix demo page to correspond refs 2026-08-29 07:23:31 +05:00
Ku6epXBOCTuK 9ff732f2cf chore: add new audit-dom script and make dom audit 2026-08-29 06:39:56 +05:00
Ku6epXBOCTuK b19ddbe45d docs: add new audit plan - resolve old audit flow 2026-08-29 05:57:11 +05:00
Ku6epXBOCTuK 795c03ac87 chore: update refs audit 2026-08-28 21:27:13 +05:00
Ku6epXBOCTuK 0d95acba89 fix: resolve design issues from refs audit 2026-08-28 19:56:50 +05:00
Ku6epXBOCTuK 6959abb159 fix: remove tile label doubling 2026-08-28 17:44:07 +05:00
Ku6epXBOCTuK 86e7b7250d refactor: update pages and refs audit 2026-08-28 17:03:30 +05:00
Ku6epXBOCTuK c0f48ecd97 feat: add new simple components 2026-08-28 17:02:52 +05:00
Ku6epXBOCTuK d30deddce0 docs: update plans and refs audit 2026-08-28 16:46:17 +05:00
Ku6epXBOCTuK 7aa50eee6c docs: update refs html extract script 2026-08-28 14:55:57 +05:00
Ku6epXBOCTuK ada96b3534 style: fix formatting via prettier 2026-08-28 14:29:54 +05:00
Ku6epXBOCTuK 10278981ba docs: update refs, update script 2026-08-28 14:27:44 +05:00
Ku6epXBOCTuK 5093b50f5c fix: update design with references 2026-08-28 13:35:09 +05:00
Ku6epXBOCTuK 65f12c931b feat: some refactor, fix some design 2026-08-28 12:30:27 +05:00
Ku6epXBOCTuK 40eee984da feat: add link to preview design 2026-08-28 11:50:50 +05:00
Ku6epXBOCTuK d373cc0daf feat: update tool page - add step card steps 2026-08-28 11:43:26 +05:00
Ku6epXBOCTuK a16a7b92c7 feat: add dropzone, update tool page 2026-08-28 10:55:51 +05:00
Ku6epXBOCTuK f990ad6767 feat: add single tool pages, wiring with toolcard 2026-08-28 08:05:20 +05:00
Ku6epXBOCTuK f4216e6868 feat: separate topbar, fix important styles 2026-08-27 23:58:57 +05:00
Ku6epXBOCTuK bcdfc4cf12 chore: add audit script for compare with refs 2026-08-27 23:52:32 +05:00
Ku6epXBOCTuK adcc255107 feat: add demo page - preselected pipeline 2026-08-27 22:44:58 +05:00
Ku6epXBOCTuK 73118ea393 feat: add list tools page 2026-08-27 22:11:04 +05:00
Ku6epXBOCTuK e9578fc15e feat: add main page 2026-08-27 22:06:16 +05:00
Ku6epXBOCTuK 06d185add6 feat: add ui kit page 2026-08-27 21:35:01 +05:00
Ku6epXBOCTuK 8437c95a28 feat: add ui components 2026-08-27 21:10:25 +05:00
Ku6epXBOCTuK ac75665deb feat: add ui components 2026-08-27 20:57:34 +05:00
Ku6epXBOCTuK bc5a5d13c4 chore: add prettier, eslint, fix some components 2026-08-27 20:34:16 +05:00
Ku6epXBOCTuK 0acfa3aeb9 feat: add new design primitives 2026-08-27 19:41:32 +05:00
Ku6epXBOCTuK 418a091505 feat: add new design2.css, add fonts 2026-08-27 19:23:30 +05:00
Ku6epXBOCTuK f08af41568 refactor: move old design to separate layout tree 2026-08-27 19:22:28 +05:00
Ku6epXBOCTuK dfb2311e32 docs: update re-design plan 2026-08-27 18:53:42 +05:00
Ku6epXBOCTuK 84534b2f58 docs: add new design references 2026-08-27 16:02:08 +05:00
Ku6epXBOCTuK 83d3ce1198 feat: add compression tools 2026-08-26 18:06:03 +05:00
Ku6epXBOCTuK 501d2dda4e feat: add quantize tools 2026-08-26 17:31:24 +05:00
Ku6epXBOCTuK 2b67414e79 feat: add text and generation tools 2026-08-26 16:08:27 +05:00
Ku6epXBOCTuK 56ebe5b53b feat: add pixel effects core tools 2026-08-26 14:13:38 +05:00
Ku6epXBOCTuK 3c1c5b2b3a fix: update search flow - first tool search 2026-08-26 12:34:41 +05:00
Ku6epXBOCTuK 84cc56cc6c feat: add geometry masks 2026-08-26 12:31:44 +05:00
Ku6epXBOCTuK d65adcd16c feat: add shape masks 2026-08-26 11:45:14 +05:00
Ku6epXBOCTuK 553601acc1 feat: add masks core tools 2026-08-26 09:26:26 +05:00
Ku6epXBOCTuK 5c37eb2e7b feat: add channel tools 2026-08-26 08:38:30 +05:00
Ku6epXBOCTuK db02cbd165 feat: add pallette tools 2026-08-26 07:38:54 +05:00
Ku6epXBOCTuK 11fc160997 docs: update plans 2026-08-26 07:27:40 +05:00
Ku6epXBOCTuK 116d7c72aa feat: add watermarks and text tools 2026-08-26 07:21:43 +05:00
Ku6epXBOCTuK b277f3c66d docs: update plan, add tool map 2026-08-26 06:32:40 +05:00
Ku6epXBOCTuK 54af7626d6 feat: add text and date draw tools 2026-08-26 05:43:03 +05:00
Ku6epXBOCTuK 38e7cd20d2 feat: add text draw core 2026-08-26 00:22:28 +05:00
Ku6epXBOCTuK 2e980fb115 docs: add text-wave plan 2026-08-26 00:05:17 +05:00
Ku6epXBOCTuK 6e0647dfbf feat: add demo page 2026-08-25 18:44:03 +05:00
Ku6epXBOCTuK 148e4b1957 feat: inline tool page - parameters beetween source and result images 2026-08-25 15:48:16 +05:00
Ku6epXBOCTuK d70cbad0d2 refactor: separate tool page for A\B testing 2026-08-25 15:22:20 +05:00
Ku6epXBOCTuK 365af6ef95 docs: update plans 2026-08-25 14:58:47 +05:00
Ku6epXBOCTuK 5e512f0bdd fix: some small css fixes 2026-08-25 14:44:31 +05:00
Ku6epXBOCTuK f1f5bd4409 feat: add theme switcher, dark theme 2026-08-25 14:32:13 +05:00
Ku6epXBOCTuK 5b22409d41 docs: update plans 2026-08-25 12:09:07 +05:00
Ku6epXBOCTuK 96b8c00129 fix: get names from categories 2026-08-25 11:44:36 +05:00
Ku6epXBOCTuK 6d770dc642 feat: add base path for deploy 2026-08-25 10:36:10 +05:00
365 changed files with 88642 additions and 4083 deletions
+11
View File
@@ -1 +1,12 @@
node_modules
# Reference source (legacy Next.js design in refs/): branding icons are not needed
refs/public/icon.svg
refs/public/icon-dark-32x32.png
refs/public/icon-light-32x32.png
refs/public/apple-icon.png
refs/public/placeholder-logo.svg
refs/public/placeholder-logo.png
refs/public/placeholder.svg
web/audit/cdp-audit*.txt
css-lint.txt
+5
View File
@@ -0,0 +1,5 @@
# Allowlist: prettier в корне форматирует только markdown docs/.
# web/ и refs/ имеют свои тулинги, refs-html/ — генерат: всё остальное
# в корне (включая появившееся позже) игнорируется целиком.
/*
!/docs
+3
View File
@@ -0,0 +1,3 @@
{
"proseWrap": "always"
}
+176
View File
@@ -0,0 +1,176 @@
# AGENTS.md
Инструкции для агентов (и IDE-ассистентов) по работе с этим репозиторием.
## Стек
- **SvelteKit** (`web/`) — основное приложение, Svelte 5 (runes).
- Менеджер пакетов: **pnpm**. Запускать команды из `web/`, либо через
обёртки в корне: `pnpm build`, `pnpm dev` (это `pnpm --dir web ...`).
Не использовать npm.
- Сборка: `pnpm --dir web build`, проверка типов: `pnpm --dir web exec svelte-check --tsconfig ./tsconfig.json`,
тесты: `pnpm --dir web test` (Vitest), e2e: `pnpm --dir web test:e2e` (Playwright,
`web/e2e/`, свой webServer на `pnpm build` + `scripts/serve-static.mjs`, порт 4173).
- Форматирование: два независимых прогона. Корневой prettier форматирует
только markdown `docs/` (`.prettierrc` + allowlist `.prettierignore` в корне,
`pnpm format:docs`, проверка — `pnpm check:docs`). Код `web/` форматируется
отдельно: `pnpm --dir web format` (Prettier + `prettier-plugin-svelte`,
конфиг `web/.prettierrc`, игнор `web/.prettierignore`). Проверка без записи:
`pnpm --dir web exec prettier --check .`. Оба прогона сразу: `pnpm format`
в корне.
- Линтинг: `pnpm --dir web lint` (ESLint, flat-конфиг `web/eslint.config.js`).
Устроен инкрементально:
- На **весь код** — парсинг TS/Svelte + правило
`@typescript-eslint/consistent-type-imports` (запрет инлайн-тип-импортов).
- Полные `recommended`-наборы (`eslint` + `typescript-eslint` + `eslint-plugin-svelte`)
навешены **только на новый код** (`src/lib/components/kit/**`,
`src/routes/preview/**`), чтобы старый код не засыпался предсуществующими
ошибками. Когда старый дизайн удалён (C19), scoped-блок убирается и
`recommended` включается на весь код.
- **Scoped-пути двигаются вместе с папками** (см. `plan-redesign.md` §10):
на C17 `src/routes/preview/**``src/routes/**` (preview переезжает на
реальные маршруты), на C19 при переименовании `kit/``ui/` — и компонентный
glob. Не оставлять устаревшие пути в `eslint.config.js`.
- Для `*.svelte` выключен `prefer-const` (пропсы в Svelte 5 пишутся через `let`).
- **Единый запуск всей дизайн-проверки: `pnpm --dir web lint:all`**
(`web/scripts/lint-all.mjs`) — гоняет по очереди: ESLint (все правила,
включая design-tokens), stylelint (`lint:css`) и токен-аудит
(`lint:tokens`). Каждый шаг выполняется, даже если предыдущий упал; выход
ненулевой, если хоть один не прошёл. Текущие «ожидаемые долги» (hardcoded
размеры в kit, не-Prefix токены в preview.css и т.п.) — техдолг: чинить
только по заведённым tasks, не игнорировать правилом.
## Правила кода
### Svelte 5: типизация props через `interface Props`
Все типизированные пропсы компонентов описываются через локальный
`interface Props`, а деструктуризация идёт через аннотацию типа при `$props()`:
```svelte
<script lang="ts">
import type { Snippet } from 'svelte';
interface Props {
label: string;
accent?: boolean;
children?: Snippet;
}
let { label, accent = false, children }: Props = $props();
</script>
```
Не использовать инлайн-дженерик `$props<{ ... }>()` — он тяжело читается и
разносит тип и деструктуризацию по разным местам. Также **не использовать
инлайн-импорты в типах** (`children?: import('svelte').Snippet;`) — все
`import type` поднимаются наверх файла. Это правило **включено в ESLint**
(`@typescript-eslint/consistent-type-imports`, `prefer: 'type-imports'`).
> Правило «всегда `interface Props` + `let {...}: Props = $props()`» стандартным
> ESLint-правилом не покрывается — остаётся конвенцией. Кастомное правило
> (eslint-plugin-svelte / свой visitor) — TODO, позже.
### Дизайн: новый визуальный язык
Переезд на новый дизайн описан в `docs/plan-redesign.md`. Ключевые
ограничения при работе с веткой `feat/redesign`:
- Глобальные стили старого и нового дизайна разнесены по layout без
наследования: старое в `web/src/app.css` (группа `(old)/`), новое в
`web/src/lib/styles/design2.css` (ветка `preview/*`). Префикс `.ds2` не
используется.
- Все повторяющиеся визуальные элементы — отдельные компоненты в
`web/src/lib/components/kit/`, даже «просто div с двумя стилями».
### Линтинг дизайн-токенов («единая точка правды» дизайна)
Запрещено «захардкоживать» дизайн: цвета, размеры, длительности и z-index
обязаны приходить из CSS-переменных. Стек: **stylelint** прогоняется по всем
CSS-файлам (`web/stylelint.config.js`), **ESLint** лезет в `<style>`-блоки
svelte-компонентов через локальный плагин `web/eslint-plugins/design-tokens/`
(постпрефес postcss AST от svelte-eslint-parser). Правила применяются к тем же
scoped-путям (`kit/**`, `preview/**`).
Команды:
- `pnpm --dir web lint:css` — stylelint по всем `src/**/*.css` (`old.css` и
`node_modules/build/.svelte-kit/static` игнорируются).
- `pnpm --dir web exec node scripts/check-tokens.mjs` — токен-аудит по
preview.css (лёгкий оркестратор поверх `web/scripts/token-audit/*`);
проверяет три вещи. Каждый цветовой
токен из `:root` обязан иметь пару в `[data-theme="dark"]` и наоборот.
Производные токены (значение содержит `var()`, напр. `hct(from var(--...))`)
из пары исключены — они наследуют тему автоматически. Выводит **варнинг** о
неиспользуемых токенах preview.css (определены, но нигде не используются) —
выход он не меняет. Провалом (exit 1) считаются только parity и hct-авторство.
- Тот же скрипт проверяет **hct-only для цветов**: ЛЮБОЕ цветовое значение в
preview.css обязано быть `hct(...)` — и литерал, и производное
`hct(from var(...) h c t)` (вся палитра считается через hct-каналы; эммит в
sRGB-hex делает postcss-плагин `web/scripts/postcss-hct.mjs`). Исключения:
только `--brand-main` / `--brand-alt` (seed-токены, любая форма) и
`color-mix(...)` (единственный легальный способ смешать два токена).
`oklch()/rgb()/#hex` в `--color-*` запрещены.
Токены-префиксы (целевой словарь дизайна):
- Цвета: `--color-*`, бренд `--brand-main` / `--brand-alt` — единственные
две переменные, которым разрешено быть hex/rgb, остальные цвета — только
`hct()` (эммит в sRGB-hex делает postcss-плагин `web/scripts/postcss-hct.mjs`)
и только в preview.css.
- Размеры: `--space-*`, `--text-*` (font-size), `--radius-*`, `--size-*`.
- Брейкпоинты: `@custom-media --bp-mobile (max-width: 640px)` /
`--bp-tablet (800px)` / `--bp-desktop (1100px)` — объявляются в preview.css,
используются как `@media (--bp-*)`. CSS-переменные в `@media` не работают,
поэтому отдельных `--bp-*` токенов нет; раскрытие делает postcss-плагин
`postcss-custom-media` (конфиг `web/postcss.config.js`, определения
подтягиваются через `@csstools/postcss-global-data`).
- z-index: `--z-*`; длительности/анимации: `--duration-*`, `--ease-*`.
- Правило `custom-property-pattern` в stylelint тестирует паттерн **без** `--`
(`--x``x`), а `declaration-property-value-disallowed-list` — целиком с `--`.
- `custom-property-empty-line-before` перенастроен (`after-custom-property` в
`ignore`, не в `except`): пустые строки между подряд идущими токенами
**свободны** и `--fix` их не удаляет — можно группировать цветовые и размерные
токены в preview.css отдельными блоками.
Правила плагина (компоненты, `eslint.config.js`):
- `design-tokens/no-hardcoded-in-svelte` — запрет прямых цветов/размеров/
длительностей/z-index и `color-mix()` (результат токенизировать в CSS).
Исключения размеров: `0`, `0px`, `1px`, проценты и unitless (`line-height`).
- `design-tokens/no-category-mismatch` — size-свойство не может использовать
color-токен (`--color-*`/`--brand-*`) и наоборот.
- `design-tokens/no-token-definition-in-svelte` — определение `--x:` в
компоненте не может содержать примитив (hex/rgb/oklch/px/rem); допустимы
производные от токенов (`var()`, `calc()`, unitless-числа).
- `design-tokens/no-undefined-in-svelte``var(--x)` в `<style>` должен быть
определён в `src/preview.css` (словарь токенов) или локально в компоненте.
Файл читается один раз и кэшируется (не `glob`-зависим).
### Изоляция веток old ↔ preview
`isolation/no-mixed-imports` (`web/eslint-plugins/isolation/no-mixed-imports.js`,
включён на весь `**/*.{ts,svelte}`) — **полная взаимная изоляция** старого UI и
новой preview-ветки. В отличие от `no-restricted-imports`, правило **резолвит**
каждый импорт (и `$lib/...`, и относительные `./`/`../`) до реального файла и
классифицирует стороны по фактическому пути, поэтому относительным импортом
правило не обойти.
- Старое: `routes/(old)/**`, `lib/registry.ts`, `lib/registry/**`,
`lib/registry-helpers.ts`, `lib/categories.ts`, `lib/tools/**`,
`lib/components/**` (кроме `kit/`).
- Новое: `routes/preview/**`, `lib/registry-new/**`, `lib/preview/**`,
`lib/registry-schema.ts`, `lib/registry-schema.test.ts`, `lib/components/kit/**`.
- Общее (разрешено обоим): `core/`, `i18n/`, `theme`, `assets/`, корневой `lib`.
- Плагин **конфигурируем** (опции `old`/`new` + `root`/`alias` в
`eslint.config.js`): перенос старых файлов в папки `old/` — это правка
glob-паттернов в настройке, а не код правила.
- Изоляция уже достигнута: старый `registry/` не тянет `registry-schema`
(пилоты add-border/add-stroke работают через `params`), `registry.ts` не
импортирует `ToolSchema`; preview-`list-tools` использует копию
`preview/tool-icons.ts`, а не старый `tools/tool-icons.ts`.
Линтер только показывает ошибки, старый код (непрефиксованные токены, hex в
preview.css) — известный техдолг, его НЕ чинить и не игнорировать правилами.
- Коммиты мелкие (< ~500 строк), атомарные
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Ku6epXBOCTuK
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+92
View File
@@ -0,0 +1,92 @@
# easy-png-tools
Browser-based PNG utility toolkit — no server, no data upload, everything runs locally.
Inspired by online services like onlinepngtools.com, but fully offline-capable and open source.
## Features
- **Pixel-level image processing** — format conversion, transparency/alpha channel, color transformations, geometry, filters/convolutions, morphology, quantization & palettes, image generation, text/watermarks, analysis & checks
- **Pipeline system (workspace)** — load image → chain multiple tool steps → sequential application → download result; pipelines saved to localStorage, exportable as JSON
- **Web Worker execution** — heavy operations run off the main thread with automatic fallback to direct calls
- **i18n** — full Russian and English localization with search matching
- **Dark/Light themes** — persisted to localStorage
- **Pure TypeScript core** — `lib/core/` operates on `ImageData`/`Uint8ClampedArray` with no DOM dependency
- **~30+ tools** across categories: transparency, color, geometry, filters, morphology, palettes, generation, text, analysis
## Tech Stack
- **SvelteKit** (Svelte 5, runes mode) with static adapter — pure static export, no server
- **Vite** + **TypeScript** (strict)
- **Vitest** for unit tests
- **ESLint** + **Prettier**
- **pnpm** (required, ^11.20.0)
- **Lucide** icons, **IBM Plex** fonts (self-hosted)
## Getting Started
### Prerequisites
- [pnpm](https://pnpm.io/) v11.20+
### Install & Run
```bash
pnpm install
pnpm dev
```
### Build
```bash
pnpm build
```
Static output is written to `web/build/`.
### Other Commands
| Command | Description |
| ------------------------ | ------------------------- |
| `pnpm --dir web test` | Run unit tests (Vitest) |
| `pnpm --dir web check` | Type-check (svelte-check) |
| `pnpm --dir web lint` | Lint (ESLint) |
| `pnpm --dir web format` | Format code (Prettier) |
| `pnpm format:docs` | Format docs (Prettier) |
| `pnpm --dir web preview` | Preview production build |
## Project Structure
```txt
easy-png-tools/
├── web/ # Main SvelteKit application
│ ├── src/
│ │ ├── lib/
│ │ │ ├── core/ # Pure TS image processing (ImageData-based)
│ │ │ ├── components/ # UI components
│ │ │ ├── tools/ # Pipeline, executor, overlay store
│ │ │ ├── i18n/ # Localization (ru/en)
│ │ │ └── registry.ts # Tool registry (source of truth)
│ │ └── routes/ # SvelteKit routes
│ └── build/ # Static export output
├── docs/ # Architecture docs, roadmap, plans
└── refs/ # Design reference (Next.js, not part of runtime)
```
## Architecture
The project is built around a **tool registry** (`registry.ts`) — each tool is a self-contained entry with `{id, title, description, category, params, run}`. Pages, forms, and pipelines are generated from this registry.
Core image processing lives in `lib/core/` and operates directly on `ImageData` objects with no browser API dependencies, making it portable to other environments (WASM, CLI) in the future.
## Roadmap
See [docs/roadmap.md](docs/roadmap.md) for the full development plan. Current phases:
1. **Phase 1** — TypeScript core + web UI (in progress)
2. **Phase 2** — Rust/WASM core for performance-critical operations
3. **Phase 3** — CLI tool
## License
[MIT](LICENSE)
+208 -74
View File
@@ -1,9 +1,13 @@
# Анализ: набор PNG-утилит для браузера
Проект: **easy-png-tools** — набор PNG-утилит, работающий полностью в браузере. Вдохновлён идеей онлайн-сервисов «все PNG-операции в одном месте»: никаких загрузок на сервер, всё считается локально.
Стек: **SvelteKit + @sveltejs/adapter-static** (статический экспорт, никакого сервера). Все данные обрабатываются на клиенте, ничего не уходит в сеть.
Проект: **easy-png-tools** — набор PNG-утилит, работающий полностью в браузере.
Вдохновлён идеей онлайн-сервисов «все PNG-операции в одном месте»: никаких
загрузок на сервер, всё считается локально. Стек: **SvelteKit +
@sveltejs/adapter-static** (статический экспорт, никакого сервера). Все данные
обрабатываются на клиенте, ничего не уходит в сеть.
Источник идеи: типовой перечень утилит, который встречается в онлайн-наборах PNG-инструментов (~296 операций).
Источник идеи: типовой перечень утилит, который встречается в онлайн-наборах
PNG-инструментов (~296 операций).
---
@@ -21,63 +25,121 @@
| Тяжёлая математика без блокировки UI | Web Workers, SharedArrayBuffer, WASM (zlib, квантование, ML) |
| Экономия памяти при больших изображениях | `ImageBitmap`, работа тайлами, downscale до декорирования |
**Вывод для архитектуры:** ~90% утилит — это одна и та же связка «декодировать → прогнать через пиксельную функцию → закодировать». Разумно сделать общий ядро (`core/pipeline.ts` + набор операций над `ImageData`) и тонькие страницы-обёртки в SvelteKit. Каждая утилита = отдельный маршрут со своим UI (drag&drop слева, превью, параметры, скачивание справа).
**Вывод для архитектуры:** ~90% утилит — это одна и та же связка «декодировать →
прогнать через пиксельную функцию → закодировать». Разумно сделать общий ядро
(`core/pipeline.ts` + набор операций над `ImageData`) и тонькие страницы-обёртки
в SvelteKit. Каждая утилита = отдельный маршрут со своим UI (drag&drop слева,
превью, параметры, скачивание справа).
---
## 2. Уровень EASY — canvas + ImageData, простые попиксельные операции и композиция
Реализация: загрузка в canvas → `getImageData()` → цикл по пикселям / перекомпоновка / отрисовка на выходной canvas → `toBlob('image/png')`. Без сложных алгоритмов, каждая утилита — часы работы.
Реализация: загрузка в canvas → `getImageData()` → цикл по пикселям /
перекомпоновка / отрисовка на выходной canvas → `toBlob('image/png')`. Без
сложных алгоритмов, каждая утилита — часы работы.
### 2.1 Конвертация форматов и текстовые представления
- **convert-png-to-jpg / convert-jpg-to-png / convert-webp-to-png / convert-png-to-webp / convert-svg-to-png** — рисование на canvas + `toBlob()` нужного типа
- **convert-bmp-to-png / convert-ico-to-png / convert-gif-to-png** (первый кадр) — нативное декодирование браузером
- **convert-png-to-jpg / convert-jpg-to-png / convert-webp-to-png /
convert-png-to-webp / convert-svg-to-png** — рисование на canvas + `toBlob()`
нужного типа
- **convert-bmp-to-png / convert-ico-to-png / convert-gif-to-png** (первый кадр)
— нативное декодирование браузером
- **convert-png-to-bmp** — BMP-формат простой, ~30 строк энкодера
- **convert-png-to-base64 / convert-base64-to-png / data-uri (в обе стороны)** — `FileReader.readAsDataURL` + строка
- **convert-png-to-bytes / convert-bytes-to-png / convert-png-to-hex / convert-hex-to-png / convert-png-to-rgb-values / convert-rgb-values-to-png** — чтение/запись `ImageData` в текст
- **convert-png-to-base64 / convert-base64-to-png / data-uri (в обе стороны)** —
`FileReader.readAsDataURL` + строка
- **convert-png-to-bytes / convert-bytes-to-png / convert-png-to-hex /
convert-hex-to-png / convert-png-to-rgb-values / convert-rgb-values-to-png** —
чтение/запись `ImageData` в текст
- **convert-png-to-gif** (один кадр) — `gifenc`, тривиально
### 2.2 Прозрачность и альфа-канал
- **create-transparent-png, remove-color-from-png(простая версия), transparent-logo-maker, remove-logo/icon-background (по цвету)** — замена цвета на прозрачность по порогу близости
- **remove-alpha-channel-from-png, fill-alpha-channel-in-png, replace-alpha-channel-in-png, extract-alpha-channel-from-png, invert-png-alpha-channel** — манипуляции только с A-компонентой
- **generate-png-alpha-mask, show-transparent-png-areas, fill-transparent-png-pixels, convert-semi-transparent-png-to-opaque** — пороги и заливки по альфе
- **create-transparent-png, remove-color-from-png(простая версия),
transparent-logo-maker, remove-logo/icon-background (по цвету)** — замена
цвета на прозрачность по порогу близости
- **remove-alpha-channel-from-png, fill-alpha-channel-in-png,
replace-alpha-channel-in-png, extract-alpha-channel-from-png,
invert-png-alpha-channel** — манипуляции только с A-компонентой
- **generate-png-alpha-mask, show-transparent-png-areas,
fill-transparent-png-pixels, convert-semi-transparent-png-to-opaque** — пороги
и заливки по альфе
- **harden-png-edges** — бинаризация альфы по порогу
### 2.3 Цвет: замена, тон, каналы, цветовые пространства
- **change-png-color, change-png-color-tone, change-png-opacity, swap-png-colors, change-png-brightness, change-png-contrast, convert-png-to-grayscale, add-sepia-tone-to-png, change-png-hue, invert-png-colors, reverse-png-colors, convert-png-to-two-colors, convert-png-to-black-and-white, create-monochrome-png, generate-single-color-png** — попиксельные арифметики/преобразования
- **extract-png-color-channels, swap-rgba-color-channels, split-png-into-rgb-components** — перестановка/выделение RGBA-каналов
- **convert-png-to-hsl/hsv/hsi/cmyk/ycbcr/lab-colors** — известные формулы конвертации (Lab — чуть больше математики, но детерминировано)
- **generate-\*-color-palette (complementary/monochromatic/analogous/triadic/tetradic), generate-similar-color-shades, color-wheel-generator, mix-multiple-colors, calculate-average-color, simulate-paint-colors, blend-two-colors, step-between-two-colors, convert-color-names-to-png** — генераторы палитр/смешение без работы с изображением
- **posterize-png, create-color-spectrum-png, create-multi-color-gradient-png, generate-png-gradient-palette** — простые градиенты/квантование уровней
- **change-png-color, change-png-color-tone, change-png-opacity,
swap-png-colors, change-png-brightness, change-png-contrast,
convert-png-to-grayscale, add-sepia-tone-to-png, change-png-hue,
invert-png-colors, reverse-png-colors, convert-png-to-two-colors,
convert-png-to-black-and-white, create-monochrome-png,
generate-single-color-png** — попиксельные арифметики/преобразования
- **extract-png-color-channels, swap-rgba-color-channels,
split-png-into-rgb-components** — перестановка/выделение RGBA-каналов
- **convert-png-to-hsl/hsv/hsi/cmyk/ycbcr/lab-colors** — известные формулы
конвертации (Lab — чуть больше математики, но детерминировано)
- **generate-\*-color-palette
(complementary/monochromatic/analogous/triadic/tetradic),
generate-similar-color-shades, color-wheel-generator, mix-multiple-colors,
calculate-average-color, simulate-paint-colors, blend-two-colors,
step-between-two-colors, convert-color-names-to-png** — генераторы
палитр/смешение без работы с изображением
- **posterize-png, create-color-spectrum-png, create-multi-color-gradient-png,
generate-png-gradient-palette** — простые градиенты/квантование уровней
### 2.4 Геометрия, композиция, холст
- **resize-png, zoom-png, crop-png, cut-png, rotate-png, skew-png, shift-png, change-png-canvas-size, change-png-aspect-ratio, fit-png-in-rectangle, convert-landscape/portrait-png-to-portrait/landscape-png** — операции через `CanvasRenderingContext2D` (`drawImage`, `rotate`, `setTransform`)
- **flip-png-horizontally/vertically, mirror-png, reverse-png** — отражение/поворот на 180°
- **center-png, center-logo, center-icon** — центровка по bounding box альфа-канала
- **duplicate-png, clone-png, multiply-png, create-png-tile, create-symmetric-png, split-png-into-parts** — тайлинг/копирование
- **create-round-png, create-square-png, create-star-shaped-png, create-wavy-shaped-png, round-png-corners** — маски-«штампы» на альфе
- **add-png-border, add-padding-to-png, add-space-around-png, remove-padding-from-png, remove-space-around-png, remove-png-padding** — расширение/усадка холста
- **add-png-background, add-signature/logo/icon/stamp-background** — подложка под прозрачность
- **resize-png, zoom-png, crop-png, cut-png, rotate-png, skew-png, shift-png,
change-png-canvas-size, change-png-aspect-ratio, fit-png-in-rectangle,
convert-landscape/portrait-png-to-portrait/landscape-png** — операции через
`CanvasRenderingContext2D` (`drawImage`, `rotate`, `setTransform`)
- **flip-png-horizontally/vertically, mirror-png, reverse-png** —
отражение/поворот на 180°
- **center-png, center-logo, center-icon** — центровка по bounding box
альфа-канала
- **duplicate-png, clone-png, multiply-png, create-png-tile,
create-symmetric-png, split-png-into-parts** — тайлинг/копирование
- **create-round-png, create-square-png, create-star-shaped-png,
create-wavy-shaped-png, round-png-corners** — маски-«штампы» на альфе
- **add-png-border, add-padding-to-png, add-space-around-png,
remove-padding-from-png, remove-space-around-png, remove-png-padding** —
расширение/усадка холста
- **add-png-background, add-signature/logo/icon/stamp-background** — подложка
под прозрачность
### 2.5 Текст, рамки, простые эффекты
- **add-text-to-png, convert-text-to-png, create-emoji-png, add-png-watermark, wrap-png-in-polaroid, create-logo-from-text, create-icon-from-text/emoji, create-custom-stamp, create-\*-stamp (round/rect/green/red/number/date-time/monogram/from-text/emoji), create-placeholder-png, create-empty-png, create-custom-png, generate-colorful-png, generate-random-png, generate-white-noise-png, generate-1x1-png, draw-png-grid** — рисование примитивов/случайных данных
- **add-noise-to-png, create-glitch-png, pixelate-png, censor-png, erase-part-of-png, randomize-png-pixels** — шум/порча байтов/блоки/кисть
- **add-text-to-png, convert-text-to-png, create-emoji-png, add-png-watermark,
wrap-png-in-polaroid, create-logo-from-text, create-icon-from-text/emoji,
create-custom-stamp, create-\*-stamp
(round/rect/green/red/number/date-time/monogram/from-text/emoji),
create-placeholder-png, create-empty-png, create-custom-png,
generate-colorful-png, generate-random-png, generate-white-noise-png,
generate-1x1-png, draw-png-grid** — рисование примитивов/случайных данных
- **add-noise-to-png, create-glitch-png, pixelate-png, censor-png,
erase-part-of-png, randomize-png-pixels** — шум/порча байтов/блоки/кисть
- **add-shadow-to-png** — смещение + лёгкое размытие + композиция
- **add-artifacts-to-png** — простой вариант: JPEG-раундтрип через canvas
### 2.6 Анализ и проверки
- **analyze-png, find-png-file-size, find-png-dimensions, check-if-png-is-landscape-or-portrait, find-png-color-count, check-if-png-is-grayscale, check-if-png-is-transparent, verify-if-image-is-png, pick-png-color, pick-logo/icon-color, png-viewer, test-png, preview-png-on-colorful-background, test-png-on-multiple-backgrounds** — чтение заголовков/ImageData/гистограмм, «пипетка» по клику
- **analyze-png, find-png-file-size, find-png-dimensions,
check-if-png-is-landscape-or-portrait, find-png-color-count,
check-if-png-is-grayscale, check-if-png-is-transparent,
verify-if-image-is-png, pick-png-color, pick-logo/icon-color, png-viewer,
test-png, preview-png-on-colorful-background,
test-png-on-multiple-backgrounds** — чтение заголовков/ImageData/гистограмм,
«пипетка» по клику
- **sort-colors** — сортировка списка палитры (не изображения)
### 2.7 Семейства signature/logo/icon/stamp — простые вариации
Почти все «-maker/-color/-resize/-crop/-rotate/-bg/-space/-padding/-grayscale/-single-color/-white/-black/-semi-transparent/-shadow» для signature/logo/icon/stamp — это переиспользование операций из разделов выше с одним цветом/боксом. Отдельно перечислены только те, что требуют алгоритмов (см. MEDIUM/HARD).
Почти все
«-maker/-color/-resize/-crop/-rotate/-bg/-space/-padding/-grayscale/-single-color/-white/-black/-semi-transparent/-shadow»
для signature/logo/icon/stamp — это переиспользование операций из разделов выше
с одним цветом/боксом. Отдельно перечислены только те, что требуют алгоритмов
(см. MEDIUM/HARD).
> Примерный объём: **≈180–190 утилит уровня EASY.**
@@ -85,54 +147,96 @@
## 3. Уровень MEDIUM — нужен настоящий алгоритм (свёртки, морфология, квантование, анимация)
Реализация: честные алгоритмы на `ImageData`, часто в Web Worker, при необходимости — обвязка вокруг готовых JS-библиотек.
Реализация: честные алгоритмы на `ImageData`, часто в Web Worker, при
необходимости — обвязка вокруг готовых JS-библиотек.
### 3.1 Фильтры и свёртки
- **blur-png, sharpen-png** — box/gaussian blur, ядро свёртки (упрощённо можно `ctx.filter`, но качественно — своя свёртка)
- **refine-png-edges, feather-png-edges, clean-png-edges** — работа с полупрозрачными краями: размытие альфы, де-контуринг, suppression полупрозрачности
- **blur-png, sharpen-png** — box/gaussian blur, ядро свёртки (упрощённо можно
`ctx.filter`, но качественно — своя свёртка)
- **refine-png-edges, feather-png-edges, clean-png-edges** — работа с
полупрозрачными краями: размытие альфы, де-контуринг, suppression
полупрозрачности
- **add-glow-effect-to-png** — расширение альфы + размытие + цветное наложение
### 3.2 Морфология и контуры (операции над альфа-маской)
- **add-stroke-to-png, add-outline-to-png, remove-stroke-from-png, remove-outline-from-png, find-png-outline** — dilation/erosion по альфе, извлечение контура
- **make-signature-thicker / make-signature-thinner, make-icon-thicker / make-icon-thinner** — dilation/erosion
- **convert-outlined-icon-to-filled-icon** — flood-fill замкнутых областей; **convert-filled-icon-to-outlined-icon** — эрозия
- **remove-png-border, remove-border-around-icon, remove-stamp-border** — детект однотонной/декоративной рамки и вырезание
- **add-stroke-to-png, add-outline-to-png, remove-stroke-from-png,
remove-outline-from-png, find-png-outline** — dilation/erosion по альфе,
извлечение контура
- **make-signature-thicker / make-signature-thinner, make-icon-thicker /
make-icon-thinner** — dilation/erosion
- **convert-outlined-icon-to-filled-icon** — flood-fill замкнутых областей;
**convert-filled-icon-to-outlined-icon** — эрозия
- **remove-png-border, remove-border-around-icon, remove-stamp-border** — детект
однотонной/декоративной рамки и вырезание
### 3.3 Квантование и цветовая кластеризация
- **quantize-png, decrease-png-color-count, set-custom-png-color-palette, add-dithering-to-png** — median cut / octree / Wu + FloydSteinberg; готовые JS-библиотеки (image-q и т.п.)
- **change-png-quality, create-low-quality-png** — симуляция потери качества через квантование + JPEG-раундтрип
- **convert-png-to-color-blocks** — мультипликативный блок-эффект/квантование по блокам
- **extract-png-color-palette, find-logo-color-scheme, extract-icon-colors** — частотная гистограмма + выбор топ-N / кластеризация
- **create-unique/popular/multi-color/outlier-color-mask** — статистика распределения цветов и выделение по маске
- **quantize-png, decrease-png-color-count, set-custom-png-color-palette,
add-dithering-to-png** — median cut / octree / Wu + FloydSteinberg; готовые
JS-библиотеки (image-q и т.п.)
- **change-png-quality, create-low-quality-png** — симуляция потери качества
через квантование + JPEG-раундтрип
- **convert-png-to-color-blocks** — мультипликативный блок-эффект/квантование по
блокам
- **extract-png-color-palette, find-logo-color-scheme, extract-icon-colors** —
частотная гистограмма + выбор топ-N / кластеризация
- **create-unique/popular/multi-color/outlier-color-mask** — статистика
распределения цветов и выделение по маске
### 3.4 Выделение объектов по цвету / фону
- **remove-png-chroma-key** — порог по цвету + деспилл (убрать зелёную окантовку)
- **remove-color-from-png (качественная версия), extract-color-from-png, separate-png-colors, separate-logo-colors, separate-icon-colors** — сегментация по близости цвета
- **convert-png-to-silhouette, convert-png-to-stencil** — порог + заливка силуэта
- **extract-stamp-from-image (по цвету печати)** — хромакей/частотный фильтр по цвету чернил
- **remove-png-background (вариант без ИИ)** — проверенный цветовой подход, знакомый по популярным онлайн-наборам PNG-утилит. Работает хорошо на однотонном/градиентном фоне и в ряде случаев удобнее ИИ. Опции и алгоритм:
- **Background Color** — выбор удаляемого цвета: пипеткой по изображению или hex/RGB
- **Percentage (похожесть цвета)** — slider; 0% = убрать только точный цвет, >0% = убрать соседние оттенки в окрестности цвета (в цветовом пространстве)
- **Delete Outer Areas (галочка)** — режим «только внешние пиксели»: удаляются только области, связанные с краями изображения (flood-fill/region-growing от границ). Выключена = удалять по всему холсту. Именно эта опция позволяет убрать фон, не задевая «пятна» того же цвета внутри объекта
- **Smooth Edge Line + Radius** — постобработка: полупрозрачная полоса толщиной N px по границе удаления (сгладить переход)
- **Preview Deleted Background** — двухцветный предпросмотр маски (что удаляется/что остаётся)
- Реализация: выбор цвета → порог по цветовому расстоянию → (опц.) flood-fill от краёв → сглаживание краёв → маска поверх превью. Всё на `ImageData`, без ML.
- **remove-png-chroma-key** — порог по цвету + деспилл (убрать зелёную
окантовку)
- **remove-color-from-png (качественная версия), extract-color-from-png,
separate-png-colors, separate-logo-colors, separate-icon-colors** —
сегментация по близости цвета
- **convert-png-to-silhouette, convert-png-to-stencil** — порог + заливка
силуэта
- **extract-stamp-from-image (по цвету печати)** — хромакей/частотный фильтр по
цвету чернил
- **remove-png-background (вариант без ИИ)** — проверенный цветовой подход,
знакомый по популярным онлайн-наборам PNG-утилит. Работает хорошо на
однотонном/градиентном фоне и в ряде случаев удобнее ИИ. Опции и алгоритм:
- **Background Color** — выбор удаляемого цвета: пипеткой по изображению или
hex/RGB
- **Percentage (похожесть цвета)** — slider; 0% = убрать только точный
цвет, >0% = убрать соседние оттенки в окрестности цвета (в цветовом
пространстве)
- **Delete Outer Areas (галочка)** — режим «только внешние пиксели»: удаляются
только области, связанные с краями изображения (flood-fill/region-growing от
границ). Выключена = удалять по всему холсту. Именно эта опция позволяет
убрать фон, не задевая «пятна» того же цвета внутри объекта
- **Smooth Edge Line + Radius** — постобработка: полупрозрачная полоса
толщиной N px по границе удаления (сгладить переход)
- **Preview Deleted Background** — двухцветный предпросмотр маски (что
удаляется/что остаётся)
- Реализация: выбор цвета → порог по цветовому расстоянию → (опц.) flood-fill
от краёв → сглаживание краёв → маска поверх превью. Всё на `ImageData`, без
ML.
### 3.5 Геометрические искажения и пиксель-сортировка
- **add-whirl-to-png** — свирл/вихрь: обратное отображение координат
- **sort-png-pixels** — pixel sorting (сортировка строк/столбцов по яркости/цвету)
- **upscale-png / downscale-png** — качественный ресемплинг (Lanczos, mipmap) поверх базового canvas-resize
- **remove-signature-background, transparent-png-signature-maker, clean-up-signature-edges, improve-low-quality-signature, clean-logo-edges, clean-icon-edges, fix-low-quality-stamp, convert-rubber-stamp-to-digital-stamp** — комбинация «цвет→прозрачность + де-шум + морфология краёв»
- **convert-image/png/jpg/icon/logo/signature-to-stamp** — стилизация в двухцветную «печать» (порог + зернистость + края)
- **sort-png-pixels** — pixel sorting (сортировка строк/столбцов по
яркости/цвету)
- **upscale-png / downscale-png** — качественный ресемплинг (Lanczos, mipmap)
поверх базового canvas-resize
- **remove-signature-background, transparent-png-signature-maker,
clean-up-signature-edges, improve-low-quality-signature, clean-logo-edges,
clean-icon-edges, fix-low-quality-stamp,
convert-rubber-stamp-to-digital-stamp** — комбинация «цвет→прозрачность +
де-шум + морфология краёв»
- **convert-image/png/jpg/icon/logo/signature-to-stamp** — стилизация в
двухцветную «печать» (порог + зернистость + края)
### 3.6 Анимации и GIF-энкодинг
- **convert-png-to-gif, slowly-reveal-png, create-disappearing-png, fade-in-png, fade-out-png, create-scrolling-png, create-png-sequence-from-gif** — декомпозиция кадров GIF / генерация последовательности кадров + `gifenc`/`UPNG` (APNG)
- **convert-png-to-gif, slowly-reveal-png, create-disappearing-png, fade-in-png,
fade-out-png, create-scrolling-png, create-png-sequence-from-gif** —
декомпозиция кадров GIF / генерация последовательности кадров +
`gifenc`/`UPNG` (APNG)
> Примерный объём: **≈71 утилита уровня MEDIUM.**
@@ -140,35 +244,65 @@
## 4. Уровень HARD — WASM, ML, длительная обработка, тонкая подгонка алгоритма
Реализация: WASM-модули, ML-модели в браузере, длительная обработка с прогресс-барами.
Реализация: WASM-модули, ML-модели в браузере, длительная обработка с
прогресс-барами.
- **compress-png / reduce-png-file-size / optimize-png** — сжатие на уровне формата: нужен качественный PNG-энкодер с контролем zlib-фильтров/битовой глубины и оптимизаторы. Решение: **pngquant / oxipng / zopfli (WASM)** + подгонка под «качество→размер» (это главный кандидат на HARD — именно здесь тюнинг бесконечен)
- **remove-png-background (вариант с ИИ)** — опциональное улучшение к MEDIUM-версии: ML-сегментация (Rembg/DeepLab в WASM или ONNX) для произвольных фото, где цветовой матчинг бессилен. Начинать можно без него — не-ИИ версия уже даёт рабочий инструмент
- **extract-signature-from-image** — детект подписи на скане/фото с произвольным фоном: де-шум, контраст, ML-детекция
- **convert-handwritten-signature-to-digital-signature** — очистка и «оцифровка» почерка (ML, денойзинг, выравнивание)
- **extract-barcode-from-image** — декодирование штрих-кодов/QR: готовая библиотека **zxing-wasm**, сопряжение с камерой/фото
- **compress-png / reduce-png-file-size / optimize-png** — сжатие на уровне
формата: нужен качественный PNG-энкодер с контролем zlib-фильтров/битовой
глубины и оптимизаторы. Решение: **pngquant / oxipng / zopfli (WASM)** +
подгонка под «качество→размер» (это главный кандидат на HARD — именно здесь
тюнинг бесконечен)
- **remove-png-background (вариант с ИИ)** — опциональное улучшение к
MEDIUM-версии: ML-сегментация (Rembg/DeepLab в WASM или ONNX) для произвольных
фото, где цветовой матчинг бессилен. Начинать можно без него — не-ИИ версия
уже даёт рабочий инструмент
- **extract-signature-from-image** — детект подписи на скане/фото с произвольным
фоном: де-шум, контраст, ML-детекция
- **convert-handwritten-signature-to-digital-signature** — очистка и «оцифровка»
почерка (ML, денойзинг, выравнивание)
- **extract-barcode-from-image** — декодирование штрих-кодов/QR: готовая
библиотека **zxing-wasm**, сопряжение с камерой/фото
> Итого **6 утилит уровня HARD** (все — опциональные улучшения/надстройки). Всё остальное (≈290) — EASY/MEDIUM.
> Итого **6 утилит уровня HARD** (все — опциональные улучшения/надстройки). Всё
> остальное (≈290) — EASY/MEDIUM.
---
## 5. Итоговая сводка
**EASY — ~185 утилит.** canvas + ImageData.
Примеры: crop, resize, rotate, конвертации, альфа-канал, палитры, текст, рамки, анализ.
**EASY — ~185 утилит.** canvas + ImageData. Примеры: crop, resize, rotate,
конвертации, альфа-канал, палитры, текст, рамки, анализ.
**MEDIUM — ~71 утилита.** Алгоритмы, Web Workers, gifenc.
Примеры: blur, sharpen, морфология, квантование, дизеринг, pixel sorting, анимации GIF, remove-background (без ИИ).
**MEDIUM — ~71 утилита.** Алгоритмы, Web Workers, gifenc. Примеры: blur,
sharpen, морфология, квантование, дизеринг, pixel sorting, анимации GIF,
remove-background (без ИИ).
**HARD — 6 утилит.** WASM (pngquant/oxipng/zxing), ML (ONNX/Rembg).
Примеры: compress/optimize, remove-background (ИИ), extract-signature, barcode.
**HARD — 6 утилит.** WASM (pngquant/oxipng/zxing), ML (ONNX/Rembg). Примеры:
compress/optimize, remove-background (ИИ), extract-signature, barcode.
---
## 6. Рекомендации по реализации
1. **Единое ядро обработки**`src/lib/core/`: загрузка файла → `ImageData`, библиотека операций (`color.ts`, `alpha.ts`, `geometry.ts`, `filters.ts`, `morphology.ts`, `quantize.ts`, `channels.ts`), экспорт через `toBlob`. Каждая утилита — тонкая SvelteKit-страница + общие компоненты (DropZone, Preview, Download).
2. **Порядок внедрения:** MVP = ~40 топовых EASY (crop, resize, rotate, convert, alpha, grayscale, brightness, палитры, watermark, add-text, add-background, pick-color, analyze). Затем остальные EASY → MEDIUM пакетами по темам (фильтры, морфология, квантование, анимации). `remove-png-background` (без ИИ) — отличная «витринная» MEDIUM-фича ранних этапов: не требует ML и переиспользует `remove-color` + flood-fill + сглаживание краёв. HARD — в последнюю очередь.
3. **Производительность:** вся пиксельная работа и тяжёлые алгоритмы — в Web Worker; большие изображения декодировать сразу в downscale для превью; прогресс-бары для MEDIUM/HARD.
4. **SEO/структура:** SvelteKit + static adapter — каждая утилита отдельный маршрут (`/crop-png`, `/rotate-png`), что даёт удобные адреса и повторяет распространённую структуру наборов PNG-инструментов.
5. **Семейства logo/icon/stamp/signature** — переиспользуют общие операции; в коде это конфиг-опции той же страницы (например `/resize-png` и `/resize-icon` = один компонент с разными подписями/дефолтами). Не плодить дублирующий код.
1. **Единое ядро обработки**`src/lib/core/`: загрузка файла → `ImageData`,
библиотека операций (`color.ts`, `alpha.ts`, `geometry.ts`, `filters.ts`,
`morphology.ts`, `quantize.ts`, `channels.ts`), экспорт через `toBlob`.
Каждая утилита — тонкая SvelteKit-страница + общие компоненты (DropZone,
Preview, Download).
2. **Порядок внедрения:** MVP = ~40 топовых EASY (crop, resize, rotate, convert,
alpha, grayscale, brightness, палитры, watermark, add-text, add-background,
pick-color, analyze). Затем остальные EASY → MEDIUM пакетами по темам
(фильтры, морфология, квантование, анимации). `remove-png-background` (без
ИИ) — отличная «витринная» MEDIUM-фича ранних этапов: не требует ML и
переиспользует `remove-color` + flood-fill + сглаживание краёв. HARD — в
последнюю очередь.
3. **Производительность:** вся пиксельная работа и тяжёлые алгоритмы — в Web
Worker; большие изображения декодировать сразу в downscale для превью;
прогресс-бары для MEDIUM/HARD.
4. **SEO/структура:** SvelteKit + static adapter — каждая утилита отдельный
маршрут (`/crop-png`, `/rotate-png`), что даёт удобные адреса и повторяет
распространённую структуру наборов PNG-инструментов.
5. **Семейства logo/icon/stamp/signature** — переиспользуют общие операции; в
коде это конфиг-опции той же страницы (например `/resize-png` и
`/resize-icon` = один компонент с разными подписями/дефолтами). Не плодить
дублирующий код.
+118
View File
@@ -0,0 +1,118 @@
# Demo fidelity: /preview/demo vs refs-html/demo.html
> ⚠️ **УТРАТИЛ АКТУАЛЬНОСТЬ — архивная копия.** Актуальный план фикса верности и
> текущий статус пунктов B/F — в `docs/plan-design-fix.md` (Этап 1). Аудит
> верности теперь автоматизирован (`pnpm audit` → `web/audit/audit-report.md`;
> пиксель-дифф не используется); темы в `refs-html` переключаются (скрипт
> инжектится в `scripts/extract-static.mjs`). Ниже — исходный текст на момент
> написания.
Метод: Playwright, viewport 1440×900, скриншоты обеих страниц + замер
вычисленных стилей ключевых элементов + пиксель-дифф (без чтения картинки
моделью). Пиксель-дифф: **24.9%** (наш скролл 1731px, реф 1579px). Зоны различий
размазаны по всей высоте → править надо глобально, не точечно.
Теперь процесс автоматизирован: `pnpm audit` (Playwright) пишет
`web/audit/audit-report.md` — light/dark токены + поэлементный дифф стилей по
тексту. Пиксель-дифф убран (неудобен для работы). Фиксы можно делать в любой
момент и перепроверять аудитом.
## Баги (объективно, правим в любом случае)
- **B1. Кнопки/инпуты рендерятся шрифтом Arial, а не IBM Plex Sans.**
Доказательство: пробы `Reset pipeline``fontFamily: Arial`. Причина: у
`button/input` не задан `font-family: inherit`. Фикс: добавить в reset
`design2.css` `button, input, select, textarea { font-family: inherit; }`.
Влияет на все страницы. (P1)
- **B2. Двойной топбар.** Страница демо рендерит свой `.topbar` (brand / AUTO
PIPELINE / help / theme / lang), который перекрывается глобальным `fixed`
Header из `preview/+layout.svelte` (в нём только theme-toggle). В рефе — один
топбар. Контент демо-топбара (brand, статус, язык) визуально спрятан за
фикс-хедером. Фикс: убрать кастомный `.topbar` из демо и сделать хедер лейаута
реф-стайл топбаром (brand + version + status + help + theme + lang) — улучшит
и C9/C10. (P1)
## Расхождения дизайна (нужно решение)
- **F1. Фон холста.** Реф: плоский серый `#EEF1F4`. Наш: белый +
blueprint-сетка. Решение: совпасть с рефом (убрать сетку на демо) или оставить
сетку как сигнатуру нового дизайна.
- **F2. Бейдж типа шага** (BACKGROUND/TRANSFORM/STYLE). Реф: приглушённый серый
`#6D787F`. Решено C16: `StepCard` рендерит `type` как приглушённый моно-текст
(не синий бейдж); индекс шага (`step-index`) — синий `#1769D2`, как в рефе.
- **F3. Подписи превью-тайлов** (SOURCE / STEP 01 / FINAL OUTPUT). Реф: синий
`#1769D2`, weight 400. Решено C16: `PreviewTile` label — синий `#1769D2`.
- **F4. Раскладка preview-stack.** Реф: одна колонка (5 строк, выше). Наш: 2
колонки (grid). Решение: одна колонка (как реф) или оставить 2 колонки как
выбор нового дизайна.
- **F5. Вес лейблов контролов.** Реф: 400. Наш (`MonoLabel`): 500. Мелко.
- **F6. Мета-подписи** (DIMENSIONS/FORMAT/SIZE): реф fs 9px, наш 10px. Мелко.
- **F7. Копирайт:** `Add tool` у нас `+ Add tool`; кнопка `Download result`
сверить стиль (в рефе синяя с белым текстом 478×42). Пробы
`Download result`/`Add tool`/`DIRECTION` вернули NOT FOUND — скорее артефакт
замера (вложенный текст/чип размера), проверить визуально.
- **F8. h1 line-height** чуть выше (наш 150px vs реф 115px) — мелкий зазор.
- **F9. Вертикальный ритм:** контент на ~152px выше, зазоры между секциями
отличаются (pipeline-head y 434 vs 400 и т.д.) — подровнять паддинги под реф.
- **F10. Внутренние отступы preview-панели** (Hide intermediate x 907 vs 1218) —
паддинг панели.
## Темы (отложено — F1-F4)
Серый фон `#EEF1F4` — это **светлая** тема. Уточнение по F1: наш токен
`--background` уже равен `#EEF1F4` (совпадает с рефом); видимое отличие — только
blueprint-сетка поверх фона, а не «белый vs серый». То есть F1 = «сетка против
плоского», не цвет фона.
Перед тем как сверять цвета бейджей/тайлов/раскладок (F2-F4), нужно сначала
зафиксировать палитры light/dark как единый источник токенов. Сейчас они есть в
`web/src/lib/styles/design2.css` (`:root` + `[data-theme="dark"]`), но не
сверены с рефом.
**Как проверить темы в refs-html:**
- Открыть `refs-html/demo.html` (и соседние `gradient.html`,
`background-remover.html`) через Playwright/браузер.
- Нажать theme-toggle в топбаре рефа (иконка луны/солнца) — переключает тему
(скорее всего меняет атрибут/класс на `<html>` или `.page` и переопределяет
CSS-переменные).
- Снять скриншоты светлой и тёмной темы, замерить computed-стиль `:root`
переменных рефа (через
`getComputedStyle(document.documentElement).getPropertyValue('--...')`) и
сопоставить с нашими токенами в `design2.css`.
- Рефы, похоже, не доделаны по тёмной теме — зафиксировать, какие именно токены
меняются и где расхождения. На основе этого выровнять наши токены, затем
вернуться к F2 (бейдж типа), F3 (тайлы), F4 (раскладка preview-stack).
Решение по F1-F4 принимается только после сверки токенов светлой/тёмной темы с
рефом.
**Состояние тем (уточнение):** наша dark-тема реализована в `design2.css`
(`[data-theme="dark"]`), но пока подключена только на preview-маршрутах (старые
страницы грузят `app.css`). Этого достаточно для сверки: токены, отличные от
light, соберём по всем `refs-html/*.html` (не только `demo.html`), остальные
токены не меняются. Light-токены уже совпадают с рефом (`--background`,
`--panel`, `--foreground`, `--muted`, `--line`, `--blue` равны).
## Состояние (C16)
Цветовые расхождения против `refs-html/demo.html` устранены: стек шрифтов сверен
с рефом (`"IBM Plex Sans", sans-serif` / `"IBM Plex Mono", monospace`), бренд
топбара — `--foreground`, индекс шага — синий, тип шага и подпись плитки — как в
рефе, `Segmented` — 10px/32px. Аудит: токены light 10 / dark 16, элементы 47.
Остаток — sub-pixel (10px→9px лейблы, letter-spacing) и структурные
(`display`/`rect` от наших flex/grid раскладок против статичного мока);
визуально незначимы, правим точечно при желании. Старые тесты (583) — без
регрессий.
## План действий (предлагаю)
> Актуальный пошаговый план фикса вынесен в `docs/plan-design-fix.md` (Этапы
> 0–1: рефакторинг + верность). Ниже — исходные наброски.
1. Сразу поправить B1 + B2 (чёткие баги, улучшают все страницы).
2. По F1–F4 — решение за тобой (совпадать с рефом дословно ИЛИ оставить как
сигнатуру нового дизайна).
3. F5–F10 — мелочи, подчистить в том же проходе, если решаем «совпадать с
рефом».
4. Переснять скриншоты и перепроверить пиксель-дифф.
+54 -23
View File
@@ -4,20 +4,36 @@
## 1. Цели и принципы
1. **Один источник правды** — все визуальные константы живут в токенах `web/src/app.css`; ни один компонент не пишет «магические» цвета/размеры напрямую.
2. **Никакого дублирования** — повторяющийся визуальный рецепт оформляется один раз: либо токен, либо глобальный утилитарный класс, либо компонент в `ui/`.
3. **Простые компоненты важнее умных** — если проще сделать отдельные `SliderField` и `TextField`, делаем два маленьких компонента, а не один конфигурируемый монстр.
4. **Scoped-стили — только для раскладки** конкретного компонента (сетка, отступы между блоками). Внешний вид контролов и текстов — из дизайн-системы.
1. **Один источник правды** — все визуальные константы живут в токенах
`web/src/app.css`; ни один компонент не пишет «магические» цвета/размеры
напрямую.
2. **Никакого дублирования** — повторяющийся визуальный рецепт оформляется один
раз: либо токен, либо глобальный утилитарный класс, либо компонент в `ui/`.
3. **Простые компоненты важнее умных** — если проще сделать отдельные
`SliderField` и `TextField`, делаем два маленьких компонента, а не один
конфигурируемый монстр.
4. **Scoped-стили — только для раскладки** конкретного компонента (сетка,
отступы между блоками). Внешний вид контролов и текстов — из дизайн-системы.
## 2. Аудит: что дублируется прямо сейчас
- **«Приглушённый текст»** (`--text-muted` + размер 0.85–0.95rem) — повторён в семи местах: `ToolPage` (.description, .hint), главная (.lead, .card-desc), лейаут (.nav-link, footer), `Preview` (.dims), `ParamForm` (label), `InfoPanel` (dt).
- **«Заголовок секции капсом»** (uppercase, letter-spacing, muted) — точная копипаста в `ToolPage` и на главной.
- **Базовые инпуты** (padding, border, radius) — живут только внутри `ParamForm`; любые новые типы контролов (слайдеры, пипетка цвета) пришлось бы копировать вручную.
- **Блок «label + отступы»** — дублировался бы в каждом `*Field`; решается обёрткой `Field.svelte`.
- **«Панель»** (surface + border + radius) — три ручные реализации одного паттерна: `DropZone`, карточки главной, строки `InfoPanel`.
- **Кнопки** — глобальные классы `.primary`/`.secondary` без компонента; состояния «занят/недоступен» каждый описывает заново.
- **Ошибка-баннер** — только в `ToolPage`, а понадобится минимум ещё раз в workspace.
- **«Приглушённый текст»** (`--text-muted` + размер 0.85–0.95rem) — повторён в
семи местах: `ToolPage` (.description, .hint), главная (.lead, .card-desc),
лейаут (.nav-link, footer), `Preview` (.dims), `ParamForm` (label),
`InfoPanel` (dt).
- **«Заголовок секции капсом»** (uppercase, letter-spacing, muted) — точная
копипаста в `ToolPage` и на главной.
- **Базовые инпуты** (padding, border, radius) — живут только внутри
`ParamForm`; любые новые типы контролов (слайдеры, пипетка цвета) пришлось бы
копировать вручную.
- **Блок «label + отступы»** — дублировался бы в каждом `*Field`; решается
обёрткой `Field.svelte`.
- **«Панель»** (surface + border + radius) — три ручные реализации одного
паттерна: `DropZone`, карточки главной, строки `InfoPanel`.
- **Кнопки** — глобальные классы `.primary`/`.secondary` без компонента;
состояния «занят/недоступен» каждый описывает заново.
- **Ошибка-баннер** — только в `ToolPage`, а понадобится минимум ещё раз в
workspace.
## 3. Токены (расширение `app.css`)
@@ -55,11 +71,13 @@
- `.text-muted` — приглушённый текст (размер наследуется);
- `.text-caption` — мелкий текст `--text-s`;
- `.heading-section` — заголовок секции капсом (вместо двух копипаст);
- `.panel` — поверхность с бордером/радиусом (DropZone, карточки, InfoPanel-строки);
- `.panel` — поверхность с бордером/радиусом (DropZone, карточки,
InfoPanel-строки);
- `.error-banner` — баннер ошибки;
- `.visually-hidden` — скрытый, но читаемый скринридерами.
Элементные стили из текущего `app.css` (голые `input, select { ... }`) убрать — внешний вид контролов переезжает в компоненты `ui/`.
Элементные стили из текущего `app.css` (голые `input, select { ... }`) убрать —
внешний вид контролов переезжает в компоненты `ui/`.
## 5. Состав UI-компонентов (`web/src/lib/components/ui/`)
@@ -76,15 +94,23 @@
| `ColorField.svelte` | `id`, `label`, `value = $bindable()` | color-ветка `ParamForm`; точка будущего расширения пипеткой |
| `EmptyState.svelte` | `title`, `hint`, слот действия | «Обработка…», будущие пустые панели исходника/результата |
`Field` — чисто раскладочная обёртка (`<div class="field"><label for>…</label><slot /></div>`), ничего не знает о типе контрола. На ней построены `TextField`, `SliderField`, `SelectField`, `ColorField`. Исключение — `CheckboxField`: у чекбокса label оборачивает контрол (кликабельная строка), поэтому у него своя раскладка.
`Field` — чисто раскладочная обёртка
(`<div class="field"><label for>…</label><slot /></div>`), ничего не знает о
типе контрола. На ней построены `TextField`, `SliderField`, `SelectField`,
`ColorField`. Исключение — `CheckboxField`: у чекбокса label оборачивает контрол
(кликабельная строка), поэтому у него своя раскладка.
## 6. Миграция существующего
1. `ParamForm` — переписывается на `Field` + `*Field`-компоненты; собственная стилизация контролов и label удаляется.
1. `ParamForm` — переписывается на `Field` + `*Field`-компоненты; собственная
стилизация контролов и label удаляется.
2. `DownloadButton` и кнопки `ToolPage` — на `Button`.
3. `DropZone`, карточки главной, строки `InfoPanel` — на `.panel` + свои раскладочные стили.
4. `ToolPage`, главная, лейаут — приглушённые тексты на `.text-*`, заголовки секций на `.heading-section`, баннер на `.error-banner`.
5. Из `app.css` удалить глобальные классы кнопок и голые стили инпутов после перехода.
3. `DropZone`, карточки главной, строки `InfoPanel` — на `.panel` + свои
раскладочные стили.
4. `ToolPage`, главная, лейаут — приглушённые тексты на `.text-*`, заголовки
секций на `.heading-section`, баннер на `.error-banner`.
5. Из `app.css` удалить глобальные классы кнопок и голые стили инпутов после
перехода.
## 7. Порядок работ
@@ -97,13 +123,18 @@
## 8. Критерии готовности
- Ни один компонент не задаёт цвет/радиус/шрифт мимо токенов или классов системы.
- Ни один компонент не задаёт цвет/радиус/шрифт мимо токенов или классов
системы.
- Все интерактивные контролы на страницах — экземпляры `ui/*`.
- `grep -E '#[0-9a-f]{3,6}' web/src/lib/components` не находит ничего, кроме токенов в `app.css`.
- Визуальной регрессии нет (ручная сверка всех страниц до/после), тесты и билд зелёные.
- `grep -E '#[0-9a-f]{3,6}' web/src/lib/components` не находит ничего, кроме
токенов в `app.css`.
- Визуальной регрессии нет (ручная сверка всех страниц до/после), тесты и билд
зелёные.
## 9. Что сознательно не делаем
- Никаких CSS-фреймворков и препроцессоров — обычный CSS, как решено на старте.
- Никаких тем/тёмного режима — но токены сразу семантические, чтобы тема добавилась заменой значений `:root`.
- Никаких сложных составных компонентов (модалки, таблицы) — они появятся, когда потребуется реальная задача.
- Никаких тем/тёмного режима — но токены сразу семантические, чтобы тема
добавилась заменой значений `:root`.
- Никаких сложных составных компонентов (модалки, таблицы) — они появятся, когда
потребуется реальная задача.
+116 -45
View File
@@ -4,38 +4,59 @@
>
> **Что сделано по плану:**
>
> - Шаг 0 — каркас: SvelteKit + adapter-static, design-токены на CSS, лейаут с навигацией по категориям ✔
> - Шаг 1 — ядро: `PixelImage`, декодирование файлов, кодирование PNG/JPEG/WebP, скачивание
> - Шаг 2 операции ядра: color / geometry / alpha / analyze, чистые функции без DOM ✔
> - Шаг 0 — каркас: SvelteKit + adapter-static, design-токены на CSS, лейаут с
> навигацией по категориям
> - Шаг 1 — ядро: `PixelImage`, декодирование файлов, кодирование PNG/JPEG/WebP,
> скачивание ✔
> - Шаг 2 — операции ядра: color / geometry / alpha / analyze, чистые функции
> без DOM ✔
> - Шаг 3 — реестр из 11 инструментов + санитизация параметров ✔
> - Шаг 4 — универсальные UI-компоненты (DropZone, Preview, ParamForm, DownloadButton, InfoPanel, ToolPage) ✔
> - Шаг 5 — маршруты: `/tools/[id]` с prerender всех страниц, главная-каталог по категориям
> - Шаг 4 — универсальные UI-компоненты (DropZone, Preview, ParamForm,
> DownloadButton, InfoPanel, ToolPage)
> - Шаг 5 — маршруты: `/tools/[id]` с prerender всех страниц, главная-каталог по
> категориям ✔
> - Шаги 6–7 — обработка ошибок, состояния, полировка; верификация ✔
>
> **Checkpoint пройден:** все 11 инструментов вручную проверены в браузере (загрузка → применение → скачивание), включая найденный и исправленный баг resize с сохранением пропорций. Тесты 75/75, svelte-check 0 ошибок, статический экспорт собирается.
> **Checkpoint пройден:** все 11 инструментов вручную проверены в браузере
> (загрузка → применение → скачивание), включая найденный и исправленный баг
> resize с сохранением пропорций. Тесты 75/75, svelte-check 0 ошибок,
> статический экспорт собирается.
>
> **Попутные улучшения сверх плана:** корневой скрипт `pnpm dev`; `sanitizeParams` против пустых/невалидных значений полей; семантика «вписать в размеры» для resize.
> **Попутные улучшения сверх плана:** корневой скрипт `pnpm dev`;
> `sanitizeParams` против пустых/невалидных значений полей; семантика «вписать в
> размеры» для resize.
## 0. Зафиксированные решения
- **Объём MVP:** только одиночные инструменты «загрузил → настроил → скачал». Пайплайн-workspace (`/workspace`) вне скоупа этого плана.
- **Набор инструментов первого среза:** ~11 базовых EASY (см. §3). Достаточно, чтобы проверить насквозь ядро → реестр → универсальную страницу.
- **Стили:** обычный CSS без фреймворка — design-токены на CSS custom properties, ноль зависимостей.
- **Стек:** SvelteKit + `@sveltejs/adapter-static` + TypeScript, пакетный менеджер pnpm (уже зафиксирован в корневом `package.json`). Весь код сайта — в `web/`.
- **Ядро без DOM:** все пиксельные операции — чистые функции над `PixelImage`; DOM/canvas допускается только в модуле IO и компонентах превью.
- **Объём MVP:** только одиночные инструменты «загрузил → настроил → скачал».
Пайплайн-workspace (`/workspace`) вне скоупа этого плана.
- **Набор инструментов первого среза:** ~11 базовых EASY (см. §3). Достаточно,
чтобы проверить насквозь ядро → реестр → универсальную страницу.
- **Стили:** обычный CSS без фреймворка — design-токены на CSS custom
properties, ноль зависимостей.
- **Стек:** SvelteKit + `@sveltejs/adapter-static` + TypeScript, пакетный
менеджер pnpm (уже зафиксирован в корневом `package.json`). Весь код сайта — в
`web/`.
- **Ядро без DOM:** все пиксельные операции — чистые функции над `PixelImage`;
DOM/canvas допускается только в модуле IO и компонентах превью.
---
## 1. Шаг 0 — Каркас проекта
1. Создать SvelteKit-приложение в `web/`: `npx sv create web` (шаблон minimal, TypeScript, без лишних аддонов).
1. Создать SvelteKit-приложение в `web/`: `npx sv create web` (шаблон minimal,
TypeScript, без лишних аддонов).
2. `pnpm install` внутри `web/`.
3. `svelte.config.js`: заменить адаптер на `@sveltejs/adapter-static`.
4. `src/routes/+layout.ts`: `export const prerender = true;` — весь сайт prerenderится в статику.
5. `src/app.css`: дизайн-токены (палитра, типографика, отступы, радиусы через CSS custom properties) + минимальный сброс стилей.
6. `src/routes/+layout.svelte`: шапка с названием сайта и навигацией по категориям инструментов (категории — константа из реестра), футер.
4. `src/routes/+layout.ts`: `export const prerender = true;` — весь сайт
prerenderится в статику.
5. `src/app.css`: дизайн-токены (палитра, типографика, отступы, радиусы через
CSS custom properties) + минимальный сброс стилей.
6. `src/routes/+layout.svelte`: шапка с названием сайта и навигацией по
категориям инструментов (категории — константа из реестра), футер.
**Проверка:** `pnpm dev` открывает пустой каркас; `pnpm build && pnpm preview` отдаёт статический экспорт.
**Проверка:** `pnpm dev` открывает пустой каркас; `pnpm build && pnpm preview`
отдаёт статический экспорт.
---
@@ -46,28 +67,42 @@
- `types.ts`
```ts
export type PixelImage = { width: number; height: number; data: Uint8ClampedArray }; // RGBA, как ImageData
export type PixelImage = {
width: number;
height: number;
data: Uint8ClampedArray;
}; // RGBA, как ImageData
```
- `io.ts` — единственное место ядра, где разрешён canvas/DOM:
- `decodeFile(file: File): Promise<PixelImage>` — `createImageBitmap(file)` → отрисовка на canvas → `getImageData`;
- `encode(img: PixelImage, mime: 'image/png' | 'image/jpeg' | 'image/webp', quality?: number): Promise<Blob>` — `putImageData` → `canvas.toBlob`;
- `decodeFile(file: File): Promise<PixelImage>` — `createImageBitmap(file)` →
отрисовка на canvas → `getImageData`;
- `encode(img: PixelImage, mime: 'image/png' | 'image/jpeg' | 'image/webp', quality?: number): Promise<Blob>`
— `putImageData` → `canvas.toBlob`;
- `downloadBlob(blob: Blob, filename: string): void`.
**Проверка:** юнит-смоук — декодировать фикстурный PNG, закодировать обратно, размеры совпадают.
**Проверка:** юнит-смоук — декодировать фикстурный PNG, закодировать обратно,
размеры совпадают.
---
## 3. Шаг 2 — Операции ядра (чистые функции)
Файлы в `web/src/lib/core/`, каждая функция: `(img, params) => new PixelImage`, без мутации входа:
Файлы в `web/src/lib/core/`, каждая функция: `(img, params) => new PixelImage`,
без мутации входа:
- `color.ts`: `grayscale(img)` (luma), `invert(img)`, `brightnessContrast(img, brightness 100..100, contrast 100..100)`.
- `geometry.ts`: `flip(img, axis)`, `rotate90(img, turns)`, `crop(img, x, y, w, h)` — индексная математика; `resize(img, w, h)` — билинейная интерполяция на TS (не `drawImage`, чтобы ядро осталось DOM-free и воспроизводимым бит-в-бит).
- `alpha.ts`: `removeColorToAlpha(img, hex, tolerancePercent)` — цветовое расстояние → альфа 0.
- `color.ts`: `grayscale(img)` (luma), `invert(img)`,
`brightnessContrast(img, brightness 100..100, contrast 100..100)`.
- `geometry.ts`: `flip(img, axis)`, `rotate90(img, turns)`,
`crop(img, x, y, w, h)` — индексная математика; `resize(img, w, h)` —
билинейная интерполяция на TS (не `drawImage`, чтобы ядро осталось DOM-free и
воспроизводимым бит-в-бит).
- `alpha.ts`: `removeColorToAlpha(img, hex, tolerancePercent)` — цветовое
расстояние → альфа 0.
- `analyze.ts`: `imageInfo(img)` → `{ width, height, hasAlpha, colorCount }`.
**Проверка:** vitest на маленьких матрицах пикселей (2×2, 3×3): flip, rotate, crop, grayscale — ожидаемые значения руками.
**Проверка:** vitest на маленьких матрицах пикселей (2×2, 3×3): flip, rotate,
crop, grayscale — ожидаемые значения руками.
---
@@ -77,8 +112,22 @@
```ts
export type ParamDef =
| { id: string; label: string; type: "number"; min?: number; max?: number; step?: number; default: number }
| { id: string; label: string; type: "select"; options: { value: string; label: string }[]; default: string }
| {
id: string;
label: string;
type: "number";
min?: number;
max?: number;
step?: number;
default: number;
}
| {
id: string;
label: string;
type: "select";
options: { value: string; label: string }[];
default: string;
}
| { id: string; label: string; type: "checkbox"; default: boolean }
| { id: string; label: string; type: "color"; default: string };
@@ -88,11 +137,15 @@ export type ToolEntry = {
description: string;
category: CategoryId;
params: ParamDef[];
run: (img: PixelImage, params: Record<string, unknown>) => Promise<PixelImage> | PixelImage;
run: (
img: PixelImage,
params: Record<string, unknown>,
) => Promise<PixelImage> | PixelImage;
};
```
Категории (константа там же): `convert`, `alpha`, `color`, `geometry`, `analyze`.
Категории (константа там же): `convert`, `alpha`, `color`, `geometry`,
`analyze`.
### Состав первого среза — 11 инструментов
@@ -110,9 +163,11 @@ export type ToolEntry = {
| 10 | `remove-color-from-png` | alpha | hex color, tolerance % |
| 11 | `png-info` | analyze | — особый случай: не картинка, а текстовая сводка |
Для `png-info` в записи реестра добавляется флаг `resultType: 'info'`; остальные — `'image'`.
Для `png-info` в записи реестра добавляется флаг `resultType: 'info'`; остальные
— `'image'`.
**Проверка:** импорт реестра в тесте — все id уникальны, у каждого `params` имеют дефолты, `run` определён.
**Проверка:** импорт реестра в тесте — все id уникальны, у каждого `params`
имеют дефолты, `run` определён.
---
@@ -120,33 +175,45 @@ export type ToolEntry = {
`web/src/lib/components/`:
- `DropZone.svelte` — drag&drop + клик; эмитит `File`; проверка MIME (PNG/JPEG/WebP/GIF/BMP).
- `Preview.svelte` — рендер `PixelImage` на canvas с fit-масштабированием; шахматный фон под прозрачность.
- `ParamForm.svelte` — генерирует контролы из `ParamDef[]`, двусторонний `bind:values`.
- `DownloadButton.svelte` — encode выбранного формата + `downloadBlob`, имя `<tool-id>.<ext>`.
- `DropZone.svelte` — drag&drop + клик; эмитит `File`; проверка MIME
(PNG/JPEG/WebP/GIF/BMP).
- `Preview.svelte` — рендер `PixelImage` на canvas с fit-масштабированием;
шахматный фон под прозрачность.
- `ParamForm.svelte` — генерирует контролы из `ParamDef[]`, двусторонний
`bind:values`.
- `DownloadButton.svelte` — encode выбранного формата + `downloadBlob`, имя
`<tool-id>.<ext>`.
- `InfoPanel.svelte` — текстовый вывод для `resultType: 'info'`.
- `ToolPage.svelte` — оркестратор страницы инструмента; состояния `idle → loaded → processing → done | error`; раскладка: слева DropZone, справа превью + форма параметров + кнопки «Применить» / «Скачать».
- `ToolPage.svelte` — оркестратор страницы инструмента; состояния
`idle → loaded → processing → done | error`; раскладка: слева DropZone, справа
превью + форма параметров + кнопки «Применить» / «Скачать».
**Правило:** новый инструмент = новая запись в реестре (+ функция операции). Компоненты и маршрут не трогаем.
**Правило:** новый инструмент = новая запись в реестре (+ функция операции).
Компоненты и маршрут не трогаем.
---
## 6. Шаг 5 — Маршруты
- `/` (`src/routes/+page.svelte`) — сетка карточек инструментов, сгруппированных по категориям; данные из реестра.
- `/` (`src/routes/+page.svelte`) — сетка карточек инструментов, сгруппированных
по категориям; данные из реестра.
- `/tools/[id]` — универсальная страница:
- `+page.ts`: `entries()` возвращает все `id` из реестра (обязательное условие prerender при adapter-static);
- `+page.ts`: `entries()` возвращает все `id` из реестра (обязательное условие
prerender при adapter-static);
- `load()` возвращает запись реестра по `id`, неизвестный id → 404;
- `+page.svelte` — тонкая обёртка над `ToolPage`.
**Проверка:** `pnpm build` — в `build/tools/` лежат все 11 страниц; главная содержит ссылки на каждую.
**Проверка:** `pnpm build` — в `build/tools/` лежат все 11 страниц; главная
содержит ссылки на каждую.
---
## 7. Шаг 6 — Ошибки и минимальная полировка
- Обработка ошибок: неподдерживаемый формат, ошибка декода, некорректные параметры crop (выход за границы — клампы или понятная ошибка).
- Состояния: «перетащите файл», спиннер во время обработки, disabled-кнопки до загрузки.
- Обработка ошибок: неподдерживаемый формат, ошибка декода, некорректные
параметры crop (выход за границы — клампы или понятная ошибка).
- Состояния: «перетащите файл», спиннер во время обработки, disabled-кнопки до
загрузки.
- Тексты описаний для всех 11 инструментов (title/description уже в реестре).
---
@@ -161,6 +228,10 @@ cd web && pnpm build # статический экспорт должен
cd web && pnpm test # vitest: ядро (геометрия/цвет) + целостность реестра
```
Ручной smoke-тест каждого из 11 инструментов: загрузка PNG → применить → скачать → открыть результат.
Ручной smoke-тест каждого из 11 инструментов: загрузка PNG → применить → скачать
→ открыть результат.
**Checkpoint MVP:** любой из 11 инструментов доступен по прямому URL с главной страницы; PNG загружается drag&drop; параметры применяются; результат скачивается; всё работает офлайн со статического хостинга; ни одной серверной зависимости.
**Checkpoint MVP:** любой из 11 инструментов доступен по прямому URL с главной
страницы; PNG загружается drag&drop; параметры применяются; результат
скачивается; всё работает офлайн со статического хостинга; ни одной серверной
зависимости.
+213
View File
@@ -0,0 +1,213 @@
# План: аудит верности `preview/*` против `refs-html` (DOM → CSS)
> **СТАТУС: РЕАЛИЗОВАН (2026-09-07) — устарел.** Скрипты
> `web/scripts/audit-dom.mjs`, `audit-css.mjs`, `audit-cdp.mjs`,
> `audit-cdp-responsive.mjs` и npm-скрипты `refs-dom-audit` / `refs-css-audit` /
> `refs-cdp-audit` на месте; старый текстовый `audit.mjs` удалён. Актуальная
> доводка верности и статус пунктов F — в `plan-design-fix.md`.
> Статус: к выполнению. Ветка `feat/redesign`. Источник правды по пикселям:
> `refs-html/*.html` (статический экспорт рефа). Актуальный рабочий аудит сейчас
> — `pnpm refs-audit` (`scripts/audit.mjs`), но он **сломан** (см. «Почему
> текущий аудит не работает») и подлежит замене.
## Почему текущий аудит не работает
`scripts/audit.mjs` сопоставляет элементы **только по тексту** и берёт в
сравнение **только листовые узлы** (у которых нет детей). Контейнеры/карточки/
панели, несущие `border-radius`, отступы и позиционирование, в сравнение
**вообще не попадают** (`byText` кладёт в мапу только элементы с непустым
`text`). Следствия:
- разница радиусов у карточек невидима (карточка — родитель без своего текста);
- позиция кнопки (`Download result`) не ловится — это структурная разница;
- `onlyOurs / onlyRef` — это просто счётчик текстовых строк на одной стороне,
**не метрика верности**.
Плюс он не делает пиксельного сравнения, то есть принципиально не может сказать
«выглядит по-другому». Это текстовый дифф стилей листовых текстов — и всё.
## Решения (подтверждены автором)
1. **Копирайт превью — через i18n `t()` с EN-базой.** Сейчас RU-литералы
хардкодом только в `preview/+page.svelte:26-27` и
`preview/list-tools/+page.svelte:31` (эти страницы не используют i18n, в
отличие от `(old)/` и компонентов `DownloadButton`/`ChainToolBlock`/…).
Заводим их через `t()` — совпадёт с рефом (EN).
2. **Имена классов НЕ сводим к рефу.** Матчинг делаем по **структурному пути**,
а не по классу. Компоненты `kit/` остаются идиоматичными, Stage 0 не
откатываем. Требуется только эквивалентность _структуры_ (теги, вложенность,
порядок сиблингов, наличие узлов, текст).
3. **Пиксельный дифф (Playwright + pixelmatch) пока НЕ делаем.** DOM+CSS-аудита
достаточно; добавим как сеть позже, если стили что-то упустят.
4. **Порядок прогона маршрутов:** `demo``list-tools` → затем 2 инструмента
(`tools/linear-gradient-png``gradient.html`, `tools/remove-background-png`
`background-remover.html`).
## Принципы
- Никаких правок старого дизайна `(old)/` и старого `ui/`.
- Коммиты атомарные (< ~500 строк), по 1–2 компонента/скрипта; после каждого —
`lint` + `svelte-check` + `build` + соответствующий аудит (diff не должен
расти без причины).
- Коммиты делает автор (агент не коммитит).
- Шрифты уже совпадают: `design2.css:1-6` грузит IBM Plex Sans/Mono через
`@fontsource`, токены `:34-35``"IBM Plex Sans"/"IBM Plex Mono"` (как в рефе
`demo.html:67-68`). Пиксельный шум от шрифтов не предвидится.
## Подход: две фазы
Корень проблемы — нет **стабильного ключа сопоставления**. Разбиваем на две
фазы: сначала доводим DOM до структурного равенства с рефом (фаза A), затем на
стабильном DOM сравниваем стили по тому же пути (фаза B). Фаза A сама по себе
ловит именно то, из-за чего возникли претензии (кнопка не в том родителе,
лишние/недостающие обёртки, перепутанный порядок) — и без эстетических решений.
---
## Фаза A — DOM/структурный аудит (`scripts/audit-dom.mjs`)
**Цель:** Svelte рендерит структуру, идентичную `refs-html` (дерево тегов,
вложенность, порядок сиблингов, наличие узлов, текст). Имена классов могут
отличаться.
### Нормализация узла (ключ сравнения)
Для каждого элемента (пропускаем `SCRIPT`/`STYLE`/`NOSCRIPT` и скрытые
поддеревья, игнорируем служебное):
- **тег** (`tagName` в lower-case);
- **классы** — как отсортированный набор (для инфо, не для матчинга);
- **нормализованный текст** — `trim`, `collapse-whitespace`, без SVG-иконок
внутри (текст иконок игнорируем);
- **порядок детей** — позиция среди сиблингов (`nth-child`).
Матчинг — по **структурному пути** от корня сравнения:
`tag:nth-child(i) > tag:nth-child(j) …`. Не по классу и не по тексту.
### Полное соответствие, без IGNORE
Оболочка (наш `TopBar`/`AppShell`/`preview-footer` vs рефовый
`.topbar`/`.footer`/`.language`) тоже сводим к рефу — **IGNORE-список не
заводим**. Сравниваем всё дерево от `<body>` (без пропуска служебного, кроме
Svelte-атрибутов и комментов гидрации), чтобы получить полное структурное
соответствие, а не только контента. Шапка/футер/переключатель языка — часть
верности; позже, после полного фикса дизайна, всё равно планируется рефакторинг
компонентов, где оболочку приведём в порядок и унифицируем. Аудит на этом этапе
просто показывает все расхождения, включая оболочечные.
### Дифф
Для каждого маршрута — набор дельт:
- `added` — у нас, нет в рефе;
- `removed` — в рефе, нет у нас;
- `moved` — тот же текст/тег, другой путь (перепутанный порядок/родитель);
- `textMismatch` — нормализованный текст различается (в т.ч. RU vs EN).
Класс-мисматч — **только информационно**, не блокирует (классы свои по решению
№2). Счётчики: `added / removed / moved / textMismatch`.
### Рендеринг
- **наше:** dev-сервер на `5179` (как сейчас в `audit.mjs`).
- **реф:** `pathToFileURL` к `refs-html/<ref>.html` (`file://`). Реф —
самодостаточный (инлайн `<style>`), рендерится полностью. Шрифты для фазы A не
важны (сравниваем структуру/текст, не растеризацию).
- Переиспользуем из текущего `audit.mjs`: `discoverRoutes()`, `EXCLUDE`
(`/preview`), `DYNAMIC` (`tools/[id]``gradient.html` /
`background-remover.html`), `refNameFor()`.
### Шаги (на каждом маршруте)
1. Прогнать `pnpm refs-dom-audit`, получить дельты.
2. Поправить структуру Svelte под реф (добавить/убрать/переставить узлы, вынести
тексты в `t()` с EN-базой).
3. Перезапустить, пока `added/removed/moved/textMismatch` ≃ 0 (кроме `IGNORE`).
---
## Фаза B — CSS-аудит по стабильному DOM (`scripts/audit-css.mjs`)
**Цель:** на совпадающем дереве сравнить вычисленные стили по тому же
структурному пути и получить «элемент по пути X: свойство → наше/реф».
### Сравниваемые свойства (computed)
`borderRadius`, позиция/размер (`getBoundingClientRect`: x/y/w/h), `padding`
(все стороны), `margin`, `color`, `backgroundColor`,
`borderTop/Right/Bottom/Left Width|Color`, `fontFamily`, `fontSize`,
`fontWeight`, `letterSpacing`, `lineHeight`, `gap`, `display`, `boxShadow`.
### Дифф
Для каждого состыкованного по пути элемента — список свойств с дельтой (наше
значение → реф). Элементы, не совпавшие по пути (остаток из фазы A), идут в
`unmatched`. Отчёт: элемент → свойство → наше/реф. Правится одной строкой CSS,
без брутфорса.
### Шаги
1. Прогнать `pnpm refs-css-audit` (только после того, как фаза A по маршруту
дала ≃ 0 структурных расхождений).
2. Поправить стили в `design2.css` / стилях компонентов.
3. Перезапустить, пока delta% по свойствам ≃ 0.
---
## Порядок маршрутов и особенности
1. **`/preview/demo`** (↔ `demo.html`) — уже ближе всего; идеальная «обкатка»
тулинга фаз A и B. Здесь же проверим, что `Download result` стоит в шапке
`.preview-top` (как в рефе), а не внизу панели.
2. **`/preview/list-tools`** (↔ `list-tools.html`) — объёмнее; **зависит от
`docs/plan-list-tools.md`**: реальный аудит имеет смысл только после того,
как страница переписана на `CatalogHeader`/`CatalogToolbar`/`CatalogGroup`
(шаги 15 плана list-tools). То есть фаза A/B для list-tools стартует после
landing этих шагов.
3. **`tools/linear-gradient-png`** (↔ `gradient.html`) и
**`tools/remove-background-png`** (↔ `background-remover.html`) —
**последние**. Известные расхождения: наши инструменты могут иметь **другие
наборы параметров / UI**, чем конкретные рефовые тулзы. Ожидаются структурные
и стилевые дельты, которые не сводятся к «подровнять паддинг». Для них —
триаж после прогона:
- реальный разрыв верности → фиксим;
- принципиальное отличие (параметр, которого нет в реф-тулзе; иной контрол) →
либо аккуратно приводим наш UI параметра к рефу, либо **документируем как
допустимое расхождение / заносим в allowlist** конфига аудита (селектор +
причина). «Скипы/фиксы» для инструментов оформляем явно, чтобы аудит не
шумел на осознанных отличиях.
---
## Структура скриптов
- `scripts/audit-dom.mjs` — фаза A (нормализация + структурный дифф).
- `scripts/audit-css.mjs` — фаза B (матчинг по пути + дифф стилей).
- `package.json`: `refs-dom-audit`, `refs-css-audit` (опционально `refs-audit`,
прогоняющий обе фазы).
- Переиспользуем из старого `audit.mjs`: `discoverRoutes`, `EXCLUDE`, `DYNAMIC`,
`refNameFor`, `waitFor` (fast-fail на 4xx/5xx).
- Отчёты: `web/audit/dom-report.{md,json}` и `web/audit/css-report.{md,json}`
(старый `audit-report.*` заменяем).
## Верификация (после каждого коммита)
- `pnpm lint`, `pnpm exec svelte-check --tsconfig ./tsconfig.json`, `pnpm build`
— без ошибок.
- `pnpm refs-dom-audit` (фаза A) / `pnpm refs-css-audit` (фаза B) по затронутому
маршруту: счётчики расхождений не растут без причины.
## Риски / заметки
- **Стабильность матчинга:** нужна аккуратная нормализация (сортировка классов,
игнор svelte-атрибутов/`data-*`, игнор whitespace-текстовых узлов, игнор
внутренностей SVG-иконок). Подстраивается по первым прогонам demo.
- **Оболочка** (TopBar/footer/язык) сейчас отличается по дизайну — сводим к рефу
в рамках полного соответствия (без IGNORE); финальная унификация компонентов —
отдельным рефакторингом позже.
- **Динамические маршруты:** `DYNAMIC` уже есть; оставляем.
- **Hover / адаптив / тёмная тема** — вне объёма статического аудита (initial
state, 1440×900, light). Отмечаем как будущие прогоны.
- **list-tools** зависит от landing `plan-list-tools.md` (шаги 15) — не
аудируем вслепую.
+593
View File
@@ -0,0 +1,593 @@
# План: составные типы параметров и полная типизация pipeline
> **СТАТУС: ЗАКРЫТ (2026-09-07).** Типизация pipeline, составные типы
> (dimension/color-pair/offset/position9/font-style/plate/gradient),
> `schema.layout`, изоляция веток и вся миграция инструментов (121/125,
> остальные 4 — отложены решениями) выполнены. Ключевые архитектурные решения
> (параллельная сборка, правило копий, изоляция old↔preview) закреплены в
> `AGENTS.md`. **Незакрытое из этого плана:** шаг 37 — физический переезд старых
> файлов в папки `old/` (перенесён в `backlog.md`), и удаление старого кода — по
> `plan-redesign.md` (C17C21). Отложенные инструменты — `watermark-image-png`
> (overlay), `png-info`, `reduce-to-size` — в `backlog.md`. Ниже — полное
> состояние на момент закрытия.
> Статус: **в реализации.** Фаза 0 (фундамент) ✔, Фаза 1 (рабочий инструмент в
> preview) ✔, Фаза 2 (простые инструменты без составных типов) — переведены все
> одиночные инструменты (18 шт). Фаза 3 — **составной тип `dimension` полностью
> переведён** (12 инструментов: create-empty, single-color, random-noise,
> linear-gradient, color-spectrum, random-colors, draw-grid, placeholder,
> fit-on-background, change-canvas-size, resize, crop); **составной тип
> `color-pair` полностью переведён** (4 инструмента: blend-two, step-colors,
> linear-gradient, two-colors); **составной тип `offset` полностью переведён**
> (4 инструмента: circle-mask, square-mask, star-mask, wavy-mask); **составной
> тип `position9` — переведены add-text-png и date-stamp-png** (водяной
> знак-картинка — отдельный шаг: требует overlay-механику в новом превью);
> **составной тип `font-style` полностью переведён** (text-to-png, add-text,
> date-stamp); **составной тип `plate` полностью переведён** (add-text,
> date-stamp); **составной тип `gradient` полностью переведён**
> (linear-gradient): вместо горизонтального/ вертикального select — **угол
> 0..360°** (новый виджет `AngleControl`: slider + кнопки-пресеты
> 0°/90°/180°/270°, переиспользуемый компонент); preview научен применять
> **генераторы** (`executeGenerate`, кнопка Generate) и рендерить все kinds
> схемы (slider/number/color/select/checkbox/dimension/
> color-pair/offset/position9/font-style/plate/gradient). **Пер-инструмент
> раскладка `schema.layout` реализована** (шаг 32): тип `ToolSchemaLayout`
> {groups: [{title?, cols?, fields}]}, рендер групп в `SchemaFields.svelte`
> (заголовок группы + сетка колонок, неупомянутые поля — в общей группе), пилот
> — `add-text` (группы Text/Placement/Plate). **UI-макеты расставлены по
> переведённым инструментам** (шаг 33): geometry (fit-on-background,
> change-canvas-size, resize, crop), маски alpha (Shape/Position), generate
> (create-empty, linear-gradient, color-spectrum, random-colors, draw-grid,
> step-colors, placeholder, text-to-png), text (add-text, date-stamp), filters
> (randomize-pixels, add-noise) — везде, где канвас отделён от параметров
> эффекта либо фигура от позиции. Следующее закрыто в
> `archive/plan-migrate-remaining.md` (2026-09-07): перевод оставшихся
> инструментов выполнен (всего 121/125 в preview; 4 хвоста —
> `watermark-image-png`, `png-info`, `reduce-to-size-png`, `compress-png` —
> отложены решениями в `backlog.md`). Незакрытые продолжения: переезд старых
> компонентов в папки `old/` (шаг 37 ниже), закрытие долгов и сбор фидбека — см.
> также `plan-design-fix.md`.
>
> Ключевые файлы нового registry: `web/src/lib/registry-new/{types,index,*}.ts`
> (по файлу на категорию: geometry/alpha/convert/analyze/filters/color/generate)
>
> - `web/src/lib/preview/categories.ts`, `web/src/lib/registry-schema.ts` (kind
> `dimension`), `kit/fields/DimensionField.svelte`,
> `preview/executor/index.ts` (`executeGenerate`), `SchemaFields.svelte`
> (полный рендер kinds). Старый `web/src/lib/registry.ts` разбит по категориям
> в `web/src/lib/registry/` (см. `registry.ts` — тонкий баррель).
## Ключевая стратегия: параллельная сборка, старый UI не трогаем
Старый UI (группа `(old)/`, `ParamForm.svelte` + старый pipeline) **продолжает
работать как сейчас, без рефакторинга**. Новый `ToolSchema<P>` строится рядом и
служит источником для **нового UI**. Инструменты переписываются под новый
registry по мере нужды — для нового UI.
Это та же логика, что в `plan-redesign.md` применена к дизайну: параллельная
сборка, изоляция от старого, затем новый становится основным и старый удаляется
вместе со старым дизайном.
**Правило копий (важно!):** если для нового UI/registry нужно внести изменения в
файл, который **прямо или косвенно** уже используется `(old)` веткой, — этот
файл **НЕ трогаем**. Вместо этого делаем **копию** в новом месте (например, в
`lib/preview/`) и правим копию. Это приводит к дублированию, но **гарантированно
не задевает старую ветку сайта**. Пример: категории — новый
`lib/preview/categories.ts` (object as const) копирует и заменяет собой
`../categories` для нового кода, старый `categories.ts` обслуживает `(old)` и
остаётся без изменений. После перехода (Фаза 5) копия становится основной, а
исходник продолжает обслуживать `(old)` UI (он не удаляется — см. шаг 35).
**Разделение схем: две независимые схемы.**
| Схема | Источник | Использование |
| --------------------- | ------------------------------------------------- | ---------------------------------------------------------------- |
| Старый `ParamDef[]` | В `ToolEntry.params`**остаётся без изменений** | Старый UI, старый pipeline (`ParamForm.svelte`, executor, chain) |
| Новый `ToolSchema<P>` | Новое поле `ToolEntry.schema` — добавляется рядом | Новый UI (kit/`ParamControl`, новый pipeline) |
- Старая и новая схемы **не выводятся друг из друга** — это два независимых
описания инструмента.
- Пока новый UI не готов — `schema` просто отсутствует у большинства
инструментов, старый UI ничего не замечает.
- Удаление старого UI (после перехода) попутно удаляет и старый `ParamDef[]`
### Разделение registry по UI (новое решение, Фаза 2+)
Помимо двух схем, registry тоже разделён по UI (по факту миграции):
- **Старый registry** (`web/src/lib/registry.ts` + `registry/` +
`registry-helpers.ts`) — работает на старом UI, использует `ParamDef[]`. **Не
трогаем**; остаётся обслуживать `(old)/` маршруты (см. шаг 35).
- **Новый registry** (`web/src/lib/registry-new/`) — строится **с нуля «как
надо»**: `ToolEntry<P>` с обязательным `schema`, типизированный `run`, **без**
`ParamDef[]` и **без** связи со старым. Импортирует core-функции
(`expandCanvas`, `strokeImage`, …) напрямую. Наполняется **по-инструментно**
по мере миграции (не разовым переводом всех 130). Preview показывает только
переведённые инструменты.
Preview (`catalog.ts`, маршруты `preview/**`, `SchemaToolView`, `SchemaFields`)
импортируют из `$lib/registry-new`. После удаления старого UI `registry-new/`
переименуется в `registry`.
**Executor/worker для нового UI (осознанное дублирование):** общий
`executor.worker.ts` резолвит инструменты по `id` в **старом** registry — для
новых инструментов их там нет. Поэтому `executor.ts` + `executor.worker.ts`
**скопированы** в `web/src/lib/preview/` и переписаны под `registry-new` +
`sanitizeSchemaParams`. Старый executor/worker под старый UI остаются
нетронутыми; `SchemaToolView` использует `$lib/preview/executor`. Это
дублирование необходимо до ухода старого UI (после — preview-executor заменяет
общий).
## Проблема (текущая)
- `ParamDef` — плоский union из 6 примитивов
(`number | slider | select | checkbox | color | text`). Параметры инструмента
— отдельные записи в плоском массиве.
- `run()`/`generate()` получают `Record<string, unknown>`; значения достаются
через `num(p, "fromColor")` — runtime string lookup **без type safety**, легко
напутать id.
- Поля инструмента **слабо связаны** (например «ширина» и «цвет» рамки — два
независимых поля без общего контракта) → общий генерализованный рендер, нет
возможности сделать идеальный пер-инструмент UI.
- Preview-система (`tool-views.ts`) **дублирует** registry (FieldDef vs
ParamDef) — одни и те же инструменты описаны дважды.
## Решения (принятые в обсуждении)
1. **Полная типизация.** `interface Params` (runtime-тип) объявляется явно в
каждом инструменте. `run`/`generate` типизируются прямо на него, а не на
`Record<string, unknown>`.
2. **Отдельная типизированная схема для UI/дефолтов/валидации.** Два понятия на
инструмент: runtime-тип + схема. Схема проверяется компилятором на
соответствие `Params` (чтобы не рассинхронизировались).
3. **Общий рендер,** но с пер-инструмент layout: общие компоненты рисуют поля по
схеме; схема инструмента задаёт layout и группировку. Без отдельного
Svelte-компонента на каждый инструмент.
4. **Составные типы** (`gradient`, `color-pair`, `dimension`, `offset`,
`position9`, `font-style`, `plate`) остаются в плане — это способ выразить в
схеме связанную группу полей и переиспользовать её на нескольких инструментах
и в UI.
5. Рано или поздно `tool-views.ts` поглощается registry (preview/lede/layout
переезжают в meta инструмента) — но это отдельный шаг, см. ниже.
6. **Старый UI не рефакторим.** Две независимые схемы: старый `ParamDef[]`
остаётся источником для старого UI без изменений; новый `ToolSchema<P>`
строится рядом для нового UI (см. «Ключевая стратегия» выше).
## Целевая архитектура (на примере «добавить рамку»)
Цель: «добавить рамку: ширина и цвет» — **один интерфейс**, а не два слабо
связанных поля.
```ts
// 1. Runtime-тип — что получает run/generate
interface AddBorderParams {
thickness: number; // рамка: ширина
color: string; // рамка: цвет
}
// 2. Типизированная UI-схема, проверяемая компилятором против Params
// Ключи схемы обязаны совпадать с полями Params; default совместим с типом.
const addBorderSchema = toolSchema<AddBorderParams>({
thickness: field.slider({ min: 1, max: 500, default: 5 }),
color: field.color({ default: "#000000" }),
layout: { group: "border", cols: 2 }, // пер-инструмент раскладка
});
// 3. Инструмент в registry
const addBorder: ToolEntry<AddBorderParams> = {
id: "add-border-png",
title: "Add border to PNG",
category: "geometry",
schema: addBorderSchema,
run: (img, p) => expandCanvas(img, 4x..., p.color), // p.thickness — type safe
};
```
### Что даёт
- `p.thickness`, `p.color` — IDE autocomplete + compile-time ошибка при ошибке.
- `run()` нельзя передать лишний/чужой ключ.
- Схема — единый источник для дефолтов, валидации и рендера.
- Схема одного инструмента может задавать layout, которого нет в общем рендере.
## Типовая система (набросок)
```ts
// ─── Поле схемы ───
interface NumberField { kind: "number"; default: number; min?: number; max?: number; step?: number }
interface SliderField { kind: "slider"; default: number; min: number; max: number; step?: number }
interface ColorField { kind: "color"; default: string }
interface SelectField { kind: "select"; default: string; options: { value: string; label: string }[] }
interface TextField { kind: "text"; default: string; placeholder?: string }
interface CheckboxField { kind: "checkbox"; default: boolean }
type FieldDef =
| NumberField | SliderField | ColorField | SelectField | TextField | CheckboxField;
// ─── «поле» описывает тип значения + схему вместе, чтобы не рассинхронизировать ───
interface Field<T> {
schema: FieldDef;
// T — ожидаемый runtime-тип этого поля (number|string|boolean)
}
// ─── Builder: field.slider({...}) возвращает Field<number> с типизированной схемой ───
const field = {
slider: (s: SliderField): Field<number> => ({ schema: s }),
number: (s: NumberField): Field<number> => ({ schema: s }),
color: (s: ColorField): Field<string> => ({ schema: s }),
select: <V extends string>(s: SelectField & { options: {value: V}[] }): Field<V> => ({ schema: s }),
text: (s: TextField): Field<string> => ({ schema: s }),
checkbox: (s: CheckboxField): Field<boolean> => ({ schema: s }),
};
// ─── toolSchema<P>: проверяет, что ключи схемы === полям P и типы сходятся ───
type FieldsOf<T> = { [K in keyof T]: Field<T[K]> } & {
layout?: { group?: string; cols?: number };
};
function toolSchema<P>(fields: FieldsOf<P>): ToolSchema<P> { ... }
// TS выдаст ошибку, если schema содержит ключ, отсутствующий в Params, или
// если type поля не совпадает с типом Params[K].
```
Ключевая идея связки: **builder `field.x<T>()` связывает runtime-тип поля с его
схемой на этапе компиляции**, а `toolSchema<P>` заставляет ключи схемы совпадать
с ключами `P`. Это и есть защита от рассинхрона.
## Составные типы (для переиспользования и группировки)
| Тип | Под-поля | Инструментов |
| ------------ | -------------------------- | ------------ |
| `dimension` | width + height | 12 |
| `color-pair` | from + to | 4 |
| `offset` | x + y | 4 |
| `gradient` | from + to + dir (+ type?) | 1 |
| `position9` | position (3×3 grid) | 3 |
| `font-style` | font + size + bold + color | 3-4 |
| `plate` | enabled + color + opacity | 2-3 |
Составной тип = группы полей, которые в интерфейсе `Params` могут
разворачиваться во вложенный объект или плоские ключи — решается на этапе
реализации. Пример для `gradient`:
```ts
interface GradientParams {
from: string;
to: string;
dir: string;
}
// схема: field.color(from), field.color(to), field.select(dir)
// плюс annotation в схеме: { group: "gradient" } → UI рисует как цветовой переход
```
## Инвентарь: какие инструменты мигрировать
### dimension (12)
resize-png*, crop-png*, fit-on-background-png, create-empty-png,
single-color-png, random-noise-png, linear-gradient-png, color-spectrum-png,
random-colors-png, draw-grid-png, placeholder-png, change-canvas-size-png
(\* у resize/crop — не просто dimension, есть специфика: keepAspect, x/y и т.д.)
### color-pair (4)
blend-two-png, step-colors-png, linear-gradient-png, two-colors-png
### offset (4)
circle-mask-png, square-mask-png, star-mask-png, wavy-mask-png
### position9 (3)
add-text-png, date-stamp-png, watermark-image-png
### font-style (3-4)
text-to-png, add-text-png, date-stamp-png
### plate (2-3)
add-text-png, date-stamp-png
## Пошаговый план перевода инструментов
> Каждый шаг — **атомарное, маленькое изменение** (в идеале 1 инструмент = 1
> diff, укладывается в правило «коммиты < ~500 строк»). После каждого шага —
> ревью; параллельно можно делать UI нового инструмента.
>
> **Принцип малых шагов:** перевод инструмента не меняет поведение старой схемы
> (`params` остаётся), не ломает старый UI. Инструмент получает ДОПОЛНИТЕЛЬНО
> новое поле `schema` + `interface Params`, старое `run`/`params` не трогаем.
> Это гарантирует, что старый UI продолжает работать без регресса.
>
> **Новый подход (решение пользователя):** сначала делаем **один полноценный
> рабочий инструмент в preview** — не эталон «на бумаге», а реально юзабельный,
> чтобы руками оценить, насколько это рабочее решение и какие возникнут
> сложности. Оцениваем, при необходимости правим инфраструктуру, и только потом
> переводим остальные инструменты тем же проверенным паттерном.
### Фаза 0 — фундамент ✔ (сделано)
Typed field builders + `Field<T>` + `toolSchema<P>()` + `ToolSchema<P>` — в
`web/src/lib/registry-schema.ts`. `ToolEntry<P>` generic + опциональное поле
`schema`. Новые `defaultSchemaParams`/`sanitizeSchemaParams`. Старый
`ParamDef[]`/pipeline не тронуты. Проверено: compile-time-защита
(лишний/неверный/отсутствующий ключ ловит TS), тесты, svelte-check без новых
ошибок.
### Фаза 1 — ОДИН полноценный рабочий инструмент в preview (срез) ✔ (сделано)
Инструмент переехал в `registry-new` и рендерится через `SchemaToolView`
(`SchemaFields`/`SchemaPreview`); применение реально работает end-to-end до
картинки. Паттерн подтверждён. `find-contour-png` — следующий в этой фазе-2.
5. **Инструмент «добавить рамку» (`add-border-png`)** — сделать полностью
рабочим в preview:
- `interface AddBorderParams { thickness: number; color: string }`
- `addBorderSchema = toolSchema<AddBorderParams>(...)` + `schema: ...` в
entry (старый `params`/`run` остаются — старый UI не регрессит).
- **Новый рендер в preview** (`kit/ToolView``ParamControl` или новый
компонент схемы): читает `ToolSchema<P>`, рисует поля рамки, применение
реально работает (end-to-end до картинки).
- Дефолты/валидация/значения — из схемы (`defaultSchemaParams`,
`sanitizeSchemaParams`).
- **Ревью-оценка:** удобно ли это, что сломалось/усложнилось, что поправить в
инфраструктуре до масштабирования. Принимается макет/решение.
Результат: один реально рабочий инструмент на новой системе = доказываем
паттерн, прежде чем переводить остальных.
### Фаза 2 — простые инструменты без составных типов (1 инструмент = 1 шаг)
Одиночные/небольшие инструменты, где `interface Params` + `schema` не требуют
составных типов. Каждый — отдельный маленький diff (~15-30 строк), тем самым
проверенным в Фазе 1 паттерном. **Переезжают в `registry-new`.**
Переведено: `add-border-png` (Фаза 1), `add-stroke-png`, `find-contour-png`,
плюс весь блок простых инструментов ниже (пункты 8-22). Следующее — Фаза 3.
6. ~~add-stroke-png (color + slider)~~ → переведён в `registry-new`
7. ~~find-contour-png (color + slider)~~ → переведён в `registry-new`
8. ~~convert-png-to-jpg (color + slider)~~ → переведён в `registry-new`
9. ~~convert-png-to-webp (slider)~~ → переведён в `registry-new`
10. ~~remove-color-from-png (color + slider)~~ → переведён в `registry-new`
11. ~~extract-color-from-png (color + slider)~~ → переведён в `registry-new`
12. ~~add-noise-png (slider + select + number)~~ → переведён в `registry-new`
13. ~~randomize-pixels-png (slider + number)~~ → переведён в `registry-new`
14. ~~pixelate-png (slider)~~ → переведён в `registry-new`
15. ~~vignette-png (slider)~~ → переведён в `registry-new`
16. ~~gamma-png (slider)~~ → переведён в `registry-new`
17. ~~temperature-png (slider)~~ → переведён в `registry-new`
18. ~~tint-png (color + slider)~~ → переведён в `registry-new`
19. ~~quantize-png (slider)~~ → переведён в `registry-new`
20. ~~custom-palette-png (text)~~ → переведён в `registry-new`
21. ~~dithering-png (slider + select)~~ → переведён в `registry-new`
22. ~~jpeg-artifacts-png (slider)~~ → переведён в `registry-new`
> **Замечание (конвертеры):** старые `convert-png-to-jpg`/`convert-png-to-webp`
> несли `output`-метаданные (`mime`/`ext`/`qualityParamId`) для выбора формата
> вывода. В новом `ToolEntry<P>` поля `output` пока нет, поэтому при переводе
> эти метаданные не перенесены — preview пока отдаёт результат как PNG. Механика
> выбора формата/качества в новом UI — отдельный шаг (не блокирует Фазу 2).
### Фаза 3 — инструменты с составными типами (по типу, затем по инструментам)
24. `dimension`**переведены все 12 инструментов** ✔ (`create-empty-png`,
`single-color-png`, `random-noise-png`, `linear-gradient-png`,
`color-spectrum-png`, `random-colors-png`, `draw-grid-png`,
`placeholder-png` — генераторы в `registry-new/generate.ts`;
`fit-on-background-png`, `change-canvas-size-png`, `resize-png`, `crop-png`
— в `registry-new/geometry.ts`). Составной тип во всех видах: вложенный
объект `size: { width, height }` в Params + `field.dimension`, виджет
`kit/fields/DimensionField.svelte`. Preview: генераторы применяются через
`executeGenerate` (кнопка Generate), `SchemaFields` рендерит все kinds схемы
(slider/number/color/select/ checkbox/dimension). Тесты: 592 passed.
25. `color-pair`**все 4 инструмента переведены** ✔ (`blend-two-png`,
`step-colors-png`, `linear-gradient-png` — генераторы в
`registry-new/generate.ts`, `two-colors-png` — run в
`registry-new/color.ts`). Составной тип во всех видах: вложенный объект
`pair: { from, to }` + `field.colorPair`, виджет
`kit/fields/schema/ColorPairControl.svelte`, kind `color-pair` в схеме
(default/sanitize). Тесты: +4 (596 passed).
26. `offset`**все 4 инструмента переведены** ✔ (`circle-mask-png`,
`square-mask-png`, `star-mask-png`, `wavy-mask-png` — run в
`registry-new/alpha.ts`). Составной тип во всех видах: вложенный объект
`offset: { x, y }` + `field.offset`, виджет
`kit/fields/schema/OffsetControl.svelte`, kind `offset` в схеме
(default/sanitize). Тесты: +4 (600 passed).
27. `position9`**переведены `add-text-png` и `date-stamp-png`** ✔ (run в
`registry-new/text.ts`, оба `domOnly`). Составной тип во всех видах:
значение — строка `Position9`, kind `position9` в схеме (default/sanitize по
`POSITION9_VALUES`), виджет 3×3 `kit/fields/schema/PositionControl.svelte`.
В `ToolEntry` добавлен флаг `domOnly` (превью-executor держит такие
инструменты вне worker). `watermark-image-png` — отдельный под-шаг: ему
нужен overlay-source (`getOverlay`/store), которого в новом превью пока нет.
Тесты: +2 (602 passed). `font-style` и `plate` на этих инструментах сводятся
в шаги 28-29.
28. `font-style`**все 3 инструмента переведены** ✔ (`text-to-png` — генератор
в `registry-new/generate.ts` (domOnly), `add-text-png`/ `date-stamp-png`
рефакторинг в `registry-new/text.ts`). Составной тип во всех видах:
вложенный объект `style: { font, size, bold, color }` + `field.fontStyle`,
виджет `kit/fields/schema/FontStyleControl.svelte`, kind `font-style` в
схеме (default/sanitize, clamp размера к min/max). Тесты: +1 (603 passed).
29. `plate`**все 2 инструмента переведены** ✔ (`add-text-png`,
`date-stamp-png` — рефакторинг в `registry-new/text.ts`). Составной тип во
всех видах: вложенный объект `plate: { enabled, color, opacity }` +
`field.plate`, виджет `kit/fields/schema/PlateControl.svelte` (чекбокс +
цвет + слайдер непрозрачности, деактивируется при выключенной плашке), kind
`plate` в схеме (default/sanitize, clamp opacity к 0..100). Покрытие тестов
расширено (дефолты/sanitize plate в существующих тестах).
30. `gradient`**переведён** ✔ (linear-gradient-png). Направление градиента —
не горизонталь/вертикаль, а **произвольный угол**: новый переиспользуемый
виджет `kit/fields/schema/AngleControl.svelte` (слайдер 0..360° + кнопки
стандартных углов 0°/90°/180°/270° с активным состоянием). Составной тип
`gradient: { from, to, angle }` во всех видах (`field.gradient`, kind
`gradient` в схеме — default/sanitize, clamp угла к 0..360, валидация
цветов; виджет `kit/fields/schema/GradientControl.svelte` — пара цветов +
AngleControl). Рендер по углу — локальный `angleGradient` в
`registry-new/generate.ts` (core-`gradientImage` не трогали: он умеет только
horizontal/vertical и используется старым UI). Семантика: 0° слева направо,
90° сверху вниз, рост угла по часовой (ось Y вниз); нормализация по полному
диапазону проекции на ось (180°/270° разворачивают градиент). Тесты:
дефолты, направление по углу (0°/90°/180°), sanitize/кламп.
### Фаза 4 — масштаб UI на остальные инструменты
31. По мере перевода инструментов в Фазе 2-3 — рендер схемы (из Фазы 1)
покрывает их автоматически; составные виджеты (dimension, color-pair,
offset, position9, font-style, plate, gradient) — по одному, каждый с ревью.
32. Пер-инструмент layout (`schema.layout`) — **реализован** ✔.
`ToolSchemaLayout.groups: { title?, cols?, fields[] }` в registry-schema;
`SchemaFields.svelte` рендерит группы (подпись + сетка `1..n` колонок,
неупомянутые поля — в общей группе в конце, без заголовка). Пилот —
`add-text-png` (Text: text+style; Placement: position+margin; Plate). Тесты:
schema-layout в registry-schema.test.ts; полные запуски чисты.
33. Для каждого переведённого инструмента — UI-макет **расставлен** ✔.
Layout-группы заданы там, где группировка содержательна (канвас отдельно от
параметров эффекта, фигура отдельно от позиции; составные виджеты — внутри
групп целиком):
- geometry: fit-on-background (Canvas/Background), change-canvas-size
(Canvas/Anchor), resize (Canvas/Scaling), crop (Offset/Crop area);
- alpha-маски: circle/square/star/wavy (Shape — с 2 колонками где плотно,
Position — offset);
- generate: create-empty (Canvas/Fill), linear-gradient (Canvas/Colors),
color-spectrum (Canvas/Spectrum), random-colors (Canvas/Random), draw-grid
(Canvas/Grid), step-colors (Colors/Output), placeholder
(Canvas/Colors/Text), text-to-png (Text/Background/Padding);
- text: add-text, date-stamp (Text/Placement/Plate);
- filters: randomize-pixels (Blocks), add-noise (Noise/Seed). Инструменты с
1–3 простыми полями остались без layout (одна общая группа).
### Фаза 5 — изоляция старого UI
34. Поглощение `tool-views.ts` (preview/lede/layout → meta инструмента) ✔.
Отдельного `tool-views.ts` в репо нет: preview сразу строился на
`registry-new`. Meta инструмента живёт в `ToolEntry` (`title`,
`description`, `category`, `run`/`generate`), layout — в `schema.layout`
(шаг 32), рендер — `SchemaToolView`/`SchemaFields`/`SchemaPreview`.
Дублирующей системы нет.
35. Старый UI **не удаляется**, а выносится в `(old)/`-маршруты и остаётся там
временно (посмотреть, как работает, сравнить с новым; старые тесты
продолжают проходить):
- маршруты `(old)/{+page,demo,list-tools,tools/[id]}` — старый дизайн, тянет
`old.css` (не `design2.css`), старый header/footer;
- старый `registry.ts`/`registry/` + `ParamDef[]` обслуживают только
`(old)/`-инструменты — не удаляются, не рефакторятся;
- удаление происходит позже, отдельным решением (когда новый UI покроет все
инструменты и ревью завершено).
36. **Линтер-изоляция веток** (гарантия, что old и preview не смешиваются) ✔.
Кастомный ESLint-плагин `web/eslint-plugins/isolation/no-mixed-imports`
Резолвит каждый импорт (и `$lib/...`, и относительные `./`/`../`) до
реального файла, классифицирует источник и цель по фактическому пути и
ругается на old→new и new→old. Конфигурация (`old`/`new` glob-паттерны,
`root`, `alias`) вынесена в настройки правила — единая точка правды:
- старое: `routes/(old)/**`, `lib/registry.ts`, `lib/registry/**`,
`lib/registry-helpers.ts`, `lib/categories.ts`, `lib/tools/**`,
`lib/components/**` (кроме `kit/`);
- новое: `routes/preview/**`, `lib/registry-new/**`, `lib/preview/**`,
`lib/registry-schema.ts`, `lib/registry-schema.test.ts`,
`lib/components/kit/**`;
- общее (разрешено обоим): всё прочее — `core/`, `i18n/`, `theme`,
`assets/`, корневой `lib` (`index.ts`, тесты). Достигнутая полная изоляция
(одиночные пересечения устранены):
- старые пилоты `registry/geometry.ts` (`add-border`) и `registry/alpha.ts`
(`add-stroke`) получали `schema` из нового `registry-schema` — убрано; оба
инструмента работают в старом UI через `params: ParamDef[]`, в preview —
через свои schema-версии в `registry-new/`;
- `registry.ts` больше не импортирует `ToolSchema` из `registry-schema`;
- preview `list-tools` тянул `TOOL_ICONS` из старого `lib/tools/tool-icons`
→ создана копия `lib/preview/tool-icons.ts` (правило копий).
37. Перенос всех старых компонентов/библиотек в папки `old/` (**следующее**):
переезд не трогает плагин — достаточно дописать один glob-паттерн в
настройку правила (например `lib/old/**`, `lib/components/old/**`), а сама
проверка работает по фактическим путям автоматически.
### Как ревьюить каждый шаг
- Diff **только один инструмент** (или фундамент без инструментов).
- Старый `params`/`run`/`defaultParams`/`sanitizeParams`**не изменены**
(кроме добаление `schema`, если это пилот).
- Старый UI и старые тесты продолжают работать — запустить `pnpm --dir web test`
и `pnpm --dir web exec svelte-check`.
- Порядок полей в `interface Params` и в `schema` совпадает; дефолты в сeme
равны старым `default` из `params`.
- Пилот из Фазы 1 проверяется **руками в preview**: применяется рамка,
дефолты/валидация/смена значений работают end-to-end.
## Поток данных (runtime)
### Старый pipeline (не трогаем)
Всё как сейчас: старое `defaultParams`/`sanitizeParams`/executor/chain работают
на `ParamDef[]` и `Record<string, unknown>`. Никаких изменений.
### Новый pipeline (строим рядом)
- **Хранилище значений** — типизировано на схему; у нового UI значения полей
собираются в `Params` по `ToolSchema<P>`.
- **defaultParams / sanitizeParams (новые)** — выводятся из `ToolSchema<P>`,
дефолты лежат в схеме. Старые функции не трогаем, добавляем новые рядом.
- **Executor/worker (новые)** — получает `Record<string, unknown>`, собирает
`Params` по схеме, вызывает `run`. Остаётся serializable.
- **Pipeline/chain (новый)** — шаги хранят значения, типизируются по схеме.
## Изменения в UI-компонентах
- **Старый UI** (`ParamForm.svelte`, группа `(old)/`) — **не трогаем.** Работает
на `ParamDef[]` как раньше.
- **Новый UI** (kit/`SchemaToolView` + `SchemaFields` + `SchemaPreview`, читает
`ToolSchema<P>`) — сейчас рендерит поля по схеме (number/slider/color).
- `tool-views.ts` отсутствует — preview/lede/layout уже живут в meta инструмента
(`ToolEntry` + `schema.layout`), дублирования нет.
## Оценка трудозатрат
| Часть | Сложность | Время |
| ----------------------------------------------------- | ------------------------ | ---------------------- |
| Typed field builders + toolSchema\<P\> | Средняя | ~2ч |
| Новые defaultParams/sanitizeParams под схему | Средняя | ~2-3ч |
| ToolEntry\<P\> generic + поле schema (рядом с params) | Низкая-Средняя | ~1-2ч |
| Новый executor/worker/pipeline под схему | Средняя | ~2-3ч |
| Миграция инструментов на interface Params + схемы | Механическая, но крупная | ~8-12ч |
| Новый рендер (kit/ParamControl) + составные виджеты | Средняя | ~4-5ч |
| Поглощение tool-views.ts | Средняя | ~1-2ч |
| Вынос старого UI в (old)/ + адаптация маршрутов | Средняя-Низкая | ~2-3ч |
| **Итого** | | **~22-32ч** (поэтапно) |
> Оценка выросла по сравнению с ранней версией плана, потому что принят путь
> «явный interface Params + схема + общий рендер с пер-инструмент layout» — это
> полный рефакторинг pipeline, а не только добавление составных типов.
>
> Старый UI/`ParamDef[]`/старый pipeline в смету **не входят** — они не
> рефакторятся, а продолжают работать на `(old)/`-маршрутах до перехода (затем
> убираются отдельным решением — см. шаг 35).
## Порядок реализации (кратко)
Детальный пошаговый план с атомарными изменениями и порядком ревью — в разделе
«Пошаговый план перевода инструментов» выше. Здесь краткая сводка фаз:
1. **Фаза 0** — фундамент: builders + `toolSchema<P>` + `ToolEntry<P>.schema` +
новые default/sanitize. Ни одного инструмента не изменено. ✔
2. **Фаза 1****один полноценный рабочий инструмент в preview** («добавить
рамку»): schema + рендер + end-to-end применение. Ручная оценка
работоспособности решения/сложностей.
3. **Фаза 2** — простые/одиночные инструменты (проверенный паттерн Фазы 1), 1
инструмент = 1 диф.
4. **Фаза 3** — инструменты с составными типами (dimension → color-pair → offset
→ position9 → font-style → plate → gradient), пилот каждого типа отдельно.
5. **Фаза 4** — масштаб UI на остальные: составные виджеты, пер-инструмент
layout (для каждого инструмента — UI-макет).
6. **Фаза 5** — изоляция: старый UI на `(old)/`-маршруты (не удаляется, остаётся
для ревью); preview на `registry-new` полностью; линтер-изоляция веток
(плагин `isolation`), затем перенос старых компонентов/библиотек в папки
`old/`.
## Зависимости
- **UI сперва доделать** — текущая активная работа; типизация params ожидает
готовности нового дизайна.
- Ветка redesign (`plan-redesign.md`) — параллельная, учесть при миграции
kit/`ParamControl.svelte` и `overlay-store`.
- Старый pipeline остаётся рабочим и шippable на всём протяжении перехода
(аналогично стратегии `plan-redesign.md`).
+139
View File
@@ -0,0 +1,139 @@
# План: доводка дизайна до верности (фикс по refs)
> **СТАТУС: ЗАКРЫТ (2026-09-07).** Актуальный дизайн (`preview/*`) ушёл вперёд
> от референсов `refs-html/` — сверка с ними больше не информативна (новые
> компоненты `SchemaToolView`/`SchemaPreview`/каталог не имеют рефовой копии).
> Аудит против рефа прекращается; качество дизайна дальше гоняется ревью руками
> и специализированными проверками (`lint:all` —
> stylelint/design-tokens/токен-аудит). Порядок удаления старого и перехода — по
> `plan-redesign.md` (C17C21).
> Статус: к выполнению. Ветка `feat/redesign`. Источники:
> `docs/archive/demo-fidelity.md` (архивная копия старого отчёта по верности,
> неактуальна), `docs/plan-redesign.md` (общая стратегия, фазы 1–4 выполнены до
> C16). Актуальный аудит верности — `pnpm refs-cdp-audit` /
> `pnpm refs-css-audit` / `pnpm refs-dom-audit` → `web/audit/audit-report.md`
> (текущий список скриптов — в `web/package.json`, см. `refs-*audit`).
## Контекст
- Новый дизайн собран параллельно под `preview/*` (см. `plan-redesign.md`),
старый сайт `(old)/` не трогаем до C17.
- `refs/` и `refs-html/` приведены к консистентному виду — текущая «правда по
пикселям». Механизм синхронизации ref→код не создаётся (см. §9 plan-redesign).
- Аудит верности автоматизирован: `pnpm refs-cdp-audit` (1440×900,
`scripts/audit-cdp.mjs`) пишет `web/audit/audit-report.md` — light/dark
токены + поэлементный дифф стилей; пиксель-дифф не используется. Палитры
токенов уже сверены с рефом (light совпадает, dark собран). Темы в `refs-html`
переключаются — скрипт инжектится в `scripts/extract-static.mjs`.
- Исходный отчёт `demo-fidelity.md` перенесён в `docs/archive/` как утративший
актуальность; его данные актуализированы ниже (Этап 1).
- Зафиксированные решения (подтверждены автором):
- **F1 — плоский фон холста**, без blueprint-сетки (как в рефе).
- **F4 — preview-stack в одну колонку** (как в рефе).
## Принципы
- Никаких правок старого дизайна `(old)/` и старого `ui/`.
- Компоненты пишем в `kit/` (переименование в `ui/` — на C19 по plan-redesign).
- Коммиты атомарные (< ~500 строк), по 1–2 компонента; после каждого —
`svelte-check` + `lint` + `build` + `refs-cdp-audit` (diff% не должен расти
без причины).
- Коммиты делает автор (агент не коммитит).
## Этап 0 — Рефакторинг (SRP / дизайн-обёртки), без изменения визуала
Гигантские страницы (`demo` ~513, `tools/[id]` ~451 строк) дробим на компоненты
единой ответственности; убираем дублирующиеся inline-стили.
Новые обёртки в `src/lib/components/kit/`:
- `FieldGrid` — сетка полей (заменяет `.controls` / `.controls.compact`).
- `ToggleRow` — лейбл + `Toggle` (дублировался в `demo` и `tools/[id]`).
- `PreviewStack` — контейнер превью-плиток, **одна колонка** (реализует F4 в
одном месте).
- `SectionLabel` — label + strong + actions (вместо `.pipeline-head` /
`.preview-top`).
- `FileChip` — пилюля файла.
- `PipelineFooter` — reset + auto-note.
- `WorkspaceLayout` — двухпанельная сетка (settings | preview) со sticky +
брейкпоинт 800px.
Декомпозиция страниц:
- `tools/[id]`: вынести `ParamControl` (диспетчер полей по `ParamDef`),
`SettingsPanel` (левая панель), `PreviewPanel` (правая панель) — колоцированно
в `routes/preview/tools/[id]/`. Страница сжимается до состояния + глёв.
- `demo`: переписать на те же обёртки + `WorkspaceHeader` (eyebrow + h1 + lede +
file-chip).
Побочный баг, устраняемый тут же: в `demo` поля сейчас обёрнуты в
`<div class="control-block"><MonoLabel>…</MonoLabel><SliderField label>`
**двойной лейбл**, т.к. `ColorField`/`SliderField`/`SelectField`/`NumberField`
уже содержат `Field` (MonoLabel) изнутри. При рефакторе убираем внешний
`MonoLabel`, оставляем само поле.
Критерий Этапа 0: визуал идентичен (audit diff% без регрессии), старые тесты
зелёные.
## Этап 1 — Верность (по refs-html; статус на момент плана)
Аудит: `pnpm refs-cdp-audit` (1440×900) пишет `web/audit/audit-report.md`
light/dark токены + поэлементный дифф стилей. Пиксель-дифф не используется.
Палитры токенов уже сверены с рефом: light совпадает (`--background`, `--panel`,
`--foreground`, `--muted`, `--line`, `--blue`), dark собран (16 токенов). Темы в
`refs-html` переключаются (скрипт инжектится в `extract-static.mjs`).
Открытые / решённые пункты (исходник — `docs/archive/demo-fidelity.md`):
- **B1** (открыт, P1): `button, input, select, textarea` рендерятся Arial —
добавить в reset `design2.css` `font-family: inherit`. Влияет на все страницы.
- **B2** (решён — проверить): двойного топбара нет. `preview/+layout.svelte`
рендерит ровно один `TopBar`; `demo` свой `.topbar` не рендерит. Подтвердить
аудитом/визуально при Этапе 1.
- **F1** (решено — плоский фон): наш `--background` уже `#EEF1F4` (как реф);
видимое отличие было в blueprint-сетке поверх фона. Фикс: убрать
`BlueprintGrid` из `AppShell`.
- **F2** (сделано C16): тип шага (`StepCard`) — приглушённый mono-текст, не
синий бейдж; индекс шага — синий `#1769D2`.
- **F3** (сделано C16): подпись превью-тайла (`PreviewTile`) — синяя `#1769D2`.
- **F4** (решено — одна колонка): реализуется в `PreviewStack` (Этап 0), колонка
= `1fr`.
- **F5** (открыт): вес `MonoLabel` 500 → 400 (как в рефе).
- **F6** (открыт): мета-подписи (MetaRow/MetaList, DIMENSIONS/FORMAT/SIZE) fs
10px → 9px.
- **F7** (низкий): копирайт/стиль — у нас `+ Add tool` (реф `Add tool`); кнопка
`Download result` — сверить с рефом (синяя, ~478×42). Проверить визуально
(замеры NOT FOUND — артефакт вложенного чипа размера).
- **F8** (низкий): `h1` line-height чуть выше рефа (150px vs 115px).
- **F9** (открыт): вертикальный ритм — подровнять паддинги секций под реф
(pipeline-head и пр.).
- **F10** (открыт): внутренние отступы preview-панели (паддинг панели).
После Этапа 1 — повторный `refs-cdp-audit`, зафиксировать остаточный diff
(sub-pixel / структурный) и обновить статус пунктов выше прямо в этом файле.
## Этап 2 — plan-redesign.md (C17C21), ОТЛОЖЕНО
Выполняется **только после** того, как новый дизайн доведён до приемлемого
уровня верности (Этапы 0–1). Сами пункты — в `plan-redesign.md` §10 Шаг 56:
- **C17**: `preview/*` → реальные маршруты, удалить `(old)/`, корневой
`+layout.svelte``design2.css`, поправить `newCode` в `eslint.config.js`
(`src/routes/preview/**``src/routes/**`).
- **C18**: правки импортов/редиректов, проверка билда.
- **C19**: удалить старое `ui/` + `app.css`, `kit/``ui/` (или оставить),
раскрыть ESLint `recommended` на весь код.
- **C20**: брейкпоинты 1200/1100/800/480; build без предупреждений; `grep` по
старым токенам/классам пуст.
- **C21**: опц. удалить `refs/` и `refs-html/` из репо.
## Порядок коммитов (Этап 0)
1. `kit/`: `FieldGrid`, `ToggleRow`, `PreviewStack`, `SectionLabel`, `FileChip`,
`PipelineFooter`, `WorkspaceLayout` — по 1–2 на коммит.
2. `tools/[id]`: `ParamControl``SettingsPanel` + `PreviewPanel` → тонкая
страница.
3. `demo`: переписать на обёртки.
После каждого коммита: `svelte-check` + `lint` + `build` + `refs-cdp-audit`.
+65 -34
View File
@@ -4,65 +4,96 @@
## 1. Принципы отбора
- Максимум переиспользования: готовые контракты записей (`run`, `preview`, формат вывода, info-режим) и существующие операции ядра.
- Новые механизмы ядра — минимальные, чистые, покрытые тестами до подключения инструментов.
- Canvas/DOM-зависимые утилиты (SVG-вход, надписи и водяные знаки, эффекты через фильтры контекста) — вне этого пакета, отдельная волна.
- Целевой объём волны: около 37 новых инструментов, каталог вырастает с 11 до ~48.
- Максимум переиспользования: готовые контракты записей (`run`, `preview`,
формат вывода, info-режим) и существующие операции ядра.
- Новые механизмы ядра — минимальные, чистые, покрытые тестами до подключения
инструментов.
- Canvas/DOM-зависимые утилиты (SVG-вход, надписи и водяные знаки, эффекты через
фильтры контекста) — вне этого пакета, отдельная волна.
- Целевой объём волны: около 37 новых инструментов, каталог вырастает с 11 до
~48.
## 2. Новые механизмы
- **Генераторы без входного файла**: у записи появляется признак «источник не нужен»; страница такого инструмента не показывает левую панель и зону перетаскивания — только параметры и результат.
- **Текстовый ввод и вывод**: часть утилит принимает вставленный текст вместо файла и выдаёт текстовый результат (панель с полем только для чтения, кнопки «копировать» и «скачать как текст»). Распространяется на base64, data-uri, hex.
- **BMP-энкодер в ядре**: 24-битный, чистая функция из пикселей в байты, с тестами на маленьких матрицах (заголовок, порядок каналов BGR, паддинг строки).
- **Генераторы без входного файла**: у записи появляется признак «источник не
нужен»; страница такого инструмента не показывает левую панель и зону
перетаскивания — только параметры и результат.
- **Текстовый ввод и вывод**: часть утилит принимает вставленный текст вместо
файла и выдаёт текстовый результат (панель с полем только для чтения, кнопки
«копировать» и «скачать как текст»). Распространяется на base64, data-uri,
hex.
- **BMP-энкодер в ядре**: 24-битный, чистая функция из пикселей в байты, с
тестами на маленьких матрицах (заголовок, порядок каналов BGR, паддинг
строки).
## 3. Состав пакета
**Конвертация — 6**
`jpg-to-png`, `webp-to-png`, `gif-to-png` (первый кадр), `bmp-to-png`, `ico-to-png` — всё через уже умеющий декодировать ввод, сохранение в PNG; плюс `png-to-bmp` на новом энкодере.
**Конвертация — 6** `jpg-to-png`, `webp-to-png`, `gif-to-png` (первый кадр),
`bmp-to-png`, `ico-to-png` — всё через уже умеющий декодировать ввод, сохранение
в PNG; плюс `png-to-bmp` на новом энкодере.
**Текстовые представления — 6**
`png-to-base64`, `base64-to-png`, `png-to-data-uri`, `data-uri-to-png`, `png-to-hex`, `hex-to-png`.
**Текстовые представления — 6** `png-to-base64`, `base64-to-png`,
`png-to-data-uri`, `data-uri-to-png`, `png-to-hex`, `hex-to-png`.
**Цвет — 9**
`change-opacity` (множитель прозрачности), `sepia`, `change-hue` (сдвиг тона), `extract-channel` (выбор R/G/B в оттенках серого), `swap-channels` (перестановка парой), `threshold-black-white` (порог яркости), `posterize` (число уровней), `two-colors` (два цвета + порог), `invert-alpha`.
**Цвет — 9** `change-opacity` (множитель прозрачности), `sepia`, `change-hue`
(сдвиг тона), `extract-channel` (выбор R/G/B в оттенках серого), `swap-channels`
(перестановка парой), `threshold-black-white` (порог яркости), `posterize`
(число уровней), `two-colors` (два цвета + порог), `invert-alpha`.
**Прозрачность и холст — 7**
`remove-alpha-channel` (подложка белым), `set-alpha-channel` (константная прозрачность), `extract-alpha-mask` (альфа в чёрно-белую маску), `add-padding` (отступ + цвет/прозрачность), `add-border` (толщина + цвет), `round-corners` (радиус), `fit-on-background` (вписать на полотно цвета с полями).
**Прозрачность и холст — 7** `remove-alpha-channel` (подложка белым),
`set-alpha-channel` (константная прозрачность), `extract-alpha-mask` (альфа в
чёрно-белую маску), `add-padding` (отступ + цвет/прозрачность), `add-border`
(толщина + цвет), `round-corners` (радиус), `fit-on-background` (вписать на
полотно цвета с полями).
**Композиция — 2**
`tile` (повтор по сетке), `center-by-alpha` (центрирование по непрозрачному содержимому).
**Композиция — 2** `tile` (повтор по сетке), `center-by-alpha` (центрирование по
непрозрачному содержимому).
**Генераторы — 4**
`create-empty` (размер + цвет или прозрачность), `single-color`, `random-noise` (с фиксированным зерном для воспроизводимости), `linear-gradient` (два цвета, направление).
**Генераторы — 4** `create-empty` (размер + цвет или прозрачность),
`single-color`, `random-noise` (с фиксированным зерном для воспроизводимости),
`linear-gradient` (два цвета, направление).
**Анализ — 3**
`is-grayscale`, `is-transparent`, `orientation` (портрет/ландшафт/квадрат) — текстовые сводки в info-режиме.
**Анализ — 3** `is-grayscale`, `is-transparent`, `orientation`
(портрет/ландшафт/квадрат) — текстовые сводки в info-режиме.
## 4. Новые операции ядра (все — чистые функции с тестами)
- Холст: расширение полотна (общая база для padding/border/fit), тайлинг, центр по bounding-box альфы, скругление углов маской радиуса.
- Цвет: сепия, сдвиг тона через HSL, множитель альфы, извлечение/перестановка каналов, пороги (яркость и двухцветный), постеризация уровней, инверсия альфы.
- Холст: расширение полотна (общая база для padding/border/fit), тайлинг, центр
по bounding-box альфы, скругление углов маской радиуса.
- Цвет: сепия, сдвиг тона через HSL, множитель альфы, извлечение/перестановка
каналов, пороги (яркость и двухцветный), постеризация уровней, инверсия альфы.
- Генерация: заливка цветом, шум с зерном, линейный градиент.
- Форматы: BMP-энкодер; кодеки текстовых представлений (base64/data-uri/hex в обе стороны).
- Форматы: BMP-энкодер; кодеки текстовых представлений (base64/data-uri/hex в
обе стороны).
- Анализ: проверка монохромности, полной непрозрачности, ориентации.
## 5. Этапы и проверки
- **A. Механизмы** — генераторный режим страницы, текстовый ввод/вывод, BMP-энкодер. Проверка: `test/check/build`.
- **B. Конвертация и текстовые представления** — 12 инструментов. Проверка: ручной прогон каждой конвертации туда-обратно.
- **A. Механизмы** — генераторный режим страницы, текстовый ввод/вывод,
BMP-энкодер. Проверка: `test/check/build`.
- **B. Конвертация и текстовые представления** — 12 инструментов. Проверка:
ручной прогон каждой конвертации туда-обратно.
- **C. Цвет** — 9 инструментов. Проверка: юнит-тесты операций + визуально.
- **D. Прозрачность, холст, композиция** — 9 инструментов. Проверка: юнит-тесты + визуально.
- **E. Генераторы и анализ** — 7 инструментов. Проверка: детерминизм шума (одно зерно — одно изображение), своды анализа.
- **F. Итог** — полный прогон, смоук всех новых инструментов «настроить → скачать», регресс старых одиннадцати.
- **D. Прозрачность, холст, композиция** — 9 инструментов. Проверка:
юнит-тесты + визуально.
- **E. Генераторы и анализ** — 7 инструментов. Проверка: детерминизм шума (одно
зерно — одно изображение), своды анализа.
- **F. Итог** — полный прогон, смоук всех новых инструментов «настроить →
скачать», регресс старых одиннадцати.
## 6. Критерии готовности
- Каталог содержит ~48 рабочих инструментов, каждый доступен страницей с формы из реестра.
- Все новые операции ядра покрыты тестами; обратимые конвертации проверены круговым прогоном (например PNG → base64 → PNG даёт те же пиксели).
- Генераторы работают без загрузки файла; текстовые представления копируются и скачиваются.
- Каталог содержит ~48 рабочих инструментов, каждый доступен страницей с формы
из реестра.
- Все новые операции ядра покрыты тестами; обратимые конвертации проверены
круговым прогоном (например PNG → base64 → PNG даёт те же пиксели).
- Генераторы работают без загрузки файла; текстовые представления копируются и
скачиваются.
- `test/check/build` зелёные.
## 7. Что сознательно не делаем
- SVG-вход, рисование текста и водяных знаков, эффекты через фильтры контекста, GIF/APNG-энкодинг, пакетная обработка — последующие волны.
- Семейства logo/icon/signature/stamp как отдельные страницы — они будут конфигурациями тех же операций после этой волны.
- SVG-вход, рисование текста и водяных знаков, эффекты через фильтры контекста,
GIF/APNG-энкодинг, пакетная обработка — последующие волны.
- Семейства logo/icon/signature/stamp как отдельные страницы — они будут
конфигурациями тех же операций после этой волны.
+47 -17
View File
@@ -4,41 +4,69 @@
## 1. Что получается
Одиннадцать новых инструментов: свободные трансформации формы (наклон, поворот на произвольный угол, зум, сдвиг), светокоррекция (гамма, автоконтраст, температура, тонирование), виньетка, эффект «артефактов сжатия» и вход SVG. Каталог вырастает с 58 до ~69.
Одиннадцать новых инструментов: свободные трансформации формы (наклон, поворот
на произвольный угол, зум, сдвиг), светокоррекция (гамма, автоконтраст,
температура, тонирование), виньетка, эффект «артефактов сжатия» и вход SVG.
Каталог вырастает с 58 до ~69.
Текстовые инструменты и водяные знаки в волну не входят — им нужен рендер текста на канвасе, это отдельная четвёртая волна.
Текстовые инструменты и водяные знаки в волну не входят — им нужен рендер текста
на канвасе, это отдельная четвёртая волна.
## 2. Новое ядро
- **Аффинные трансформации** (`core/affine.ts`): единая функция обратного отображения — для каждого пикселя результата вычисляется исходная координата матрицей и семплируется билинейно (общий семплер, вынесенный из `resize`). Поверх неё: наклон по осям, поворот на произвольный угол (с расширением холста под новые габариты), зум к центру, сдвиг.
- **Свет и цвет** (`core/color.ts` дополнения): гамма-коррекция через LUT, автоконтраст (растяжка по гистограмме яркости с отсечкой хвостов), температура (множители тёплых/холодных каналов), тонирование (умножение RGB на цвет с сохранением яркостного баланса).
- **Виньетка** (`core/effects.ts`): радиальное затемнение от центра, сила слайдером; чистая математика без canvas.
- **IO-расширения** (`core/io.ts`): декодирование SVG из текста/файла через blob-URL и `Image`; раундтрип «закодировать JPEG с качеством Q → декодировать обратно» для эффекта артефактов сжатия.
- **Аффинные трансформации** (`core/affine.ts`): единая функция обратного
отображения — для каждого пикселя результата вычисляется исходная координата
матрицей и семплируется билинейно (общий семплер, вынесенный из `resize`).
Поверх неё: наклон по осям, поворот на произвольный угол (с расширением холста
под новые габариты), зум к центру, сдвиг.
- **Свет и цвет** (`core/color.ts` дополнения): гамма-коррекция через LUT,
автоконтраст (растяжка по гистограмме яркости с отсечкой хвостов), температура
(множители тёплых/холодных каналов), тонирование (умножение RGB на цвет с
сохранением яркостного баланса).
- **Виньетка** (`core/effects.ts`): радиальное затемнение от центра, сила
слайдером; чистая математика без canvas.
- **IO-расширения** (`core/io.ts`): декодирование SVG из текста/файла через
blob-URL и `Image`; раундтрип «закодировать JPEG с качеством Q → декодировать
обратно» для эффекта артефактов сжатия.
## 3. Инструменты — 11
- **Геометрия:** `skew-png` (углы X/Y), `rotate-free-png` (любой угол, холст расширяется), `zoom-png` (масштаб ≥100 % к центру), `shift-png` (сдвиг со смещением содержимого).
- **Фильтры:** `vignette-png` (сила затемнения), `jpeg-artifacts-png` (имитация пережатия).
- **Цвет:** `gamma-png` (0.13.0), `auto-contrast-png` (без параметров), `temperature-png` (−100 тёплый…+100 холодный или наоборот — подпись честная), `tint-png` (цвет + сила).
- **Геометрия:** `skew-png` (углы X/Y), `rotate-free-png` (любой угол, холст
расширяется), `zoom-png` (масштаб ≥100 % к центру), `shift-png` (сдвиг со
смещением содержимого).
- **Фильтры:** `vignette-png` (сила затемнения), `jpeg-artifacts-png` (имитация
пережатия).
- **Цвет:** `gamma-png` (0.13.0), `auto-contrast-png` (без параметров),
`temperature-png` (−100 тёплый…+100 холодный или наоборот — подпись честная),
`tint-png` (цвет + сила).
- **Конвертация:** `svg-to-png` (текст SVG слева, как base64-режим).
## 4. Этапы и проверки
- **A. Ядро** — семплер, аффинный модуль, свет/цвет, эффекты + точные тесты: поворот квадрата на 90° совпадает с существующим `rotate90`, зум ×2 удваивает размеры и центр остаётся центром, гамма 1 — идентичность, автоконтраст растягивает известный диапазон, виньетка симметрична и не трогает центр при нулевой силе. Проверка: `pnpm test/check`.
- **B. IO** — SVG-декод и JPEG-раундтрип. Проверка: сборка; живой прогон на этапе D.
- **C. Реестр** — одиннадцать записей, иконки, тесты целостности. Проверка: `test/build`, страницы в экспорте.
- **A. Ядро** — семплер, аффинный модуль, свет/цвет, эффекты + точные тесты:
поворот квадрата на 90° совпадает с существующим `rotate90`, зум ×2 удваивает
размеры и центр остаётся центром, гамма 1 — идентичность, автоконтраст
растягивает известный диапазон, виньетка симметрична и не трогает центр при
нулевой силе. Проверка: `pnpm test/check`.
- **B. IO** — SVG-декод и JPEG-раундтрип. Проверка: сборка; живой прогон на
этапе D.
- **C. Реестр** — одиннадцать записей, иконки, тесты целостности. Проверка:
`test/build`, страницы в экспорте.
- **D. Итог** — полный прогон, смоук §7.
## 5. Смоук-чеклист
1. Наклон X/Y по отдельности и вместе — форма искажается предсказуемо, без чёрных полос при малых углах.
1. Наклон X/Y по отдельности и вместе — форма искажается предсказуемо, без
чёрных полос при малых углах.
2. Поворот на 30° — холст вырос, углы прозрачны, содержимое повернуто.
3. Зум 200 % — содержимое увеличено к центру, холст прежнего размера, края обрезаны.
3. Зум 200 % — содержимое увеличено к центру, холст прежнего размера, края
обрезаны.
4. Сдвиг уводит содержимое за край, противоположный край заполняется фоном.
5. Гамма 0.5 темнее, 2.0 светлее, 1.0 — без изменений.
6. Автоконтраст на тусклом фото вытягивает диапазон.
7. Температура ±50 заметно теплеет/холодеет.
8. Тонирование красным даёт равномерный оттенок, белые области окрашиваются сильнее всего.
8. Тонирование красным даёт равномерный оттенок, белые области окрашиваются
сильнее всего.
9. Виньетка затемняет только края, центр не трогает.
10. Артефакты: качество 10 даёт видимые квадраты JPEG.
11. SVG вставленный слева декодируется в результат справа; битый SVG — баннер.
@@ -46,8 +74,10 @@
## 6. Критерии готовности
- Одиннадцать инструментов работают и покрыты юнит-тестами ядра (кроме IO-дом-части).
- Аффинные операции опираются на общий билинейный семплер; дублирования сэмплинга нет.
- Одиннадцать инструментов работают и покрыты юнит-тестами ядра (кроме
IO-дом-части).
- Аффинные операции опираются на общий билинейный семплер; дублирования
сэмплинга нет.
- Каталог ~69 инструментов; `test/check/build` зелёные, смоук §5 пройден.
## 7. Что сознательно не делаем
+97
View File
@@ -0,0 +1,97 @@
# План: закрытие EASY/MEDIUM-пробелов из сравнения с onlinepngtools
> **СТАТУС: ВЫПОЛНЕН (2026-09-07) — устарел.** Все волны W1–W9 закрыты. Идеи из
> раздела «Вне очереди» (region-инструменты, мультифайловый вывод, анимационные,
> HARD-серия) перенесены в `backlog.md`.
> Статус: черновик на ревью
## Принципы
- Волны собираются вокруг общего математического ядра: одна волна = один
движок + N инструментов на нём.
- Каждая волна оформляется отдельным планом при старте; здесь — очередь и
состав.
- Порядок: сначала кластеры, дающие много инструментов малой кровью, затем
одиночные эффекты, MEDIUM-пакеты в конце.
- Полный список параметров каждого инструмента живёт в карте инструментов
(tools-map.md); здесь только состав и ядро.
## Очередь волн
### W1. Палитры — ВЫПОЛНЕНА (11 инструментов)
Ядро: RGB↔HSL + гармонии цветового круга. Вывод: свотч-полотно (генератор).
Состав: color-wheel, complementary, monochromatic, analogous, triadic, tetradic,
similar-shades, sort-colors. Плюс утилиты смешения тем же ядром: mix-colors,
average-color, blend-two, step-between (+4, итого 12).
### W2. Каналы и пространства — ВЫПОЛНЕНА (6 инструментов)
Ядро: матрицы преобразования RGB→(HSL/HSV/HSI/CMYK/YCbCr/LAB) + визуализация
выбранного компонента серым или окрашенно. Общий select «компонент» + select
«режим отображения».
### W3. Маски по свойствам пикселей — ВЫПОЛНЕНА (7 инструментов)
Ядро: предикат над пикселем → бинарная маска (с инверсией и подсветкой цветом).
Состав: show-transparent, show-grayscale, show-color, light-mask, dark-mask,
unique-color-mask, extract-by-color.
### W4. Фигурные маски — ВЫПОЛНЕНА (4 инструмента)
Ядро: SDF фигуры (круг/квадрат/звезда/волна) → альфа-маска с fit-режимами.
Состав: circle-mask, square-mask, star-mask, wavy-mask.
### W5. Геометрия-добивки — ВЫПОЛНЕНА (5 инструментов)
Ядро: bbox по альфе (trim) переиспользуется тремя инструментами источника.
Состав: trim-empty-space (закрывает remove border/space), change-canvas-size,
change-aspect-ratio, landscape↔portrait, symmetric-copy.
### W6. Эффекты лёгкие — ВЫПОЛНЕНА (6 инструментов; color-blocks покрыт pixelate)
Состав: pixelate, color-blocks, randomize-pixels (seed), add-noise,
feather-edges, clean-edges, silhouette. Shadow/glow — сюда же, если потянет
этап: оба = размытая альфа + смещение + цвет (ядро blur уже есть).
### W7. Конвертеры и генераторы — ВЫПОЛНЕНА (11 инструментов)
PNG↔bytes, PNG↔rgb-values (текстовые результаты), verify-is-png (анализ
сигнатуры), text-to-png (domText без входной картинки), emoji-to-png,
placeholder-png, color-spectrum, colorful-random (seed), draw-grid.
multi-color-gradient — если успеем новый тип параметра «список цветов», иначе
перенос.
### W8. Цветовые MEDIUM — ВЫПОЛНЕНА (4 инструмента; median-cut + FloydSteinberg/Bayer, custom-palette через text-параметр)
Ядро: квантование (median-cut или k-means). Состав: quantize (k),
decrease-color-count (=quantize с пресетами), custom-palette (маппинг на список
цветов — нужен тип параметра «список»), dithering (FloydSteinberg/Bayer поверх
квантования).
### W9. Сжатие — ВЫПОЛНЕНА (3 инструмента; optimize/change-quality осознанно не выделены — покрыты пресетами compress и W8)
Без wasm честное управление размером PNG ограничено: реальный рычаг —
квантование (W8) + обрезка метаданных + итеративный подбор под целевой KB.
Состав: compress (пресеты усилий), reduce-to-size (целевой KB, бинарный поиск по
k), optimize (re-encode), change-quality/low-quality — переосмыслить как пресеты
W8; jpeg-artifacts уже покрывает «испортить».
## Вне очереди
- Region-инструменты
(censor/erase/pixelate-area/blur-area/sharpen-area/reverse-area) — ждут UI
выделения области на превью; отдельное решение.
- Мультифайловый вывод (split-parts, gif-frames, separate-colors) — ждут
механизм «результат = набор файлов».
- Анимационные (slow-reveal/fade/scrolling) — выход не PNG; отдельное решение о
формате.
- HARD: glitch, barcode, signature-extract, handwritten-digital.
- Нишевые серии logo/icon/stamp/signature — не копируем.
## Оценка масштаба
W1W7 дают **~47 новых инструментов**, почти все EASY. W8 добавляет ядро
квантования (открывает custom-palette/dithering и улучшает W9). Суммарно каталог
достигает ~120+ при текущих 71.
+37 -14
View File
@@ -4,43 +4,65 @@
## 1. Что получается
Выдача поиска на главной превращается из списка строк в сетку карточек: слева иконка инструмента, справа название и описание. Инструменты получают два новых поля реестра — `popularity` (приоритет в выдаче) и `icon` (идентификатор иконки). Иконки переиспользуются в каталоге `/list-tools` и в легендах шагов цепочки. Поиск работает по-прежнему: нечёткий скоринг, клавиатура ↑↓ Enter Esc.
Выдача поиска на главной превращается из списка строк в сетку карточек: слева
иконка инструмента, справа название и описание. Инструменты получают два новых
поля реестра — `popularity` (приоритет в выдаче) и `icon` (идентификатор
иконки). Иконки переиспользуются в каталоге `/list-tools` и в легендах шагов
цепочки. Поиск работает по-прежнему: нечёткий скоринг, клавиатура ↑↓ Enter Esc.
## 2. Данные реестра
- **`popularity?: number`** (0–100, по умолчанию 50) — приоритет внутри равного скоринга запроса. Значения проставляются вручную осознанно: базовые операции (обрезка, размер, удаление фона, конвертации JPG/PNG) — 80–95; цветовые и геометрия — 40–70; узкие и генераторы — 10–30.
- **`icon?: string`** — имя иконки из набора Lucide. Явный словарь-маппинг для всех инструментов в одном месте; отсутствие записи даёт нейтральную заглушку (Wrench).
- Тест целостности расширяется: популярность в диапазоне, иконка — непустая строка из разрешённого набора.
- **`popularity?: number`** (0–100, по умолчанию 50) — приоритет внутри равного
скоринга запроса. Значения проставляются вручную осознанно: базовые операции
(обрезка, размер, удаление фона, конвертации JPG/PNG) — 80–95; цветовые и
геометрия — 40–70; узкие и генераторы — 10–30.
- **`icon?: string`** — имя иконки из набора Lucide. Явный словарь-маппинг для
всех инструментов в одном месте; отсутствие записи даёт нейтральную заглушку
(Wrench).
- Тест целостности расширяется: популярность в диапазоне, иконка — непустая
строка из разрешённого набора.
## 3. Иконки
Зависимость `lucide-svelte` (MIT) — один пакет, иконки рендерятся компонентами, дерево не раздувается за счёт выборочного импорта.
Зависимость `lucide-svelte` (MIT) — один пакет, иконки рендерятся компонентами,
дерево не раздувается за счёт выборочного импорта.
Маппинг ведётся одним объектом `tool-icons.ts` рядом с реестром: 51 запись вида `'crop-png': Crop`. Подбор по смыслу: обрезка — Crop, размер — Scaling, поворот — RotateCw, отражение — FlipHorizontal2, оттенок — Palette, размытие — Droplets, резкость — Focus, удаление фона — Scissors, анализ — Info и так далее; для семейств конвертаций — FileInput/FileOutput варианты.
Маппинг ведётся одним объектом `tool-icons.ts` рядом с реестром: 51 запись вида
`'crop-png': Crop`. Подбор по смыслу: обрезка — Crop, размер — Scaling, поворот
— RotateCw, отражение — FlipHorizontal2, оттенок — Palette, размытие — Droplets,
резкость — Focus, удаление фона — Scissors, анализ — Info и так далее; для
семейств конвертаций — FileInput/FileOutput варианты.
Иконка появляется в трёх местах: карточка выдачи, карточка каталога `/list-tools`, легенда шага цепочки («Шаг N: [icon] Название»).
Иконка появляется в трёх местах: карточка выдачи, карточка каталога
`/list-tools`, легенда шага цепочки («Шаг N: [icon] Название»).
## 4. Карточки выдачи
- Сетка: одна колонка на узком экране, две — начиная со среднего героя.
- Композиция карточки: квадрат иконки слева, справа название (полужирное) и однострочное описание с обрезкой.
- Композиция карточки: квадрат иконки слева, справа название (полужирное) и
однострочное описание с обрезкой.
- Состояния hover и активной с клавиатуры — тот же акцент подсветки, что сейчас.
- Сортировка выдачи: скоринг запроса ↓, затем популярность ↓, затем название.
## 5. Этапы и проверки
- **A. Реестр** — поля `popularity` и `icon`, словарь иконок на все 51, расширение тестов целостности. Проверка: `pnpm test/check`.
- **B. Карточки поиска** — сетка выдачи с иконками и учётом популярности, клавиатура без изменений. Проверка: визуально, `check/build`.
- **C. Распространение** — иконки в каталоге `/list-tools` и легендах звеньев. Проверка: визуально, `build`.
- **A. Реестр** — поля `popularity` и `icon`, словарь иконок на все 51,
расширение тестов целостности. Проверка: `pnpm test/check`.
- **B. Карточки поиска** — сетка выдачи с иконками и учётом популярности,
клавиатура без изменений. Проверка: визуально, `check/build`.
- **C. Распространение** — иконки в каталоге `/list-tools` и легендах звеньев.
Проверка: визуально, `build`.
- **D. Итог** — полный прогон и смоук §8.
## 6. Смоук-чеклист
1. Пустой запрос на герое показывает популярные первыми после первого символа.
2. Запрос «обрез» поднимает карточку обрезки выше прочих совпадений.
3. У каждой карточки есть иконка; у большинства — разные, соответствующие смыслу.
3. У каждой карточки есть иконка; у большинства — разные, соответствующие
смыслу.
4. Наведение и стрелки подсвечивают карточку; Enter открывает инструмент.
5. Открытый инструмент работает как раньше: параметры в колонках, результат, скачивание.
5. Открытый инструмент работает как раньше: параметры в колонках, результат,
скачивание.
6. Цепочка: легенда каждого шага показывает иконку выбранного инструмента.
7. `/list-tools`: карточки категорий с иконками, ссылки работают.
8. Регресс: старые сценарии (загрузка файла, цепочка, скачивание) не изменились.
@@ -54,7 +76,8 @@
## 8. Что сознательно не делаем
- Автоматический расчёт популярности из статистики использования — значения ручные.
- Автоматический расчёт популярности из статистики использования — значения
ручные.
- Собственный набор SVG-иконок — берём готовый открытый.
- Переводы названий и поиск на других языках — отдельная i18n-волна.
- Изображения-превью в карточках поиска — только иконка и текст.
+76 -22
View File
@@ -1,52 +1,106 @@
# План: i18n — русский и английский
> **СТАТУС: ВЫПОЛНЕН 25.08.2026.** Базовая локаль — английский (реестр), русский — перевод в словаре; строки инструментов в реестре переведены на английский в рамках этапа C.
> **СТАТУС: ВЫПОЛНЕН 25.08.2026.** Базовая локаль — английский (реестр), русский
> — перевод в словаре; строки инструментов в реестре переведены на английский в
> рамках этапа C.
## 1. Что получается
Сайт работает на двух языках: русский (базовый) и английский. Переключатель в шапке, выбор сохраняется в localStorage, тег `html lang` обновляется на клиенте. Переведено всё: каркас интерфейса, карточки инструментов, цепочки, поиск, тексты ошибок. Поиск находит инструменты по запросу на любом из двух языков независимо от активной локали.
Сайт работает на двух языках: русский (базовый) и английский. Переключатель в
шапке, выбор сохраняется в localStorage, тег `html lang` обновляется на клиенте.
Переведено всё: каркас интерфейса, карточки инструментов, цепочки, поиск, тексты
ошибок. Поиск находит инструменты по запросу на любом из двух языков независимо
от активной локали.
Каталог и поведение не меняются: ни новых инструментов, ни изменений логики существующих.
Каталог и поведение не меняются: ни новых инструментов, ни изменений логики
существующих.
## 2. Решения
- Без сегментов языка в URL. Статика пререндерится на русском; переключение на клиенте, выбор живёт в localStorage. hreflang/canonical и вторая копия страниц для поисковиков — не в этой волне.
- Ядро не знает о локалях. Ошибки в core выбрасываются стабильными кодами (например ERR_BAD_HEX); перевод кода в человекочитаемый текст происходит в слое исполнителя/UI.
- Английский — базовая локаль и фолбэк: строки инструментов в реестре записаны по-английски. Переводы живут в секции tools словарей по ключу id инструмента (title, description, подписи параметров, подписи опций select, тексты-результаты анализаторов); сейчас перевод — русский в ru.ts. Доступ из компонентов только через хелперы toolTitle/toolDescription/paramLabel/optionLabel. Так каждая строка имеет единственный источник: en — реестр, переводы — словари.
- Без сегментов языка в URL. Статика пререндерится на русском; переключение на
клиенте, выбор живёт в localStorage. hreflang/canonical и вторая копия страниц
для поисковиков — не в этой волне.
- Ядро не знает о локалях. Ошибки в core выбрасываются стабильными кодами
(например ERR_BAD_HEX); перевод кода в человекочитаемый текст происходит в
слое исполнителя/UI.
- Английский — базовая локаль и фолбэк: строки инструментов в реестре записаны
по-английски. Переводы живут в секции tools словарей по ключу id инструмента
(title, description, подписи параметров, подписи опций select,
тексты-результаты анализаторов); сейчас перевод — русский в ru.ts. Доступ из
компонентов только через хелперы
toolTitle/toolDescription/paramLabel/optionLabel. Так каждая строка имеет
единственный источник: en — реестр, переводы — словари.
## 3. Структура i18n-модуля
- web/src/lib/i18n/locale.svelte.ts — состояние активной локали (runes), персист в localStorage (ключ locale, guard на SSR), обновление document.documentElement.lang.
- web/src/lib/i18n/t.ts — функция перевода с фолбэком на базовый язык и интерполяцией переменных вида {n}.
- web/src/lib/i18n/ru.ts и en.ts — словари одного типа Dict; тип экспортируется из ru.ts, en.ts обязан ему соответствовать — пропущенные ключи ловятся на компиляции.
- Секции словаря: header, home, catalog, toolPage, chain, sourceCard, resultCard, paramsCard, textInput, textResult, download, infoPanel, dropZone, search, ui (aria кнопок слайдера и пипетки), categories, errors, tools.
- web/src/lib/i18n/locale.svelte.ts — состояние активной локали (runes), персист
в localStorage (ключ locale, guard на SSR), обновление
document.documentElement.lang.
- web/src/lib/i18n/t.ts — функция перевода с фолбэком на базовый язык и
интерполяцией переменных вида {n}.
- web/src/lib/i18n/ru.ts и en.ts — словари одного типа Dict; тип экспортируется
из ru.ts, en.ts обязан ему соответствовать — пропущенные ключи ловятся на
компиляции.
- Секции словаря: header, home, catalog, toolPage, chain, sourceCard,
resultCard, paramsCard, textInput, textResult, download, infoPanel, dropZone,
search, ui (aria кнопок слайдера и пипетки), categories, errors, tools.
## 4. Этапы и проверки
Каждый этап заканчивается зелёными test/check/build и ревью.
- A. Инфраструктура — модуль состояния, функция перевода с фолбэком и интерполяцией, словари-скелеты со всеми секциями (заполнены header и categories как пилот), переключатель RU/EN в шапке, реактивный lang. Тесты: фолбэк на ru при отсутствии ключа в en, интерполяция, персист локали.
- B. Каркас интерфейса — перенос всех хардкод-строк компонентов и маршрутов в словари: layout (шапка, футер), главная (герой, «вернуть последний», title), каталог, карточки источника/результата/параметров, текстовый вход и результат, «Показать маску», DropZone/DropOverlay, кнопка скачивания (busyText), InfoPanel плюс форматирование чисел через тег локали вместо жёсткого ru-RU, ToolPage (легенды, «Шаг n», ошибки шага), ChainToolBlock, placeholder и пустой результат поиска, страницы инструмента (title, 404). Обёрточные сообщения исполнителя («Ошибка исполнения в воркере») — тоже здесь.
- C. Контент реестра — переводы 69 инструментов (title/description/labels/options) в tools-секции словаря en; компоненты получают строки через хелперы toolTitle/toolDescription/paramLabel/optionLabel с фолбэком на реестр. Тест целостности: у каждого инструмента есть непустые перевод title/description, полный набор param-ключей и подписи всех опций select; лишние ключи словарей отсутствуют.
- D. Поиск по любому языку — нормализация запроса и полей: нижний регистр, ё→е, снятие диакритики через NFD; скоринг против активной и базовой локали сразу, лучший балл побеждает; сортировка совпадений через Intl.Collator активной локали. Тесты нормализации: ё/е, диакритика, английский запрос при русской локали и наоборот.
- E. Ошибки на кодах — около 40 мест throw в core, registry, pipeline заменяются на коды; исполнитель переводит код в текст активной локали, неизвестные сообщения показываются как есть. Пять тестов, матчащих русские подстроки ошибок (executor, pipeline, registry, io, alpha), переходят на коды — это упрощает ассерты. Русские describe/it в тестах не трогаем.
- A. Инфраструктура — модуль состояния, функция перевода с фолбэком и
интерполяцией, словари-скелеты со всеми секциями (заполнены header и
categories как пилот), переключатель RU/EN в шапке, реактивный lang. Тесты:
фолбэк на ru при отсутствии ключа в en, интерполяция, персист локали.
- B. Каркас интерфейса — перенос всех хардкод-строк компонентов и маршрутов в
словари: layout (шапка, футер), главная (герой, «вернуть последний», title),
каталог, карточки источника/результата/параметров, текстовый вход и результат,
«Показать маску», DropZone/DropOverlay, кнопка скачивания (busyText),
InfoPanel плюс форматирование чисел через тег локали вместо жёсткого ru-RU,
ToolPage (легенды, «Шаг n», ошибки шага), ChainToolBlock, placeholder и пустой
результат поиска, страницы инструмента (title, 404). Обёрточные сообщения
исполнителя («Ошибка исполнения в воркере») — тоже здесь.
- C. Контент реестра — переводы 69 инструментов
(title/description/labels/options) в tools-секции словаря en; компоненты
получают строки через хелперы toolTitle/toolDescription/paramLabel/optionLabel
с фолбэком на реестр. Тест целостности: у каждого инструмента есть непустые
перевод title/description, полный набор param-ключей и подписи всех опций
select; лишние ключи словарей отсутствуют.
- D. Поиск по любому языку — нормализация запроса и полей: нижний регистр, ё→е,
снятие диакритики через NFD; скоринг против активной и базовой локали сразу,
лучший балл побеждает; сортировка совпадений через Intl.Collator активной
локали. Тесты нормализации: ё/е, диакритика, английский запрос при русской
локали и наоборот.
- E. Ошибки на кодах — около 40 мест throw в core, registry, pipeline заменяются
на коды; исполнитель переводит код в текст активной локали, неизвестные
сообщения показываются как есть. Пять тестов, матчащих русские подстроки
ошибок (executor, pipeline, registry, io, alpha), переходят на коды — это
упрощает ассерты. Русские describe/it в тестах не трогаем.
- F. Итог — полный прогон, смоук чеклист §6, архивация плана.
## 5. Смоук-чеклист
1. Переключение RU/EN в шапке мгновенно меняет язык без перезагрузки; после перезагрузки выбор сохранён.
1. Переключение RU/EN в шапке мгновенно меняет язык без перезагрузки; после
перезагрузки выбор сохранён.
2. Тег html lang соответствует активной локали.
3. Главная, каталог, страница инструмента, цепочка из трёх звеньев — везде переведено, без смеси языков.
4. Форма параметров: подписи полей и опции select переведены у произвольных пяти инструментов из разных категорий.
5. Битый вход (некорректный HEX в тонировании, нулевой размер) показывает ошибку на активном языке.
6. Поиск: «повер» находит повороты при русской локали; «rotate» находит их же; при английской локали наоборот. Диакритика и ё не мешают совпадению.
3. Главная, каталог, страница инструмента, цепочка из трёх звеньев — везде
переведено, без смеси языков.
4. Форма параметров: подписи полей и опции select переведены у произвольных пяти
инструментов из разных категорий.
5. Битый вход (некорректный HEX в тонировании, нулевой размер) показывает ошибку
на активном языке.
6. Поиск: «повер» находит повороты при русской локали; «rotate» находит их же;
при английской локали наоборот. Диакритика и ё не мешают совпадению.
7. Числа в панели информации отформатированы по локали.
8. Скачивание, пипетка, маски, слайдеры работают одинаково на обеих локалях — функциональность не задета.
8. Скачивание, пипетка, маски, слайдеры работают одинаково на обеих локалях —
функциональность не задета.
## 6. Критерии готовности
- Оба словаря типобезопасны и полны; тест целостности секции tools зелёный.
- Ни одного пользовательского русского строки вне словарей (кроме тестовых describe/it).
- Ни одного пользовательского русского строки вне словарей (кроме тестовых
describe/it).
- test/check/build зелёные, смоук пройден.
## 7. Что сознательно не делаем
+83
View File
@@ -0,0 +1,83 @@
# План: параметры между исходником и результатом
> **СТАТУС: УСТАРЕЛ (2026-09-07).** Раскладка описывалась для старого
> `ToolPage`; перекрыта редизайном (`plan-redesign.md`) — страницы инструментов
> теперь `SchemaToolView`/`SchemaPreview` в новом дизайне. Сама идея «инлайн:
> исходник → параметры → результат» живёт в `backlog.md` (№2).
> Статус: план к выполнению.
## 1. Что получается
Альтернативная раскладка этапа инструмента: три колонки «Исходник | Параметры |
Результат» на широких экранах, вертикальный стек на узких. Цель — отдать вариант
дизайнеру как отправную точку. Откат к текущей раскладке — замена одной строки в
`ToolPage`.
## 2. Решения
- Два взаимозаменяемых компонента этапа с одинаковым набором пропсов; `ToolPage`
рендерит один тег. Попробовать новое — поменять компонент в этом теге; вернуть
старое — поменять обратно. Никаких флагов и конфигов.
- Общая типизация пропсов в одном файле рядом с компонентами; сами карточки
(`SourceCard`, `ResultCard`, `TextInputCard`, `ParamsCard`) переиспользуются
как есть — вся логика исполнения, маски, пипетки и автозапуска остаётся в
`ToolPage` и приходит сверху.
- Chain-звенья используют ту же панельную систему «Вход | Параметры | Результат»
— общие классы `.pane` / `.pane-legend` / `.pane-params` вынесены в app.css,
вертикальные поля в узкой колонке заданы один раз глобально.
- Один порог отзывчивости: три колонки от ~75rem, ниже — стек «исходник →
параметры → результат». Промежуточные перестроения не придумываем до фидбека
дизайнера.
## 3. Компоненты
```txt
src/lib/components/stage/
stage-props.ts # интерфейс StageProps + сборщик дефолтов
ToolStageClassic.svelte # текущая разметка .tool-stage, перенесена 1:1 из ToolPage
ToolStageInline.svelte # новая сетка [source | params | result]
```
- `ToolStageClassic` — вырезка существующего шаблона со стилями; после
извлечения страница должна быть пиксель-в-пиксель прежней.
- `ToolStageInline` — grid-колонки
`minmax(0, 1fr) | фикс-ширина параметров | minmax(0, 1fr)`; легенда
«Параметры» над средней колонкой в том же стиле edge-legend.
- Модификаторы состояний через классы на корне компонента: `no-params`
(инструменты без параметров → две колонки), `no-source` (генераторы →
параметры и результат), текстовый вход до декодирования занимает левую ячейку
как сейчас.
## 4. Этапы и проверки
Каждый этап заканчивается зелёными test/check/build и ревью.
- **A. Извлечение классики** — перенос разметки и стилей этапа из `ToolPage` в
`ToolStageClassic` без изменений поведения; пропсы собираются в одном объекте.
Критерий: страница неотличима, diff по логике `ToolPage` нулевой.
- **B. Инлайн-раскладка** — `ToolStageInline`, переключение в `ToolPage` на
новый компонент, прогон смоука §5. Ревью в браузере; решение дизайнера —
потом, код уже позволяет жить в обоих вариантах.
## 5. Смоук
1. Замена компонента этапа — одна строка; обе версии собираются без
предупреждений.
2. Инструмент без параметров (например Автоконтраст): две колонки, пустой
середины нет.
3. Генератор без исходника: параметры и результат в две колонки.
4. Текстовый инструмент до декодирования: вход слева, параметры в центре, пустое
состояние результата справа.
5. Маска, пипетка, слайдеры в средней колонке; изменение параметра перезапускает
инструмент как раньше.
6. Узкий экран: стек без горизонтального скролла, порядок исходник → параметры →
результат.
7. Легенды присутствуют у всех трёх зон в обеих раскладках.
## 6. Что сознательно не делаем
- Редизайн визуального языка (цвета, типографика, формы) — материал для
дизайнера, не для этого плана.
- Новая логика: исполнение, автосохранение, пресеты — раскладка ничего не знает
про исполнение.
+103
View File
@@ -0,0 +1,103 @@
# Plan: `/preview/list-tools` → design2 (catalog)
> **СТАТУС: ВЫПОЛНЕН (2026-09-07) — устарел.** Каталог `/preview/list-tools`
> переписан на `CatalogHeader` / `CatalogToolbar` / `CatalogGroup` / `ToolCard`
> (kit), поиск и фильтры функциональны. Доводка верности против рефа остаётся в
> рамках `plan-design-fix.md`.
Ветка `feat/redesign`. Цель — привести каталог инструментов к рефу
`refs-html/list-tools.html`. Сейчас страница вообще не на design2: по
мульти-аудиту `onlyOurs=257, onlyRef=54` (демо-страница — `18/11`).
## Текущее состояние
- `src/routes/preview/list-tools/+page.svelte` использует собственную вёрстку
(`.catalog` / `.cat-head` / `.cat-grid`) + `Panel` / `PanelHeading` /
`ToolCard` (kit). Нет поиска, фильтров, счётчика, навигация по категориям
сделана через `Panel`-блоки, а не через `.catalog-groups`.
- Реф (`list-tools.html`) строит:
- `.catalog-page``.catalog-head` (`.eyebrow`, `h1` «Tool catalog», `p`
lede, `.catalog-total` «NN TOOLS AVAILABLE» с большим числом),
- `.catalog-toolbar` (`.catalog-search` + `.catalog-filters` с кнопками),
- `.catalog-groups` (grid `repeat(2,minmax(0,1fr))`, gap `56px 28px`) →
`.catalog-group` (`.group-title` «CAT · NN TOOLS») → `.tool-cards`
`.tool-card`.
- `.tool-card` (реф) — `display:grid`, колонки `42px minmax(0,1fr) 24px 18px`,
`min-height:106px`, `gap:14px`, `padding:16px`: `.tool-icon` (42×42 синий
размытый квадрат), `.tool-copy` (`strong` 13px mono + `span` 12px mono muted),
`.tool-index` (10px mono, сверху), `.tool-arrow` (появляется при hover).
- Токены `design2.css` уже совпадают с рефом (`--background #eef1f4`,
`--panel #f8fafb`, `--foreground #17212b`, `--muted #6d7883`,
`--line #cbd3da`, `--blue #1769d2`, `--radius 4px`) — править токены не нужно.
## Шаги
1. **Restyle `kit/ToolCard.svelte`** под реф (меняется «на месте» — используется
ещё в `preview/+page` и `preview/kit`, стиль унифицируется, ок):
- grid-колонки `42px minmax(0,1fr) 24px 18px`, `align-items:center`,
`gap:14px`, `min-height:106px`, `padding:16px`, `border:1px solid line`,
`background:panel`, `border-radius:radius`.
- `.tool-icon`: 42×42 `place-items:center`,
`background:color-mix(in srgb, var(--blue) 12%, transparent)`,
`color:var(--blue)` (реф использует `--panel-strong`; вместо нового токена
берём этот `color-mix`).
- `.tool-copy strong`: `600 13px var(--font-mono)`; `span`: `12px/1.5 mono`
`muted`.
- `.tool-index`: `10px mono muted`, `align-self:start`.
- `.tool-arrow`: `color:var(--blue)`, `opacity:0``1` +
`translate(2px,-2px)` на `.tool-card:hover`.
- DOM-порядок как в рефе: `icon, copy, index, arrow`.
- `href` через `resolve` из `$app/paths` (убрать `svelte-ignore`).
2. **`kit/CatalogHeader.svelte`**: `.eyebrow` «EASY-PNG-TOOLS / CATALOG», `h1`
«Tool catalog», lede; `.catalog-total` с динамическим `TOOLS.length` (реф
показывает `32`).
3. **`kit/CatalogToolbar.svelte`**: `.catalog-search` (иконка + `<input>`) и
`.catalog-filters` (кнопки по категориям). Состояние `query` / `category`
поднимается наружу через `bind:` (или callback-пропы), чтобы страница
фильтровала список. Фильтры функциональные (клиентский фильтр по названию +
категории), не декоративные.
4. **`kit/CatalogGroup.svelte`**: `.group-title` (uppercase label категории +
«NN TOOLS») + слот со списком карточек; сетка `.tool-cards``gap:10px`
(реф), на мобиле (`@media max-width:800px`) всё схлопывается в 1 колонку.
5. **Переписать `preview/list-tools/+page.svelte`** как тонкую композицию:
- обёртка `.catalog-page` (`padding:60px clamp(24px,4vw,72px) 72px`);
- `<CatalogHeader />`;
- `<CatalogToolbar bind:query bind:category />`;
- `.catalog-groups` из отфильтрованных групп (по `CATEGORIES`, пересчитав
`tools` с учётом `query`/`category`);
- внутри `<CatalogGroup>``<ToolCard>` на каждый tool
(`href= "/preview/tools/{id}"`, `icon` из `tool-icons`, `index` по порядку,
`title`/`description` из registry);
- убрать `Panel`/`PanelHeading` из этого маршрута.
6. **Категории/локализация**: labels привести к рефу (реф: ALL / CONVERT /
TRANSPARENCY / COLOR / GEOMETRY / FILTERS / ANALYZE). Наши `CATEGORIES`
(`convert/alpha/color/geometry/filters/text/analyze/generate`) — сопоставить
(`alpha`→TRANSPARENCY и т.п.); точное сопоставление уточнить по рефу и
`i18n/ru.ts`. Фильтры = ALL + те же label'ы. Тексты заголовков/lede — по рефу
(англ. «Tool catalog» / «Focused utilities for working with PNG…»), либо
оставить RU-вариант, если это сознательное отклонение (зафиксировать).
7. **Адаптив**: перенести правила рефа (`@media max-width:800px`) —
`.catalog-groups` → 1fr, `.tool-card``38px minmax(0,1fr) 18px`, скрыть
`.tool-index`/показать `.tool-arrow`, `.catalog-head` в колонку и т.д.
## Верификация
- `pnpm lint`, `pnpm exec svelte-check --tsconfig ./tsconfig.json`, `pnpm build`
— без ошибок.
- `pnpm refs-audit` (теперь мульти-маршрутный) → глянуть секцию
`/preview/list-tools`: `onlyOurs`/`onlyRef` должны упасть с `257/54` примерно
до уровня demo (`18/11`). Остаточный diff (если будет) зафиксировать в этом
файле.
## Примечания
- `ToolCard` правится на месте — влияет на `preview/+page` и `preview/kit` (там
тоже карточки каталога, унификация уместна).
- Старый дизайн (`routes/(old)/**`, `components/search/**`) не трогать.
- Коммиты делает автор (агент не коммитит).
+55 -24
View File
@@ -4,15 +4,25 @@
## 1. Зачем
- Первые операции настоящего алгоритмического уровня: свёртки и заливка связной области.
- Витринная фича каталога — удаление однотонного фона без нейросетей: порог по цвету + заливка от краёв изображения + сглаживание границы маски.
- Эти операции реально блокируют UI-поток на больших картинках — повод включить давно подготовленный путь: исполнитель `executeStep` переезжает в Web Worker, страницы не меняются вовсе.
- Первые операции настоящего алгоритмического уровня: свёртки и заливка связной
области.
- Витринная фича каталога — удаление однотонного фона без нейросетей: порог по
цвету + заливка от краёв изображения + сглаживание границы маски.
- Эти операции реально блокируют UI-поток на больших картинках — повод включить
давно подготовленный путь: исполнитель `executeStep` переезжает в Web Worker,
страницы не меняются вовсе.
## 2. Инструменты — 3
- **Удалить фон PNG** (`remove-background-png`, категория «Прозрачность») — параметры: цвет фона, допуск похожести %, режим «только внешние области» (заливка от краёв вместо глобального совпадения), сглаживание границы (проходы мажоритарного фильтра маски). Даёт превью маски через существующий контракт `preview`.
- **Размытие PNG** (`blur-png`, новая категория «Фильтры») — гауссово приближение тремя проходами разделяемого бокса; радиус слайдером 1–32 px.
- **Резкость PNG** (`sharpen-png`, «Фильтры») — свёртка ядром резкости с регулируемой силой (смесь исходного и обработанного), сила слайдером 0100 %.
- **Удалить фон PNG** (`remove-background-png`, категория «Прозрачность») —
параметры: цвет фона, допуск похожести %, режим «только внешние области»
(заливка от краёв вместо глобального совпадения), сглаживание границы (проходы
мажоритарного фильтра маски). Даёт превью маски через существующий контракт
`preview`.
- **Размытие PNG** (`blur-png`, новая категория «Фильтры») — гауссово
приближение тремя проходами разделяемого бокса; радиус слайдером 1–32 px.
- **Резкость PNG** (`sharpen-png`, «Фильтры») — свёртка ядром резкости с
регулируемой силой (смесь исходного и обработанного), сила слайдером 0–100 %.
## 3. Новое ядро (чистые функции, детерминированные, покрытые тестами)
@@ -21,46 +31,67 @@
- разделяемый бокс-блюр и сборка гауссова приближения из трёх проходов;
- ядро резкости и смешивание с оригиналом по силе.
- `core/background.ts`
- построение маски удаления по цветовому расстоянию (та же метрика, что у удаления цвета);
- построение маски удаления по цветовому расстоянию (та же метрика, что у
удаления цвета);
- заливка связной области от всех граничных пикселей (очередь, без рекурсии);
- мажоритарное сглаживание маски заданным числом проходов;
- сборка результата: альфа 0 у удалённых пикселей; плюс функция маски для превью.
- сборка результата: альфа 0 у удалённых пикселей; плюс функция маски для
превью.
## 4. Web Worker
- Точка исполнения одна — `tools/executor.ts`; меняется только её внутренность.
- Воркер-файл импортирует реестр и исполняет операцию по `toolId`; картинка приходит копией (структурный клон), результат возвращается передаваемым буфером.
- Ленивая инициализация воркера при первом вызове; если воркер недоступен (старый браузер/тестовая среда) — автоматический откат на прямое исполнение в главном потоке. Обе ветви обязаны давать побайтово одинаковый результат.
- Генераторы и текстовые представления остаются в главном потоке — они мгновенные.
- Воркер-файл импортирует реестр и исполняет операцию по `toolId`; картинка
приходит копией (структурный клон), результат возвращается передаваемым
буфером.
- Ленивая инициализация воркера при первом вызове; если воркер недоступен
(старый браузер/тестовая среда) — автоматический откат на прямое исполнение в
главном потоке. Обе ветви обязаны давать побайтово одинаковый результат.
- Генераторы и текстовые представления остаются в главном потоке — они
мгновенные.
- Пипетка и лупа работают с уже готовыми картинками и не зависят от переноса.
## 5. Этапы и проверки
- **A. Ядро** — свёртки и фон с тестами на малых матрицах (точные ожидания руками). Проверка: `pnpm test/check`.
- **B. Реестр** — категория «Фильтры», три записи, превью маски. Проверка: тесты целостности, визуально.
- **C. Web Worker** — перенос исполнителя, отккат-ветвь. Проверка: результаты до/после переключения идентичны байт-в-байт на фиксированной картинке и параметрах; `test/build` зелёные.
- **A. Ядро** — свёртки и фон с тестами на малых матрицах (точные ожидания
руками). Проверка: `pnpm test/check`.
- **B. Реестр** — категория «Фильтры», три записи, превью маски. Проверка: тесты
целостности, визуально.
- **C. Web Worker** — перенос исполнителя, отккат-ветвь. Проверка: результаты
до/после переключения идентичны байт-в-байт на фиксированной картинке и
параметрах; `test/build` зелёные.
- **D. Итог** — полный прогон и смоук §8.
## 6. Смоук-чеклист
1. Размытие: радиус 1 и 32 — плавно усиливается, прозрачные края не темнеют.
2. Резкость: сила 0 не меняет картинку; сила 100 заметно подчёркивает края без цветового мусора.
2. Резкость: сила 0 не меняет картинку; сила 100 заметно подчёркивает края без
цветового мусора.
3. Удаление фона на логотипе с однотонным фоном: фон исчез, содержимое цело.
4. Режим «только внешние области»: дырка внутри объекта того же цвета остаётся непрозрачной.
5. Сглаживание 0/2/4 прохода — граница от жёсткой к мягкой, результат воспроизводим.
6. Превью маски показывает белым удаляемую область; скачивание всегда отдаёт результат.
7. Во время размытия большой картинки интерфейс остаётся отзывчивым (чип «Пересчёт…», скролл живой).
8. Цепочка: размытие → резкость → удаление фона работает; каждый блок скачивает свой промежуток.
4. Режим «только внешние области»: дырка внутри объекта того же цвета остаётся
непрозрачной.
5. Сглаживание 0/2/4 прохода — граница от жёсткой к мягкой, результат
воспроизводим.
6. Превью маски показывает белым удаляемую область; скачивание всегда отдаёт
результат.
7. Во время размытия большой картинки интерфейс остаётся отзывчивым (чип
«Пересчёт…», скролл живой).
8. Цепочка: размытие → резкость → удаление фона работает; каждый блок скачивает
свой промежуток.
9. Регресс старых инструментов и главной-поиска без изменений поведения.
## 7. Критерии готовности
- Три новых инструмента проходят смоук §6; операции ядра покрыты точными юнит-тестами.
- Исполнение изображений идёт через воркер там, где он доступен, с гарантированно одинаковым результатом обеих ветвей.
- Три новых инструмента проходят смоук §6; операции ядра покрыты точными
юнит-тестами.
- Исполнение изображений идёт через воркер там, где он доступен, с
гарантированно одинаковым результатом обеих ветвей.
- `test/check/build` зелёные.
## 8. Что сознательно не делаем
- Многопроходное гауссово ядро «по-настоящему» (O(r²)) — трёхпроходного боксового приближения достаточно визуально и оно быстрее.
- Многопроходное гауссово ядро «по-настоящему» (O(r²)) — трёхпроходного
боксового приближения достаточно визуально и оно быстрее.
- Хромакей по нескольким цветам, ИИ-сегментация — следующие волны.
- Отмена выполняющегося прогона воркера — устаревшие результаты отбрасываются токеном, сама работа завершается фоном.
- Отмена выполняющегося прогона воркера — устаревшие результаты отбрасываются
токеном, сама работа завершается фоном.
+43 -19
View File
@@ -4,17 +4,26 @@
## 1. Зачем
Одна пара функций — расширение и сужение непрозрачных областей — открывает целое семейство пользовательских операций: обводка логотипов и иконок, контуры, утолщение и утончение подписей, удаление мелкого мусора и закрытие дырок. Всё это частые запросы в наборах PNG-утилит, а ядро получается компактным и легко покрывается тестами.
Одна пара функций — расширение и сужение непрозрачных областей — открывает целое
семейство пользовательских операций: обводка логотипов и иконок, контуры,
утолщение и утончение подписей, удаление мелкого мусора и закрытие дырок. Всё
это частые запросы в наборах PNG-утилит, а ядро получается компактным и легко
покрывается тестами.
## 2. Инструменты — 7 (все в категории «Прозрачность»)
- **Обвести PNG** (`add-stroke-png`) — кольцо выбранного цвета вокруг непрозрачного содержимого; параметры: цвет, толщина 1–10 px.
- **Найти контур PNG** (`find-contour-png`) — линия по краю непрозрачных областей; параметр: толщина линии.
- **Обвести PNG** (`add-stroke-png`) — кольцо выбранного цвета вокруг
непрозрачного содержимого; параметры: цвет, толщина 110 px.
- **Найти контур PNG** (`find-contour-png`) — линия по краю непрозрачных
областей; параметр: толщина линии.
- **Утолщить PNG** (`make-thicker-png`) — расширение содержимого на N пикселей.
- **Утончить PNG** (`make-thinner-png`) — сужение содержимого на N пикселей.
- **Жёсткие края PNG** (`harden-alpha-png`) — бинаризация альфы по порогу: полупрозрачность уходит, остаётся два состояния.
- **Убрать мусор PNG** (`despeckle-alpha-png`) — открытие: удаляет одиночные полупрозрачные пиксели и мелкие крапинки.
- **Закрыть дыры PNG** (`close-holes-png`) — закрытие: заполняет одиночные прозрачные точки внутри объекта.
- **Жёсткие края PNG** (`harden-alpha-png`) — бинаризация альфы по порогу:
полупрозрачность уходит, остаётся два состояния.
- **Убрать мусор PNG** (`despeckle-alpha-png`) — открытие: удаляет одиночные
полупрозрачные пиксели и мелкие крапинки.
- **Закрыть дыры PNG** (`close-holes-png`) — закрытие: заполняет одиночные
прозрачные точки внутри объекта.
Все операции применяются к альфа-каналу; RGB пикселей сохраняется.
@@ -23,49 +32,64 @@
Чистые функции над бинарной маской и над альфа-каналом:
- `buildAlphaMask(img)` — 1 там, где альфа > 0.
- `dilateMask(mask, w, h, radius, shape)` — расширение; `shape`: квадрат или диск (для обводок диск даёт ровную круглую линию).
- `dilateMask(mask, w, h, radius, shape)` — расширение; `shape`: квадрат или
диск (для обводок диск даёт ровную круглую линию).
- `erodeMask(...)` — зеркальная операция.
- `dilateImage / erodeImage(img, radiusPx)` — применяют маскирование к альфе изображения, RGB сохраняется.
- `dilateImage / erodeImage(img, radiusPx)` — применяют маскирование к альфе
изображения, RGB сохраняется.
- Сборщики:
- обводка: `dilate(mask) mask` → новое кольцо заливается цветом;
- контур: `mask erode(mask)` → линия;
- открытие: `erode → dilate`; закрытие: `dilate → erode`.
- `hardenAlpha(img, thresholdPercent)` — отдельно от морфологии, но той же волной.
- `hardenAlpha(img, thresholdPercent)` — отдельно от морфологии, но той же
волной.
Структурирующий элемент при радиусе r — диск радиуса r (расстояние до центра ≤ r); квадрат не используем, чтобы углы обводок не были угловатыми.
Структурирующий элемент при радиусе r — диск радиуса r (расстояние до центра ≤
r); квадрат не используем, чтобы углы обводок не были угловатыми.
## 4. Web Worker
Ничего не требуется: новые операции попадают в реестр, исполнитель автоматически исполняет их в воркере. Тесты ядра идут мимо воркера (чистые функции).
Ничего не требуется: новые операции попадают в реестр, исполнитель автоматически
исполняет их в воркере. Тесты ядра идут мимо воркера (чистые функции).
## 5. Этапы и проверки
- **A. Ядро** — `morphology.ts` + точные тесты на матрицах 5×5: расширение диска, сужение, открытие убирает одиночный пиксель, закрытие закрывает одиночную дыру, кольцо обводки нужного цвета и толщины. Проверка: `pnpm test/check`.
- **B. Реестр** — семь записей, тест целостности обновляется (51 → 58). Проверка: `test/build`, визуально.
- **A. Ядро** — `morphology.ts` + точные тесты на матрицах 5×5: расширение
диска, сужение, открытие убирает одиночный пиксель, закрытие закрывает
одиночную дыру, кольцо обводки нужного цвета и толщины. Проверка:
`pnpm test/check`.
- **B. Реестр** — семь записей, тест целостности обновляется (51 → 58).
Проверка: `test/build`, визуально.
- **C. Итог** — полный прогон, смоук §7.
## 6. Критерии готовности
- Семь новых инструментов работают на прозрачных PNG: обводка, контур, толще/тоньше, жёсткие края, убрать мусор, закрыть дыры.
- Семь новых инструментов работают на прозрачных PNG: обводка, контур,
толще/тоньше, жёсткие края, убрать мусор, закрыть дыры.
- Все операции детерминированы и покрыты юнит-тестами с ручными ожиданиями.
- Исполнение автоматически идёт в воркере.
- `test/check/build` зелёные, смоук §7 пройден.
## 7. Смоук-чеклист
1. Обводка толщиной 3 вокруг круглого стикера — ровное кольцо выбранного цвета, без угловатости.
2. Толщина 1 — тонкая аккуратная линия; 10 — толстая, форма содержимого не искажается сверх роста.
1. Обводка толщиной 3 вокруг круглого стикера — ровное кольцо выбранного цвета,
без угловатости.
2. Толщина 1 — тонкая аккуратная линия; 10 — толстая, форма содержимого не
искажается сверх роста.
3. Контур показывает только край содержимого линией.
4. Толще/тоньше на тонкой надписи — штрихи растут/сжимаются без разрывов при умеренных значениях.
4. Толще/тоньше на тонкой надписи — штрихи растут/сжимаются без разрывов при
умеренных значениях.
5. Жёсткие края: полупрозрачность исчезает, силуэт сохраняется.
6. Убрать мусор убирает одиночные точки-крапинки, не трогая крупные области.
7. Закрыть дыры заполняет одиночные прозрачные точки внутри логотипа.
8. Цепочка: обводка после удаления фона работает; каждое звено скачивает свой промежуток.
8. Цепочка: обводка после удаления фона работает; каждое звено скачивает свой
промежуток.
9. Регресс остальных инструментов без изменений.
## 8. Что сознательно не делаем
- Морфология по яркости для непрозрачных сканов — волна фокусируется на альфе прозрачных PNG.
- Морфология по яркости для непрозрачных сканов — волна фокусируется на альфе
прозрачных PNG.
- Выбор структурирующего элемента пользователем — всегда диск.
- Feather/свечение краёв — отдельная будущая задача поверх размытия.
- Произвольные SVG-маски и градиентные обводки.
+181
View File
@@ -0,0 +1,181 @@
# План: дожать перевод инструментов в preview и переезд ветки `old/`
> **СТАТУС: ЗАКРЫТ (2026-09-07).** Перевод каталога завершён — 121 из 125
> инструментов в `registry-new`; 4 хвоста отложены решениями и перенесены в
> `backlog.md` (`watermark-image-png` → overlay-райз, `png-info` → exif-райз,
> `reduce-to-size-png` → download-райз, `compress-png` → закрыт дубликатом).
> Дальнейшие «хвосты плана» — переезд старых компонентов в папки `old/`,
> закрытие долгов и сбор фидбека — ~уже учитываются в `plan-composite-params.md`
> (шаги 3637) и `plan-design-fix.md`, поэтому план закрыт, а не продолжен.
> Архивная копия, ниже — состояние на момент закрытия.
> Статус: **переведено 121 из 125; остальные 4 — отложены/закрыты решениями.**
> Текущее состояние: **121 инструмент переведён в `registry-new`**, старый UI
> уже живёт на `(old)/`-маршрутах, линтер-изоляция old↔preview на месте (плагин
> `isolation/no-mixed-imports`). Здесь — что сделать, чтобы:
>
> 1. в preview был **весь** каталог инструментов (плюс закрыты известные долги);
> 2. физически перенести старую ветку в папки `old/` (шаг 37 основного плана);
> 3. после переезда провести детальную проверку в реальных условиях и собрать
> отзывы пользователей (правки поверх — отдельными задачами).
>
> Порядок: перевод остатка → починить известные долги → переезд в `old/` →
> пользовательская проверка/фидбек → (позже, отдельным решением) удаление
> старого кода.
## Текущая картина (факты)
- `registry.ts` + `registry/`**125 инструментов**; `registry-new/`**121**.
Не переведены только: `png-info` (отложен, хвост-фича с exif), `compress-png`
и `reduce-to-size-png` (закрыто решениями, см. ниже), `watermark-image-png`
(overlay — единственный реальный остаток).
- Preview (`/preview`) показывает переведённые; старый UI работает на
`/`-маршрутах (группа `(old)/`): `+page`, `demo`, `list-tools`, `tools/[id]`.
Переведено всё кроме четырёх хвостов: color 18, filters 3, geometry 13, alpha
13 (+watermark-image в остатке), generate 10 (mix/sort), analyze 12 (6 масок +
verify + 5 вердиктов), convert 14 (jpg/webp/bmp + 5 png→texт + 6 text→png),
text `watermark-tile-png`.
- Плагин `isolation/no-mixed-imports` (`web/eslint-plugins/isolation/`)
полностью разделяет ветки: старый `lib/registry.ts|/registry/**`,
`lib/registry-helpers.ts`, `lib/categories.ts`, `lib/tools/**`,
`lib/components/**`(кроме `kit/`) vs новый `routes/preview/**`,
`lib/registry-new/**`, `lib/preview/**`, `lib/registry-schema.ts`,
`lib/components/kit/**`. Общее: `core/`, `i18n/`, `theme`, `assets/`, корневой
`lib`.
- Правило копий (из основного плана): файл, используемый `(old)`, не трогаем —
делаем копию в `lib/preview/` (пример: `lib/preview/tool-icons.ts`).
- Известные долги (предсуществующие, не создавались нами): ошибка
`ToolCard.svelte:19:36` (svelte-check), фейл `palette.test.ts` (ENOENT
`src/app.css` — из-за него `pnpm test` красный при 607 passed), ~335
design-tokens ошибок `lint:all` в старых kit-компонентах, a11y-warning в
`ColorField.svelte`, пустой ruleset в `Toggle.svelte`, неиспользуемые токены в
`preview.css`.
## Скоуп перевода: переведено всё переводимое (отложено 4 хвоста)
Каталог дожат до предела текущих решений: переведено 121 из 125. Оставшиеся 4
инструмента **отложены осознанно** (не делаем сейчас):
- **`watermark-image-png` (alpha)** — **отложен**. Нужна overlay-механика
(второе изображение-знак через `getOverlay`/store + параметры
scale/opacity/position/margin/aspect); решается отдельно, вместе с вопросом,
как второй источник изображения вписывается в schema-driven preview.
- **`png-info`** — **отложен осознанно**: будет серьёзно дорабатываться
отдельным райзом (exif-теги, редактирование, структурированный вывод), для
него нужен свой отдельный случай в UI, не «ещё один text/verdict».
- **`compress-png`** — **закрыт как дубликат** `decrease-color-count-png`:
пресеты сжатия 192/96/44 добавлены прямо в схему `decrease-color-count`.
- **`reduce-to-size-png`** — **отложен** до достройки кнопки Download (backlog
№7, «экспорт с лимитом размера»).
### Переведено в этом проходе (text-механика)
- **Типы:** `ToolEntry.input: "file" | "text" | "none"`,
`ToolEntry.result: "image" | "text" | "verdict"`, `OutputFormat` (mime/ext/
qualityParamId) для download.
- **Executor:** `executeFromText` / `executeToText` / `executeTextToText`
(прямые, без worker), экспорт в `preview/executor/index.ts`.
- **UI:** `SchemaTextSource` (textarea + Render), `SchemaTextResult` (кол-блок с
Copy/Download .txt или бейдж-вердикт по префиксу Yes/No), интеграция в
`SchemaPreview`/`SchemaToolView` (режимы file/text/generate, рендер
image/text/verdict, скрытие image-download для text-результата).
- **Инструменты:** convert 5×png→text (base64/data-uri/hex/bytes/rgb-values),
6×text→png (base64/data-uri/hex/bytes/rgb-values/svg, +width), analyze
verify-is-png (text→text) и 5 вердиктов (grayscale/file-size/transparent/
orientation). Доработка: `base64ToBytes` сужен до `Uint8Array<ArrayBuffer>`
(совместимость с `decodeBytes`).
- **Гейты:** eslint 0, prettier чист, svelte-check — только предсуществующий
ToolCard, тесты — только предсуществующий фейл `palette.test.ts`.
## Известные долги перед переездом (завести tasks и закрыть)
По AGENTS.md техдолг «чинится только по заведённым tasks, не игнорируется»:
`lint:all` (~335 design-tokens в kit), неиспользуемые токены preview.css, a11y
`ColorField`, пустой ruleset `Toggle`. Плюс два, мешающих зелёной проверке
до/после переезда: svelte-check error `ToolCard.svelte:19:36` и фейл
`palette.test.ts` (ENOENT `src/app.css`). После их закрытия целевое состояние
проверок перед переездом: `pnpm --dir web exec svelte-check` → 0 errors,
`pnpm --dir web test` → зелёный, `pnpm --dir web lint:all` → только задокумент.
ированный остаток (или 0).
## Шаги перевода остатка (порядок)
1. **Простые пачки****выполнено**: color (18) → filters (3) → geometry (13) →
alpha (13/14, кроме watermark-image) → generate (10, включая mix/sort через
kind `colors`) → analyze-маски (6) → text `watermark-tile-png`.
2. **Text-механика****выполнено**: типы `input`/`result` + output-формат,
executor `executeFromText`/`toText`/`textToText`, UI
`SchemaTextSource`/`SchemaTextResult` + интеграция в preview, перевод
конвертеров convert (5 png→text + 6 text→png) и analyze (verify + 5
вердиктов). `png-info` отложен отдельным райзом (exif).
3. **Шаг про overlay не делается**`watermark-image-png` **отложен решением**
(см. «Скоуп перевода» выше), отдельным райзом вместе с вопросом второго
источника изображения в schema-driven preview.
4. После категории — `schema.layout` для сгруппированных инструментов (шаг 32-33
паттерн) и ручная проверка в `/preview`.
## Переезд в `old/` (шаг 37 основного плана)
> Скоп-сдвиг: «Scoped-пути двигаются вместе с папками» — обновляется
> `eslint.config.js` (isolation-паттерны и design-tokens glob), не код правила.
1. **Что переезжает:** `lib/registry.ts`, `lib/registry/**`,
`lib/registry-helpers.ts`, `lib/categories.ts`, `lib/tools/**`,
`lib/components/**` (кроме `kit/`) → папки `old/` (например
`lib/old/registry …`, `lib/old/tools`, `lib/old/components`) — то есть всё
старое в одном поддереве. `routes/(old)/` остаётся как есть (уже отделено).
2. **Обновить конфиг плагина** `isolation/no-mixed-imports`: старые glob-ы
`lib/registry/**…` заменить на `lib/old/**` (+ `lib/old/components/**` и
т.п.), «новое» и «общее» не меняются. Проверить, что правило по-прежнему
разделяет (тест фолс-позитивов: временный импорт old↔new ловится).
3. **Импорты внутри старого кода** — относительные пути остаются рабочими (папки
двигаются целиком); поправить только то, что ссылалось на `$lib/...` в другом
стиле. Старые тесты (`registry.test.ts` и др.) переезжают вместе с
исходниками и продолжают проходить.
4. **Гейты переезда:** `svelte-check` 0 errors, `pnpm --dir web test` зелёный,
`prettier --check .` чистый, `lint:all` = задокументированный остаток, обе
ветки открываются (старая `/`, новая `/preview`).
## Проверка и сбор отзывов после переезда
1. **Регресс-чеклист по каталогу** — пройти все инструменты в `/preview`:
дефолты совпадают со старыми, валидация/клампы поведения не меняют, результат
эквивалентен старому UI (сверить на одинаковых входах).
2. **Тяжёлые кейсы руками:** генераторы, маски, font-style/plate/gradient на
всем наборе, text-source, output-формат. (`watermark-image` — позже, после
overlay-райза.)
3. **Сбор фидбека** — отдельные задачи на каждый найденный фикс; чеклист
зафиксировать в этом документе по мере находок (или ссылкой на tasks).
4. Сверка каталога с `tools-map.md` (покрытие/дедупликация
`convert-png-to-jpg`/`webp`), при необходимости — обновить cards/мета.
5. **Решение об удалении старого** — после завершения ревью и фидбека, отдельным
шагом: тогда чистятся `lib/old/**`, `routes/(old)/**`, `old.css`, `exports`,
изоляционный плагин (glob-ы больше не нужны) и открываются «правило
копий»-дубли.
## Как ревьюить каждый шаг (гайды)
- Диф на **один инструмент** (или фундамент без инструментов) < ~500 строк.
- Старый `params`/`run`/`defaultParams`/`sanitizeParams` не изменены; в
`registry-new` копия логики + `schema` (дефолты равны старым).
- Старый UI и старые тесты продолжают работать: `pnpm --dir web test`,
`pnpm --dir web exec svelte-check`.
- Новый инструмент проверяется **руками в `/preview`** end-to-end (применение +
сброс дефолтов + смена значений).
- После переезда каждый move-шаг гоняет 4 гейта (см. выше).
## Оценка трудозатрат
| Часть | Сложность | Оценка |
| ---------------------------------------------------------------------- | -------------- | ------------------------- |
| Простые инструменты (color/filters/geometry/alpha/generate/маски/text) | Низкая-Средняя | **выполнено** (62 шт) |
| Text-механика + конвертеры/вердикты | Средняя | **выполнено** (16 шт) |
| watermark-image / png-info / reduce-to-size (отложены решениями) | Высокая | отложено, отдельные райзы |
| Долги (ToolCard, palette.test.ts, lint:all остаток) | Средняя | ~2-4ч |
| Переезд в `old/` + конфиг плагина + гейты | Средняя-Низкая | ~2-4ч |
| Проверка/фидбек + правки | Зависит | ~3-6ч |
| **Итого** | | **~9-18ч** по фазам |
> Оценки ориентировочные; особые случаи — с ревью решения, объём финальных
> правок по фидбеку не предсказуем.
+60 -25
View File
@@ -4,36 +4,63 @@
## 1. Целевое поведение
1. «Процентные» параметры (яркость, контраст, качество JPEG/WebP, порог похожести) — слайдеры с числовым значением рядом; точные координаты и размеры (resize/crop) остаются полями ввода.
2. У цветовых параметров появляется кнопка-переключатель «Пипетка»: активна → клик по превью исходника снимает цвет пикселя и подставляет его в поле цвета; после выбора режим гаснет.
3. В режиме пипетки возле курсора видна лупа: увеличенный блок пикселей вокруг точки с перекрестием и подписью hex текущего пикселя; клик выбирает центр перекрестия.
4. Активная пипетка видна на превью перекрестием курсора; повторный клик по кнопке отменяет режим без выбора.
5. Всё работает поверх текущего процесса: авто-применение с задержкой, токен запусков, маски — без изменений.
1. «Процентные» параметры (яркость, контраст, качество JPEG/WebP, порог
похожести) — слайдеры с числовым значением рядом; точные координаты и размеры
(resize/crop) остаются полями ввода.
2. У цветовых параметров появляется кнопка-переключатель «Пипетка»: активна →
клик по превью исходника снимает цвет пикселя и подставляет его в поле цвета;
после выбора режим гаснет.
3. В режиме пипетки возле курсора видна лупа: увеличенный блок пикселей вокруг
точки с перекрестием и подписью hex текущего пикселя; клик выбирает центр
перекрестия.
4. Активная пипетка видна на превью перекрестием курсора; повторный клик по
кнопке отменяет режим без выбора.
5. Всё работает поверх текущего процесса: авто-применение с задержкой, токен
запусков, маски — без изменений.
## 2. Слайдеры
- В реестре новый тип параметра `'slider'` с обязательными `min`/`max`; валидируется как число (дефолт в диапазоне, минимум меньше максимума).
- Форма маппит его на готовый `SliderField` из `ui/`; санитизация параметров обрабатывает slider так же, как number.
- Переводятся: яркость и контраст, качество JPEG, качество WebP, порог похожести. Остальные инструменты — без изменений.
- В реестре новый тип параметра `'slider'` с обязательными `min`/`max`;
валидируется как число (дефолт в диапазоне, минимум меньше максимума).
- Форма маппит его на готовый `SliderField` из `ui/`; санитизация параметров
обрабатывает slider так же, как number.
- Переводятся: яркость и контраст, качество JPEG, качество WebP, порог
похожести. Остальные инструменты — без изменений.
## 3. Пипетка и лупа
- В ядро добавляется чистая функция `rgbToHex(r, g, b)` → строка вида `#rrggbb`, с юнит-тестами; чтение пикселя — из уже отрисованного canvas превью исходника через `getImageData(x, y, 1, 1)`.
- `Preview.svelte` получает опциональный режим пипетки: пропсы «активна» и колбэк «цвет выбран»; курсор перекрестие, клик вычисляет координату пикселя с учётом разницы CSS-размера canvas и его внутренней ширины.
- **Лупа** — компонент `PipetteLoupe.svelte`: канвас около 110×110 px у курсора (позиция fixed, `pointer-events: none`); рисует блок 15×15 пикселей из исходника с увеличением ×8 без сглаживания, перекрестие в центре, подпись hex текущего пикселя. Координаты блока клампятся у краёв изображения; лупа скрыта вне превью и вне режима пипетки. Тач-устройства: прямой тап выбирает пиксель без лупы.
- Проводка: кнопка у каждого цветового поля в карточке параметров → состояние «активное цветовое поле» живёт в оркестраторе → выбор цвета записывает hex в это поле и выключает режим.
- Пипетка работает только по левой панели (исходнику): удаляемый цвет и подложка берутся с оригинала.
- В ядро добавляется чистая функция `rgbToHex(r, g, b)` → строка вида `#rrggbb`,
с юнит-тестами; чтение пикселя — из уже отрисованного canvas превью исходника
через `getImageData(x, y, 1, 1)`.
- `Preview.svelte` получает опциональный режим пипетки: пропсы «активна» и
колбэк «цвет выбран»; курсор перекрестие, клик вычисляет координату пикселя с
учётом разницы CSS-размера canvas и его внутренней ширины.
- **Лупа** — компонент `PipetteLoupe.svelte`: канвас около 110×110 px у курсора
(позиция fixed, `pointer-events: none`); рисует блок 15×15 пикселей из
исходника с увеличением ×8 без сглаживания, перекрестие в центре, подпись hex
текущего пикселя. Координаты блока клампятся у краёв изображения; лупа скрыта
вне превью и вне режима пипетки. Тач-устройства: прямой тап выбирает пиксель
без лупы.
- Проводка: кнопка у каждого цветового поля в карточке параметров → состояние
«активное цветовое поле» живёт в оркестраторе → выбор цвета записывает hex в
это поле и выключает режим.
- Пипетка работает только по левой панели (исходнику): удаляемый цвет и подложка
берутся с оригинала.
## 4. Этапы и проверки
- **A. Слайдеры** — тип в реестре, перевод четырёх инструментов, тесты реестра и санитизации, тест `rgbToHex`. Проверка: `pnpm test/check`, визуально слайдеры.
- **B. Пипетка** — режим в Preview, проводка через карточки до полей цвета. Проверка: смоук §6 пп. 5–7.
- **B2. Лупа** — компонент лупы поверх режима пипетки. Проверка: смоук §6 пп. 8–10.
- **A. Слайдеры** — тип в реестре, перевод четырёх инструментов, тесты реестра и
санитизации, тест `rgbToHex`. Проверка: `pnpm test/check`, визуально слайдеры.
- **B. Пипетка** — режим в Preview, проводка через карточки до полей цвета.
Проверка: смоук §6 пп. 5–7.
- **B2. Лупа** — компонент лупы поверх режима пипетки. Проверка: смоук §6 пп.
810.
- **C. Итог** — полный прогон `test/check/build`.
## 5. Изменения по файлам
- `registry.ts` — тип `'slider'` в `ParamDef`, перевод четырёх записей, ветка в `sanitizeParams`.
- `registry.ts` — тип `'slider'` в `ParamDef`, перевод четырёх записей, ветка в
`sanitizeParams`.
- `ParamForm.svelte` — ветка slider → `SliderField`.
- `core/color.ts``rgbToHex`.
- `Preview.svelte` — режим пипетки (курсор, клик, колбэк).
@@ -44,21 +71,29 @@
## 6. Смоук-чеклист
1. Яркость/контраст — слайдеры со значением справа, результат обновляется сам при перетаскивании.
1. Яркость/контраст — слайдеры со значением справа, результат обновляется сам
при перетаскивании.
2. Качество JPEG/WebP — слайдер от 1 до 100.
3. Порог похожести — слайдер от 0 до 100.
4. Resize и crop остались числовыми полями.
5. Удаление цвета: «Пипетка» → клик по красной области исходника → поле цвета получил hex этого пикселя, режим погас, результат пересчитался.
6. Повторный клик по кнопке или выбор без подтверждения — режим снят, цвет не изменился.
5. Удаление цвета: «Пипетка» → клик по красной области исходника → поле цвета
получил hex этого пикселя, режим погас, результат пересчитался.
6. Повторный клик по кнопке или выбор без подтверждения — режим снят, цвет не
изменился.
7. Подложка JPEG: пипетка проставляет цвет подложки тем же способом.
8. В режиме пипетки лупа следует за курсором, перекрестие показывает пиксель под ним, подпись hex совпадает с выбранным при клике.
9. У края картинки блок лупи корректно клампится — за границей тянется чёрный/прозрачный фон, выбор по-прежнему точен.
10. Курсор ушёл с превью или режим выключен — лупа скрыта; клики по кнопкам под превью не перехватываются лупой.
11. Слайдеры и пипетка дружат с быстрыми правками: одна финальная версия результата, чип «Пересчёт…» на месте.
8. В режиме пипетки лупа следует за курсором, перекрестие показывает пиксель под
ним, подпись hex совпадает с выбранным при клике.
9. У края картинки блок лупи корректно клампится — за границей тянется
чёрный/прозрачный фон, выбор по-прежнему точен.
10. Курсор ушёл с превью или режим выключен — лупа скрыта; клики по кнопкам под
превью не перехватываются лупой.
11. Слайдеры и пипетка дружат с быстрыми правками: одна финальная версия
результата, чип «Пересчёт…» на месте.
## 7. Критерии готовности
- Все процентные параметры — слайдеры; числовые поля остались только там, где нужен точный ввод.
- Все процентные параметры — слайдеры; числовые поля остались только там, где
нужен точный ввод.
- Цвет любого color-параметра можно взять кликом с исходника.
- `test/check/build` зелёные, чеклист §6 пройден.
+194
View File
@@ -0,0 +1,194 @@
# План: тестирование preview перед промоушеном в главную
> Статус: **реализация завершена**. Playwright-набор в `web/e2e/` (108 тестов:
> 104 прошли, 4 `test.fixme` — известные баги №1–4 ниже), ручной чек-лист —
> `docs/checklist-manual-testing.md`. План ветки preview
> (`web/src/routes/preview/**`, регистр `lib/registry-new/`, 121 инструмент)
> перед шагом C17 из `docs/plan-redesign.md` (переезд preview → реальные
> маршруты).
>
> Охват: **документация плана** — реализация тестов (Playwright-раннер, сами
> тесты) делается отдельными задачами после ревью этого документа.
>
> Приоритет: **отсутствие падений/ошибок** > пиксельная корректность. Дизайн
> доделываем по ходу, визуальная сверка — ручная и вторичная.
>
> Команды для проверки в процессе:
>
> - `pnpm --dir web test` — существующий Vitest (42 файла, 121 инструмент
> частично покрыт юнит-тестами).
> - `pnpm --dir web test:e2e` — Playwright (`web/e2e/`), сборка +
> `scripts/serve-static.mjs` на 4173.
> - `pnpm --dir web exec svelte-check --tsconfig ./tsconfig.json` — проверка
> типов.
> - `pnpm --dir web lint:all` — ESLint + stylelint + токен-аудит.
> - `pnpm --dir web build` — статическая сборка (adapter-static).
## 0. Что тестируем
Preview-ветка — клиентское SPA на SvelteKit (adapter-static, всё в браузере,
никакого API). Структура:
| Маршрут | Что это |
| ------------------------------------- | -------------------------------------------------------------------------------------------- |
| `/preview` | Workspace: поиск + сетка `ToolCard` (топ-24), «Open last project» (stub), футер |
| `/preview/kit` | Витрина всех kit-компонентов (light/dark) |
| `/preview/list-tools` | Каталог: 8 групп, 121 инструмент, фильтр по категории, поиск |
| `/preview/tools/[id]` | Страница инструмента: `SchemaToolView` (настройки + live preview), prerendered на все 121 id |
| `/preview/tools/xxx` (несуществующий) | 404 → `EmptyState` «Tool not found» |
Инструменты: **121**, категории CONVERT / TRANSPARENCY (alpha) / COLOR /
GEOMETRY / FILTERS / TEXT / ANALYZE / GENERATE. Режимы инструмента:
- **file** (transform): upload → `run` → картинка → download;
- **text** (convert-source): ввод текста → `runFromText` → картинка, либо
`textToText` → текстовый результат;
- **verdict** (`result: "verdict"`, file): `toText` → текстовый вердикт;
- **text-output** (`result: "text"`, file): `toText` → текстовый результат;
- **generate**: без входа, `executeGenerate` → картинка.
Исполнение: `preview/executor` — worker с fallback на main-thread (domOnly / нет
Worker / ошибка worker). Auto-run с debounce 200 мс.
## 1. Функциональные блоки для проверки
### A. Инфраструктура и навигация
1. Все 4 маршрута + 404 открываются без console errors, uncaught exceptions и
failed requests.
2. Поиск на `/preview` — фильтрация по названию/описанию, пустой результат →
аккуратное empty-state.
3. Каталог `/preview/list-tools` — 8 групп, счётчик 121, фильтр категории,
иконки не падают.
4. Тема light/dark и язык RU/EN переключаются на превью-страницах без регресса
рендера.
5. 404 на несуществующий id инструмента → EmptyState, без краша.
6. Prerender: `pnpm --dir web build` проходит; все 121 страницы
`/preview/tools/[id]` генерируются (проверка `entries()`/`getTool`).
### B. Конвейер инструмента (file-инструменты)
7. Upload → авто-run → результатный тайл появился, поле ошибки пустое.
8. Download — событие download, имя `<toolId>.<ext>`, MIME соответствует
`output`.
9. Reset — возвращает дефолты, сбрасывает результат и ошибку.
10. Смена параметра → авто-перезапуск (debounce 200 мс) без «залипания»
running-стейта.
11. Worker-исполнение и fallback-исполнение оба дают результат.
### C. Текстовые инструменты и вердикты
12. Text-source: ввод → текстовый результат или картинка; copy/download .txt.
13. Вердикты (analyze): корректный вердикт на PNG с альфой, на PNG без альфы, на
не-PNG входе → без падений.
14. Пустой text-source → run пропускается без ошибки.
### D. Генераторы
15. Каждый генератор по дефолтам создаёт картинку; смена размеров/цвета
перегенерирует; канва ожидаемого размера.
## 2. Репрезентативная выборка по категориям (для «нет падений»)
Полный прогон всех 121 — опционально (C/all); минимальный чек — по группам:
| Категория | Кол-во | Охват для «нет падений» |
| ------------ | ------ | --------------------------------------------------------------------------------- |
| CONVERT | 14 | convert-png-to-jpg, convert-png-to-webp, png-to-base64, base64-to-png, svg-to-png |
| TRANSPARENCY | 20 | remove-background, round-corners, add-stroke, remove-color-from, circle-mask |
| COLOR | 25 | grayscale, invert, black-and-white, quantize, dithering, png-to-hsl |
| GEOMETRY | 18 | resize, crop, rotate, flip, add-border, tile |
| FILTERS | 8 | blur, sharpen, pixelate, add-noise |
| TEXT | 3 | add-text, date-stamp, watermark-tile |
| ANALYZE | 12 | extract-color-from, show-transparent-pixels, verify-is-png, png-is-transparent |
| GENERATE | 21 | create-empty, linear-gradient, two-colors, placeholder, text-to-png |
## 3. Playwright (авто) против ручного — разбиение
### ✅ Playwright — детерминированные сквозные потоки и «нет падений»
- Все маршруты + 404: открытие, отсутствие console errors / uncaught exceptions
/ failed requests (слушатели `page.on('console'/'pageerror')`).
- Поиск и каталог: фильтрация, счётчики, переходы.
- Upload (`setInputFiles` с фикстурой PNG) → авто-результат → Download (ожидание
`download` события, проверка имени/расширения/suggestedFilename).
- Reset / смена параметра → перегенерация, running не «залипает».
- Тема light/dark и язык RU/EN.
- Текстовые инструменты: ввод → текстовый результат → копия/скачивание.
- Вердикты на подготовленных фикстурах (PNG с альфой, PNG без альфы,
JPG/некартинка).
- Генераторы: дефолты → результат появился, размер канвы ожидаемый.
- 404 и empty-state.
- Worker и fallback: сравнить оба пути рендера (инжект отключения Worker).
### 🔄 Playwright + ручная сверка
- Пиксельная корректность результата (сравнение `<canvas>` через
`toDataURL`/тест-per-pixel) — по приоритету вторично; эталоны задаются
вручную, делать отдельной задачей.
- Реальные пользовательские картинки (не фикстуры) — вручную.
### ✋ Ручное тестирование
- **Краевые PNG:** большие (мегапиксели), 1×1, анимация, палитровые,
чекерборд/полупрозрачность, повреждённые файлы → нет зависаний, понятные
сообщения об ошибке.
- **Края параметров:** экстремальные значения, NaN/пустые поля, инвалидные цвета
в плашках/градиентах → корректная санитизация (silent repair).
- **Производительность/отзывчивость:** задержка авто-реранов на слайдерах, нет
«залипания» UI, скролл при длинных настройках.
- **Визуальный слой дизайна** (доделываем по ходу): сетки, выравнивание, отступы
на брейкпоинтах mobile/tablet/desktop (640/800/1100).
- **Буфер обмена** (copy) в реальном браузере.
- **Drag-n-drop** файла на dropzone (UX-ощущения).
- **Разные браузеры:** Firefox/Safari-нюансы (webkit в Playwright опционален).
- **Субъективная корректность:** dithering, two-colors, watermark, текст
не-ASCII/кириллица в add-text/date-stamp/watermark-tile.
## 4. Критерии приёмки (доступ в главную)
1. Все 4 маршрута + 404 открываются без console errors и исключений.
2. Репрезентативный прогон по 8 категориям: ни один инструмент не падает и не
показывает поле ошибки.
3. Upload → результат → download завершается для file-инструментов;
текст/вердикты/генераторы дают корректный output-тип.
4. `pnpm --dir web test`, `pnpm --dir web lint:all`, `pnpm --dir web build`
зелёные.
5. Тема и язык переключаются без регрессов.
## 5. Оценка покрытия
- **~60–70%** «нет падений/ошибок» закрывается Playwright детерминированно
(маршруты, сквозные потоки, отсутствие ошибок).
- **~30–40%** — руками: краевые PNG/параметры, реальные пользовательские файлы,
буфер обмена, кросс-браузеры, визуальный/производительный слой, субъективная
корректность.
## 6. Шаги реализации (после ревью)
1. ✅ Playwright-раннер: `web/playwright.config.ts` (webServer на
`pnpm build + `scripts/serve-static.mjs` `--port
4173`), папка `web/e2e/`, npm-скрипт `test:e2e`.
2. ✅ Фикстуры PNG (генерируются в рантайме, `web/e2e/helpers/fixtures.ts`): с
альфой, без альфы, 1×1, большой, повреждённый, ландшафт.
3. ✅ E2E по блокам A–D и секции 3 (авто-часть): `navigation.spec.ts`,
`catalog.spec.ts`, `pipeline.spec.ts`, `text-and-verdicts.spec.ts`,
`tools-smoke.spec.ts` (матрица ~71 инструмента), `generators.spec.ts`,
`known-issues.spec.ts` (fixme).
4. ✅ Ручной чек-лист из секции 3 (ручная часть) —
`docs/checklist-manual-testing.md` (разделы A–G, включая известные баги).
5. ✅ Найденные баги (зафиксированы как `test.fixme` + чек-лист §G):
1. генераторы (21/121) не имеют кнопки «Generate» и полей схемы — результат
через UI недостижим;
2. `resize-png` с дефолтом 0×0 → `errors.resizeSize` при любом входе;
3. `crop-png` с дефолтом 0×0 → `errors.cropSize` при любом входе;
4. ключи ошибок показываются сырыми (`errors.*`) вместо переведённого текста
из `en.ts`/`ru.ts`. Исправления — отдельными атомарными задачами по
конвенции AGENTS.md (после правок: `lint:all`, `test:e2e`).
## 7. Итоговое состояние на момент сдачи
- `pnpm --dir web test:e2e` — **104 passed, 4 skipped (fixme), 0 failed**.
- `pnpm --dir web lint:all` и `svelte-check` — зелёные.
- Визуальный слой, краевые PNG/параметры, кросс-браузеры — остаются на ручной
прогон по `docs/checklist-manual-testing.md`.
+81
View File
@@ -0,0 +1,81 @@
# План: волна 4 — текст, штампы, водяные знаки
> **СТАТУС: ВЫПОЛНЕН 25.08.2026.**
## 1. Что получается
Четыре инструмента новой категории «Текст»: надпись на изображении, дата-штамп,
защитная плитка (повторяющийся полупрозрачный текст под углом), водяной знак
картинкой. Каталог вырастает с 69 до 73. Это первые операции, рисующие через
canvas 2D — класс perceptual по роадмапу: точные пиксельные тесты на них не
пишутся, юнит-тестами покрывается чистая математика раскладки.
## 2. Решения
- Вся геометрия — чистые функции без DOM: якорь из 9-позиционной сетки →
координаты, перенос текста по ширине с инъекцией измерителя, раскладка плитки
с углом и шагом. Canvas-вызовы — тонкий слой рядом (`core/textdraw.ts`), в
юнит-тесты не попадает.
- Один движок рендера текста на все три текстовых инструмента: шрифт из
системного набора (sans/serif/mono), размер, цвет, жирность, прозрачность,
поворот. Различия инструментов — только в пресетах параметров и количестве
слоёв (одна надпись vs плитка).
- Дата-штамп — надпись с автотекстом: строка формата (`YYYY-MM-DD hh:mm`)
разворачивается в текущую дату в момент запуска; свой мини-форматтер без
зависимостей.
- Водяной знак картинкой — первый двухисточниковый инструмент: вторая картинка
грузится отдельной зоной в этапе, живёт только в состоянии страницы и
сознательно не сохраняется ни в workspace-pipeline, ни в экспорте JSON; после
восстановления цепочки знак нужно выбрать заново. Ограничение фиксируем в
описании инструмента.
- Новая категория `text` встаёт после «Фильтров»; названия в обоих словарях,
порядок в CATEGORIES один.
## 3. Инструменты
- `add-text-png` — параметры: текст (строка), размер (слайдер 8–200), цвет,
шрифт (select), жирность (чекбокс), позиция (select 3×3: углы/стороны/центр),
отступ (слайдер). Опциональная подложка: цвет + прозрачность плашки за
текстом.
- `date-stamp-png` — тот же набор минус текст, плюс формат даты; дефолт — нижний
правый угол.
- `watermark-tile-png` — текст, размер, цвет, прозрачность (0–100), угол
(-90…90), шаг по X/Y; плитка заполняет весь холст.
- `watermark-image-png` — второй источник (загрузка PNG с альфой), масштаб знака
(% от ширины холста), прозрачность, позиция 3×3, отступ.
## 4. Этапы и проверки
Каждый этап заканчивается зелёными test/check/build и ревью.
- **A. Фундамент** — категория `text` (categories + словари + порядок),
`core/textdraw.ts`: anchor9, wrap, tileLayout с юнит-тестами на математике
(координаты углов/центра, переносы слов, плотность плитки), тонкий
canvas-модуль `domText.ts` (без тестов). Реестр пока пуст в категории.
- **B. Надпись и дата** — `add-text-png`, `date-stamp-png` на общем движке;
иконки, переводы, записи реестра. Смоук: обе локали, тёмная тема (плашка
читаема), длинный текст переносится и не вылезает за край.
- **C. Плитка** — `watermark-tile-png`; смоук: отрицательные и нулевой угол,
прозрачность 0/50/100, мелкий шаг не подвешивает страницу на больших холстах
(ограничение количества плиток капом).
- **D. Знак картинкой** — двухисточниковый UX: вторая drop-зона в этапе
(компонент рядом с SourceCard, виден только у этого инструмента),
масштаб/позиция/отступ; ограничение про сохранение — в описании и подсказке.
Этап режется без ущерба остальным, если решим отложить.
- **E. Итог** — полный прогон, смоук §6, архивация плана.
## 5. Критерии готовности
- 73 инструмента; вся математика раскладки покрыта юнит-тестами; canvas-слой
изолирован в одном модуле.
- test/check/build зелёные; новые инструменты работают в цепочке как обычные
шаги (кроме источника знака — см. ограничение).
## 6. Что сознательно не делаем
- Выбор произвольных пользовательских шрифтов (файлами) и веб-шрифтов — только
системный набор.
- Кернинг/линии/многострочное редактирование с абзацами — один абзац с
автопереносом.
- Сохранение картинки-знака в пайплайне и её передача между сессиями.
- Текст по контуру, градиентные заливки текста.
+61
View File
@@ -0,0 +1,61 @@
# План: светлая и тёмная тема
> **СТАТУС: ВЫПОЛНЕН 25.08.2026.**
## 1. Что получается
Переключатель light/dark в шапке рядом с языковым. Выбор в localStorage
(`theme`), первый визит — по `prefers-color-scheme`. Тёмная палитра —
переопределение токенов селектором `[data-theme='dark']`; компоненты не
правятся.
## 2. Решения
- Два состояния, без «авто»: ключ хранит только явный выбор, отсутствие ключа =
системная тема.
- Анти-вспышка: инлайн-скрипт в `app.html` до гидрации ставит `data-theme` из
localStorage или `matchMedia`; пререндеренная разметка остаётся светлой.
- `color-scheme: dark` на корне — нативные контролы (input[type=color], range,
select, скроллбары) темнеют без правок.
- Модуль `theme.svelte.ts` по образцу locale: состояние, персист, применение
атрибута. Стартовую установку делает скрипт из `app.html`, модуль
синхронизирует runes-состояние после гидрации.
## 3. Палитра
- Переопределяем: `--bg`, `--surface`, `--border`, `--text`, `--text-muted`.
- Акцент `#2563eb` не меняется; `--accent-hover` — светлее для тёмного фона.
- `--danger` осветлить для читаемости.
- `--check-a`/`--check-b` шахматки — приглушённые серые: паттерн прозрачности
остаётся узнаваемым, но не слепит.
- `--shadow-card` — почти чёрная с низкой альфой вместо синей.
- Все `color-mix(… var(--surface))` пересчитаются сами — проверить визуально
легенды, recalc-бейдж, drag-оверлеи.
## 4. Этапы и проверки
Каждый этап заканчивается зелёными test/check/build и ревью.
- **A. Инфраструктура** — `theme.svelte.ts`, скрипт в `app.html`, переключатель
в шапке, черновая тёмная палитра. Тесты: персист выбора, игнор мусора в
localStorage, применение атрибута (стаб document).
- **B. Палитра** — выверка значений: герой, каталог, страница инструмента,
цепочка, баннер ошибки, маска, текстовые инструменты; фокус-ринги и hover; обе
локали.
## 5. Смоук
1. Переключение мгновенное, без перезагрузки; выбор переживает её же.
2. При системной тёмной теме первый визит открывается сразу тёмным.
3. Нативные контролы выглядят тёмными.
4. Шахматка различима, легенды и бейджи читаемы.
5. Баннер ошибки и пипетка заметны на тёмных панелях.
## 6. Критерий готовности
Grep: ни одного цвета вне токенов в компонентах и маршрутах.
## 7. Не делаем
Три позиции в переключателе; пользовательские цветовые схемы; автосмену по
времени суток; отдельную тему для превью изображений.
+56 -24
View File
@@ -4,57 +4,87 @@
## 1. Зачем
- Страница инструмента (`ToolPage.svelte`) разрослась: состояние процесса, загрузка файлов, вставка из буфера и разметка трёх зон живут в одном файле.
- Назревают два требования к процессу: параметры применяются автоматически без кнопки; у части утилит на экране показывается одно — визуализация фильтра/маски, а скачивается (и в будущем передаётся следующему инструменту) другое — результат.
- Страница инструмента (`ToolPage.svelte`) разрослась: состояние процесса,
загрузка файлов, вставка из буфера и разметка трёх зон живут в одном файле.
- Назревают два требования к процессу: параметры применяются автоматически без
кнопки; у части утилит на экране показывается одно — визуализация
фильтра/маски, а скачивается (и в будущем передаётся следующему инструменту)
другое — результат.
## 2. Контракт «результат и маска» в реестре
- `run(img, params)` — как сейчас: возвращает итоговое изображение. Его скачивают и будут передавать дальше по цепочке.
- Запись реестра получает опциональное поле `preview?(img, params)` — возвращает картинку для показа на экране (маску/фильтр).
- Если у записи есть `preview`, в карточке результата появляется переключатель «Показать маску». Включён — на превью маска, выключен (по умолчанию) — результат. Скачивание всегда отдаёт результат `run`, независимо от положения переключателя.
- Первая реализация — удаление цвета: в ядро добавляется чистая функция `colorMask(img, hex, tolerance)` — чёрно-белая маска, где удаляемые пиксели белые; с юнит-тестами; `preview` записи вызывает её.
- `run(img, params)` — как сейчас: возвращает итоговое изображение. Его
скачивают и будут передавать дальше по цепочке.
- Запись реестра получает опциональное поле `preview?(img, params)` — возвращает
картинку для показа на экране (маску/фильтр).
- Если у записи есть `preview`, в карточке результата появляется переключатель
«Показать маску». Включён — на превью маска, выключен (по умолчанию) —
результат. Скачивание всегда отдаёт результат `run`, независимо от положения
переключателя.
- Первая реализация — удаление цвета: в ядро добавляется чистая функция
`colorMask(img, hex, tolerance)` — чёрно-белая маска, где удаляемые пиксели
белые; с юнит-тестами; `preview` записи вызывает её.
## 3. Декомпозиция страницы
Новая папка `components/tool/`; карточки — чисто отображательные, состояние живёт только в оркестраторе:
Новая папка `components/tool/`; карточки — чисто отображательные, состояние
живёт только в оркестраторе:
- **`SourceCard.svelte`** — левая половина сцены: пусто → зона перетаскивания; загружено → превью исходника под оверлеем замены с кнопкой «Заменить изображение».
- **`ResultCard.svelte`** — правая половина: пустые состояния, превью результата или сводка, чип «Пересчёт…», кнопка скачивания, при наличии `preview` — переключатель маски.
- **`ParamsCard.svelte`** — форма параметров и подпись «Изменения применяются автоматически»; для инструментов без параметров не выводится.
- **`ToolPage.svelte`** остаётся оркестратором: состояние (исходник, результат, сводка, статус, ошибка, значения), декодирование файлов, вставка из буфера, сброс, токен запусков и эффект с задержкой. Ожидаемо худеет примерно вдвое.
- **`SourceCard.svelte`** — левая половина сцены: пусто → зона перетаскивания;
загружено → превью исходника под оверлеем замены с кнопкой «Заменить
изображение».
- **`ResultCard.svelte`** — правая половина: пустые состояния, превью результата
или сводка, чип «Пересчёт…», кнопка скачивания, при наличии `preview`
переключатель маски.
- **`ParamsCard.svelte`** — форма параметров и подпись «Изменения применяются
автоматически»; для инструментов без параметров не выводится.
- **`ToolPage.svelte`** остаётся оркестратором: состояние (исходник, результат,
сводка, статус, ошибка, значения), декодирование файлов, вставка из буфера,
сброс, токен запусков и эффект с задержкой. Ожидаемо худеет примерно вдвое.
Карточки переиспользуются будущим многошаговым режимом.
## 4. Реактивные параметры (в этом же проходе)
- Кнопки «Применить» больше нет ни у одного инструмента.
- Изменение параметра запускает вычисление через 300 мс после последнего изменения; каждое новое изменение перезапускает таймер.
- Токен запусков: если во время вычисления параметры поменялись, устаревший ответ отбрасывается.
- Загрузка и замена файла считают сразу и подавляют ровно один авто-запуск эффекта.
- Изменение параметра запускает вычисление через 300 мс после последнего
изменения; каждое новое изменение перезапускает таймер.
- Токен запусков: если во время вычисления параметры поменялись, устаревший
ответ отбрасывается.
- Загрузка и замена файла считают сразу и подавляют ровно один авто-запуск
эффекта.
- Ошибка пересчёта — баннер сверху, прежний результат не сбрасывается.
## 5. Этапы и проверки
- **A. Декомпозиция** — перенос разметки в три карточки один к одному, поведение не меняется. Проверка: `check/build`, визуально идентично.
- **B. Реактивность** — токен, задержка, чип пересчёта, минус кнопка. Проверка: смоук §6 пп. 1–6.
- **C. Маска** — типы реестра, `colorMask` с тестами, переключатель в карточке результата, пример на удалении цвета. Проверка: тесты + смоук §6 пп. 7–9.
- **A. Декомпозиция** — перенос разметки в три карточки один к одному, поведение
не меняется. Проверка: `check/build`, визуально идентично.
- **B. Реактивность** — токен, задержка, чип пересчёта, минус кнопка. Проверка:
смоук §6 пп. 1–6.
- **C. Маска** — типы реестра, `colorMask` с тестами, переключатель в карточке
результата, пример на удалении цвета. Проверка: тесты + смоук §6 пп. 7–9.
- **D. Итог** — полный прогон `test/check/build`.
## 6. Смоук-чеклист
1. Загрузка файла → результат появляется сразу.
2. Изменить параметр → около 0.3 с и результат обновился сам.
3. Быстро изменить яркость несколько раз → одна финальная версия, промежуточные не мелькают.
3. Быстро изменить яркость несколько раз → одна финальная версия, промежуточные
не мелькают.
4. Во время пересчёта чип виден, предыдущий результат не исчезает.
5. Инструмент без параметров и информационная страница ведут себя как раньше.
6. Некорректные параметры (обрезка вне картинки) → баннер ошибки, прежний результат цел.
7. Переключатель «Показать маску» у удаления цвета: включён — белым по чёрному видно, что удалится; выключен — обычный полупрозрачный результат.
8. С включённой маской скачивание всё равно сохраняет прозрачный результат, а не маску.
6. Некорректные параметры (обрезка вне картинки) → баннер ошибки, прежний
результат цел.
7. Переключатель «Показать маску» у удаления цвета: включён — белым по чёрному
видно, что удалится; выключен — обычный полупрозрачный результат.
8. С включённой маской скачивание всё равно сохраняет прозрачный результат, а не
маску.
9. Замена исходника возвращает переключатель маски в выключенное состояние.
## 7. Критерии готовности
- `ToolPage` содержит только оркестрацию состояния и эффектов; вся разметка панелей — в `components/tool/*`.
- `ToolPage` содержит только оркестрацию состояния и эффектов; вся разметка
панелей — в `components/tool/*`.
- Кнопки «Применить» нет ни на одной странице.
- Скачивание всегда соответствует `run`; экран может показывать `preview`.
- `test/check/build` зелёные, смоук §6 пройден.
@@ -62,5 +92,7 @@
## 8. Что сознательно не делаем
- Вынос вычислений в веб-воркер; настраиваемая задержка — фиксированные 300 мс.
- Редактор масок (кисть, слои, история) — только автогенерация из параметров инструмента.
- Передача результата между инструментами — придёт вместе с многошаговым режимом позже.
- Редактор масок (кисть, слои, история) — только автогенерация из параметров
инструмента.
- Передача результата между инструментами — придёт вместе с многошаговым режимом
позже.
+74 -30
View File
@@ -4,48 +4,89 @@
>
> **Что сделано по этапам:**
>
> - A — общая MIME-валидация `isSupportedImage` + `unsupportedImageMessage` в `core/io.ts`; `DropZone` переведён на них ✔
> - B — постоянная двухпанельная раскладка: одна карточка-«сцена» с равными половинами «Исходник | Результат», кнопки внутри своих половин, параметры — отдельной карточкой ниже; контент сайта растянут на всю ширину окна
> - CCtrl+V: `<svelte:window onpaste>`, поиск image-item в буфере, общая валидация; текстовые вставки не затрагиваются ✔
> - D — drag-замена: компонент `DropOverlay` с вуалью-подсказкой и счётчиком `dragenter/dragleave` против мерцания; тот же счётчик в `DropZone` ✔
> - E — верификация: тесты 85/85, svelte-check 0 ошибок, статический билд с 11 страницами; ручной смоук-чеклист §5 пройден
> - A — общая MIME-валидация `isSupportedImage` + `unsupportedImageMessage` в
> `core/io.ts`; `DropZone` переведён на них
> - Bпостоянная двухпанельная раскладка: одна карточка-«сцена» с равными
> половинами «Исходник | Результат», кнопки внутри своих половин, параметры —
> отдельной карточкой ниже; контент сайта растянут на всю ширину окна
> - C — Ctrl+V: `<svelte:window onpaste>`, поиск image-item в буфере, общая
> валидация; текстовые вставки не затрагиваются ✔
> - D — drag-замена: компонент `DropOverlay` с вуалью-подсказкой и счётчиком
> `dragenter/dragleave` против мерцания; тот же счётчик в `DropZone` ✔
> - E — верификация: тесты 85/85, svelte-check 0 ошибок, статический билд с 11
> страницами; ручной смоук-чеклист §5 пройден ✔
>
> **Отступления от исходного плана в ходе реализации:** вместо пропа `overlay` у `DropZone` сделан отдельный компонент-обёртка `DropOverlay` (drag-события ловит контент под курсором); раскладка доработана по ревью — одна карточка равных половин вместо двух независимых колонок, сайт без ограничения ширины.
> **Отступления от исходного плана в ходе реализации:** вместо пропа `overlay` у
> `DropZone` сделан отдельный компонент-обёртка `DropOverlay` (drag-события
> ловит контент под курсором); раскладка доработана по ревью — одна карточка
> равных половин вместо двух независимых колонок, сайт без ограничения ширины.
## 1. Целевое поведение
1. **Постоянная двухпанельная раскладка** страницы инструмента — отдельного состояния «страница загрузки» больше нет:
- **Слева — исходник**, панель есть всегда. Не загружено → пустое дроп-поле (drag&drop или клик). Загружено → превью исходника.
- **Справа — результат**: до загрузки — `EmptyState` «Результат появится здесь»; во время обработки — `EmptyState` «Обработка…»; после — превью результата (для info-инструмента — `InfoPanel`).
2. **Ctrl+V** — картинка из буфера обмена вставляется как исходник с любого места страницы.
3. **Замена перетаскиванием** — новый файл, брошенный на левую панель с загруженной картинкой, заменяет исходник (с оверлеем-подсказкой во время перетаскивания).
4. **Кнопка «Заменить»** под превью исходника (переносится туда текущая «Загрузить другое изображение»).
1. **Постоянная двухпанельная раскладка** страницы инструмента — отдельного
состояния «страница загрузки» больше нет:
- **Слева — исходник**, панель есть всегда. Не загружено → пустое дроп-поле
(drag&drop или клик). Загружено → превью исходника.
- **Справа — результат**: до загрузки — `EmptyState` «Результат появится
здесь»; во время обработки — `EmptyState` «Обработка…»; после — превью
результата (для info-инструмента — `InfoPanel`).
2. **Ctrl+V** — картинка из буфера обмена вставляется как исходник с любого
места страницы.
3. **Замена перетаскиванием** — новый файл, брошенный на левую панель с
загруженной картинкой, заменяет исходник (с оверлеем-подсказкой во время
перетаскивания).
4. **Кнопка «Заменить»** под превью исходника (переносится туда текущая
«Загрузить другое изображение»).
На мобильной ширине панели складываются в столбец: исходник сверху, результат снизу.
На мобильной ширине панели складываются в столбец: исходник сверху, результат
снизу.
## 2. Изменения по файлам
Новые компоненты не вводим — `EmptyState`, `Preview`, `DropZone` покрывают всё. Меняем три файла:
Новые компоненты не вводим — `EmptyState`, `Preview`, `DropZone` покрывают всё.
Меняем три файла:
- **`core/io.ts`** — экспорт `isSupportedImage(file): boolean`: общая MIME-проверка на базе `ACCEPTED_IMAGE_TYPES`.
- **`DropZone.svelte`** — переходит на `isSupportedImage`; новый опциональный проп `overlay` — режим «невидимая зона приёма поверх контента» для замены перетаскиванием.
- **`ToolPage.svelte`** — постоянная сетка из двух панелей; `<svelte:window onpaste>`; drag-оверлей левой панели; кнопки замены/сброса переезжают в левую панель; правая панель — три состояния через `EmptyState`.
- **`core/io.ts`** — экспорт `isSupportedImage(file): boolean`: общая
MIME-проверка на базе `ACCEPTED_IMAGE_TYPES`.
- **`DropZone.svelte`** — переходит на `isSupportedImage`; новый опциональный
проп `overlay` — режим «невидимая зона приёма поверх контента» для замены
перетаскиванием.
- **`ToolPage.svelte`** — постоянная сетка из двух панелей;
`<svelte:window onpaste>`; drag-оверлей левой панели; кнопки замены/сброса
переезжают в левую панель; правая панель — три состояния через `EmptyState`.
## 3. Этапы и проверки
- **A. Общая валидация** — `isSupportedImage` в `io.ts`, `DropZone` переведён на неё. Проверка: `pnpm check`.
- **B. Раскладка** — две постоянные колонки в ToolPage; правая панель через EmptyState; кнопки уходят в левую панель; обёртка `{#if !source}` на всю страницу убирается. Проверка: визуальный осмотр всех инструментов, `check/build`.
- **C. Ctrl+V** — `onpaste` на окне → поиск image-item в `clipboardData` → тот же `handleFile`; не-изображения игнорируются молча. Проверка: ручной смоук со скриншотом из буфера.
- **D. Drag-замена** — оверлей DropZone(overlay) при dragover на левой панели; drop → `handleFile` нового файла. Проверка: ручной смоук «бросить файл на загруженный исходник».
- **E. Итог** — полный ручной смоук-чеклист (§5). Проверка: `test/check/build` зелёные.
- **A. Общая валидация** — `isSupportedImage` в `io.ts`, `DropZone` переведён на
неё. Проверка: `pnpm check`.
- **B. Раскладка** — две постоянные колонки в ToolPage; правая панель через
EmptyState; кнопки уходят в левую панель; обёртка `{#if !source}` на всю
страницу убирается. Проверка: визуальный осмотр всех инструментов,
`check/build`.
- **C. Ctrl+V** — `onpaste` на окне → поиск image-item в `clipboardData` → тот
же `handleFile`; не-изображения игнорируются молча. Проверка: ручной смоук со
скриншотом из буфера.
- **D. Drag-замена** — оверлей DropZone(overlay) при dragover на левой панели;
drop → `handleFile` нового файла. Проверка: ручной смоук «бросить файл на
загруженный исходник».
- **E. Итог** — полный ручной смоук-чеклист (§5). Проверка: `test/check/build`
зелёные.
## 4. Нюансы и решения
- **Мерцание dragleave** — классическая проблема оверлеев: используем счётчик `dragenter/dragleave`, оверлей скрывается только когда счётчик обнулился и произошёл `drop`/`dragend`.
- **Paste при фокусе в поле параметров** — обрабатываем всегда: если в буфере именно изображение, пользователь очевидно хочет заменить исходник; текстовые вставки обработчик не трогает.
- **Форматы буфера** — скриншоты приходят как PNG, часть приложений отдаёт JPEG/WebP; все проходят общую `isSupportedImage`. GIF/BMP из буфера редки, но тоже допустимы.
- **Info-инструменты** — ведут себя так же: слева исходник, справа после загрузки появляется `InfoPanel`.
- **Состояние ошибки** — баннер остаётся сверху страницы; неудачная замена не сбрасывает старый исходник.
- **Мерцание dragleave** — классическая проблема оверлеев: используем счётчик
`dragenter/dragleave`, оверлей скрывается только когда счётчик обнулился и
произошёл `drop`/`dragend`.
- **Paste при фокусе в поле параметров** — обрабатываем всегда: если в буфере
именно изображение, пользователь очевидно хочет заменить исходник; текстовые
вставки обработчик не трогает.
- **Форматы буфера** — скриншоты приходят как PNG, часть приложений отдаёт
JPEG/WebP; все проходят общую `isSupportedImage`. GIF/BMP из буфера редки, но
тоже допустимы.
- **Info-инструменты** — ведут себя так же: слева исходник, справа после
загрузки появляется `InfoPanel`.
- **Состояние ошибки** — баннер остаётся сверху страницы; неудачная замена не
сбрасывает старый исходник.
## 5. Чеклист ручного смоука
@@ -54,7 +95,8 @@
3. Ctrl+V со скриншотом → исходник заменился, результат пересчитался.
4. Drag файла на загруженную левую панель → оверлей виден, после drop — замена.
5. Отмена перетаскивания (Esc / мимо) → оверлей скрылся, исходник не изменился.
6. Бросок неподдерживаемого файла (например .txt) → баннер ошибки, старый исходник цел.
6. Бросок неподдерживаемого файла (например .txt) → баннер ошибки, старый
исходник цел.
7. Кнопка «Заменить» открывает выбор файла.
8. png-info: работает та же схема, справа сводка.
9. Мобильная ширина: панели в столбец, порядок исходник → результат.
@@ -62,7 +104,8 @@
## 6. Критерии готовности
- На странице инструмента нет состояния «только дропзона на всю страницу» — раскладка двухпанельная всегда.
- На странице инструмента нет состояния «только дропзона на всю страницу» —
раскладка двухпанельная всегда.
- Загрузка работает тремя способами: клик, drag&drop (включая замену), Ctrl+V.
- Ошибочные файлы и отмена перетаскивания не портят текущий исходник.
- `test/check/build` зелёные; чеклист §5 пройден.
@@ -71,4 +114,5 @@
- Множественная загрузка / пакетная обработка — вне скоупа этого плана.
- Камера/скриншот-инструменты захвата — только буфер обмена.
- Прогресс-бары декодирования — файлы читаются быстро, достаточно индикации «Обработка…».
- Прогресс-бары декодирования — файлы читаются быстро, достаточно индикации
«Обработка…».
+92 -37
View File
@@ -1,75 +1,130 @@
# План: главная страница как рабочая область с цепочкой
> **СТАТУС: ВЫПОЛНЕН 2026-08-23.** Заменяет идею отдельной страницы рабочего пространства.
> **СТАТУС: ВЫПОЛНЕН 2026-08-23.** Заменяет идею отдельной страницы рабочего
> пространства.
## 1. Модель
1. **Главная страница — это рабочая область.** Без выбранного инструмента на ней большой поиск: начните вводить название — список отфильтровывается; выбор разворачивает инструмент прямо здесь, без перехода.
2. Развернутый инструмент выглядит как привычная страница: слева исходник, справа результат со своей кнопкой скачивания, параметры — в отдельной карточке под сценой.
3. Под результатом рядом с кнопкой скачивания — кнопка **«⛓ Chain with…»**: добавляет пустую секцию, внутри которой такой же поиск; выбор заполняет секцию следующим инструментом. Так цепочка растёт сколько угодно.
4. Каждая заполненная секция цепи — полноценная: свои параметры, своё превью, своя кнопка скачивания промежуточного результата. Крестик убирает секцию; кнопка «✂ Remove chain» снимает всю цепь разом (без подтверждения).
5. Чекбокс «Показать маску» живёт в карточке параметров базового инструмента и влияет только на его превью: в цепь всегда передаётся сам результат.
6. Сетка-каталог инструментов переезжает на отдельную страницу `/list-tools` — для SEO и визуального поиска. Основная работа происходит на главной; в шапке появляются ссылки на обе.
7. Прямые адреса `/tools/<id>` сохраняются: тот же развернутый инструмент для глубоких ссылок.
8. Цепочка переживает перезагрузку (автосохранение в localStorage). Экспорт/импорт файлом отложен до появления CLI/wasm-части.
1. **Главная страница — это рабочая область.** Без выбранного инструмента на ней
большой поиск: начните вводить название — список отфильтровывается; выбор
разворачивает инструмент прямо здесь, без перехода.
2. Развернутый инструмент выглядит как привычная страница: слева исходник,
справа результат со своей кнопкой скачивания, параметры — в отдельной
карточке под сценой.
3. Под результатом рядом с кнопкой скачивания — кнопка **«⛓ Chain with…»**:
добавляет пустую секцию, внутри которой такой же поиск; выбор заполняет
секцию следующим инструментом. Так цепочка растёт сколько угодно.
4. Каждая заполненная секция цепи — полноценная: свои параметры, своё превью,
своя кнопка скачивания промежуточного результата. Крестик убирает секцию;
кнопка «✂ Remove chain» снимает всю цепь разом (без подтверждения).
5. Чекбокс «Показать маску» живёт в карточке параметров базового инструмента и
влияет только на его превью: в цепь всегда передаётся сам результат.
6. Сетка-каталог инструментов переезжает на отдельную страницу `/list-tools`
для SEO и визуального поиска. Основная работа происходит на главной; в шапке
появляются ссылки на обе.
7. Прямые адреса `/tools/<id>` сохраняются: тот же развернутый инструмент для
глубоких ссылок.
8. Цепочка переживает перезагрузку (автосохранение в localStorage).
Экспорт/импорт файлом отложен до появления CLI/wasm-части.
## 2. Архитектура
- Страница инструмента уже собрана как самодостаточный компонент, принимающий запись реестра, — он и становится встраиваемым блоком. Маршруты `/tools/[id]` и главная используют один и тот же компонент: первая — по адресу из URL, вторая — по выбору из поиска.
- Состояние главной: выбран ли инструмент; если да — рендерится блок инструмента, целиком владеющий своим исходником, параметрами, результатом и цепочкой.
- Страница инструмента уже собрана как самодостаточный компонент, принимающий
запись реестра, — он и становится встраиваемым блоком. Маршруты `/tools/[id]`
и главная используют один и тот же компонент: первая — по адресу из URL,
вторая — по выбору из поиска.
- Состояние главной: выбран ли инструмент; если да — рендерится блок
инструмента, целиком владеющий своим исходником, параметрами, результатом и
цепочкой.
- Смена инструмента на главной (через повторный поиск) сбрасывает блок целиком.
- Цепочка хранится массивом шагов; у пустого звена инструмент ещё не выбран — такие звенья не сохраняются в localStorage и не участвуют в прогоне.
- Исполнение — последовательная свёртка: выход каждого звена — вход следующего; результаты всех звеньев доступны их секциям. Общий авто-раннер даёт токен отмены и задержку ~300 мс.
- Цепочка хранится массивом шагов; у пустого звена инструмент ещё не выбран —
такие звенья не сохраняются в localStorage и не участвуют в прогоне.
- Исполнение — последовательная свёртка: выход каждого звена — вход следующего;
результаты всех звеньев доступны их секциям. Общий авто-раннер даёт токен
отмены и задержку ~300 мс.
## 3. Переиспользование и доработки существующего
Готовое берётся как есть: `SourceCard`, `Preview`, `ParamForm`, `DownloadButton`, `EmptyState`, модуль пайплайна, авто-раннер, `PNG_OUTPUT`.
Готовое берётся как есть: `SourceCard`, `Preview`, `ParamForm`,
`DownloadButton`, `EmptyState`, модуль пайплайна, авто-раннер, `PNG_OUTPUT`.
Доработки:
- **Блок страницы инструмента** — расширяется секцией цепочки: список заполненных звеньев, каждое со своими параметрами, превью и скачиванием, плюс пустые звенья с поиском. Кнопки «⛓» и «✂ Remove chain» — рядом со скачиванием базового результата.
- **`ParamsCard`** — опциональный блок «Показать маску» (флаг preview у инструмента + двусторонний флаг).
- **Карточка результата** — упрощается: получает готовую картинку, маска уходит в карточку параметров.
- **Существующая главная-каталог** — переезжает на маршрут `/list-tools` почти без изменений.
- **Блок страницы инструмента** — расширяется секцией цепочки: список
заполненных звеньев, каждое со своими параметрами, превью и скачиванием, плюс
пустые звенья с поиском. Кнопки «⛓» и «✂ Remove chain» — рядом со скачиванием
базового результата.
- **`ParamsCard`** — опциональный блок «Показать маску» (флаг preview у
инструмента + двусторонний флаг).
- **Карточка результата** — упрощается: получает готовую картинку, маска уходит
в карточку параметров.
- **Существующая главная-каталог** — переезжает на маршрут `/list-tools` почти
без изменений.
Удаляется: черновой маршрут рабочего пространства и его компоненты списка/селектора — вместо них новые, под вертикальную модель.
Удаляется: черновой маршрут рабочего пространства и его компоненты
списка/селектора — вместо них новые, под вертикальную модель.
## 4. Новые компоненты
- **`ToolSearch.svelte`** — поиск с нечётким совпадением: поле ввода, отфильтрованный список (регистронезависимая подпоследовательность по названию и описанию, приоритет совпадению с начала), клавиатура ↑↓ Enter Esc; два размера — герой на главной и компактный внутри пустого звена. Предлагает только инструменты-трансформеры.
- **`ChainStep.svelte`** — секция одного звена: пустое звено показывает компактный поиск; заполненное — заголовок с номером и крестиком, форму параметров, превью результата, кнопку скачивания.
- **`ChainBar.svelte`** — строка под результатом базового инструмента: «⛓ Chain with…» и «✂ Remove chain» (видна только при непустой цепи).
- **`ToolSearch.svelte`** — поиск с нечётким совпадением: поле ввода,
отфильтрованный список (регистронезависимая подпоследовательность по названию
и описанию, приоритет совпадению с начала), клавиатура ↑↓ Enter Esc; два
размера — герой на главной и компактный внутри пустого звена. Предлагает
только инструменты-трансформеры.
- **`ChainStep.svelte`** — секция одного звена: пустое звено показывает
компактный поиск; заполненное — заголовок с номером и крестиком, форму
параметров, превью результата, кнопку скачивания.
- **`ChainBar.svelte`** — строка под результатом базового инструмента: «⛓ Chain
with…» и «✂ Remove chain» (видна только при непустой цепи).
## 5. Этапы и проверки
- **A. Поиск и главная** — `ToolSearch`, герой поиска на главной, встраивание блока инструмента, перенос каталога на `/list-tools`, ссылки в шапке. Проверка: `check/build`, визуально.
- **B. Цепочка** — состояние цепи в блоке инструмента, пустые звенья с поиском, прогон, промежуточные превью и скачивания, ошибки звеньев. Проверка: смоук §8 пп. 1–6.
- **C. Маска и упрощение** — блок маски в `ParamsCard`, упрощение карточки результата. Проверка: смоук §8 п. 7.
- **D. Хранение и итог** — автосохранение и восстановление цепочки, удаление чернового маршрута рабочего пространства, полный регресс каталога, `test/check/build`.
- **A. Поиск и главная** — `ToolSearch`, герой поиска на главной, встраивание
блока инструмента, перенос каталога на `/list-tools`, ссылки в шапке.
Проверка: `check/build`, визуально.
- **B. Цепочка** — состояние цепи в блоке инструмента, пустые звенья с поиском,
прогон, промежуточные превью и скачивания, ошибки звеньев. Проверка: смоук §8
пп. 16.
- **C. Маска и упрощение** — блок маски в `ParamsCard`, упрощение карточки
результата. Проверка: смоук §8 п. 7.
- **D. Хранение и итог** — автосохранение и восстановление цепочки, удаление
чернового маршрута рабочего пространства, полный регресс каталога,
`test/check/build`.
## 6. Смоук-чеклист
1. На главной — большое поле поиска; ввод фильтрует список по мере набора; Enter выбирает подсвеченный.
2. Выбранный инструмент разворачивается на той же странице: исходник, параметры, результат.
3. «⛓ Chain with…» добавляет пустую секцию с компактным поиском; выбор заполняет её.
1. На главной — большое поле поиска; ввод фильтрует список по мере набора; Enter
выбирает подсвеченный.
2. Выбранный инструмент разворачивается на той же странице: исходник, параметры,
результат.
3. «⛓ Chain with…» добавляет пустую секцию с компактным поиском; выбор заполняет
её.
4. У каждой секции своё превью и своё скачивание промежуточного результата.
5. Крестик убирает секцию; «✂ Remove chain» снимает все звенья разом.
6. Ошибка звена — баннер «Шаг N (имя): …»; последние успешные результаты секций не исчезают.
7. «Показать маску» переключает превью базового инструмента; в цепь идёт результат, скачивание базового отдаёт результат, не маску.
8. Перезагрузка восстанавливает цепочку заполненных звеньев; пустые звенья при этом исчезают.
6. Ошибка звена — баннер «Шаг N (имя): …»; последние успешные результаты секций
не исчезают.
7. «Показать маску» переключает превью базового инструмента; в цепь идёт
результат, скачивание базового отдаёт результат, не маску.
8. Перезагрузка восстанавливает цепочку заполненных звеньев; пустые звенья при
этом исчезают.
9. Быстрые правки подряд — один финальный пересчёт всей вертикали.
10. `/list-tools` открывает старый каталог по категориям; прямые адреса `/tools/<id>` работают.
10. `/list-tools` открывает старый каталог по категориям; прямые адреса
`/tools/<id>` работают.
## 7. Критерии готовности
- Работа начинается с главной: поиск → инструмент → цепочка звеньями, каждый со своим превью и скачиванием.
- Отдельной страницы рабочего пространства нет; каталог доступен на `/list-tools`; прямые ссылки инструментов работают.
- Работа начинается с главной: поиск → инструмент → цепочка звеньями, каждый со
своим превью и скачиванием.
- Отдельной страницы рабочего пространства нет; каталог доступен на
`/list-tools`; прямые ссылки инструментов работают.
- Пустые звенья не попадают в сохранение и не ломают прогон.
- `test/check/build` зелёные, смоук §6 пройден.
## 8. Что сознательно не делаем
- Ветвления и прицепывание от середины цепи — новое звено всегда добавляется в конец.
- Ветвления и прицепывание от середины цепи — новое звено всегда добавляется в
конец.
- Пипетка и маски внутри звеньев цепи — они остаются фичей базового инструмента.
- Экспорт и импорт цепочки файлом — отложено до появления CLI/wasm-части.
- Несколько параллельных инструментов на главной одновременно — один активный блок за раз.
- Несколько параллельных инструментов на главной одновременно — один активный
блок за раз.
+173 -1
View File
@@ -1,3 +1,175 @@
# Backlog
(пусто — выполненные волны лежат в docs/archive/; кандидат на следующую волну: текст и водяные знаки — add-text, watermark, штампы, см. roadmap фаза 1.5 и план wave3 §7.)
## Мелочи всякие
- [ ] сгенерировать или найти файлы для ручной проверки `A. Краевые PNG-файлы`
- [ ] Переделать внешний вид инструментов без настроек, например Remove alpha
channel PNG "remove-alpha-channel-png". Сейчас выглядит слишком пустым.
- [ ] Убрать описание формата в отображении исходник\результат - это должно быть
в кнопке Download
- [ ] Кнопка download должна сразу писать какой формат сейчас, быть с селектором
формата и подсвечивать предупреждение если формат не совместим (альфа
канал)
- [ ] Пересмотреть поля - Plate, Position, Font Style - сейчас второй label
- [ ] Font Style - обдумать выбор шрифта (google fonts???) как организовать
шрифты с кириллицей? сделать галочку "только с кириллицей"???
- [ ] Посмотреть темную тему - слишком темная???
- [ ] на картинке результате при работе дергается высота надписи (высота иконки)
- [ ] Глянуть что за ошибка (воспроизвелось на blur-png, в том числе и повторно)
[баг в хроме](https://issues.chromium.org/issues/556160936)
`Uncaught TypeError: Cannot read properties of undefined (reading 'startTime')`
`at et.reportAllChanges (anonymous:2:19429)`
- [ ] сделать загрузку картинки исходника по ctrl+v и drag-and-drop (dropzone)
- [ ] не работают генераторы картинок (create-empty-png)
- [ ] square-mask-png - проверить работу - параметры неверно считаются. width -
100, height - 100, x - 100, y - 100, обрезает 50% на 50% картинку
- [ ] прикрутить кастомный color picker (есть в old), добавить прозрачность
(range?)
- [ ] проверить настройки range slider на crop-png - слишком большие значения
- [ ] для range slider field - добавить поле для ввода цифр и кнопку сброса
значения
- [ ] для keep Aspect field - пересчитывать второе поле, учитывать какое было
изменено
- [ ] поле ввода текста - сделать по умолчанию нормального размера, а не одну
строку для svg - форматирование или подсветку синтаксиса??? для bin\hex -
форматирование по колонкам??? для hex\bytes\rgba - пример в placeholder
- [ ] обновить страницу kit или сделать storybook
- [ ] придумать процесс редактирования и настройки pipeline - drag-n-drop,
удаление, добавление инструментов. переход один инструмент - pipeline без
перезагрузки
## Переезд
- [ ] составить план переноса preview -> корень, старый корень -> old
- [ ] добавить i18n в preview
## Баги preview
Зарегистрированы при написании e2e (`web/e2e/`, `pnpm --dir web test:e2e`) и
ручного чек-листа — см. `docs/checklist-manual-testing.md` (раздел G). Каждый
зафиксирован как `test.fixme`. После фикса бага: убрать соответствующий
`test.fixme` и отметить пункт G в чек-листе как пройденный.
1. **Генераторы без кнопки «Generate»** — все 21 инструмент категории GENERATE
открываются, но в UI нет кнопки Generate и полей схемы (только RU/EN/Reset);
результат через UI недостижим. В `SchemaToolView.svelte` `isGenerator`
определяется как `tool.generate && !tool.run`, кнопка рисуется только если
`ongenerate` прокинут, а для генераторов с дефолтным `input: "file"` — нет.
Ожидание: поля схемы + кнопка «Generate», результат-картинка по дефолтам.
Fixme: `web/e2e/generators.spec.ts`. Кнопки и не должно быть, все процессы
должны запускаться автоматически
2. **resize-png с дефолтами падает** — схема по умолчанию `size: 0×0` +
`keepAspect: true` → при загрузке ЛЮБОГО PNG сразу `errors.resizeSize`
(alert), результата нет. `registry-new/geometry.ts`. Ожидание: осмысленный
дефолт-размер (например, исходный) либо кнопка, а не авто-ошибка.
3. **crop-png с дефолтами падает** — аналогично: `width: 0, height: 0` → сразу
`errors.cropSize`. `registry-new/geometry.ts`. Ожидание: базовый кроп по
умолчанию либо внятное предзаполнение.
4. **Сырые ключи ошибок в UI** — alert на tool-странице показывает сырой ключ
i18n (`errors.resizeSize`), а не сообщение из `en.ts` / `ru.ts` (там
`resizeSize: "Width and/or height must be positive"`). Проверяется на
resize-png / crop-png. Ожидание: человекочитаемое сообщение на текущем языке.
Fixme: `web/e2e/known-issues.spec.ts` (тест «error message localized»).
## Идеи
1. **Менять первый инструмент цепочки** — оценка M. Сейчас базовый инструмент
зафиксирован при открытии страницы; хочется заменить его на другой без
разборки цепочки: шаги и их параметры сохраняются, вход перечитывается.
Открытый вопрос: что делать с несовместимыми параметрами (text-source ↔
file).
2. **UI-эксперимент: параметры между исходником и результатом** — оценка S/M.
Раскладка «Исходник → Параметры → Результат» в один ряд вместо параметров
отдельным блоком снизу. Проверить на узких экранах; возможно за флагом/A-B,
чтобы сравнить с текущей. Перекрыто редизайном: старый план
(`docs/archive/plan-inline-params.md`) закрыт, страницы инструментов теперь
`SchemaToolView`/`SchemaPreview`. Эксперимент переосмыслить уже для нового
этапа инструментов (сетка source|params|result), если понадобится.
3. **Сворачивать инструменты в chain** — оценка S. Тоггл сворачивания звена до
заголовка «Шаг n: название» (превью скрываются). Состояние свёрнутости
помнить в workspace-pipeline.
4. **Несколько цепочек: сохранение и загрузка** — оценка M. Именованные цепочки
в localStorage (список, создать/переименовать/удалить), быстрое переключение.
Формат экспортного JSON расширить опциональным полем имени, старые файлы
читаются как безымянные.
5. **Batch-обработка архивов выбранным chain** — оценка L. Вход — zip-архив
картинок (распаковка в браузере), прогон текущего или выбранного сохранённого
chain по каждому файлу, сборка результата обратно в zip для скачивания.
Зависит от п.4 (выбор цепочки) и от экспортного формата pipeline JSON;
прогресс и ошибки — пофайлово.
6. **Избранные инструменты (fav tools)** — оценка S/M. Тоггл-звёздочка на
карточках каталога, результатах поиска и в шапке страницы инструмента; список
id в localStorage. Избранное показывается отдельной секцией сверху каталога и
поднимается в выдаче поиска (бонус к popularity при скоринге).
7. **Конвертация формата — НЕ отдельный инструмент, а выбор в кнопке Download**
— оценка M/L. Инструменты `convert-png-to-jpg` / `convert-png-to-webp` (и др.
форматы) не должны жить как самостоятельные инструменты цепочки. Вместо этого
— бесшовное внедрение выбора формата/качества прямо в кнопку/диалог
**Download**: юзер скачивает результат в нужном формате (mime/ext/quality), а
pipeline при этом не усложняется лишним звеном. **Сделано (2026-09):**
механическая часть реализована в preview — `ToolEntry.output`
(`OutputFormat`: mime/ext/qualityParamId) в `registry-new/types.ts`;
`SchemaToolView.download()` кодирует по `output` (jpg/webp/bmp уже настроены;
`png-to-bmp` переведён сюда же). **Осталось (UX-райз «Download»):**
- выбор формата/качества прямо в кнопке/диалоге Download для любого
результата (не отдельными конвертерами);
- экспорт с лимитом размера → закрывает отложенный `reduce-to-size-png`
(целевой KB, бинарный поиск по k из квантования). Проработать вместе с
достройкой download-флоу нового UI.
8. **`watermark-image-png` (водяной знак картинкой)** — оценка M. Отложен из
миграции (`archive/plan-migrate-remaining.md`). Нужна overlay-механика:
второй источник изображения (getOverlay/store), параметры scale/opacity/
position/margin/aspect. Решается вместе с вопросом, как второй источник
картинки вписывается в schema-driven preview (`SchemaToolView`).
9. **`png-info` (детальная информация о PNG)** — оценка M/L. Отложен из
миграции. Отдельный райз: exif-теги, редактирование, структурированный вывод;
нужен свой отдельный случай в UI, не «ещё один text/verdict».
10. **Region-инструменты** (censor/erase/pixelate-area/blur-area/sharpen-area/
reverse-area) — ждут UI выделения области на превью. Из
`archive/plan-gap-waves.md` («вне очереди»).
11. **Мультифайловый вывод** (split-parts, gif-frames, separate-colors) — ждут
механизма «результат = набор файлов». Из `archive/plan-gap-waves.md`.
12. **Анимационные** (slow-reveal/fade/scrolling) — выход не PNG; отдельное
решение о формате. Из `archive/plan-gap-waves.md`.
13. **HARD-серия** (glitch, barcode, signature-extract, handwritten-digital). Из
`archive/plan-gap-waves.md`.
14. **Переезд старых файлов в папки `old/`** — оценка M. Шаг 37 закрытого
`archive/plan-composite-params.md``archive/plan-migrate-remaining.md`):
перенос `lib/registry.ts`, `lib/registry/**`, `lib/registry-helpers.ts`,
`lib/categories.ts`, `lib/tools/**`, `lib/components/**` (кроме `kit/`) в
`lib/old/**`; в настройке isolation-плагина — один glob-паттерн
(`lib/old/**`), «новое»/«общее» не меняются. Предшествует C17
(`plan-redesign.md`, Шаг 5). Гейты: svelte-check 0, тесты зелёные, prettier
чист, `lint:all` = задокументированный остаток, обе ветки открываются.
15. **Техдолг (чинить по заведённым задачам, не игнорировать правила)**:
- svelte-check error `ToolCard.svelte:19:36` (`goto(string)`).
- фейл `palette.test.ts` (ENOENT `src/app.css` — из-за него `pnpm test`
красный, хотя тесты 607 passed).
- `lint:all`: ~335 design-tokens ошибок в старых kit-компонентах;
неиспользуемые токены preview.css; a11y-warning `ColorField`; пустой
ruleset `Toggle.svelte`. Целевое состояние перед переездом `old/` (см.
№14): svelte-check 0 errors, `pnpm test` зелёный, `lint:all` — только
задокументированный остаток.
16. **Контраст текста на акцентных кнопках — пересмотреть после готовности
сайта**. Текст `--color-background` на фоне `--color-main`/`--color-accent`
(тон 48) даёт контраст ~4.1–4.3 (light) — ниже AA 4.5 для обычного текста;
`palette.test.ts` держит для кнопок порог 4.0 (AA large-text) осознанно, как
компромисс текущего дизайна. После полного завершения редизайна пересмотреть
тона (например поднять tone кнопок до ~55) и вернуть порог 4.5. Также
проверить `--color-danger` (dark, тон 55): текст на нём 4.93 — ок, но
light-danger тон 48 = 4.25 на границе.
+76
View File
@@ -0,0 +1,76 @@
# Чек-лист ручного тестирования preview
> Вспомогательный документ к `docs/archive/plan-testing-preview.md` и e2e-тестам
> (`web/e2e/`, `pnpm --dir web test:e2e`). Автоматизированная часть закрывает
> «нет падений/ошибок»; здесь — то, что руками, и что Playwright не покрывает.
>
> Перед прогоном: `pnpm --dir web build`, поднять локально `web/` (dev или build
> → `node scripts/serve-static.mjs`).
>
> Критерий приёмки большинства пунктов: **нет зависит, нет «залипания»,
> сообщения об ошибках человекочитаемые**.
## A. Краевые PNG-файлы
- [ ] Огромное изображение (мегапиксели) — загрузка не висит, прогресс есть.
- [ ] PNG 1×1 — инструменты не падают (проверено авто: flip-png, см. suite).
- [ ] PNG с полупрозрачностью/чётким альфа-краёв (чёрно-белая шахматка) —
remove-background, feather-edges, clean-edges дают корректные края.
- [ ] Палитровый PNG (индексированные цвета) — конвертируется/анализируется.
- [ ] Анимированный/APNG, если попадётся — не ломает конвейер.
- [ ] Повреждённый/не-PNG файл — понятная ошибка, нет краша (авто-чека в
pipeline.spec.ts).
- [ ] PNG с битой CRCh / 16-bit — как ведут себя анализаторы (ориентация,
размер, прозрачность).
## B. Края параметров
- [ ] Экстремальные значения слайдеров (0 и max) в blur/sharpen/pixelate — не
«залипает» running.
- [ ] Пустые/NaN поля (напр. width/height) — санитизация (silent repair), нет
красного alert-текста в виде `errors.*`.
- [ ] Невалидные цвета в плашках/градиентах (мисс-спелл `#xyz`, короткие hex,
именные цвета) — silent repair или понятная ошибка.
- [ ] Пустой text-source на text-инструменте — run пропускается, ошибок нет
(кейс из плана п.14).
- [ ] Смена параметра после Reset — авто-перезапуск работает (Debounce 200 мс).
## C. Буфер обмена и drag-n-drop
- [ ] В реальном браузере кнопка copy на текстовом результате (png-to-base64 и
т.п.) кладёт текст в буфер (в Playwright не тестируем из-за permissions).
- [ ] Drag-n-drop файла на dropzone страницы инструмента (не только файл-диалог)
— UX-ощущения и корректность.
## D. Производительность и отзывчивость
- [ ] Долгие авто-рераны на больших картинках (jpeg-artifacts, dithering,
quantize на 4К) — UI не фризит, канселяция/дебаунс срабатывает.
- [ ] Скролл страницы при длинных настройках (например, text-инструменты) — нет
залипаний, панели не прыгают.
- [ ] Вкладка не «съедает» память при 10+ повторах blur/sharpen на мегапикселе
(утечек blob-URL/bitmap быть не должно).
## E. Визуальный слой (доделываем по ходу)
- [ ] Сетки/выравнивание на брейкпоинтах 640 / 800 / 1100 (mobile/tablet/
desktop) на всех 4 маршрутах.
- [ ] Тема light/dark: контраст вердиктов (Yes/No), статус «LIVE PREVIEW»,
мета-инфо бликов не даёт.
- [ ] Иконки в каталоге на всех категориях — не «бьются» (missing icon).
- [ ] Язык RU/EN на превью-страницах — переключение без регресса рендера.
## F. Субъективная корректность
- [ ] dithering / two-colors / quantize — результат «по ощущениям» соответствует
описанию.
- [ ] watermark-tile / add-text / date-stamp на кириллице — рендер текста
корректный (не «кракозябры»).
- [ ] remove-background на сложной полупрозрачности — края не «звенят».
## Как долго
- Полный прогон: ~40 минут (все разделы, включая мегапиксели и 4К) в двух
браузерах.
- Быстрый смоук (< 15 мин): A (кроме мегапикселей), B (выборочно), C, F
выборочно, G обязательно.
+432
View File
@@ -0,0 +1,432 @@
# План: переезд на новый дизайн (refs) — параллельная сборка
> Статус: план к выполнению. Шаги 5–6 (C17–C21) остаются **следующим этапом**.
> Они больше не привязаны к «верности против рефа»:
> `docs/archive/plan-design-fix.md` закрыт (2026-09-07, дизайн ушёл вперёд от
> `refs-html/`), аудит против рефа не информативен и прекращён. Дальнейшее
> качество — ручное ревью + `lint:all`. Шаг 37 (переезд старых файлов в папки
> `old/`) вынесен в `backlog.md`.
>
> Источники: `refs/` — Next/React-референс (правда по пикселям и интерактиву),
> `refs-html/` — статические HTML-снимки для быстрого просмотра в браузере.
>
> **Стратегия изменена.** Вместо in-place рестайла (старый план A–F, правка
> общих компонентов на месте с алиасами токенов) — **параллельная сборка**:
> новый дизайн строится отдельно, изолируется от старого, проверяется целиком,
> затем становится основным и старый удаляется. Старое приложение остаётся
> рабочим и шippable на всём протяжении перехода.
## 0. Почему меняем стратегию
- **Старый план (A–F):** правим `ui/*` и страницы на месте, держим старые токены
как алиасы, чтобы ничего не сломалось до этапа F. Риск: общие компоненты
редактируются вживую → регресс старого UI во время работы, новый дизайн нельзя
принять изолированно.
- **Новый подход (5 фаз):** (1) строим новый ui-kit, (2) строим новые страницы
рядом, не трогая старые, (3) проверяем новый дизайн, (4) делаем его основным,
(5) удаляем старый. Ниже фазы 1–5 — это требуемые шаги.
- **Главный технический риск и его решение.** Сейчас `web/src/app.css`
импортируется в **корневом** `web/src/routes/+layout.svelte:2` и потому
грузится на ВСЕХ маршрутах, включая новые. Решение — **не префиксовать новый
CSS (никакого `.ds2`), а разнести глобальные стили по layout'ам без
наследования**: (а) убрать `import '../app.css'` из корневого layout; (б)
старые маршруты обернуть группой `(old)/` с собственным layout, импортирующим
`app.css`; (в) новые маршруты (`preview/*`) — собственный layout,
импортирующий `design2.css`. SvelteKit делает code-splitting CSS по маршрутам,
а стили компонентов скоупятся сами — поэтому глобальные стили каждого дизайна
живут только в своей ветке, и префикс-обёртка `.ds2` не нужна. Старые `ui/` и
страницы не трогаются до фазы 5.
## 1. Источники и их оценка
### `refs-html/` — витрина разметки
Покрытие достаточное:
| Файл | Что показывает | Оценка |
| ------------------------- | ----------------------------------------- | ---------------- |
| `index.html` | Главная: pipeline-воркспейс + превью | ✅ |
| `demo.html` | Зеркало `index.html` (идентичны) | ⚠️ дубликат |
| `gradient.html` | Инструмент «Градиент»: настройки + превью | ✅ (тёмная тема) |
| `background-remover.html` | Инструмент «Удаление фона»: source/result | ✅ (тёмная тема) |
| `list-tools.html` | Каталог инструментов | ✅ |
- **Статика без JS** — интерактив (тумблеры/слайдеры/перетаскивание) брать из
кода `refs/`, не из HTML. HTML — только визуальная правда по разметке/классам.
- По 4 файлам собран инвентарь классов — это и есть спецификация kit'а:
`app-shell`, `page-grid`, `eyebrow`, `brand-mark`, `topbar`, `footer`,
`panel-heading`, `setting-group`, `settings-footer`, `settings-panel`,
`segmented`, `status`, `step-card` (`step-index`/`step-heading`/`step-type`/
`step-body`/`drag`), `color-field` (`swatch`/`native-color`),
`preview-toolbar`/`checker`
(`tile-canvas`/`remover-canvas`/`comparison-grid`), `result-meta`/`meta-row`,
`tool-card`/`image-card` (`tool-icon`/`tool-index`/ `tool-copy`),
`code-block`, `icon-btn`, `download-btn`, `reset-btn`, `lede`, `auto-note`,
`label`, `toggle`/`toggle-group`, `direction-grid`, `gradient-bar`.
- **Дыры в refs-html:** нет состояний empty/error/loading, нет глобальных
модалок/тултипов, i18n не представлен (все лейблы — английские брендовые).
Решения — см. §9.
## 2. Токены (фундамент нового дизайна)
Живут в `src/lib/styles/design2.css` как обычный `:root { … }` (плюс
`[data-theme='dark'] { … }`). Файл грузится **только** layout'ом новой ветки
(§5), поэтому глобально не конфликтует со старым `app.css`. Маппинг старых
токенов `web/src/app.css` → новые:
| Старый | Новый | Light | Dark |
| -------------- | -------------- | --------- | ------------------------ |
| `--bg` | `--background` | `#eef1f4` | `#11171d` |
| `--surface` | `--panel` | `#f8fafb` | `#182129` |
| `--text` | `--foreground` | `#17212b` | `#e8eef2` |
| `--text-muted` | `--muted` | `#6d7883` | `#91a0ac` |
| `--border` | `--line` | `#cbd3da` | `#33414c` |
| `--accent` | `--blue` | `#1769d2` | `#54a2ff` |
| — | `--cyan` | `#00a8c7` | `#00a8c7` |
| — | `--amber` | `#bd7411` | `#bd7411` |
| — | `--success` | `#25a96a` | `#25a96a` (статус-точки) |
| `--danger` | без изменений | `#e5484d` | `#ff7479` |
Прочее:
- Радиусы: один `--radius: 4px` (мягкость старой темы уходит).
- Шрифты: `IBM Plex Sans` + `IBM Plex Mono`, self-host через `@fontsource`
(offline-first, CDN нельзя). Mono — рабочий шрифт для лейблов/метаданных/
цифр; Sans — заголовки и текст. Задаются на `.app-shell` (корень новой ветки),
чтобы не зависеть от старых глобальных правил `body`.
- Фон-сетка (32px blueprint-grid) и шахматка канвасов — утилиты `.page-grid` и
`.checker-canvas` внутри `.ds2`, не токены.
- Тёмная тема — на `[data-theme='dark']` (инлайн-скрипт уже есть в `app.html`);
класс `.dark-mode` из рефа не переносим, используем наш механизм. На
переходный период **никаких алиасов старых токенов** (старые страницы сами
живут на своём `app.css`).
## 3. Базовые примитивы дизайн-системы (все — компоненты)
Правило фазы 1: **любой повторяющийся паттерн — компонент**, даже «просто div с
двумя стилями». Никакой копипасты классов между страницами. Каждый примитив ниже
— отдельный `.svelte` в `src/lib/components/kit/`.
- `MonoLabel` — микро-лейбл: mono 10px, uppercase, letter-spacing .1em, muted;
слот-модификатор accent (синие eyebrow/version).
- `StatusDot` / `StatusLine` — точка `--success` + подпись («AUTO PIPELINE»,
«LIVE PREVIEW»).
- `Panel` / `PanelHeading` / `SettingGroup` / `SettingsFooter` — панель с
1px-бордером, составные части с разделителями.
- `CheckerCanvas` — рабочая поверхность превью: тёмная подложка + шахматка.
- `MetaRow` / `MetaList` — пары «CAPTION значение».
- `Segmented` — сегмент-переключатель (язык, тип градиента, пресеты).
- `Badge` — тег типа шага (BACKGROUND/TRANSFORM/STYLE), чип «✓ AUTO».
- `StepCard` — нумерованная карточка этапа: индекс `01`, drag-handle, Badge
типа, заголовок, кнопка удаления; слот для параметров.
- `ToolCard` — карточка каталога (icon, index, title, copy, arrow).
- `ImageCard` / `PreviewTile` — label сверху + canvas-превью.
- `AppShell` — корневой контейнер `.app-shell` + `.page-grid` (обёртка новой
ветки маршрутов); задаёт шрифты и фон нового дизайна.
- `BlueprintGrid` — фоновая сетка (используется в `AppShell`).
- `IconButton`, `Icon` (обёртка `lucide-svelte`, 1617px, muted).
## 4. Минимальный UI-kit (`src/lib/components/kit/`)
Контролы, поверх примитивов §3. API проектируем сразу под новый дизайн (обратной
совместимости со старым `ui/` не нужно — старые страницы его продолжают
использовать).
| Компонент | Назначение |
| ------------------------- | ----------------------------------------------------------------- |
| `Button.svelte` | `primary` (solid blue) / `secondary` (outline) / `ghost` + `icon` |
| `Field.svelte` | обёртка: `MonoLabel` + слот значения/контрола |
| `SliderField.svelte` | range + `<output>`, опц. строка подсказок |
| `ColorField.svelte` | swatch + hex-input + нативный picker |
| `TextField.svelte` | текстовый ввод в стиле дизайна |
| `SelectField.svelte` | селект в стиле дизайна |
| `CheckboxField.svelte` | чекбокс → switch-toggle 32×18 |
| `Toggle.svelte` | переключатель (для toggle-group) |
| `SegmentedControl.svelte` | на базе `Segmented` — RU/EN, Linear/Radial, пресеты |
| `Badge.svelte` | на базе `Badge` |
| `MetaList.svelte` | на базе `MetaRow` |
| `EmptyState.svelte` | пустое состояние под новый стиль |
| `CodeBlock.svelte` | блок кода (gradient-art / direction-grid) |
| `DownloadButton.svelte` | кнопка скачивания (можно переиспользовать идею старой) |
Иконки: `lucide-svelte` (те же пути, MIT), 1617px, stroke, muted.
## 5. Изоляция старого и нового (критично)
Без префиксов и обёрток — только за счёт того, какие layout грузят какой CSS.
- **Корневой layout `src/routes/+layout.svelte`** перестаёт импортировать
`../app.css`. В нём остаётся только общая логика (init темы/locale) и
нейтральный `<div>{children}</div>` без глобальных стилей дизайна.
- **Старая ветка `src/routes/(old)/+layout.svelte`** импортирует `app.css` и
содержит текущую шапку/подвал (разметку из нынешнего корневого layout). Все
старые страницы переносятся внутрь `(old)/` (группа в скобках не меняет URL):
`(old)/+page.svelte`, `(old)/demo/...`, `(old)/list-tools/...`,
`(old)/tools/[id]/...`.
- **Новая ветка `src/routes/preview/+layout.svelte`** импортирует `design2.css`
и рендерит `<AppShell>`. Сюда же — новые страницы (§6).
- Благодаря code-splitting CSS в SvelteKit: на старых маршрутах грузится только
`app.css`, на `preview/*` — только `design2.css`. Скоуп стилей
Svelte-компонентов добавляет второй уровень изоляции. Никаких `.ds2`.
- Переключатель для ручной проверки: временный флаг `previewDesign` в
`localStorage` + кнопка в старой шапке («Preview v2»), ведущая на `/preview`.
На фазе 4 флаг убираем.
> Почему не `(new)`-группа с теми же путями вместо `preview/*`: две группы не
> могут одновременно определять один и тот же URL (`(old)/+page.svelte` и
> `(new)/+page.svelte` оба → `/` = конфликт маршрутов). Поэтому новая ветка
> живёт под префиксом `preview/`, а на фазе 4 файлы переносятся на реальные пути
> и группа `(old)` удаляется.
>
> Это совпадает с рекомендованным в документации SvelteKit паттерном: «put your
> entire app inside one or more groups except the routes that should not inherit
> the common layouts» — старое целиком в `(old)`, новое снаружи. Синтаксис
> `@`-breakout (`+layout@.svelte`, `+page@.svelte`) здесь не требуется:
> «выломаться» можно лишь до корня, а нам нужно, чтобы именно у корня НЕ было
> `app.css`; если позже захотим держать новые маршруты внутри группы, `@`
> пригодится для сброса родительского layout.
## 6. Маршрутизация параллельных страниц
Новые страницы живут в **отдельной ветке `preview/*`**, не трогая старые:
```txt
src/routes/+layout.svelte # нейтральный, БЕЗ app.css
src/routes/(old)/+layout.svelte # импорт app.css + старая шапка/подвал
src/routes/(old)/+page.svelte # / (старое)
src/routes/(old)/demo/+page.svelte # /demo
src/routes/(old)/list-tools/...
src/routes/(old)/tools/[id]/...
src/routes/preview/+layout.svelte # импорт design2.css, <AppShell>
src/routes/preview/+page.svelte # /preview — воркспейс (index/demo)
src/routes/preview/demo/+page.svelte # /preview/demo
src/routes/preview/list-tools/+page.svelte
src/routes/preview/tools/[id]/+page.svelte
```
- Каждая новая страница импортирует **только** `kit/*`. Старые страницы не
изменяются (только перемещены в `(old)/`).
- Фаза 4: содержимое `preview/*` переносится на реальные маршруты (move-файлы),
группа `(old)/` удаляется, `design2.css` подключается в корневом
`+layout.svelte` (как новый основной слой), временный флаг/кнопка убираются.
## 7. Фазы выполнения (1–5 по запросу)
Каждая фаза заканчивается зелёными `test`/`check`/`build` и ревью в браузере
(обе темы) против `refs-html/`.
- **Фаза 1 — Фундамент + UI-kit.** Токены §2, `design2.css` по §5, все примитивы
§3 и контролы §4 как отдельные компоненты. Критерий: демо-страница kit'а
(`/preview` или временная) показывает все состояния (light/dark,
hover/disabled, все варианты Button/Segmented/Badge); unit-тесты форм не
падают; **старые страницы визуально не изменились** (изоляция работает).
- **Фаза 2 — Новые страницы.** Под `preview/*` (§6): воркспейс, demo,
list-tools, tool-page (двухпанельная раскладка из рефа: settings-panel +
preview-panel, step-cards для chain). Критерий: разметка соответствует
`refs-html/index.html`, `gradient.html`, `background-remover.html`,
`list-tools.html`; интерактив (слайдеры/тоглы/загрузка/автозапуск) работает в
новом стиле; старые маршруты продолжают работать как раньше.
- **Фазa 3 — Проверка нового дизайна.** Смоук §8 (обе темы, узкий экран, 800px),
скриншоты против `refs-html/`, прогон старого тест-сьюта для доказательства
отсутствия регресса, RU/EN-локализация новых лейблов. Критерий: визуальное
совпадение с рефом, зелёные тесты, записанные скриншоты приняты.
- **Фаза 4 — Сделать новый дизайн основным.** Перенос `preview/*` → реальные
маршруты; `design2.css` подключается в корневом `+layout.svelte`; удаление
preview-группы и временного флага/кнопки. Старые страницы теперь не
используются (мёртвый код), но физически ещё в репо. Критерий: реальные
маршруты отдают новый дизайн, тесты зелёные, билд без предупреждений.
- **Фаза 5 — Удалить старый дизайн.** Удаление старых страниц, старого `ui/`,
старого `app.css` (и его токенов), неиспользуемых классов; переименование
`kit/``ui/` (финальное имя) либо оставление `kit/` как основного — решается
на фазе 1. Финальный проход по брейкпоинтам 1200/1100/800/480. Критерий:
`grep` по старым токенам/классам пуст, build чистый, старый дизайн нигде не
упоминается.
Оценка объёма: Фаза 1 — фундамент+kit (~1 день), Фаза 2 — страницы (~1–2 дня),
Фаза 3 — проверка (~полдня), Фаза 4–5 — переключение и чистка (~полдня).
## 8. Смоук (после фазы 3, контроль на фазе 4)
1. Светлая и тёмная тема: сетка фона, панели, шахматка канвасов читаются,
контраст mono-лейблов достаточный.
2. Инструмент с параметрами (градиент): slider/color/toggle в стиле
setting-groups, автозапуск не регрессировал.
3. Удаление фона: сравнение source/result, маска, пипетка работают на новой
шахматке.
4. Chain из 3+ инструментов: step-cards рендерятся, удаление/сброс работают.
5. Узкий экран 800px: настройки над превью, sticky выключается как в рефе.
6. RU/EN: новые лейблы локализованы; mono-лейблы рефа — либо ключи i18n, либо
осознанно английские брендовые элементы (см. §9).
## 9. Открытые вопросы
- **Имя финального каталога компонентов:** оставить `kit/` как основное имя или
переименовать в `ui/` на фазе 5 (удалив старое `ui/`). Рекомендую сразу писать
в `kit/`, на фазе 5 переименовать в `ui/`. Решить на фазе 1.
- **Изоляция через layout:** `app.css` вынесен из корневого layout в
`(old)/+layout.svelte`; новый `design2.css` — только в `preview/+layout`.
Проверить на фазе 1, что на `/preview/*` не попадают стили старого дизайна
(DevTools → вкладка Styles, нет правил из `app.css`).
- **Переключатель для проверки:** рекомендую `localStorage`-флаг + кнопка
«Preview v2» в старой шапке + группа `preview/*`. Убрать на фазе 4.
- **Mono-лейблы рефа на английском** (`PROCESSING PIPELINE`): завести i18n-
ключи, но допустить часть английских брендовых («приборная панель»).
- **Версия в шапке/футере:** из `package.json` через `define` или захардкодить —
мелочь, решить при сборке topbar.
- **`refs/` и `refs-html/` — одноразовый источник, НЕ поддерживаемый.** После
внедрения нового дизайна все правки дизайна делаются сразу в продакшен-коде
(`design2.css` + `kit/`), а не в `refs/`. Механизм синхронизации «ref → код»
не нужен и не создаётся. `refs/` и `refs-html/` можно удалить из репо на фазе
5 (по желанию), они не являются частью рантайма.
## 10. План исполнения (мелкие коммиты)
Ветка `feat/redesign`. Каждый шаг — отдельный коммит, цель < ~500 строк. Перед
стартом: `test`/`build` зелёные на базе (якорь регрессии).
### Шаг 0. Изоляция layout (без дизайна)
- [x] **[C1]** Сделать корневой `routes/+layout.svelte` нейтральным: убрать
`import '../app.css'`, оставить только init темы/locale и
`<div>{children}</div>`.
- [x] **[C2]** Создать `routes/(old)/+layout.svelte` (импорт `app.css` + старая
шапка/подвал) и перенести старые страницы (`+page`, `demo`, `list-tools`,
`tools/[id]`) в `(old)/`. URL не меняются. Проверка: старый сайт
идентичен. _(файлы — move, объём строк минимален)_
### Шаг 1. Фундамент нового дизайна
- [x] **[C3]** Создать `src/lib/styles/design2.css` (токены `:root` +
`[data-theme='dark']` по §2) и подключить `@fontsource/ibm-plex-sans` +
`@fontsource/ibm-plex-mono` (self-host, offline-first). подключить шрифты
в `design2.css` (self-host, offline-first).
- [x] **[C4]** _(слит в C3: шрифты нельзя подключить до создания `design2.css`)_
`[data-theme='dark']` (таблица §2). Без утилитарных классов.
- [x] **[C5]** Примитивы batch 1: `AppShell`, `BlueprintGrid`, `CheckerCanvas`,
`MonoLabel`, `StatusDot`/`StatusLine`.
- [x] **[C6]** Примитивы batch 2: `Panel`, `PanelHeading`, `SettingGroup`,
`SettingsFooter`, `MetaRow`/`MetaList`, `Segmented`, `Badge`, `StepCard`,
`ToolCard`, `ImageCard`/`PreviewTile`.
- [x] **[C7]** Контролы: `Icon`, `IconButton`, `Button`, `Field`, `SliderField`,
`ColorField`, `TextField`, `SelectField`, `CheckboxField`, `Toggle`,
`SegmentedControl` (= `Segmented` из C6), `EmptyState`, `CodeBlock`,
`DownloadButton`.
### Шаг 2. Витрина kit (проверка компонентов)
- [x] **[C8]** `preview/+layout.svelte` (импорт `design2.css` + `<AppShell>` +
переключатель темы) и временная `preview/kit` страница со всеми
состояниями (light/dark, hover/disabled, варианты Button/Segmented/Badge и
пр.)
### Шаг 3. Новые страницы
- [x] **[C9]** `/preview` — воркспейс (eyebrow, h1, lede, поиск, «открыть
последний»). Поиск фильтрует `TOOLS` из `$lib/registry`; карточки
результата пока ведут на `#` (реальные `/preview/tools/[id]` подключим на
C12, иначе prerender падает на 404).
- [x] **[C10]** `/preview/list-tools` — каталог карточек (группировка по
`CATEGORIES`, `ToolCard`). Ссылки пока `#` (реальные `/preview/tools/[id]`
подключим на C12).
- [x] **[C11]** `/preview/demo` — зеркало воркспейса (4 шага + панель превью),
статичный приёмочный стенд.
- [x] **[C12]** `/preview/tools/[id]` — data-driven settings-panel +
preview-panel (gradient live, остальные — плейсхолдер); `NumberField`,
`entries()` для prerender, ToolCard-ссылки подключены.
- [x] **[C13]** `/preview/tools/[id]` — comparison-grid для file-source
инструментов (`Dropzone` + before/after `CheckerCanvas`, `result-meta`);
охватывает background-remover и прочие.
- [x] **[C14]** `/preview/tools/[id]` — исполняемая цепочка: `StepCard`-шаги
(параметры inline) + «Add step» (chainable), живой прогон пайплайна на
исходнике, превью каждого шага + итог + Download.
### Шаг 4. Переключатель и проверка
- [x] **[C15]** В старой шапке ссылка «Preview v2» → `/preview`, ставит
`localStorage['easy-png-tools:preview-v2']='1'`.
- [x] **[C16]** База под дизайн: рабочий переключатель тем (persist в
localStorage, применяется на `<html>`; проверено — фон
`#eef1f4``#11171d`), `TopBar` приведён к виду `demo.html` (бренд + синий
хлебный крош `/* страница */`, безрамочные иконки, язык — active =
`--foreground`/`--background`), страницы `/preview/*` на всю ширину (убран
`max-width`). Точечное выравнивание к рефу отложено: реф-дизайн
неконсистентен. (доказательство отсутствия регресса). Фиксы по результатам
— отдельными мелкими коммитами.
### Шаг 5. Сделать новый дизайн основным — СЛЕДУЮЩИЙ ЭТАП
> Раньше планировалось после «верности к рефу» (Этапы 0–1
> `docs/archive/plan-design-fix.md`) — план закрыт, гейт снят (2026-09-07).
> Перед стартом C17 выполнить переезд старых файлов в папки `old/` (backlog:
> «Переезд старых файлов в `old/`», шаг 37 закрытого
> `docs/archive/plan-composite-params.md`). Пока не трогаем `preview/*` и
> `(old)/`.
- [ ] **[C17]** Перенести `preview/*` → реальные маршруты (move), удалить
`(old)/`, корневой `+layout.svelte` импортирует `design2.css` + новый
shell. Убрать флаг/кнопку. **И обновить `newCode` в
`web/eslint.config.js`:** заменить `src/routes/preview/**` на
`src/routes/**` (после переезда все маршруты — новый дизайн), компонентный
glob `src/lib/components/kit/**` пока оставить.
- [ ] **[C18]** Правки импортов/редиректов после переноса, проверка билда.
### Шаг 6. Удаление старого
- [ ] **[C19]** Удалить старое `ui/`, прочие старые компоненты, старый
`app.css`, неиспользуемые классы. Переименовать `kit/``ui/` (если
решено на C1 — тогда в `eslint.config.js` обновить и компонентный glob).
**И раскрыть ESLint `recommended` на весь код** — убрать scoped-блок
`newCode` в `web/eslint.config.js` (см. AGENTS.md), прогнать `lint` и
починить всплывшие ошибки в оставшемся коде. _Примечание: доменные модули
(`lib/core`, `lib/registry`, тесты и т.п.) — это не «старый дизайн», у них
свой линт-долг; расширение `recommended` на них может выдать много ошибок.
Решить на C19: либо чиним сразу, либо расширяем scoped только на
`src/routes/**` + `src/lib/components/**`._
- [ ] **[C20]** Финальный проход по брейкпоинтам 1200/1100/800/480; build без
предупреждений; `grep` по старым токенам/классам пуст.
- [ ] **[C21]** _(опционально)_ Удалить `refs/` и `refs-html/` из репо — они
больше не источник правды.
> Если какой-то коммит тянет за собой > 500 строк (например, C6/C7), разбивать
> на под-коммиты по 2–4 компонента. Тесты/билд прогонять после каждого коммита,
> чтобы регрессия локализовалась одним шагом назад.
>
> **Атомарность и точки «поломки» между коммитами.**
>
> - C1+C2 уже сделаны вместе (связаны: нейтральный root без `(old)`-layout
> ломает старый сайт). Аналогично связанных пар больше нет.
> - C3 и C4 слиты: шрифты подключаются внутри `design2.css`, поэтому файл должен
> существовать в том же коммите.
> - Коммиты **C3→C16 не трогают старый сайт** `(old)/` — они только добавляют
> новые файлы/маршруты, сломать старый сайт на них нельзя.
> - **C17 — единственный атомарный переключатель**: перенос `preview/*` на
> реальные пути + удаление `(old)/` + смена root-layout должны быть в ОДНОМ
> коммите (иначе конфликт маршрутов или пропажа страниц). Не дробить.
> - **C19 должен идти строго после C17** (старый `app.css`/`ui/` удаляем только
> когда old-сайт уже выключен).
> - Очерёдность зависимостей: `design2.css` (C3) и kit-компоненты (C5–C7) должны
> существовать до страниц, которые их импортируют (C8–C14).
## Бэклог полировки (после C9–C21, отдельным проходом)
Конкретные расхождения витрины `preview/kit` с `refs-html/`. Править системно в
конце, а не по ходу сборки базы.
- **Toggle** в дизайне прямоугольный, у нас — закруглённый. Сделать
прямоугольным (или уточнить радиус у свича отдельно от общих токенов).
- **Радиусы бордеров** не совпадают: в `refs` радиусы вообще 0, а мы взяли
ненулевой `--radius` для всех бордеров. Проверить токен `--radius` против
референсов; возможно он должен быть 0 (или близко), а скругления оставить
только там, где они реально есть в дизайне.
- **DownloadButton**: нет отступа между текстом и размером файла (`size`-чип).
Добавить `gap`/отступ внутри кнопки.
- **Поля ввода (controls)**: выглядят без паддингов (возможно артефакт самой
витрины, но визуально плохо). Проверить паддинги в `TextField`/`SelectField`/
`ColorField`/`SliderField` против референса и поправить.
- **Segmented**: на витрине последняя кнопка растянута и нечитаемо, что это
сегмент. Проверить, как `Segmented` выглядит в реальных раскладках (C9–C14);
возможно нужен фиксированный/авто размер сегментов или перенос.
+103
View File
@@ -0,0 +1,103 @@
# План: закрытие техдолга перед переездом `old/` (C17)
> Статус: план к выполнению. Ветка `feat/redesign`. Входит в гейт перед
> переездом старых файлов в `old/` (backlog №14; см. также
> `archive/plan-migrate-remaining.md` — техдолг «чинить по заведённым задачам,
> не игнорировать правилами»). Целевое состояние по завершении: `svelte-check`
> 0 errors, `pnpm test` → зелёный, `lint:all` → без ошибок (допустим только
> задокументированный остаток), дизайн-токены → единый словарь `preview.css`.
## Состав техдолга (на 2026-09-07)
- **svelte-check error**`ToolCard.svelte:19:36`: `resolve(normalizedHref)`
принимает только типизированный роут, а `normalizedHref: string`. Красный
svelte-check, висит на каждом прогоне.
- **фейл теста**`src/lib/palette.test.ts`: `ENOENT src/app.css` (файл
разъехался на `old.css` + `design2.css` при редизайне). Тест проверял
контрасты палитры по токенам старого `app.css`
(`text/surface/bg/link/ accent/danger/border`). При гейтах `pnpm test`
красный, хотя 607 тестов проходят.
- **design-tokens в `kit/**`** — ~335 ошибок `lint:all` от локального плагина
`eslint-plugins/design-tokens` (захардкоженные цвета/размеры/длительности/
z-index/`color-mix()` в `<style>`-блоках svelte), плюс stylelint по CSS.
- **a11y-предупреждение**`ColorField.svelte`.
- **пустой ruleset**`Toggle.svelte`.
## Этапы
### Этап 1 — «зелёные» гейты (мелкие, по ~10–40 строк каждый)
1. **ToolCard** — убрать ошибку типов: либо валидировать `href` против набора
известных маршрутов, либо явно протащить типизированный роут из каталога.
Проверка: svelte-check 0 errors.
2. **palette.test.ts** — переписать чтение на новый словарь: тест контрастов
гоняется по токенам `preview.css` (`:root` + `[data-theme="dark"]`, цвета
`--color-*`/`--brand-*`). Отдельно: взять из нового дизайна, какие пары
реально проверить (текст/фон, muted, акцент/бренд, бордер). Если часть
проверок потеряла смысл в новом дизайне — сократить, не растягивать.
3. **a11y `ColorField` + пустой ruleset `Toggle`** — пофиксить прямо в этих
компонентах.
Гейт этапа: `svelte-check` 0 errors, `pnpm test` зелёный.
### Этап 2 — миграция `kit/**` на design-токены (main объём)
Цель — `lint:all` без ошибок design-tokens (все примитивы/`color-mix()` уходят в
токены `preview.css`).
**Вариант А (основной): авто-фикс одноразовым скриптом.** Писать скрипт ТОЛЬКО
если он гарантированно ничего не испортит. Гарантии:
- Скрипт — одноразовый (`web/scripts/fix-design-tokens.mjs`), после прогона
удаляется либо помечается неактуальным.
- Строгая карта замен: примитив → токен **1:1, только если значение точно
совпадает с уже существующим токеном словаря** `preview.css` (цвет →
`--color-*`/`--brand-*`, радиус → `--radius-*`, размер →
`--space-*`/`--size-*`, длительность → `--duration-*`, z-index → `--z-*`).
Никаких новых токенов скрипт не генерирует.
- Замены — только примитивы, не содержащие `var()`/`calc()`/`color-mix()`;
исключения по правилам плагина (0, 0px, 1px, проценты, unitless) не трогаются.
`color-mix()` → выносится в `preview.css` как производный `--color-*` токен
(может требовать ручного решения имени).
- Скрипт обрабатывает только `src/lib/components/kit/**` и `src/preview.css`
(scoped-пути плагина) и только `<style>`-блоки/`.css` — логику/разметку не
меняет, только строки стилей.
- После прогона: обязательный дифф-ревью (все изменения — замена значений на
`var(--x)`, ничего удалено/переставлено), затем полный `lint:all` +
`svelte-check` + `pnpm test`. Если хоть что-то вне «замена примитива на токен»
— скрипт считается проваленным, откатываем и переходим на вариант Б.
**Вариант Б (страховка): руками, пакетами по компонентам.** Если скрипт не
удался — чистим/заводим токены вручную, пакетами по компонентам (1–2 на задачу,
диф < ~500 строк). Отсутствующие токены (например `--space-2xl`/`--space-panel`,
уже дважды обходились в SchemaTextSource) добавляем в словарь `preview.css`
осознанно.
Гейт этапа: `pnpm --dir web lint:all` — только допустимый остаток
(неиспользуемые токены — варнинг), плюс регресс-прогон тестов и svelte-check.
### Этап 3 — чистота словаря `preview.css`
- Разобрать варнинги неиспользуемых токенов (check-tokens): либо использовать,
либо удалить из словаря.
- После этапа 2 — повторный аудит токенов
(`pnpm --dir web exec node scripts/check-tokens.mjs`): parity light/dark,
hct-only, без мёртвых токенов.
### Этап 4 — финальные гейты (гейт переезда `old/`)
1. `pnpm --dir web exec svelte-check --tsconfig ./tsconfig.json` → 0 errors
2. `pnpm --dir web test` → зелёный
3. `pnpm --dir web exec prettier --check .` → чистый
4. `pnpm --dir web lint:all` → 0 (или только задокументированный остаток)
5. Обе ветки открываются: старый UI `/`, новый `/preview`
После зеленых гейтов — закрытие задач и переход к `backlog` №14 (переезд старых
файлов в `old/`).
## Порядок и коммиты
- Коммиты атомарные, < ~500 строк (AGENTS). По завершении каждой мини-задачи —
ревью (агент не коммитит).
- Техдолг чинится по заведённым задачам, правила линтеров не отключаются и не
игнорируются.
+88 -36
View File
@@ -1,17 +1,39 @@
# План разработки: easy-png-tools
> **Статус (2026-09-07):** Фаза 1 (полноценный TS-сайт) в основном выполнена —
> сайт живёт, каталог переведён в типизированный `registry-new` (121/125), идёт
> редизайн на `preview/*` по `plan-redesign.md` (параллельная ветка, старый UI
> на `(old)/`). Фазы 28 (эталоны, Rust/wasm, CLI, harness) — будущие, разделам
> ниже не запущены.
## 0. Решения
- **Набор PNG-утилит, всё в браузере** (SvelteKit + `adapter-static`), данные не покидают машину.
- **Первый и главный приоритет — полностью рабочий TS-сайт**: максимум простых инструментов уровня EASY, UX/UI, пайплайны (несколько инструментов подряд), описания инструментов. Сайтом можно пользоваться уже после этой фазы.
- **Никакого wasm/Rust на этом этапе.** TS-реализация на canvas + `ImageData` — эталон алгоритмов, из которого позже генерируются эталонные файлы для проверки Rust-порта. Rust-ядро, WASM и CLI — отдельные фазы позже, когда сайт живёт.
- **Пайплайн = JSON-список шагов** `{tool, params}`. Формат закладывается сейчас и потом станет общим для браузера, wasm и CLI.
- **Реестр инструментов** (id, title, description, схема параметров, `run`) — источник истины: из него генерируются страницы, формы, пайплайны. Позже — общий формат для Rust-парсера.
- Валидация будущего Rust-порта — эталонные файлы, сгенерированные из TS-реализации. Сравнение: **бит-в-бит** для чистых пиксельных операций, **perceptual diff** для canvas-зависимых.
- **Единая точка исполнения инструментов.** Страницы вызывают операции только через `web/src/lib/tools/executor.ts::executeStep` — нигде напрямую `tool.run`. Контракт уже асинхронный (`Promise<PixelImage>`), поэтому:
- когда появятся тяжёлые MEDIUM-операции (свёртки, квантование), внутренности `executeStep` переезжают в Web Worker — воркер импортирует тот же чистый core как TS-фолбэк, алгоритмы не дублируются, вызывающий код не меняется;
- в WASM-фазе тот же воркер принимает wasm-модуль и диспетчеризует по `toolId` — свитч A/B из фазы 5 сводится к замене реализации внутри исполнителя;
- до появления MEDIUM-операций воркер не вводится: EASY-операции быстры и не блокируют UI.
- **Набор PNG-утилит, всё в браузере** (SvelteKit + `adapter-static`), данные не
покидают машину.
- **Первый и главный приоритет — полностью рабочий TS-сайт**: максимум простых
инструментов уровня EASY, UX/UI, пайплайны (несколько инструментов подряд),
описания инструментов. Сайтом можно пользоваться уже после этой фазы.
- **Никакого wasm/Rust на этом этапе.** TS-реализация на canvas + `ImageData`
эталон алгоритмов, из которого позже генерируются эталонные файлы для проверки
Rust-порта. Rust-ядро, WASM и CLI — отдельные фазы позже, когда сайт живёт.
- **Пайплайн = JSON-список шагов** `{tool, params}`. Формат закладывается сейчас
и потом станет общим для браузера, wasm и CLI.
- **Реестр инструментов** (id, title, description, схема параметров, `run`) —
источник истины: из него генерируются страницы, формы, пайплайны. Позже —
общий формат для Rust-парсера.
- Валидация будущего Rust-порта — эталонные файлы, сгенерированные из
TS-реализации. Сравнение: **бит-в-бит** для чистых пиксельных операций,
**perceptual diff** для canvas-зависимых.
- **Единая точка исполнения инструментов.** Страницы вызывают операции только
через `web/src/lib/tools/executor.ts::executeStep` — нигде напрямую
`tool.run`. Контракт уже асинхронный (`Promise<PixelImage>`), поэтому:
- когда появятся тяжёлые MEDIUM-операции (свёртки, квантование), внутренности
`executeStep` переезжают в Web Worker — воркер импортирует тот же чистый
core как TS-фолбэк, алгоритмы не дублируются, вызывающий код не меняется;
- в WASM-фазе тот же воркер принимает wasm-модуль и диспетчеризует по `toolId`
— свитч A/B из фазы 5 сводится к замене реализации внутри исполнителя;
- до появления MEDIUM-операций воркер не вводится: EASY-операции быстры и не
блокируют UI.
## 1. Структура репозитория (этап TS)
@@ -33,15 +55,28 @@ easy-png-tools/
### Фаза 1 — Полноценный TS-сайт (единственный приоритет)
**Цель: сайт, которым можно пользоваться.** Без wasm, без Rust, без clamp-семантики (в TS это делает `Uint8ClampedArray` сам).
**Цель: сайт, которым можно пользоваться.** Без wasm, без Rust, без
clamp-семантики (в TS это делает `Uint8ClampedArray` сам).
1.1 **Каркас и UX/UI.** SvelteKit + static adapter, дизайн-система (цвета, типографика, компоненты), общий лейаут, шапка с навигацией по категориям.
1.1 **Каркас и UX/UI.** SvelteKit + static adapter, дизайн-система (цвета,
типографика, компоненты), общий лейаут, шапка с навигацией по категориям.
1.2 **Ядро.** Тип `Image` (обёртка над `ImageData`), загрузка/декодирование файла, кодирование и скачивание, библиотека операций (color/alpha/geometry/format/text/analyze). Всё без DOM-зависимостей внутри `core`.
1.2 **Ядро.** Тип `Image` (обёртка над `ImageData`), загрузка/декодирование
файла, кодирование и скачивание, библиотека операций
(color/alpha/geometry/format/text/analyze). Всё без DOM-зависимостей внутри
`core`.
1.3 **Реестр инструментов.** Одна запись = `{ id, title, description, category, params: ParamDef[], run }`. Универсальный рендерер: страница инструмента и форма параметров строятся из записи реестра автоматически; маршруты-утилиты генерируются из реестра на билде. Новый инструмент = новая запись + функция `run`.
1.3 **Реестр инструментов.** Одна запись =
`{ id, title, description, category, params: ParamDef[], run }`. Универсальный
рендерер: страница инструмента и форма параметров строятся из записи реестра
автоматически; маршруты-утилиты генерируются из реестра на билде. Новый
инструмент = новая запись + функция `run`.
1.4 **Пайплайн-workspace** (`/workspace`): загрузил изображение → список шагов (инструмент + его параметры) → последовательное применение с превью каждого шага → скачивание финального результата. Добавление/удаление/перестановка шагов. Сохранение пайплайнов в localStorage, экспорт/импорт JSON. Общие компоненты: DropZone, Preview, ParamForm, Download, PipelineSteps.
1.4 **Пайплайн-workspace** (`/workspace`): загрузил изображение → список шагов
(инструмент + его параметры) → последовательное применение с превью каждого шага
→ скачивание финального результата. Добавление/удаление/перестановка шагов.
Сохранение пайплайнов в localStorage, экспорт/импорт JSON. Общие компоненты:
DropZone, Preview, ParamForm, Download, PipelineSteps.
1.5 **Массовая реализация EASY-инструментов** по категориям:
@@ -50,29 +85,36 @@ easy-png-tools/
- цвет: замена, тон, каналы, оттенки серого, инверсии;
- геометрия и холст: resize, crop, rotate, flip, border, padding, background;
- текст и простые эффекты: watermark, add-text, рамки, шум, pixelate;
- анализ: размеры, палитра, проверки, просмотр.
Приоритет — ширина, не глубина: как можно больше простых инструментов.
- анализ: размеры, палитра, проверки, просмотр. Приоритет — ширина, не глубина:
как можно больше простых инструментов.
1.6 **Полировка.** Описания и подсказки для всех инструментов, состояния загрузки/ошибок, доступность, пустые состояния.
1.6 **Полировка.** Описания и подсказки для всех инструментов, состояния
загрузки/ошибок, доступность, пустые состояния.
**Checkpoint:** загрузил PNG → применил пайплайн из N шагов → сохранил и вернул пайплайн → скачал результат. Все инструменты 1.5 работают на сайте. Ни одного wasm.
**Checkpoint:** загрузил PNG → применил пайплайн из N шагов → сохранил и вернул
пайплайн → скачал результат. Все инструменты 1.5 работают на сайте. Ни одного
wasm.
### Фаза 2 — Тестовые PNG и генератор эталонов
- Фикстурные входы: градиенты/паттерны/шум + пара «настоящих» PNG.
- Правило: сравниваем **распакованные пиксели**, не байты файла.
- **Checkpoint:** в `tests/reference/<op>/` лежат `input-*.png` + `expected-*.png`.
- **Checkpoint:** в `tests/reference/<op>/` лежат `input-*.png` +
`expected-*.png`.
### Фаза 3 — Playwright-тесты
- `tools/gen-references.ts`: грузит страницу, гоняет TS-эталон, сохраняет `expected-*.png`.
- `tools/gen-references.ts`: грузит страницу, гоняет TS-эталон, сохраняет
`expected-*.png`.
- Регресс: изменение TS-ядра ловится тестами.
- **Checkpoint:** регресс-тесты падают при изменении выхода эталона.
### Фаза 4 — Rust-ядро
- Порт 1:1 чистых операций (первым делом те, что покрыты эталонными файлами) с `Image` ↔ TS `ImageData`.
- `clamp_to_u8` с семантикой `ToUint8Clamp` (round-half-to-even, ≥255 → 255) + юнит-тесты на краях.
- Порт 1:1 чистых операций (первым делом те, что покрыты эталонными файлами) с
`Image` ↔ TS `ImageData`.
- `clamp_to_u8` с семантикой `ToUint8Clamp` (round-half-to-even, ≥255 → 255) +
юнит-тесты на краях.
- `cargo test` читает эталонные файлы напрямую, **без браузера**.
- **Checkpoint:** `cargo test` зелёный на всех эталонных файлах.
@@ -91,14 +133,18 @@ easy-png-tools/
### Фаза 7 — Сквозная проверка идентичности
- Матрица: **эталонные файлы (из TS) ↔ Rust-native ↔ Rust-wasm**, одна команда `harness compare`.
- Матрица: **эталонные файлы (из TS) ↔ Rust-native ↔ Rust-wasm**, одна команда
`harness compare`.
- **Checkpoint:** сходимость по всем покрытым операциям во всех трёх рантаймах.
### Фаза 8 — Масштабирование и продвинутое
- Порт оставшихся EASY → MEDIUM (свёртки, морфология, квантование; кодеки через крейт `image`).
- Canvas-зависимые операции (текст, градиенты, штампы, `ctx.filter`): остаются в TS с perceptual-классом либо реимплементация в Rust.
- batch-страница: дроп папки → wasm по всем файлам → zip. localStorage-пресеты (тот же JSON, что у CLI).
- Порт оставшихся EASY → MEDIUM (свёртки, морфология, квантование; кодеки через
крейт `image`).
- Canvas-зависимые операции (текст, градиенты, штампы, `ctx.filter`): остаются в
TS с perceptual-классом либо реимплементация в Rust.
- batch-страница: дроп папки → wasm по всем файлам → zip. localStorage-пресеты
(тот же JSON, что у CLI).
- HARD: compress/optimize через oxipng/zopfli (в Rust — нативно).
## 3. Правила сравнения (для фаз 4–7)
@@ -114,19 +160,25 @@ easy-png-tools/
## 4. Риски и контрмеры
- **Реестр разрастается, страницы дублируются** — один универсальный рендерер страниц/форм из записей реестра; инструмент = данные + `run`.
- **Семантика пайплайна** — зафиксировать: каждый шаг применяется к результату предыдущего; единый тип `Image` на всём пути; схема параметров на шаге.
- **Canvas-операции не бит-в-бит** — класс perceptual закладывается на Фазе 2–3, а не в конце.
- **WASM + Vite** — спайк на Фазе 5 до массовой интеграции (риск отложен сознательно: сайт уже живёт на TS).
- **Расползание реестра при будущем Rust-порте** — схема параметров в JSON, её можно парсить и из Rust.
- **Разные декодеры PNG** — фикстурные PNG генерировать самим, чтобы не тащить чужие артефакты.
- **Реестр разрастается, страницы дублируются** — один универсальный рендерер
страниц/форм из записей реестра; инструмент = данные + `run`.
- **Семантика пайплайна** — зафиксировать: каждый шаг применяется к результату
предыдущего; единый тип `Image` на всём пути; схема параметров на шаге.
- **Canvas-операции не бит-в-бит** — класс perceptual закладывается на Фазе 2–3,
а не в конце.
- **WASM + Vite** — спайк на Фазе 5 до массовой интеграции (риск отложен
сознательно: сайт уже живёт на TS).
- **Расползание реестра при будущем Rust-порте** — схема параметров в JSON, её
можно парсить и из Rust.
- **Разные декодеры PNG** — фикстурные PNG генерировать самим, чтобы не тащить
чужие артефакты.
## 5. Команды верификации
```bash
cd web && npm run dev # разработка (Фаза 1+)
cd web && npm run build # статический экспорт
npx playwright test # эталоны + UI-регресс (Фаза 3+)
pnpm dev # разработка (Фаза 1+, обёртка `pnpm --dir web dev`)
pnpm build # статический экспорт (обёртка `pnpm --dir web build`)
pnpm --dir web exec playwright test # эталоны + UI-регресс (Фаза 3+)
cargo test # Rust-ядро против эталонных файлов (Фаза 4+)
harness compare tests/reference # сквозная сверка native + wasm (Фаза 7)
```
+77
View File
@@ -0,0 +1,77 @@
# Хранить данные для сортировки по популярности инструментов
Идея выложить на Vercel и использовать Vercel KV (на базе Upstash Redis) — это
отличный, рабочий и очень быстрый в реализации вариант. Для 150 инструментов
объем данных будет крошечным, но здесь есть пара критически важных нюансов по
лимитам, о которых нужно знать на старте, чтобы бесплатный тариф не отключился в
первый же месяц. [1]
## ⚠️ Подводный камень: Лимиты Vercel KV на Free-тарифе
На бесплатном тарифе (Hobby) для Vercel KV действуют следующие ограничения: [2]
- 30 000 запросов в месяц (суммарно чтение + запись).
- Ограничение около 3 000 запросов в день. [2, 3]
Если делать запрос к базе на каждый просмотр страницы («посещение»), лимит в 30
тысяч улетит мгновенно при первой же паре сотен посетителей в день.
## 💡 Как сделать это правильно и бесплатно
Чтобы не выйти за рамки лимитов, архитектуру нужно построить экономно:
## 1. Чтение: Сортировка на этапе сборки (ISR / SSG)
Не нужно запрашивать KV из Edge-функции при каждом открытии сайта пользователем.
- Используйте Incremental Static Regeneration (ISR) (если вы делаете на Next.js)
или раз в сутки запускайте Cron-сид (Vercel Cron).
- Сайт раз в несколько часов запрашивает весь список из KV (всего 1 запрос на
получение общей структуры), сортирует инструменты и кеширует эту страницу как
статическую.
- Результат: 100 000 пользователей увидят отсортированный список, а вы потратите
всего пару запросов к KV в день.
## 2. Запись: Защита от накрутки и экономия («Добавление в избранное»)
«Избранное» — идеальная метрика для KV, так как её нажимают редко. А вот
«Посещения» лучше отправлять не на каждый клик, а пачками (батчингом) или
использовать для просмотров бесплатную Vercel Web Analytics (там лимит 25 000
событий в месяц обновляется отдельно).
---
## 💻 Оптимальная структура данных в Redis (KV)
Вместо 150 отдельных ключей, храните всё в одной хэш-таблице (Hash) или
сортированном множестве (Sorted Set). Это сэкономит количество запросов. Вариант
структуры через Hash:
```js
// Запись / инкремент (в API роуте Vercel)
import { kv } from "@vercel/kv";
//Когда пользователь зашел на инструмент 'crop-png'
await kv.hincrby("tools:visits", "crop-png", 1);
// Когда добавил в избранное
await kv.hincrby("tools:favorites", "crop-png", 1);
```
Как забрать всё одним запросом для сортировки:
```js
// Получаем сразу все просмотры (вернет объект вида { 'crop-png': '120', 'resize-png': '85' })
const allVisits = await kv.hgetall("tools:visits");
const allFavorites = await kv.hgetall("tools:favorites");
// Дальше соединяем в массив на клиенте/сервере и сортируем:
// Популярность = Просмотры + (Избранное * 5) <- так лайки весят больше
```
## ⚡ Альтернатива: Cloudflare Workers + KV
Если вы боитесь упереться в 30 000 запросов на Vercel, рассмотрите связку
Cloudflare Workers + Workers KV. Там на бесплатном тарифе дают: [4]
- 100 000 запросов на запись в сутки.
- 10 000 000 (10 млн) запросов на чтение в сутки.
- Для пет-проекта этого хватит навсегда, даже если вы будете делать запросы
напрямую с фронтенда без кеширования.
+234
View File
@@ -0,0 +1,234 @@
# Карта инструментов: реализовано · добавить · идеи
> Живой документ для сверки с каталогом. Формат записи:
> `id / название — параметры через запятую`. Источник сравнения:
> onlinepngtools.com (311 уникальных; 107 из них — нишевые клоны серий
> Logo*/Icon*/Stamp*/Signature* поверх обычных операций).
---
## 1. Реализовано
### Конвертация
- jpg-to-png — без параметров
- webp-to-png — без параметров
- gif-to-png — первый кадр
- bmp-to-png — без параметров
- ico-to-png — выбор размера кадра
- png-to-bmp — 24-бит, фон вместо альфы
- convert-png-to-jpg — background, quality
- convert-png-to-webp — quality
- svg-to-png — width результата
- png-to-base64 / base64-to-png — строка
- png-to-data-uri / data-uri-to-png — строка
- png-to-hex — rrggbbaa по строкам / hex-to-png — tokens + width
- png-to-bytes — десятичные RGBA-байты по строкам / bytes-to-png — tokens +
width
- png-to-rgb-values — rgba(r,g,b,a) по пикселям / rgb-values-to-png — числа +
width
- svg-to-png — width результата
- verify-is-png — текстовый источник (base64/data-uri), вердикт по сигнатуре
### Прозрачность
- change-png-opacity — percent
- set-alpha-channel-png — percent
- remove-alpha-channel-png — без параметров
- extract-alpha-mask-png — без параметров
- invert-alpha-png — без параметров
- remove-background-png — color, tolerance, outerOnly, smooth
- remove-color-from-png — targetColor, tolerance
- add-stroke-png — color, thickness
- find-contour-png — color, thickness
- make-thicker-png / make-thinner-png — radius
- harden-alpha-png — threshold
- feather-edges-png — radius (размытие только альфы)
- clean-edges-png — radius (defringe: RGB от ближайшего непрозрачного)
- watermark-image-png — вторая картинка-знак (загружается на странице), scale,
opacity, position, margin
- despeckle-alpha-png / close-holes-png — radius
- center-by-alpha-png — без параметров
- round-corners-png — radius
- circle-mask-png — size (диаметр, % меньшей стороны), offsetX, offsetY
- square-mask-png — widthPct, heightPct, offsetX, offsetY
- star-mask-png — points, innerRadius, size, rotation, offsetX, offsetY
- wavy-mask-png — size, amplitude, waves, phase, offsetX, offsetY
### Цвет
- grayscale-png / invert-colors-png / sepia-png / auto-contrast-png — без
параметров
- adjust-brightness-contrast-png — brightness, contrast
- change-png-hue — degrees
- extract-channel-png — channel (r/g/b)
- swap-channels-png — pair (r-g/r-b/g-b)
- black-and-white-png — threshold
- posterize-png — levels
- two-colors-png — lightColor, darkColor, threshold
- temperature-png — percent
- gamma-png — value
- tint-png — color, strength
### Квантование и палитры
- quantize-png — colors (k, median-cut)
- decrease-color-count-png — maxColors (пресеты 2…256)
- custom-palette-png — colors (hex через запятую, ближайший цвет)
- dithering-png — colors (k), pattern (FloydSteinberg / Bayer 4×4)
### Разложение каналов
- png-to-hsl / png-to-hsv / png-to-hsi — component (h/s/l и т.п.), display (gray
| space-as-rgb)
- png-to-cmyk — component (c/m/y/k), display
- png-to-ycbcr — component (y/cb/cr), display
- png-to-lab — component (l/a/b), display
### Геометрия
- resize-png — width (0=авто), height (0=авто), keepAspect
- crop-png — x, y, width, height
- rotate-png — angle (90/180/270)
- flip-png — axis (h/v)
- skew-png — degX, degY
- rotate-free-png — angle
- zoom-png — scale
- shift-png — offsetX, offsetY, color фона
- add-padding-png — padding, transparent, color
- add-border-png — thickness, color
- fit-on-background-png — width, height, transparent, color
- tile-png — columns, rows
- trim-empty-space-png — threshold (альфа)
- change-canvas-size-png — width, height, anchor (3×3)
- change-aspect-ratio-png — ratio (пресеты), mode (crop/pad)
- swap-orientation-png — target (portrait/landscape)
- symmetric-copy-png — axis, keepSide
### Фильтры
- blur-png — radius
- sharpen-png — strength
- vignette-png — strength
- jpeg-artifacts-png — quality (имитация пережатия jpg/webp)
- pixelate-png — blockSize (закрывает и их Color Blocks)
- randomize-pixels-png — blockSize, seed
- add-noise-png — amount, mode (mono/color), seed
- silhouette-png — color, threshold
### Анализ
- png-info — размеры, альфа, число цветов
- png-is-transparent / png-is-grayscale / png-orientation — текстовый вердикт
### Маски по свойствам пикселей
- show-transparent-png — color, opacity (подсветка прозрачных/полупрозрачных)
- show-grayscale-pixels-png — tolerance, mode (binary/highlight),
highlightColor, highlightOpacity
- show-color-pixels-png — tolerance, mode, highlightColor, highlightOpacity
- light-pixel-mask-png — threshold, mode, highlightColor, highlightOpacity
- dark-pixel-mask-png — threshold, mode, highlightColor, highlightOpacity
- unique-color-mask-png — rarity (макс. повторов), mode, highlightColor,
highlightOpacity
- extract-color-from-png — color, tolerance (оставить близкие, остальное
прозрачным)
### Генерация
- create-empty-png — width, height, transparent, color
- single-color-png — width, height, color
- random-noise-png — width, height, seed
- linear-gradient-png — width, height, fromColor, toColor, direction
- text-to-png — text, fontSize, font, bold, color, transparentBg,
backgroundColor, padding
- emoji-to-png — emoji, size
- placeholder-png — width, height, backgroundColor, color, showText
- color-spectrum-png — width, height, direction, saturation, lightness
- random-colors-png — width, height, blockSize, seed
- draw-grid-png — width, height, cols, rows, lineWidth, color, transparentBg
### Текст
- add-text-png — text, fontSize, color, font, bold, position (3×3), margin,
plate, plateColor, plateOpacity
- date-stamp-png — format, fontSize, color, font, bold, position, margin, plate,
plateColor, plateOpacity
- watermark-tile-png — text, fontSize, color, opacity, angle, stepX, stepY,
font, bold
### Палитры и цветовые утилиты
- color-wheel-generator — size, кольца/сектора, показ hex при клике (у нас —
статичный свотч-полотно)
- complementary-palette — baseColor
- monochromatic-palette — baseColor, count
- analogous-palette — baseColor, spread
- triadic-palette — baseColor
- tetradic-palette — baseColor
- similar-shades-palette — baseColor, count, range
- sort-colors — источник (палитра-картинка или список), порядок (hsl/luma)
- mix-colors — colors[], веса?
- average-color — colors[]; blend-two — a, b, steps; step-between — a, b, steps
(три частных случая одного движка)
---
## 2. Можно добавить — из onlinepngtools
### Разложение каналов — остаток
- separate-colors — minShare слоя (MEDIUM, мультифайловый вывод → пока идея)
### Края и силуэт — остаток
- glow — radius, color, intensity
- shadow — offsetX, offsetY, blur, color, alpha
### Эффекты — остаток
- censor-region / erase-region — область (MEDIUM: нужен UI выделения → см. идеи)
- whirl — угол, центр, радиус (MEDIUM)
### Сортировка/блоки пикселей
- sort-pixels — blockSize, ключ (яркость/канал), направление (MEDIUM)
- slow-reveal / fade-in / fade-out / disappear — анимационные (→ идеи)
### Сжатие и качество — реализовано (см. Конвертация: compress, reduce-to-size; jpeg-artifacts; W8 quantize)
### Генераторы — остаток
- multi-color-gradient — список стопов (нужен новый тип параметра → MEDIUM-UI)
### Конвертеры — остаток
- png-to-gif — MEDIUM (однокадровый GIF-энкодер руками)
- gif-to-frames — MEDIUM (мультифайловый вывод → идея)
- change-bit-depth — MEDIUM (пересборка PNG)
### Прочее единичное
- pick-a-color — пипетка уже есть в превью; отдельная страница не планируется
(покрыто)
- preview/test на цветных фонах — фича превью, не инструмент (решить позже)
- extract-barcode — HARD, вне планов
---
## 3. Идеи на рассмотрение (нужна архитектура или спорная ценность)
- **Region-инструменты** — требуется UI выделения прямоугольника/ластика на
превью: censor-region, erase-region, pixelate-area, blur-area, sharpen-area,
reverse-colors-area. Один раз делаем selection-компонент — получаем сразу
шесть инструментов.
- **Мультифайловый вывод** — сейчас инструмент отдаёт одну картинку:
split-into-parts, gif-to-frames, separate-colors, multiply-grid-as-files.
Нужен механизм «результат = набор файлов» (zip?).
- **Анимационные эффекты** — slow-reveal, fade-in/out, disappearing, scrolling:
это видео/GIF на выходе, а не PNG. Отдельное решение о формате результата.
- **HARD-хвост** — glitch-art, extract-signature, handwritten→digital,
extract-barcode.
- **Нишевые серии** (logo/icon/stamp/signature — 107 клонов у источника) —
сознательно не копируем: это обычные операции над конкретным контентом, у нас
они доступны через базовые инструменты + цепочки.
+10 -2
View File
@@ -6,7 +6,12 @@
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "pnpm --dir web dev",
"build": "pnpm --dir web build"
"build": "pnpm --dir web build",
"format": "pnpm format:docs && pnpm --dir web format",
"format:docs": "prettier --write docs --log-level warn",
"check:docs": "prettier --check docs",
"html": "pnpm --dir refs build && node scripts/extract-static.mjs refs/.next/server/app refs-html",
"cdp-audit": "pnpm --dir web refs-cdp-audit:large"
},
"keywords": [],
"author": "Ku6epXBOCTuK",
@@ -18,5 +23,8 @@
"onFail": "download"
}
},
"type": "module"
"type": "module",
"devDependencies": {
"prettier": "^3.9.6"
}
}
+52 -37
View File
@@ -8,51 +8,51 @@ importers:
packageManagerDependencies:
'@pnpm/exe':
specifier: ^11.20.0
version: 11.22.0
version: 11.25.0
pnpm:
specifier: ^11.20.0
version: 11.22.0
version: 11.25.0
packages:
'@pnpm/exe@11.22.0':
resolution: {integrity: sha512-B1SGeKm+v9pX9YkzMmrnO2FbgBd8TDwzZ3jSj6J6ThxdyGvI4TsOfMeHARW4Wb25visPpmMDfIXrU76EZdJM4g==}
'@pnpm/exe@11.25.0':
resolution: {integrity: sha512-X19R2uC+VAJ4UJQE9c/PCdOXbDaWnZtad7WUrTHBFQuMVaK9MAHbyO/WgahQCuRtTmJkLbxcWKKCk+JeTYFm/g==}
hasBin: true
'@pnpm/linux-arm64@11.22.0':
resolution: {integrity: sha512-xzzn3jYG9QaiFZPaHcWM3yX4Fm9UGz5E42mzpbvUEvXYf5O9fwNolenOhGLpRl2qS5u35fjZSvDZbWlOwHMcug==}
'@pnpm/linux-arm64@11.25.0':
resolution: {integrity: sha512-ra8akqhzsbcOhKSJ9fFV8H+Oc9uGQAcp/XmIBEdT+8hPPoZCit3+RNCHmg8bLoNvpSLtRvZE0WFCMj7WyzxeBA==}
cpu: [arm64]
os: [linux]
'@pnpm/linux-x64@11.22.0':
resolution: {integrity: sha512-isvaPctGinbsM2hsTtRsMarN8Sr5QhXDTNmn8Xv9Lp1PjincCvH2RBbhpo+xYIOxzgls1dtQSdgoORUbfRVALQ==}
'@pnpm/linux-x64@11.25.0':
resolution: {integrity: sha512-pQl/L10diKQCbF73viRrtVU8qVWMTudUCSG1uZ+EWBNKtU9Sbxe6Q378diXDb1uTwSgUVMQoypxlC1MTzh7qvQ==}
cpu: [x64]
os: [linux]
'@pnpm/linuxstatic-arm64@11.22.0':
resolution: {integrity: sha512-i4J+AQWW0T3JBdXaLsvxu8ZmMG1HLS6JZQESdOR9uBv8Zumb4yg8GYT83eWRLacr6ngVdhEBiC3W4eOG64MFbA==}
'@pnpm/linuxstatic-arm64@11.25.0':
resolution: {integrity: sha512-cYcrbB/xN1N6/5VUlFuHblb1gNyJgZv1CF5Pk1T0sHJxQMY4DFJV3OqHVAgahxyUfSTaQcVLvH1H2JFvd/+sgw==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@pnpm/linuxstatic-x64@11.22.0':
resolution: {integrity: sha512-QYzk8jhSuSbVthW/OxEOhU3f3zhjpw4HqgedrlwbVNb7btCWn5del2Hb5PsYYka2PbmKq5EtF5IzYN8Yp1CfxQ==}
'@pnpm/linuxstatic-x64@11.25.0':
resolution: {integrity: sha512-HXDtaeQod16DDMUUcVLIMxvEc1jKn7IYsNPwbwAPs/Je/d3umRPJi3Ejjdn6e9qpXN6Oon+yvSsJr7B9oDt6KA==}
cpu: [x64]
os: [linux]
libc: [musl]
'@pnpm/macos-arm64@11.22.0':
resolution: {integrity: sha512-Io8Axk5kutPgMuAfOg1QGj3J0/KpLvH5iiPcmp7up8D4q7BNWT02ndQo3RyGWqrlkf5nwspM41GFpWTShrZ4Aw==}
'@pnpm/macos-arm64@11.25.0':
resolution: {integrity: sha512-m/eAgEqKhiSexGxWPHNXNnSRI0hudymg6K7LbrU2EoDs9IgJ28OKEz9mGxMzhkYBweEquR4k5teMNes/aToy9w==}
cpu: [arm64]
os: [darwin]
'@pnpm/win-arm64@11.22.0':
resolution: {integrity: sha512-QgaRuKGQKov7xW2utPCgDT83fn/PU5cD6HFgib48oTslz7wv26E89d4jMCxL4GRmEL2YCfkYuj5ITj1oVWg5WQ==}
'@pnpm/win-arm64@11.25.0':
resolution: {integrity: sha512-oAeECbtZ+eJziaBmPUkwJM8Dx7KVQ5FO367AXTjD2HGfB5ob1Bcu5hoUF5RBTgDBiP9fRQ1u13uAEpqar/6NLA==}
cpu: [arm64]
os: [win32]
'@pnpm/win-x64@11.22.0':
resolution: {integrity: sha512-iWYsiSwpgxqur+TwsnSoPdOQaEfd4ygB84mb5tJUOgim6PHr1xhKJBndaQzH+At/WkLxLdYN+K8dYc4M7naezg==}
'@pnpm/win-x64@11.25.0':
resolution: {integrity: sha512-8/n+wCc0a8TRYTMFqti93Vh0cscdJ25xfMyYSDdXrLUh2ccxSFuS+SE7cNPjd/nOXoFAJvdW0kwfbyRPLa16/w==}
cpu: [x64]
os: [win32]
@@ -116,45 +116,45 @@ packages:
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
engines: {node: '>=8'}
pnpm@11.22.0:
resolution: {integrity: sha512-H/hwxMYTPf2I+yr8Rt0T1H8JyXlLQ4xv20fKmMrzvBY4HuC+k6CRuOOCTPAfiJ9G19niCRD7C+GrD7W6qA3WIQ==}
pnpm@11.25.0:
resolution: {integrity: sha512-XN6SW08HX3Jetx+64YpC/+eEUkeJ8ZthxzHLhyHsKKruFg4BqNWvT+2ypCzb8wDv4j2zVrDUoXtNY+EfirfJVg==}
engines: {node: '>=22.13'}
hasBin: true
snapshots:
'@pnpm/exe@11.22.0':
'@pnpm/exe@11.25.0':
dependencies:
'@reflink/reflink': 0.1.19
detect-libc: 2.1.2
optionalDependencies:
'@pnpm/linux-arm64': 11.22.0
'@pnpm/linux-x64': 11.22.0
'@pnpm/linuxstatic-arm64': 11.22.0
'@pnpm/linuxstatic-x64': 11.22.0
'@pnpm/macos-arm64': 11.22.0
'@pnpm/win-arm64': 11.22.0
'@pnpm/win-x64': 11.22.0
'@pnpm/linux-arm64': 11.25.0
'@pnpm/linux-x64': 11.25.0
'@pnpm/linuxstatic-arm64': 11.25.0
'@pnpm/linuxstatic-x64': 11.25.0
'@pnpm/macos-arm64': 11.25.0
'@pnpm/win-arm64': 11.25.0
'@pnpm/win-x64': 11.25.0
'@pnpm/linux-arm64@11.22.0':
'@pnpm/linux-arm64@11.25.0':
optional: true
'@pnpm/linux-x64@11.22.0':
'@pnpm/linux-x64@11.25.0':
optional: true
'@pnpm/linuxstatic-arm64@11.22.0':
'@pnpm/linuxstatic-arm64@11.25.0':
optional: true
'@pnpm/linuxstatic-x64@11.22.0':
'@pnpm/linuxstatic-x64@11.25.0':
optional: true
'@pnpm/macos-arm64@11.22.0':
'@pnpm/macos-arm64@11.25.0':
optional: true
'@pnpm/win-arm64@11.22.0':
'@pnpm/win-arm64@11.25.0':
optional: true
'@pnpm/win-x64@11.22.0':
'@pnpm/win-x64@11.25.0':
optional: true
'@reflink/reflink-darwin-arm64@0.1.19':
@@ -194,7 +194,7 @@ snapshots:
detect-libc@2.1.2: {}
pnpm@11.22.0: {}
pnpm@11.25.0: {}
---
lockfileVersion: '9.0'
@@ -205,4 +205,19 @@ settings:
importers:
.: {}
.:
devDependencies:
prettier:
specifier: ^3.9.6
version: 3.9.6
packages:
prettier@3.9.6:
resolution: {integrity: sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==}
engines: {node: '>=14'}
hasBin: true
snapshots:
prettier@3.9.6: {}
+42
View File
@@ -0,0 +1,42 @@
# refs-html — статические снимки нового дизайна
Отформатированный HTML, вытащенный из Next-референса `refs/`, чтобы смотреть
макеты как референс по разметке и классам: открыл файл в браузере — увидел
дизайн.
## Файлы
| Файл | Маршрут в нашем приложении (audit) | Что показывает |
| ------------------------- | -------------------------------------- | --------------------------------------------------- |
| `demo.html` | `/preview/demo` | Пайплайн-воркспейс + панель превью |
| `list-tools.html` | `/preview/list-tools` | Каталог инструментов (группы, поиск, фильтры) |
| `gradient.html` | `/preview/tools/linear-gradient-png` | Инструмент «Градиент»: настройки + превью + CSS |
| `background-remover.html` | `/preview/tools/remove-background-png` | Инструмент «Удаление фона»: сравнение source/result |
> `index.html` не используется: в Next-рефе это зеркало `demo` (главная == `/easy-png-tools/demo`),
> а в нашем приложении `/preview` — своя страница (воркспейс). Файл не генерируется
> экстрактором (`scripts/extract-static.mjs` пропускает `index.html`) и исключён
> из аудита (`EXCLUDE` в `web/scripts/audit.mjs`).
## Особенности
- **Это статика без JS**: все `<script>` удалены, интерактивности нет.
Тумблеры/кнопки/слайдеры не кликаются — это витрина состояний по умолчанию.
- Разметка отформатирована (отступы), CSS темы заинлайнен и причёсан —
файлы автономны, работают с `file://`.
- Тема по умолчанию: `gradient.html` и `background-remover.html` отрисованы
в тёмной (так в коде рефа), `index.html`/`demo.html` — в светлой. Чтобы
посмотреть светлый вариант тёмных страниц — убрать класс `dark-mode` у
`.app-shell`; для тёмной главной — добавить его же.
- Источник правды по пикселям — код в `refs/`; здесь только витрина.
## Как перегенерировать
```powershell
cd refs
pnpm install # если ещё не ставили
node node_modules\next\dist\bin\next build # output: 'export' включён в next.config.mjs
node extract-static.mjs .next\server\app ..\refs-html # форматирование, инлайн-CSS, без скриптов
```
Скрипт: `refs/extract-static.mjs`.
File diff suppressed because it is too large Load Diff
+2910
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+15
View File
@@ -0,0 +1,15 @@
# v0 sandbox internal files
__v0_runtime_loader.js
__v0_devtools.tsx
__v0_jsx-dev-runtime.ts
.snowflake/
.v0-trash/
.vercel/
# Environment variables
.env*.local
# Common ignores
node_modules
.next/
.DS_Store
@@ -0,0 +1,29 @@
'use client'
import TopBar from '@/components/top-bar'
import { useMemo, useState } from 'react'
import { Download, RotateCcw, Upload, Check } from 'lucide-react'
export default function BackgroundRemoverPage() {
const [color, setColor] = useState('#E8EEF2')
const [similarity, setSimilarity] = useState(72)
const [outerOnly, setOuterOnly] = useState(true)
const [showMask, setShowMask] = useState(false)
const [downloaded, setDownloaded] = useState(false)
const previewBackground = showMask ? 'repeating-conic-gradient(#7b8791 0 25%, #cbd3da 0 50%) 50% / 28px 28px' : 'repeating-conic-gradient(#d7dce0 0 25%, #f3f5f6 0 50%) 50% / 28px 28px'
const subjectStyle = useMemo(() => ({ background: showMask ? '#596773' : 'linear-gradient(145deg,#1769d2 0 38%,#00a8c7 38% 66%,#bd7411 66%)', opacity: showMask ? .88 : 1, clipPath: `polygon(23% 11%, 73% 8%, 89% 30%, 79% 81%, 52% 93%, 17% 78%, 8% 39%)` }), [showMask])
const reset = () => { setColor('#E8EEF2'); setSimilarity(72); setOuterOnly(true); setShowMask(false) }
return <main className="app-shell">
<TopBar section="BACKGROUND REMOVER" status="AUTO PROCESSING" />
<div className="tool-page"><div className="eyebrow">PNG PROCESSING <span>/</span> SINGLE TOOL</div><div className="tool-title"><div><h1>Remove background.</h1><p className="lede">Select a background color and tune the edge detection. Changes are processed automatically in your browser.</p></div><span className="tool-status"><i /> LIVE PREVIEW</span></div>
<div className="remover-layout"><section className="settings-panel"><div className="panel-heading"><div><span className="label">REMOVER SETTINGS</span><strong>Configure detection</strong></div><span className="step-type">TOOL 02</span></div>
<div className="setting-group"><label>BACKGROUND COLOR</label><div className="color-field"><span className="swatch" style={{ background: color }} /><input value={color} onChange={e => setColor(e.target.value)} /><input className="native-color" type="color" value={color} onChange={e => setColor(e.target.value)} aria-label="Choose background color" /></div><div className="color-reference"><span style={{ background: color }} /> sampled from image background</div></div>
<div className="setting-group"><label>COLOR SIMILARITY <output>{similarity}%</output></label><input type="range" min="0" max="100" value={similarity} onChange={e => setSimilarity(Number(e.target.value))} /><div className="range-hints"><span>strict edges</span><span>more removal</span></div></div>
<div className="setting-group toggle-group"><label><span>OUTER COLOR ONLY</span><input type="checkbox" checked={outerOnly} onChange={e => setOuterOnly(e.target.checked)} /><b className="toggle" /></label><p>Only remove connected background pixels from the edges.</p></div>
<div className="setting-group toggle-group"><label><span>SHOW MASK</span><input type="checkbox" checked={showMask} onChange={e => setShowMask(e.target.checked)} /><b className="toggle" /></label><p>Preview the detected transparency mask.</p></div>
<div className="settings-footer"><button className="reset-btn" onClick={reset}><RotateCcw size={14} /> Reset</button><span className="auto-note"><i /> updates automatically</span></div>
</section>
<section className="remover-preview"><div className="preview-toolbar"><div><span className="label">SOURCE / RESULT</span><strong>comparison.png</strong></div><div className="preview-actions"><span className="processed"><Check size={14} /> processed</span><button className="download-btn" onClick={() => setDownloaded(true)}><Download size={15} /> {downloaded ? 'Downloaded' : 'Download result'}</button></div></div><div className="comparison-grid"><div className="image-card"><div className="image-label"><span>SOURCE</span><b>original.png</b></div><div className="remover-canvas source-canvas"><div className="subject subject-source" style={subjectStyle}><span>OBJECT</span></div><span className="canvas-size">1200 × 800</span></div></div><div className="image-card"><div className="image-label"><span>RESULT</span><b>{showMask ? 'mask-preview.png' : 'removed-bg.png'}</b></div><div className="remover-canvas" style={{ background: previewBackground }}><div className="subject" style={subjectStyle}><span>{showMask ? 'MASK' : 'PNG'}</span></div><span className="canvas-size">1200 × 800</span></div></div></div><div className="result-meta"><span>FORMAT <b>PNG-24</b></span><span>ALPHA <b>{showMask ? 'MASK' : 'ENABLED'}</b></span><span>SIMILARITY <b>{similarity}%</b></span></div></section></div>
</div><footer className="footer"><span>easy-png-tools <b>v2.4.0</b></span><span>background remover · local-only</span><span>© 2024</span></footer>
</main>
}
+1
View File
@@ -0,0 +1 @@
export { default } from '../../page'
+33
View File
@@ -0,0 +1,33 @@
'use client'
import TopBar from '@/components/top-bar'
import { useMemo, useState } from 'react'
import { Check, ChevronDown, Copy, Download, RotateCcw } from 'lucide-react'
const directions = ['0° →', '45° ↗', '90° ↑', '135° ↖', '180° ←', '225° ↙', '270° ↓', '315° ↘']
export default function GradientPage() {
const [start, setStart] = useState('#1769D2')
const [end, setEnd] = useState('#00A8C7')
const [direction, setDirection] = useState(135)
const [opacity, setOpacity] = useState(100)
const [type, setType] = useState<'linear' | 'radial'>('linear')
const [copied, setCopied] = useState(false)
const gradient = useMemo(() => type === 'linear' ? `linear-gradient(${direction}deg, ${start} 0%, ${end} 100%)` : `radial-gradient(circle, ${start} 0%, ${end} 100%)`, [direction, end, start, type])
const css = `background: ${gradient};\nopacity: ${opacity / 100};`
const reset = () => { setStart('#1769D2'); setEnd('#00A8C7'); setDirection(135); setOpacity(100); setType('linear') }
const copyCss = async () => { await navigator.clipboard?.writeText(css); setCopied(true); setTimeout(() => setCopied(false), 1400) }
return <main className="app-shell">
<TopBar section="GRADIENT" status="LIVE PREVIEW" />
<div className="tool-page"><div className="eyebrow">PNG PROCESSING <span>/</span> SINGLE TOOL</div><div className="tool-title"><div><h1>Gradient background.</h1><p className="lede">Create a clean, export-ready gradient with precise control over color, direction and transparency.</p></div><span className="tool-status"><i /> LIVE PREVIEW</span></div>
<div className="gradient-layout"><section className="settings-panel"><div className="panel-heading"><div><span className="label">GRADIENT SETTINGS</span><strong>Configure output</strong></div><span className="step-type">TOOL 01</span></div>
<div className="setting-group"><label>GRADIENT TYPE</label><div className="segmented wide"><button className={type === 'linear' ? 'selected' : ''} onClick={() => setType('linear')}>Linear</button><button className={type === 'radial' ? 'selected' : ''} onClick={() => setType('radial')}>Radial</button></div></div>
<div className="setting-group"><label>COLOR STOPS</label><div className="color-row"><div className="color-field"><span className="swatch" style={{ background: start }} /><input value={start} onChange={e => setStart(e.target.value)} /></div><span className="stop-arrow"></span><div className="color-field"><span className="swatch" style={{ background: end }} /><input value={end} onChange={e => setEnd(e.target.value)} /></div></div><div className="gradient-bar" style={{ background: gradient }} /></div>
<div className="setting-group"><label>DIRECTION <output>{direction}°</output></label><input type="range" min="0" max="360" value={direction} onChange={e => setDirection(Number(e.target.value))} /><div className="direction-grid">{directions.map((item, i) => <button key={item} className={direction === i * 45 ? 'selected' : ''} onClick={() => setDirection(i * 45)}>{item}</button>)}</div></div>
<div className="setting-group"><label>OPACITY <output>{opacity}%</output></label><input type="range" min="0" max="100" value={opacity} onChange={e => setOpacity(Number(e.target.value))} /></div>
<div className="settings-footer"><button className="reset-btn" onClick={reset}><RotateCcw size={14} /> Reset</button><span className="auto-note"><i /> updates automatically</span></div>
</section>
<section className="gradient-preview"><div className="preview-toolbar"><div><span className="label">OUTPUT PREVIEW</span><strong>gradient.png</strong></div><div className="preview-actions"><button className="secondary-btn" onClick={copyCss}>{copied ? <Check size={15} /> : <Copy size={15} />} {copied ? 'Copied' : 'Copy CSS'}</button><button className="download-btn"><Download size={15} /> Download PNG <ChevronDown size={14} /></button></div></div><div className="large-canvas"><div className="gradient-art" style={{ background: gradient, opacity: opacity / 100 }}><div className="art-mark">PNG</div><span>easy-png-tools</span></div></div><div className="code-block"><div><span className="label">GENERATED CSS</span><button className="icon-btn" onClick={copyCss} aria-label="Copy CSS"><Copy size={14} /></button></div><pre>{css}</pre></div></section></div>
</div><footer className="footer"><span>easy-png-tools <b>v2.4.0</b></span><span>gradient tool · local-only</span><span>© 2024</span></footer>
</main>
}
@@ -0,0 +1,28 @@
"use client"
import TopBar from '@/components/top-bar'
import Link from "next/link"
import { useMemo, useState } from "react"
import { ArrowUpRight, FileImage, Filter, Search, Sparkles } from "lucide-react"
const groups = [
{ name: "CONVERT", tools: [["Convert JPG to PNG", "Re-encode JPEG files as lossless PNG while preserving transparency.", "/easy-png-tools/gradient"], ["Convert WebP to PNG", "Turn WebP images into a universal PNG format for any workflow."], ["PNG to Base64", "Encode an image as a base64 string for embedding in code or styles."], ["PNG to Data URI", "Build a complete data URI ready for HTML and CSS."], ["Convert PNG to JPG", "Composite transparency over a selected backdrop and export JPEG."]] },
{ name: "TRANSPARENCY", tools: [["Remove background PNG", "Remove a solid background by color, tolerance, or edge-connected regions.", "/easy-png-tools/background-remover"], ["Extract alpha mask", "Turn the alpha channel into a clean black-and-white mask."], ["Round corners PNG", "Clip the image corners by a precise radius percentage."], ["Outline PNG", "Add a colored ring around opaque content with adjustable thickness."], ["Change PNG opacity", "Multiply the alpha channel while keeping the original colors unchanged."]] },
{ name: "COLOR", tools: [["Create gradient PNG", "Generate a smooth transition between two colors with direction controls.", "/easy-png-tools/gradient"], ["Grayscale PNG", "Convert the image to luminance-based shades of gray."], ["Invert colors PNG", "Invert every color channel while leaving alpha untouched."], ["Brightness & contrast", "Adjust brightness and contrast across a controlled range."], ["Temperature PNG", "Make an image warmer or cooler with a single precise control."]] },
{ name: "GEOMETRY", tools: [["Resize PNG", "Scale an image with bilinear interpolation and optional aspect lock."], ["Crop PNG", "Cut a rectangular area with exact coordinates and dimensions."], ["Rotate PNG", "Rotate by 90, 180, or 270 degrees without quality loss."], ["Flip PNG", "Mirror the image horizontally or vertically."], ["Add padding to PNG", "Expand the canvas on all sides by a chosen number of pixels."]] },
{ name: "FILTERS", tools: [["Blur PNG", "Apply a fast Gaussian-style blur with transparent edge handling."], ["Sharpen PNG", "Emphasize edges with an adjustable sharpening kernel."], ["Vignette PNG", "Smoothly darken the image edges while preserving the center."], ["JPEG artifacts", "Simulate low-quality JPEG recompression for testing."]] },
{ name: "ANALYZE", tools: [["PNG info", "Inspect dimensions, alpha presence, and unique color count."], ["Check grayscale", "Report whether the image contains only shades of gray."], ["Check transparency", "Detect transparent and semi-transparent pixels."], ["PNG orientation", "Classify the image as portrait, landscape, or square."]] },
]
export default function ListToolsPage() {
const [query, setQuery] = useState("")
const [active, setActive] = useState("ALL")
const filtered = useMemo(() => groups.map(group => ({ ...group, tools: group.tools.filter(([name, description]) => (active === "ALL" || active === group.name) && `${name} ${description}`.toLowerCase().includes(query.toLowerCase())) })).filter(group => group.tools.length), [query, active])
return <main className="app-shell"><TopBar section="CATALOG" status="LOCAL MODE / READY" /><div className="catalog-page">
<div className="catalog-head"><div><div className="eyebrow">EASY-PNG-TOOLS / CATALOG</div><h1>Tool catalog</h1><p>Focused utilities for working with PNG. Inspect, transform, and export locally in your browser.</p></div><div className="catalog-total"><b>32</b><span>TOOLS<br />AVAILABLE</span></div></div>
<div className="catalog-toolbar"><label className="catalog-search"><Search size={16} /><input aria-label="Search tools" placeholder="Search tools..." value={query} onChange={e => setQuery(e.target.value)} /></label><div className="catalog-filters"><Filter size={15} />{["ALL", ...groups.map(g => g.name)].map(name => <button key={name} className={active === name ? "active" : ""} onClick={() => setActive(name)}>{name}</button>)}</div></div>
<div className="catalog-groups">{filtered.map(group => <section className="catalog-group" key={group.name}><div className="group-title"><span>{group.name}</span><i>{String(group.tools.length).padStart(2, "0")} TOOLS</i></div><div className="tool-cards">{group.tools.map(([name, description, href], index) => <Link className="tool-card" href={href || "#"} key={name} onClick={e => { if (!href) e.preventDefault() }}><span className="tool-icon"><FileImage size={19} /></span><span className="tool-copy"><strong>{name}</strong><span>{description}</span></span><span className="tool-index">{String(index + 1).padStart(2, "0")}</span><ArrowUpRight size={16} className="tool-arrow" /></Link>)}</div></section>)}</div>
{!filtered.length && <div className="catalog-empty"><Sparkles size={18} /> No tools match your search.</div>}
<footer className="catalog-footer">ALL OPERATIONS RUN LOCALLY <span></span> YOUR FILES NEVER LEAVE THIS DEVICE</footer>
</div></main>
}
File diff suppressed because one or more lines are too long
+18
View File
@@ -0,0 +1,18 @@
import { Analytics } from '@vercel/analytics/next'
import type { Metadata, Viewport } from 'next'
import './globals.css'
export const metadata: Metadata = {
title: 'easy-png-tools / Demo',
description: 'Technical workspace for building PNG processing pipelines.',
generator: 'easy-png-tools',
}
export const viewport: Viewport = {
colorScheme: 'light',
themeColor: '#eef1f4',
}
export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) {
return <html lang="ru" className="bg-background"><body className="antialiased">{children}{process.env.NODE_ENV === 'production' && <Analytics />}</body></html>
}
+40
View File
@@ -0,0 +1,40 @@
'use client'
import TopBar from '@/components/top-bar'
import { useMemo, useState } from 'react'
import { ArrowDownToLine, Check, ChevronDown, Download, GripVertical, Link2, MoreHorizontal, Plus, RotateCcw, Settings2, SlidersHorizontal, Upload, X } from 'lucide-react'
const initialSteps = [
{ id: 1, title: 'Gradient background', type: 'BACKGROUND' },
{ id: 2, title: 'Remove background', type: 'TRANSFORM' },
{ id: 3, title: 'Add outline', type: 'STYLE' },
{ id: 4, title: 'Round corners', type: 'STYLE' },
]
function PreviewTile({ label, sublabel, style, active = false }: { label: string; sublabel: string; style: React.CSSProperties; active?: boolean }) {
return <div className={`preview-tile ${active ? 'active' : ''}`}><div className="tile-canvas"><div className="image-preview" style={style}><span className="sample-icon">PNG</span><span>easy-png-tools</span></div></div><div className="tile-label"><span>{label}</span><b>{sublabel}</b></div></div>
}
export default function Page() {
const [steps, setSteps] = useState(initialSteps)
const [angle, setAngle] = useState(135)
const [radius, setRadius] = useState(18)
const [outline, setOutline] = useState(2)
const [language, setLanguage] = useState('RU')
const [showIntermediate, setShowIntermediate] = useState(true)
const [gradient, setGradient] = useState('#DCEBFF')
const gradientStyle = useMemo(() => ({ background: `linear-gradient(${angle}deg, ${gradient}, #8BC8F5)` }), [angle, gradient])
const finalStyle = { ...gradientStyle, borderRadius: radius, boxShadow: `0 0 0 ${outline}px #16202B` }
return <main className="app-shell">
<TopBar section="WORKSPACE" status="AUTO PIPELINE" />
<div className="page-grid">
<section className="workspace"><div className="eyebrow">PNG PROCESSING <span>/</span> WORKSPACE</div><div className="title-row"><div><h1>Build your image pipeline.</h1><p className="lede">Chain simple tools together. Every change is processed automatically and previewed at each stage.</p></div><div className="file-chip"><Upload size={15} /><span>source.png</span><b>1.8 MB</b></div></div>
<div className="pipeline-head"><div><span className="label">PROCESSING PIPELINE</span><strong>{steps.length} active steps <em> LIVE</em></strong></div><button className="add-btn" onClick={() => setSteps([...steps, { id: Date.now(), title: 'New adjustment', type: 'STYLE' }])}><Plus size={15} /> Add tool</button></div>
<div className="steps-list">{steps.map((step, index) => <article className="step-card" key={step.id}><div className="step-index">{String(index + 1).padStart(2, '0')}</div><GripVertical className="drag" size={16} /><div className="step-body"><div className="step-heading"><div><span className="step-type">{step.type}</span><h2>{step.title}</h2></div><div className="step-tools"><span className="check"><Check size={12} /> AUTO</span><button aria-label="Remove step" onClick={() => setSteps(steps.filter(item => item.id !== step.id))}><X size={16} /></button><MoreHorizontal size={17} /></div></div>{index === 0 ? <div className="controls"><div className="control-block"><label>COLOR</label><div className="color-field"><span className="swatch" style={{ background: gradient }} /><input value={gradient} onChange={e => setGradient(e.target.value)} aria-label="Gradient color" /><ChevronDown size={14} /></div></div><div className="control-block"><label>DIRECTION <output>{angle}°</output></label><input type="range" min="0" max="360" value={angle} onChange={e => setAngle(Number(e.target.value))} /></div><div className="control-block"><label>OPACITY <output>100%</output></label><div className="segmented"><button className="selected">100</button><button>75</button><button>50</button><button>25</button></div></div></div> : index === 2 ? <div className="controls compact"><div className="control-block"><label>WIDTH <output>{outline}px</output></label><input type="range" min="0" max="8" value={outline} onChange={e => setOutline(Number(e.target.value))} /></div><div className="control-block"><label>COLOR</label><div className="color-field"><span className="swatch dark" /><input value="#16202B" readOnly /></div></div></div> : index === 3 ? <div className="controls compact"><div className="control-block"><label>RADIUS <output>{radius}px</output></label><input type="range" min="0" max="48" value={radius} onChange={e => setRadius(Number(e.target.value))} /></div><div className="toggle-row"><span>Preserve aspect ratio</span><button className="toggle on" aria-label="Preserve aspect ratio"><i /></button></div></div> : <div className="transform-note"><SlidersHorizontal size={15} /> Automatic subject detection enabled</div>}</div></article>)}</div>
<div className="pipeline-footer"><button className="reset-btn" onClick={() => setSteps(initialSteps)}><RotateCcw size={14} /> Reset pipeline</button><span className="auto-note"><i /> changes are applied automatically</span></div>
</section>
<section className="preview-panel"><div className="preview-top"><div><span className="label">PIPELINE OUTPUTS</span><strong>Visual history</strong></div><div className="preview-actions"><button className="history-toggle top-toggle" onClick={() => setShowIntermediate(!showIntermediate)} aria-expanded={showIntermediate}>{showIntermediate ? 'Hide intermediate' : 'Show intermediate'} <ChevronDown size={15} className={showIntermediate ? 'rotated' : ''} /></button><button className="download-btn"><Download size={16} /> Download result <ArrowDownToLine size={14} /></button><div className="preview-meta"><div><span>DIMENSIONS</span><b>1200 × 800 px</b></div><div><span>FORMAT</span><b>PNG-24</b></div><div><span>SIZE</span><b>1.2 MB</b></div></div><button className="icon-btn" aria-label="Preview settings"><Settings2 size={17} /></button></div></div><div className="preview-stack"><PreviewTile label="SOURCE" sublabel="original.png · 1200 × 800" style={{ background: '#8d9aa5' }} />{showIntermediate && <><PreviewTile label="STEP 01" sublabel="gradient applied" style={gradientStyle} /><PreviewTile label="STEP 02" sublabel="background removed" style={{ ...gradientStyle, clipPath: 'inset(10% 8% 10% 8% round 12px)' }} /><PreviewTile label="STEP 03" sublabel="outline added" style={{ ...gradientStyle, boxShadow: `0 0 0 ${outline}px #16202B` }} /></>}<PreviewTile label="FINAL OUTPUT" sublabel="ready · PNG-24" style={finalStyle} active /></div><p className="preview-note">Output is generated in-browser. Your files never leave this device.</p></section>
</div><footer className="footer"><span>easy-png-tools <b>v2.4.0</b></span><span><Link2 size={13} /> pipeline is local-only</span><span>© 2024</span></footer>
</main>
}
+21
View File
@@ -0,0 +1,21 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "base-nova",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "",
"css": "app/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}
+36
View File
@@ -0,0 +1,36 @@
"use client"
import Link from "next/link"
import { CircleHelp, Moon, Sun } from "lucide-react"
import { usePathname } from "next/navigation"
import { useEffect, useState } from "react"
const links = [
["Workspace", "/"],
["Catalog", "/easy-png-tools/list-tools"],
["Gradient", "/easy-png-tools/gradient"],
["Background remover", "/easy-png-tools/background-remover"],
] as const
export function TopBar({ section = "WORKSPACE", status = "LOCAL MODE / READY" }: { section?: string; status?: string }) {
const pathname = usePathname()
const [dark, setDark] = useState(false)
useEffect(() => {
const stored = window.localStorage.getItem("ep-theme")
const isDark = stored === "dark"
setDark(isDark)
document.documentElement.classList.toggle("dark-mode", isDark)
}, [])
const toggleTheme = () => {
const next = !dark
setDark(next)
document.documentElement.classList.toggle("dark-mode", next)
window.localStorage.setItem("ep-theme", next ? "dark" : "light")
}
return <header className="topbar"><Link href="/" className="brand"><span className="brand-mark">EP</span><span>easy-png-tools</span><span className="version">/ {section}</span></Link><nav className="top-nav" aria-label="Primary navigation">{links.map(([label, href]) => <Link key={href} className={pathname === href ? "active" : ""} href={href}>{label}</Link>)}</nav><div className="top-actions"><span className="status"><i /> {status}</span><button className="icon-btn" aria-label="Help"><CircleHelp size={17} /></button><button className="icon-btn" aria-label="Toggle theme" onClick={toggleTheme}>{dark ? <Sun size={17} /> : <Moon size={17} />}</button><div className="language"><button className="active">RU</button><span>/</span><button>EN</button></div></div></header>
}
export default TopBar
+58
View File
@@ -0,0 +1,58 @@
import { Button as ButtonPrimitive } from '@base-ui/react/button'
import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from '@/lib/utils'
const buttonVariants = cva(
"group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground [a]:hover:bg-primary/80',
outline:
'border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50',
secondary:
'bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground',
ghost:
'hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50',
destructive:
'bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40',
link: 'text-primary underline-offset-4 hover:underline',
},
size: {
default:
'h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2',
xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
lg: 'h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2',
icon: 'size-8',
'icon-xs':
"size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
'icon-sm':
'size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg',
'icon-lg': 'size-9',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
},
)
function Button({
className,
variant = 'default',
size = 'default',
...props
}: ButtonPrimitive.Props & VariantProps<typeof buttonVariants>) {
return (
<ButtonPrimitive
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
}
export { Button, buttonVariants }
+6
View File
@@ -0,0 +1,6 @@
import { clsx, type ClassValue } from 'clsx'
import { twMerge } from 'tailwind-merge'
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
+7
View File
@@ -0,0 +1,7 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts";
import "./.next/types/root-params.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
+11
View File
@@ -0,0 +1,11 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
typescript: {
ignoreBuildErrors: true,
},
images: {
unoptimized: true,
},
}
export default nextConfig
+37
View File
@@ -0,0 +1,37 @@
{
"name": "my-project",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start"
},
"dependencies": {
"@base-ui/react": "^1.5.0",
"@vercel/analytics": "1.6.1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^1.16.0",
"next": "16.3.0",
"react": "^19",
"react-dom": "^19",
"shadcn": "^4.8.0",
"tailwind-merge": "^3.3.1",
"tw-animate-css": "^1.4.0"
},
"devDependencies": {
"@tailwindcss/postcss": "^4.3.3",
"@types/node": "^24",
"@types/react": "^19",
"@types/react-dom": "^19",
"postcss": "^8.5",
"tailwindcss": "^4.3.3",
"typescript": "5.7.3"
},
"pnpm": {
"overrides": {
"hono": "4.12.25"
}
}
}
+3928
View File
File diff suppressed because it is too large Load Diff
+2
View File
@@ -0,0 +1,2 @@
allowBuilds:
msw: set this to true or false
+8
View File
@@ -0,0 +1,8 @@
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
'@tailwindcss/postcss': {},
},
}
export default config
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="1200" fill="none"><rect width="1200" height="1200" fill="#EAEAEA" rx="3"/><g opacity=".5"><g opacity=".5"><path fill="#FAFAFA" d="M600.709 736.5c-75.454 0-136.621-61.167-136.621-136.62 0-75.454 61.167-136.621 136.621-136.621 75.453 0 136.62 61.167 136.62 136.621 0 75.453-61.167 136.62-136.62 136.62Z"/><path stroke="#C9C9C9" stroke-width="2.418" d="M600.709 736.5c-75.454 0-136.621-61.167-136.621-136.62 0-75.454 61.167-136.621 136.621-136.621 75.453 0 136.62 61.167 136.62 136.621 0 75.453-61.167 136.62-136.62 136.62Z"/></g><path stroke="url(#a)" stroke-width="2.418" d="M0-1.209h553.581" transform="scale(1 -1) rotate(45 1163.11 91.165)"/><path stroke="url(#b)" stroke-width="2.418" d="M404.846 598.671h391.726"/><path stroke="url(#c)" stroke-width="2.418" d="M599.5 795.742V404.017"/><path stroke="url(#d)" stroke-width="2.418" d="m795.717 796.597-391.441-391.44"/><path fill="#fff" d="M600.709 656.704c-31.384 0-56.825-25.441-56.825-56.824 0-31.384 25.441-56.825 56.825-56.825 31.383 0 56.824 25.441 56.824 56.825 0 31.383-25.441 56.824-56.824 56.824Z"/><g clip-path="url(#e)"><path fill="#666" fill-rule="evenodd" d="M616.426 586.58h-31.434v16.176l3.553-3.554.531-.531h9.068l.074-.074 8.463-8.463h2.565l7.18 7.181V586.58Zm-15.715 14.654 3.698 3.699 1.283 1.282-2.565 2.565-1.282-1.283-5.2-5.199h-6.066l-5.514 5.514-.073.073v2.876a2.418 2.418 0 0 0 2.418 2.418h26.598a2.418 2.418 0 0 0 2.418-2.418v-8.317l-8.463-8.463-7.181 7.181-.071.072Zm-19.347 5.442v4.085a6.045 6.045 0 0 0 6.046 6.045h26.598a6.044 6.044 0 0 0 6.045-6.045v-7.108l1.356-1.355-1.282-1.283-.074-.073v-17.989h-38.689v23.43l-.146.146.146.147Z" clip-rule="evenodd"/></g><path stroke="#C9C9C9" stroke-width="2.418" d="M600.709 656.704c-31.384 0-56.825-25.441-56.825-56.824 0-31.384 25.441-56.825 56.825-56.825 31.383 0 56.824 25.441 56.824 56.825 0 31.383-25.441 56.824-56.824 56.824Z"/></g><defs><linearGradient id="a" x1="554.061" x2="-.48" y1=".083" y2=".087" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><linearGradient id="b" x1="796.912" x2="404.507" y1="599.963" y2="599.965" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><linearGradient id="c" x1="600.792" x2="600.794" y1="403.677" y2="796.082" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><linearGradient id="d" x1="404.85" x2="796.972" y1="403.903" y2="796.02" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><clipPath id="e"><path fill="#fff" d="M581.364 580.535h38.689v38.689h-38.689z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

+33
View File
@@ -0,0 +1,33 @@
{
"compilerOptions": {
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"target": "ES6",
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./*"]
}
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts"
],
"exclude": ["node_modules"]
}

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