Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6f1771cb07 | ||
|
|
987f4535c8 | ||
|
|
20e07fae85 | ||
|
|
a71252dc0c | ||
|
|
751e8d97a0 |
@@ -1,5 +1,12 @@
|
||||
# План: аудит верности `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`), но он
|
||||
@@ -1,5 +1,17 @@
|
||||
# План: составные типы параметров и полная типизация pipeline
|
||||
|
||||
> **СТАТУС: ЗАКРЫТ (2026-09-07).** Типизация pipeline, составные типы
|
||||
> (dimension/color-pair/offset/position9/font-style/plate/gradient),
|
||||
> `schema.layout`, изоляция веток и вся миграция инструментов (121/125,
|
||||
> остальные 4 — отложены решениями) выполнены. Ключевые архитектурные решения
|
||||
> (параллельная сборка, правило копий, изоляция old↔preview) закреплены в
|
||||
> `AGENTS.md`.
|
||||
> **Незакрытое из этого плана:** шаг 37 — физический переезд старых файлов
|
||||
> в папки `old/` (перенесён в `backlog.md`), и удаление старого кода —
|
||||
> по `plan-redesign.md` (C17–C21). Отложенные инструменты —
|
||||
> `watermark-image-png` (overlay), `png-info`, `reduce-to-size` — в `backlog.md`.
|
||||
> Ниже — полное состояние на момент закрытия.
|
||||
|
||||
> Статус: **в реализации.** Фаза 0 (фундамент) ✔, Фаза 1 (рабочий инструмент
|
||||
> в preview) ✔, Фаза 2 (простые инструменты без составных типов) — переведены
|
||||
> все одиночные инструменты (18 шт). Фаза 3 — **составной тип `dimension`
|
||||
@@ -31,10 +43,12 @@
|
||||
> step-colors, placeholder, text-to-png), text (add-text, date-stamp),
|
||||
> filters (randomize-pixels, add-noise) — везде, где канвас отделён от
|
||||
> параметров эффекта либо фигура от позиции.
|
||||
> Следующее см. в `plan-migrate-remaining.md`: дожать перевод оставшихся
|
||||
> 85 инструментов в preview, закрыть известные долги, переезд в папки `old/`
|
||||
> (шаг 37, линтер-изоляция уже на месте) и последующая проверка в реальных
|
||||
> условиях с фидбеком.
|
||||
> Следующее закрыто в `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)
|
||||
@@ -1,10 +1,19 @@
|
||||
# План: доводка дизайна до верности (фикс по refs)
|
||||
|
||||
> **СТАТУС: ЗАКРЫТ (2026-09-07).** Актуальный дизайн (`preview/*`) ушёл вперёд
|
||||
> от референсов `refs-html/` — сверка с ними больше не информативна
|
||||
> (новые компоненты `SchemaToolView`/`SchemaPreview`/каталог не имеют
|
||||
> рефовой копии). Аудит против рефа прекращается; качество дизайна дальше
|
||||
> гоняется ревью руками и специализированными проверками
|
||||
> (`lint:all` — stylelint/design-tokens/токен-аудит). Порядок удаления старого
|
||||
> и перехода — по `plan-redesign.md` (C17–C21).
|
||||
|
||||
> Статус: к выполнению. Ветка `feat/redesign`.
|
||||
> Источники: `docs/archive/demo-fidelity.md` (архивная копия старого отчёта по
|
||||
> верности, неактуальна), `docs/plan-redesign.md` (общая стратегия, фазы 1–4
|
||||
> выполнены до C16). Актуальный аудит верности — `pnpm refs-audit` (Playwright) →
|
||||
> `web/audit/audit-report.md`.
|
||||
> выполнены до C16). Актуальный аудит верности — `pnpm refs-cdp-audit` /
|
||||
> `pnpm refs-css-audit` / `pnpm refs-dom-audit` → `web/audit/audit-report.md`
|
||||
> (текущий список скриптов — в `web/package.json`, см. `refs-*audit`).
|
||||
|
||||
## Контекст
|
||||
|
||||
@@ -12,7 +21,8 @@
|
||||
старый сайт `(old)/` не трогаем до C17.
|
||||
- `refs/` и `refs-html/` приведены к консистентному виду — текущая «правда по
|
||||
пикселям». Механизм синхронизации ref→код не создаётся (см. §9 plan-redesign).
|
||||
- Аудит верности автоматизирован: `pnpm refs-audit` (Playwright, 1440×900) пишет
|
||||
- Аудит верности автоматизирован: `pnpm refs-cdp-audit` (1440×900,
|
||||
`scripts/audit-cdp.mjs`) пишет
|
||||
`web/audit/audit-report.md` — light/dark токены + поэлементный дифф стилей;
|
||||
пиксель-дифф не используется. Палитры токенов уже сверены с рефом (light
|
||||
совпадает, dark собран). Темы в `refs-html` переключаются — скрипт инжектится
|
||||
@@ -28,7 +38,7 @@
|
||||
- Никаких правок старого дизайна `(old)/` и старого `ui/`.
|
||||
- Компоненты пишем в `kit/` (переименование в `ui/` — на C19 по plan-redesign).
|
||||
- Коммиты атомарные (< ~500 строк), по 1–2 компонента; после каждого —
|
||||
`svelte-check` + `lint` + `build` + `refs-audit` (diff% не должен расти без причины).
|
||||
`svelte-check` + `lint` + `build` + `refs-cdp-audit` (diff% не должен расти без причины).
|
||||
- Коммиты делает автор (агент не коммитит).
|
||||
|
||||
## Этап 0 — Рефакторинг (SRP / дизайн-обёртки), без изменения визуала
|
||||
@@ -69,7 +79,7 @@
|
||||
|
||||
## Этап 1 — Верность (по refs-html; статус на момент плана)
|
||||
|
||||
Аудит: `pnpm refs-audit` (Playwright, 1440×900) пишет `web/audit/audit-report.md` —
|
||||
Аудит: `pnpm refs-cdp-audit` (1440×900) пишет `web/audit/audit-report.md` —
|
||||
light/dark токены + поэлементный дифф стилей. Пиксель-дифф не используется.
|
||||
Палитры токенов уже сверены с рефом: light совпадает (`--background`,
|
||||
`--panel`, `--foreground`, `--muted`, `--line`, `--blue`), dark собран (16
|
||||
@@ -102,7 +112,7 @@ light/dark токены + поэлементный дифф стилей. Пик
|
||||
(pipeline-head и пр.).
|
||||
- **F10** (открыт): внутренние отступы preview-панели (паддинг панели).
|
||||
|
||||
После Этапа 1 — повторный `refs-audit`, зафиксировать остаточный diff (sub-pixel /
|
||||
После Этапа 1 — повторный `refs-cdp-audit`, зафиксировать остаточный diff (sub-pixel /
|
||||
структурный) и обновить статус пунктов выше прямо в этом файле.
|
||||
|
||||
## Этап 2 — plan-redesign.md (C17–C21), ОТЛОЖЕНО
|
||||
@@ -128,4 +138,4 @@ light/dark токены + поэлементный дифф стилей. Пик
|
||||
страница.
|
||||
3. `demo`: переписать на обёртки.
|
||||
|
||||
После каждого коммита: `svelte-check` + `lint` + `build` + `refs-audit`.
|
||||
После каждого коммита: `svelte-check` + `lint` + `build` + `refs-cdp-audit`.
|
||||
@@ -1,5 +1,9 @@
|
||||
# План: закрытие EASY/MEDIUM-пробелов из сравнения с onlinepngtools
|
||||
|
||||
> **СТАТУС: ВЫПОЛНЕН (2026-09-07) — устарел.** Все волны W1–W9 закрыты.
|
||||
> Идеи из раздела «Вне очереди» (region-инструменты, мультифайловый вывод,
|
||||
> анимационные, HARD-серия) перенесены в `backlog.md`.
|
||||
|
||||
> Статус: черновик на ревью
|
||||
|
||||
## Принципы
|
||||
@@ -1,5 +1,10 @@
|
||||
# План: параметры между исходником и результатом
|
||||
|
||||
> **СТАТУС: УСТАРЕЛ (2026-09-07).** Раскладка описывалась для старого
|
||||
> `ToolPage`; перекрыта редизайном (`plan-redesign.md`) — страницы инструментов
|
||||
> теперь `SchemaToolView`/`SchemaPreview` в новом дизайне. Сама идея «инлайн:
|
||||
> исходник → параметры → результат» живёт в `backlog.md` (№2).
|
||||
|
||||
> Статус: план к выполнению.
|
||||
|
||||
## 1. Что получается
|
||||
@@ -1,5 +1,10 @@
|
||||
# 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`).
|
||||
@@ -1,5 +1,15 @@
|
||||
# План: дожать перевод инструментов в preview и переезд ветки `old/`
|
||||
|
||||
> **СТАТУС: ЗАКРЫТ (2026-09-07).** Перевод каталога завершён — 121 из 125
|
||||
> инструментов в `registry-new`; 4 хвоста отложены решениями и перенесены в
|
||||
> `backlog.md` (`watermark-image-png` → overlay-райз, `png-info` → exif-райз,
|
||||
> `reduce-to-size-png` → download-райз, `compress-png` → закрыт дубликатом).
|
||||
> Дальнейшие «хвосты плана» — переезд старых компонентов в папки `old/`,
|
||||
> закрытие долгов и сбор фидбека — ~уже учитываются в `plan-composite-params.md`
|
||||
> (шаги 36–37) и `plan-design-fix.md`, поэтому план закрыт, а не продолжен.
|
||||
|
||||
> Архивная копия, ниже — состояние на момент закрытия.
|
||||
|
||||
> Статус: **переведено 121 из 125; остальные 4 — отложены/закрыты решениями.**
|
||||
> Текущее
|
||||
> состояние: **121 инструмент переведён в `registry-new`**, старый UI уже живёт
|
||||
+57
-6
@@ -7,7 +7,7 @@
|
||||
|
||||
2. **UI-эксперимент: параметры между исходником и результатом** — оценка S/M.
|
||||
Раскладка «Исходник → Параметры → Результат» в один ряд вместо параметров отдельным блоком снизу. Проверить на узких экранах; возможно за флагом/A-B, чтобы сравнить с текущей.
|
||||
Ждет нового дизайна
|
||||
Перекрыто редизайном: старый план (`docs/archive/plan-inline-params.md`) закрыт, страницы инструментов теперь `SchemaToolView`/`SchemaPreview`. Эксперимент переосмыслить уже для нового этапа инструментов (сетка source|params|result), если понадобится.
|
||||
|
||||
3. **Сворачивать инструменты в chain** — оценка S.
|
||||
Тоггл сворачивания звена до заголовка «Шаг n: название» (превью скрываются). Состояние свёрнутости помнить в workspace-pipeline.
|
||||
@@ -23,8 +23,59 @@
|
||||
|
||||
7. **Конвертация формата — НЕ отдельный инструмент, а выбор в кнопке Download** — оценка M/L.
|
||||
Инструменты `convert-png-to-jpg` / `convert-png-to-webp` (и др. форматы) не должны жить как самостоятельные инструменты цепочки. Вместо этого — бесшовное внедрение выбора формата/качества прямо в кнопку/диалог **Download**: юзер скачивает результат в нужном формате (mime/ext/quality), а pipeline при этом не усложняется лишним звеном.
|
||||
Связано с планом типизации: в новом `ToolEntry<P>` под `run` в `registry-new` нет поля `output` (mime/ext/qualityParamId), которое есть у старых конвертеров — см. `plan-composite-params.md`, замечание по Фазе 2. Формат-метаданные должны переехать в понятие «формат результата» на уровне скачивания, а не отдельного инструмента.
|
||||
**Решение «не сейчас» (исследование, 2026-09):**
|
||||
- В старом UI этот паттерн уже работает: `convert-*`-инструменты не конвертируют в `run`, а только настраивают `output` (mime/ext/qualityParamId), который читает `DownloadButton` → `encode(img, mime, quality)`. То есть формат уже фактически «поле скачивания» в старом pipeline.
|
||||
- В новом preview `registry-new` поля `output` нет, `executor.worker` возвращает только пиксели, а `SchemaToolView.download()` захардкожен на `image/png` + имя `with-border.png`. Делать ретработу сейчас = параллельный спец-проект.
|
||||
- **Когда делать:** вместе с достройкой нового download/сохранения в новом UI (в рамках `plan-composite-params`, Фаза 4/UI). Тогда выбор формата встраивается в download-flow естественно, без выдёргивания отдельным проектом. Не начинать, пока не закрыта типизация 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 на границе.
|
||||
|
||||
+11
-7
@@ -1,8 +1,10 @@
|
||||
# План: переезд на новый дизайн (refs) — параллельная сборка
|
||||
|
||||
> Статус: план к выполнению. Шаги 5–6 (C17–C21) **отложены** до вывода нового
|
||||
> дизайна на приемлемый уровень верности — см. `docs/plan-design-fix.md`
|
||||
> (Этапы 0–1: рефакторинг + верность по `demo-fidelity.md`).
|
||||
> Статус: план к выполнению. Шаги 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-снимки для быстрого просмотра в браузере.
|
||||
@@ -343,11 +345,13 @@ src/routes/preview/tools/[id]/+page.svelte
|
||||
(доказательство отсутствия регресса). Фиксы по результатам — отдельными
|
||||
мелкими коммитами.
|
||||
|
||||
### Шаг 5. Сделать новый дизайн основным — ОТЛОЖЕНО
|
||||
### Шаг 5. Сделать новый дизайн основным — СЛЕДУЮЩИЙ ЭТАП
|
||||
|
||||
> Выполняется только после Этапов 0–1 из `docs/plan-design-fix.md` (новый
|
||||
> дизайн доведён до приемлемой верности). Пока не трогаем `preview/*` и
|
||||
> `(old)/`.
|
||||
> Раньше планировалось после «верности к рефу» (Этапы 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.
|
||||
|
||||
@@ -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). По завершении каждой мини-задачи —
|
||||
ревью (агент не коммитит).
|
||||
- Техдолг чинится по заведённым задачам, правила линтеров не отключаются и не
|
||||
игнорируются.
|
||||
+9
-3
@@ -1,5 +1,11 @@
|
||||
# План разработки: easy-png-tools
|
||||
|
||||
> **Статус (2026-09-07):** Фаза 1 (полноценный TS-сайт) в основном выполнена —
|
||||
> сайт живёт, каталог переведён в типизированный `registry-new` (121/125),
|
||||
> идёт редизайн на `preview/*` по `plan-redesign.md` (параллельная ветка,
|
||||
> старый UI на `(old)/`). Фазы 2–8 (эталоны, Rust/wasm, CLI, harness) —
|
||||
> будущие, разделам ниже не запущены.
|
||||
|
||||
## 0. Решения
|
||||
|
||||
- **Набор PNG-утилит, всё в браузере** (SvelteKit + `adapter-static`), данные не покидают машину.
|
||||
@@ -124,9 +130,9 @@ easy-png-tools/
|
||||
## 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)
|
||||
```
|
||||
|
||||
@@ -6,17 +6,15 @@
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
href: string;
|
||||
id: string;
|
||||
description?: string;
|
||||
index?: number;
|
||||
icon?: Component<{ size?: number; class?: string }>;
|
||||
}
|
||||
let { title, href, description, index, icon }: Props = $props();
|
||||
|
||||
let normalizedHref = $derived(href === "#" ? "/" : href);
|
||||
let { title, id, description, index, icon }: Props = $props();
|
||||
</script>
|
||||
|
||||
<a class="tool-card" href={resolve(normalizedHref)}>
|
||||
<a class="tool-card" href={resolve("/preview/tools/[id]", { id })}>
|
||||
{#if icon}<span class="tool-icon"><Icon {icon} size={19} /></span>{/if}
|
||||
<span class="tool-copy">
|
||||
<strong>{title}</strong>
|
||||
|
||||
@@ -22,8 +22,8 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="control-block">
|
||||
<label>{label}</label>
|
||||
<label class="control-block">
|
||||
<span class="control-title">{label}</span>
|
||||
<div class="color-field">
|
||||
<span class="swatch" class:dark style="background:{value}"></span>
|
||||
<input
|
||||
@@ -35,13 +35,13 @@
|
||||
/>
|
||||
{#if chevron}<ChevronDown class="icon" size={14} />{/if}
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<style>
|
||||
.control-block :global(.icon) {
|
||||
color: var(--foreground);
|
||||
}
|
||||
label {
|
||||
.control-title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font: 10px var(--font-mono);
|
||||
|
||||
@@ -42,6 +42,4 @@
|
||||
.toggle-knob {
|
||||
transition: transform 0.12s ease;
|
||||
}
|
||||
.toggle.on .toggle-knob {
|
||||
}
|
||||
</style>
|
||||
|
||||
+147
-67
@@ -1,39 +1,122 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
// @ts-expect-error node types are not wired into svelte-check; vitest resolves fine
|
||||
import { readFileSync } from "node:fs";
|
||||
import { Color } from "@panmdaa/colors";
|
||||
|
||||
declare const process: { cwd(): string };
|
||||
|
||||
const css = readFileSync("src/app.css", "utf8");
|
||||
const css = readFileSync("src/preview.css", "utf8");
|
||||
|
||||
type Tokens = Record<string, string>;
|
||||
type Hct = { h: number; c: number; t: number };
|
||||
|
||||
function parseBlock(css: string, selector: string): Tokens {
|
||||
const escaped = selector.replace("[", "\\[").replace("]", "\\]");
|
||||
const match = css.match(new RegExp(`${escaped}\\s*\\{([^}]*)\\}`));
|
||||
const tokens: Tokens = {};
|
||||
const hctCache = new Map<string, Hct>();
|
||||
|
||||
function resolveHct(value: string, env: Map<string, string>): Hct | null {
|
||||
const key = `${value}@${env.get("--brand-main")}`;
|
||||
const cached = hctCache.get(key);
|
||||
if (cached) return cached;
|
||||
const out = resolveHctRaw(value, env, new Set());
|
||||
if (out) hctCache.set(key, out);
|
||||
return out;
|
||||
}
|
||||
|
||||
function resolveHctRaw(
|
||||
value: string,
|
||||
env: Map<string, string>,
|
||||
seen: Set<string>,
|
||||
): Hct | null {
|
||||
const text = value.trim();
|
||||
const literal = text.match(/^hct\(\s*([\d.]+)\s+([\d.]+)\s+([\d.]+)\s*\)$/);
|
||||
if (literal) {
|
||||
return { h: +literal[1], c: +literal[2], t: +literal[3] };
|
||||
}
|
||||
|
||||
const from = text.match(/^hct\(from\s+var\((--[\w-]+)\)\s+(.+)\)$/s);
|
||||
if (!from) return null;
|
||||
const seedTokens = env.get(from[1]);
|
||||
if (seedTokens === undefined || seen.has(from[1])) return null;
|
||||
const seenNext = new Set(seen);
|
||||
seenNext.add(from[1]);
|
||||
const seed = resolveHct(seedTokens, env) ?? fromHex(seedTokens);
|
||||
if (!seed) return null;
|
||||
|
||||
const channels = splitTopLevel(from[2], " ");
|
||||
if (channels.length !== 3) return null;
|
||||
const h = channel(channels[0], seed);
|
||||
const c = channel(channels[1], seed);
|
||||
const t = channel(channels[2], seed);
|
||||
if (h === null || c === null || t === null) return null;
|
||||
return { h: ((h % 360) + 360) % 360, c: Math.max(0, c), t: clamp(t, 0, 100) };
|
||||
}
|
||||
|
||||
function channel(raw: string, seed: Hct): number | null {
|
||||
const text = raw.trim();
|
||||
if (text === "h") return seed.h;
|
||||
if (text === "c") return seed.c;
|
||||
if (text === "t") return seed.t;
|
||||
const calc = /^calc\(\s*([htc])\s*([+-])\s*([\d.]+)\s*\)$/.exec(text);
|
||||
if (calc) {
|
||||
const base = seed[calc[1] as keyof Hct];
|
||||
return calc[2] === "+" ? base + +calc[3] : base - +calc[3];
|
||||
}
|
||||
const num = Number.parseFloat(text);
|
||||
return Number.isFinite(num) ? num : null;
|
||||
}
|
||||
|
||||
function splitTopLevel(text: string, delimiter: string): string[] {
|
||||
const parts: string[] = [];
|
||||
let depth = 0;
|
||||
let current = "";
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const ch = text[i];
|
||||
if (ch === "(") depth++;
|
||||
else if (ch === ")") depth--;
|
||||
if (ch === delimiter && depth === 0) {
|
||||
if (current.trim()) parts.push(current.trim());
|
||||
current = "";
|
||||
} else {
|
||||
current += ch;
|
||||
}
|
||||
}
|
||||
if (current.trim()) parts.push(current.trim());
|
||||
return parts;
|
||||
}
|
||||
|
||||
function fromHex(hex: string): Hct | null {
|
||||
const m = /^#[0-9a-f]{6}$/i.exec(hex.trim());
|
||||
if (!m) return null;
|
||||
const c = Color.from(m[0] as `#${string}`);
|
||||
return { h: c.hue, c: c.chroma, t: c.tone };
|
||||
}
|
||||
|
||||
function clamp(value: number, min: number, max: number): number {
|
||||
return Math.min(max, Math.max(min, value));
|
||||
}
|
||||
|
||||
function parseTheme(css: string, selector: RegExp): Map<string, string> {
|
||||
const env = new Map<string, string>();
|
||||
const match = css.match(selector);
|
||||
for (const [, name, value] of (match?.[1] ?? "").matchAll(
|
||||
/--([a-z-]+):\s*([^;]+);/g,
|
||||
)) {
|
||||
tokens[name] = value.trim();
|
||||
env.set(`--${name}`, value.trim());
|
||||
}
|
||||
return tokens;
|
||||
return env;
|
||||
}
|
||||
|
||||
function resolveVar(tokens: Tokens, value: string): string {
|
||||
const ref = /^var\(--([a-z-]+)\)$/.exec(value);
|
||||
return ref ? (tokens[ref[1]] ?? value) : value;
|
||||
const lightEnv = parseTheme(css, /:root\s*\{([^}]*)\}/);
|
||||
const darkEnv = parseTheme(css, /\[data-theme="dark"\]\s*\{([^}]*)\}/);
|
||||
|
||||
function colorOf(env: Map<string, string>, name: string): string {
|
||||
const raw = env.get(name);
|
||||
if (!raw) throw new Error(`missing token ${name}`);
|
||||
const hct = resolveHct(raw, env) ?? fromHex(raw);
|
||||
if (!hct) throw new Error(`cannot resolve ${raw}`);
|
||||
return Color.fromHct(hct.h, hct.c, hct.t).toHexColor();
|
||||
}
|
||||
|
||||
function luminance(hex: string): number {
|
||||
const full =
|
||||
hex.length === 4
|
||||
? hex
|
||||
.slice(1)
|
||||
.split("")
|
||||
.map((c) => c + c)
|
||||
.join("")
|
||||
: hex.slice(1);
|
||||
const full = hex.slice(1);
|
||||
const [r, g, b] = [0, 2, 4]
|
||||
.map((i) => parseInt(full.slice(i, i + 2), 16) / 255)
|
||||
.map((v) => (v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4));
|
||||
@@ -46,61 +129,58 @@ function ratio(fgHex: string, bgHex: string): number {
|
||||
return (Math.max(a, b) + 0.05) / (Math.min(a, b) + 0.05);
|
||||
}
|
||||
|
||||
function mix(fgHex: string, alpha: number, bgHex: string): string {
|
||||
const parse = (h: string) =>
|
||||
[0, 2, 4].map((i) => parseInt(h.slice(1 + i, 3 + i), 16));
|
||||
const f = parse(fgHex);
|
||||
const b = parse(bgHex);
|
||||
const out = f.map((v, i) => Math.round(alpha * v + (1 - alpha) * b[i]));
|
||||
return `#${out.map((v) => v.toString(16).padStart(2, "0")).join("")}`;
|
||||
}
|
||||
|
||||
const cssText = css;
|
||||
const light = parseBlock(css, ":root");
|
||||
const darkRaw = parseBlock(css, "[data-theme='dark']");
|
||||
const dark: Tokens = { ...light, ...darkRaw };
|
||||
|
||||
function colorOf(theme: Tokens, name: string): string {
|
||||
return resolveVar(theme, theme[name]);
|
||||
}
|
||||
|
||||
function bannerBg(theme: Tokens): string {
|
||||
return mix(colorOf(theme, "danger"), 0.08, colorOf(theme, "surface"));
|
||||
}
|
||||
|
||||
describe.each([
|
||||
["light", light],
|
||||
["dark", dark],
|
||||
["light", lightEnv],
|
||||
["dark", darkEnv],
|
||||
])("контраст палитры (%s)", (_name, theme) => {
|
||||
const c = (n: string) => colorOf(theme, n);
|
||||
|
||||
it("основной текст на поверхности ≥ 7", () => {
|
||||
expect(ratio(c("text"), c("surface"))).toBeGreaterThanOrEqual(7);
|
||||
it("основной текст на фоне и панели ≥ 4.5", () => {
|
||||
expect(
|
||||
ratio(
|
||||
colorOf(theme, "--color-text"),
|
||||
colorOf(theme, "--color-background"),
|
||||
),
|
||||
).toBeGreaterThanOrEqual(4.5);
|
||||
expect(
|
||||
ratio(colorOf(theme, "--color-text"), colorOf(theme, "--color-panel")),
|
||||
).toBeGreaterThanOrEqual(4.5);
|
||||
});
|
||||
|
||||
it("вторичный текст на фоне и поверхности ≥ 4.5", () => {
|
||||
expect(ratio(c("text-muted"), c("surface"))).toBeGreaterThanOrEqual(4.5);
|
||||
expect(ratio(c("text-muted"), c("bg"))).toBeGreaterThanOrEqual(4.5);
|
||||
it("вторичный текст (muted) на фоне и панели ≥ 4.5", () => {
|
||||
expect(
|
||||
ratio(
|
||||
colorOf(theme, "--color-text-muted"),
|
||||
colorOf(theme, "--color-background"),
|
||||
),
|
||||
).toBeGreaterThanOrEqual(4.5);
|
||||
expect(
|
||||
ratio(
|
||||
colorOf(theme, "--color-text-muted"),
|
||||
colorOf(theme, "--color-panel"),
|
||||
),
|
||||
).toBeGreaterThanOrEqual(4.5);
|
||||
});
|
||||
|
||||
it("цвет ссылок на поверхности и фоне ≥ 4.5", () => {
|
||||
expect(ratio(c("link"), c("surface"))).toBeGreaterThanOrEqual(4.5);
|
||||
expect(ratio(c("link"), c("bg"))).toBeGreaterThanOrEqual(4.5);
|
||||
it("текст-цвет на акцентной кнопке ≥ 4.0 (AA large-text)", () => {
|
||||
expect(
|
||||
ratio(
|
||||
colorOf(theme, "--color-background"),
|
||||
colorOf(theme, "--color-main"),
|
||||
),
|
||||
).toBeGreaterThanOrEqual(4.0);
|
||||
expect(
|
||||
ratio(
|
||||
colorOf(theme, "--color-background"),
|
||||
colorOf(theme, "--color-accent"),
|
||||
),
|
||||
).toBeGreaterThanOrEqual(4.0);
|
||||
});
|
||||
|
||||
it("белая подпись на акцентной кнопке ≥ 4.5", () => {
|
||||
expect(ratio(c("accent-contrast"), c("accent"))).toBeGreaterThanOrEqual(
|
||||
4.5,
|
||||
);
|
||||
});
|
||||
|
||||
it("текст баннера ошибки ≥ 4.5", () => {
|
||||
expect(ratio(c("danger-strong"), bannerBg(theme))).toBeGreaterThanOrEqual(
|
||||
4.5,
|
||||
);
|
||||
});
|
||||
|
||||
it("бордер панели различим ≥ 1.15", () => {
|
||||
expect(ratio(c("border"), c("surface"))).toBeGreaterThanOrEqual(1.15);
|
||||
it("бордер панели различим на фоне ≥ 1.15", () => {
|
||||
expect(
|
||||
ratio(
|
||||
colorOf(theme, "--color-border"),
|
||||
colorOf(theme, "--color-background"),
|
||||
),
|
||||
).toBeGreaterThanOrEqual(1.15);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
{#each results as tool (tool.id)}
|
||||
<ToolCard
|
||||
title={tool.title}
|
||||
href="/preview/tools/{tool.id}"
|
||||
id={tool.id}
|
||||
description={tool.description}
|
||||
/>
|
||||
{/each}
|
||||
|
||||
@@ -22,7 +22,10 @@
|
||||
import IconButton from "$lib/components/kit/ui/IconButton.svelte";
|
||||
import Segmented from "$lib/components/kit/ui/Segmented.svelte";
|
||||
import Toggle from "$lib/components/kit/ui/Toggle.svelte";
|
||||
import { Download, ImageOff, Palette, Plus, Trash2 } from "@lucide/svelte";
|
||||
import { Download, ImageOff, Plus, Trash2 } from "@lucide/svelte";
|
||||
|
||||
import { TOOLS } from "$lib/registry-new";
|
||||
import { TOOL_ICONS } from "$lib/preview/tool-icons";
|
||||
|
||||
let mode = $state("preview");
|
||||
let radius = $state(8);
|
||||
@@ -32,23 +35,9 @@
|
||||
let lossless = $state(true);
|
||||
let animated = $state(false);
|
||||
|
||||
const tools = [
|
||||
{
|
||||
title: "Gradient",
|
||||
href: "#",
|
||||
description: "Linear & radial gradients",
|
||||
},
|
||||
{
|
||||
title: "Resize",
|
||||
href: "#",
|
||||
description: "Pixel-perfect scaling",
|
||||
},
|
||||
{
|
||||
title: "Quantize",
|
||||
href: "#",
|
||||
description: "Reduce color depth",
|
||||
},
|
||||
];
|
||||
const showcaseTools = TOOLS.filter((tool) =>
|
||||
["linear-gradient-png", "resize-png", "quantize-png"].includes(tool.id),
|
||||
);
|
||||
</script>
|
||||
|
||||
<div class="showcase">
|
||||
@@ -152,13 +141,13 @@
|
||||
|
||||
<Panel title="Tools" eyebrow="catalog">
|
||||
<div class="tool-grid">
|
||||
{#each tools as tool, i (tool.title)}
|
||||
{#each showcaseTools as tool, i (tool.id)}
|
||||
<ToolCard
|
||||
title={tool.title}
|
||||
href={tool.href}
|
||||
id={tool.id}
|
||||
description={tool.description}
|
||||
index={i + 1}
|
||||
icon={Palette}
|
||||
icon={TOOL_ICONS[tool.id]}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
{#each group.tools as tool, i (tool.id)}
|
||||
<ToolCard
|
||||
title={tool.title}
|
||||
href={`/preview/tools/${tool.id}`}
|
||||
id={tool.id}
|
||||
description={tool.description}
|
||||
index={i + 1}
|
||||
icon={TOOL_ICONS[tool.id]}
|
||||
|
||||
Reference in New Issue
Block a user