Files
easy-png-tools2/docs/plan-fields-audit-fixes.md
T
2026-10-04 07:18:50 +05:00

430 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# План: исправления по аудиту UX/UI
> Статус: **draft** — на ревью.
>
> Находки — `docs/tools-audit.md` (что плохо, с датой прохода). Этот документ
> задаёт **порядок правок** полей и раскладки волнами; сами задачи — в
> `docs/backlog.md`. Проверки и гейты — `docs/quality-gates.md`; точки касания
> при добавлении вида поля — `docs/architecture.md`, раздел 4.
>
> **Идентификаторы шагов — `FA-*` (fields-audit).** Буквы `C*`/`Q*` в других
> документах относятся к архивным волнам редизайна и здесь не используются.
>
> **Как читать волны.** Шаг с пометкой «реализовано» — закрытый, его правку
> можно не повторять; шаг без пометки — работа. Пометка стоит в первом абзаце
> шага, а не в заголовке, чтобы не потерять её при чтении оглавления. Задачи,
> которые в бэклоге, здесь не дублируются: этот документ задаёт **порядок**,
> backlog — **учёт** (`docs/backlog.md`).
>
> **В этом документе инструменты названы по `id`** (`registry/tools/`), а не по
> адресу страницы: поля и раскладка принадлежат схеме инструмента, а страница
> только ссылается на список шагов (`Page.steps`), где сейчас ровно один
> инструмент, а в будущем может быть несколько. Поэтому «у страницы `crop-png`
> есть поля x/y» неверно — поля есть у инструмента `crop`, а страница лишь
> показывает его. `id` не несёт png-интента: страница `crop-png` вызывает
> `id: "crop"`, страница `png-to-hsl` — `id: "to-hsl"` (модель —
> `docs/architecture.md`, раздел 3).
>
> **Как переносить пункты аудита.** `docs/tools-audit.md` назван по страницам,
> потому что это то, что видит пользователь. При переносе пункта в шаг мапить
> slug → `id` через `page.steps[0].id` в `registry/pages/`. Названия аудита вида
> `color-wheel-generator` и `webp-to-png` в реестре отсутствуют (реальная
> страница — `color-wheel-png`, инструмент — `color-wheel`).
## 0. Ключевое решение: select → buttons
Существующий `Segmented.svelte` — компонент с группированными кнопками (общая
рамка, разделители между сегментами). Для параметров инструментов он не
подходит: при 3+ опциях сегменты сжимаются, текст не читается.
**Решение:** новый компонент `ButtonsControl` — раздельные outline-кнопки с
отступами. Каждая опция — отдельная `<button>` со своей рамкой, активная
подсвечивается заливкой (`--color-main`). Не зависит от `Segmented`.
### Компонент `ButtonsControl`
```svelte
<!-- web/src/lib/components/fields/schema/ButtonsControl.svelte -->
<div class="buttons" role="group">
{#each options as opt}
<button
class="btn"
class:selected={value === opt.value}
aria-pressed={value === opt.value}
onclick={() => select(opt.value)}
>{opt.label}</button>
{/each}
</div>
```
```css
.buttons {
display: flex;
flex-wrap: wrap;
gap: var(--space-m);
}
.btn {
padding: var(--space-m) var(--space-l);
border: var(--size-border) solid var(--color-border);
border-radius: var(--radius-s);
font: var(--font-size-s) var(--font-mono);
color: var(--color-text-muted);
background: transparent;
cursor: pointer;
}
.btn.selected {
background: var(--color-main);
color: var(--color-background);
border-color: var(--color-main);
}
```
### Схема: `field.buttons()`
Новый тип в `registry-schema.ts`:
```ts
export interface ButtonsSpec<V extends string = string> extends FieldSpecBase {
kind: "buttons";
default: V;
options: { value: V; label: string }[];
}
```
Фабрика:
```ts
buttons: <V extends string>(s: Omit<ButtonsSpec<V>, "kind">): Field<V> => ({
spec: { kind: "buttons", ...s },
}),
```
Регистрация: добавить `"buttons": {} as ButtonsSpec` в `fieldSpecs`, добавить
`ButtonsControl` в `FIELDS` в `SchemaFields.svelte`, добавить дефолт/санитайз в
`sanitizeSchemaParams` и `defaultSchemaParams`.
---
## 1. Волна A — инфраструктура (1 коммит)
### FA-A1. Новый тип `buttons` в схеме
Файлы:
- `web/src/lib/registry-schema.ts` — `ButtonsSpec`, фабрика `field.buttons()`,
кейс в `sanitizeSchemaParams`, кейс в `defaultSchemaParams`
- `web/src/lib/components/fields/schema/ButtonsControl.svelte` — новый компонент
- `web/src/lib/components/SchemaFields.svelte` — импорт + запись в `FIELDS`
### FA-A2. Seed → input + randomize
Для `field.number()` с `kind: "seed"` (или новый подтип `field.seed()`) —
рендерить поле ввода + кнопку «Random» вместо range slider.
Решение: новый вид `field.seed()` (наследует от number, но рендерится иначе).
Или проще: опциональное поле `variant?: "seed"` в `NumberSpec` — тогда
`RangeControl` покажет input + кнопку.
Решение: **`variant: "seed"` в `NumberSpec`** — меньше новых типов.
Файлы:
- `web/src/lib/registry-schema.ts` — `NumberSpec.variant?: "seed"`
- `web/src/lib/components/fields/schema/RangeControl.svelte` — условный рендер
- `web/src/lib/registry/tools/filters.ts` — `randomizePixels`, `addNoiseTool`:
поле seed → `{ ...field.number(...), spec: { ...spec, variant: "seed" } }`
(или пересоздать через `field.seed()`)
---
## 2. Волна B — select → buttons (по файлам)
Каждый шаг — один коммит, < 500 строк. Меняем `field.select()` →
`field.buttons()` в перечисленных ниже схемах.
### FA-B1. geometry.ts
| Инструмент | Поле | Опции |
| ------------------- | -------- | ----------------------------------------- |
| rotate | angle | 90° / 180° / 270° |
| flip | axis | horizontal / vertical |
| swap-orientation | target | portrait / landscape |
| symmetric-copy | axis | vertical / horizontal |
| symmetric-copy | keepSide | left / right / top / bottom |
| change-aspect-ratio | ratio | 1:1 / 4:3 / 3:4 / 3:2 / 2:3 / 16:9 / 9:16 |
| change-aspect-ratio | mode | crop / pad |
**Исключение:** `change-canvas-size` (anchor, 9 опций) — оставить
`field.select()`, т.к. 9 кнопок в ряд не поместятся. Аналогично `position9` (уже
отдельный компонент `PositionControl`).
### FA-B2. color.ts
| Инструмент | Поле | Опции |
| -------------------- | --------- | ---------------------------------------------------- |
| dithering | pattern | floyd-steinberg / bayer |
| extract-channel | channel | red / green / blue |
| swap-channels | pair | r-g / r-b / g-b |
| decrease-color-count | maxColors | 2 / 4 / 8 / 16 / 32 / 44 / 64 / 96 / 128 / 192 / 256 |
**Исключение:** инструменты разложения каналов (`to-hsl`, `to-hsv`, `to-hsi`,
`to-cmyk`, `to-ycbcr`, `to-lab`, поле `component`) — 3-6 опций, но это
динамические инструменты, оставить `select`.
### FA-B3. analyze.ts
| Инструмент | Поле | Опции |
| --------------------- | ---- | ------------------ |
| show-transparent | mode | binary / highlight |
| show-grayscale-pixels | mode | (тот же) |
| show-color-pixels | mode | (тот же) |
| light-pixel-mask | mode | (тот же) |
| dark-pixel-mask | mode | (тот же) |
| unique-color-mask | mode | (тот же) |
Пять инструментов из шести берут `mode` из общего `maskBaseFields` — правка в
одном месте. Исключение — `show-transparent`: он переопределяет `mode` своими
опциями и дефолтом `highlight`, поэтому его `mode` правится отдельно.
### FA-B4. filters.ts
| Инструмент | Поле | Опции |
| ---------- | ---- | ------------ |
| add-noise | mode | mono / color |
### FA-B5. generate.ts
| Инструмент | Поле | Опции |
| -------------- | ------------ | --------------------- |
| color-spectrum | direction | horizontal / vertical |
| step-colors | layout | grid / strip |
| complementary | layout | grid / strip |
| triadic | layout | grid / strip |
| tetradic | layout | grid / strip |
| analogous | layout | grid / strip |
| monochromatic | layout | grid / strip |
| shades | layout | grid / strip |
| sort-colors | layout | grid / strip |
| mix-colors | (нет select) | — |
---
## 3. Волна C — обязательные фиксы (баги + дефолты)
### FA-C1. Source-aware defaults для dimension-полей — реализовано
Source-aware defaults реализованы: общий resolver в `registry-schema`
(`applySourceDefaults`). Для `resize` и `crop` дефолт после загрузки равен
размерам текущего source; до загрузки используется положительный fallback `1×1`.
Один-sided `0` у resize сохраняет режим auto. Проверено тестом
`registry.test.ts` («resize и crop получают размеры текущего source»).
### FA-C2. symmetric-copy: "keep side" не работает
Баг в `run()`: `keepSide` не учитывает `axis`. При `axis: "vertical"` (удвоение
ширины) работают только `left`/`right`, `top`/`bottom` не имеют смысла. Нужно:
либо фильтровать опции `keepSide` в зависимости от `axis` (reactive schema),
либо нормализовать в `run()`.
**Решение:** reactive — при смене `axis` сбрасывать `keepSide` на допустимое
значение. Пока достаточно в `run()`: если `axis === "vertical"` и `keepSide` ∈
{top, bottom} → заменить на `left`.
### FA-C3. Source-aware bounds (отдельный следующий шаг) — не начат
Source-aware **defaults** реализованы через `SchemaContext` и
`applySourceDefaults`. Динамические min/max для crop x/y, shift и resize не
реализованы и остаются отдельной задачей: bounds не следует смешивать с default
resolver.
**Что нужно спроектировать и завести:**
1. **Где берём размер.** `SchemaToolView` уже знает размер после `decodeFile()`
→ `PixelImage` (width/height); для bounds понадобится передать этот context в
`SchemaFields` и контролы.
2. **Как схеме описать зависимость.** В спеках добавить ссылку на размер
источника вместо жёстких чисел. Кандидат:
```ts
x: field.slider({
label: "fields.x",
bound: "sourceWidth", // min/max = ±sourceWidth
default: 0,
}),
```
`BoundSpec = "sourceWidth" | "sourceHeight" | "minSide" | "maxSide"`,
применимо к `NumberSpec`, `SliderSpec`, `OffsetSpec`, `DimensionSpec`.
3. **Что делает RangeControl.** При `bound` вычисляет min/max из текущего
размера источника; при смене картинки диапазон пересчитывается, текущее
значение пере-клампится.
4. **Первые потребители:** crop (x/y → ±sourceWidth/sourceHeight), shift
(offsetX/offsetY), resize (max → maxSide).
Это отдельный рефакторинг; source-aware defaults не заменяют bounds.
### FA-C4. verify-is-png: неверное название — решение принято, шаг не выполнен
Находка про **страницу**, а не про инструмент: страница `verify-is-png` обещает
проверку PNG-файла, а проверяет текст (base64 / data-uri). Сам инструмент назван
честно — `id: "verify-png"`, png-интент в нём означает входные данные. Адрес
страницы — `slug` `verify-is-png` (`docs/architecture.md`, раздел 3), поэтому
«неверное название» относится к `slug`, а не к `id`.
Варианты:
- `title` → «Verify PNG data», `description` → уточнить; `slug` и `id` не
трогать
- либо переименовать `slug` → `verify-png-data` с редиректом со старого URL
(нужен вордстат, иначе страница выпадает из индекса)
**Решение:** поменять title и description, имя страницы не трогать.
Переименование `slug` меняет адрес, имя файла, ключи `pages.*` и иконку, то есть
это операция с SEO-последствиями, а не строчка в реестре — она не делается в
одиночку, вместе с решением по `docs/plan-seo.md` §5.
---
## 4. Волна D — обязательные фиксы (новые параметры)
### FA-D1. remove-alpha-channel: выбор цвета фона
Сейчас захардкожен `#ffffff`. Добавить `field.color({ default: "#ffffff" })`.
Файл: `alpha.ts`, схема `removeAlphaChannelSchema`.
### FA-D2. extract-alpha-mask: галочка «инвертировать»
Добавить `field.checkbox({ label: "fields.invertMask", default: false })`. В
`run()`: если `invert`, вызвать `invertAlpha()` после `extractAlphaMask()`.
Файл: `alpha.ts`.
### FA-D3. change-canvas-size: position9 вместо select
`anchor` сейчас `field.select()` с 9 опциями → `field.position9()`.
Файл: `geometry.ts`. Уже есть `PositionControl`.
### FA-D4. Generate: добавить height
Инструменты без `size`/`height`:
- blend-two — добавить
`height: field.slider({ min: 1, max: 1024, default: 512 })`
- step-colors — аналогично
- complementary, triadic, tetradic, analogous, monochromatic, shades — все
используют `paletteBaseSchema` (общий объект), добавить `height` туда
- `sort-colors` — своя схема, `paletteBaseSchema` не использует, `height`
добавлять вручную
- mix-colors — добавить height
Файл: `generate.ts`.
### FA-D5. add-border: прозрачность цвета
Текущий `field.color()` не поддерживает alpha. Пока что: оставить как есть
(прозрачность не поддерживается нативным color picker). Отметить в аудите как
blocked.
---
## 5. Волна E — полировка (некритично)
### FA-E1. Плейсхолдер "нет параметров"
Инструменты с пустой схемой (`EmptyParams`) выглядят странно. Добавить
`EmptyState` или подсказку "No parameters — click Run".
Файл: `SchemaToolView.svelte` — показать подсказку если `schema.fields` пуст.
### FA-E2. quantize: пресеты
`field.slider` с max 64 → добавить пресеты (8, 16, 32, 64) как кнопки под
слайдером. Либо `field.buttons()` с 4 опциями вместо слайдера.
### FA-E3. trim-empty-space: 0-254 → проценты
Текущий `min: 0, max: 254` — нестандартно. Добавить переключатель px/% (аналог
шага FA-A1 — `variant: "alpha-threshold"` в NumberSpec).
### FA-E4. add-text: plate offset
Plate снизу имеет большой отступ — добавить галочку "compact plate" или
уменьшить дефолтный padding.
### FA-E5. from-emoji: выбор эмодзи
Поле `emoji` → добавить группу часто используемых эмодзи как кнопки-пресеты над
полем ввода.
---
## 6. Верификация
Проверки и их минимальный набор — `docs/quality-gates.md`, раздел «Быстрые
подмножества»; правила e2e — `docs/testing-strategy.md`. Для волн с изменением
пользовательского потока (новый контрол, новая раскладка) обязателен ручной
смоук в браузере: часть находок — про воспринимаемость, которую не ловит ни один
гейт. `pnpm verify` — перед ревью волны.
---
## 7. Не покрыто / требует решения
Эти пункты из аудита не попали в волны A–E. Требуют либо дополнительного
исследования, либо нормативного решения, либо отложены.
### 7.1 Требуют normative решения
- **Pixel ↔ Percent** (circle-mask, square-mask, star-mask, wavy-mask — %→px,
round-corners — px→%): нужен компонент переключателя единиц в слайдере.
Требует правки `RangeControl` + всех схем — в волны A–E не входит, нужен
отдельный этап.
- **Smoothing checkbox** для масок (circle-mask, square-mask, star-mask,
wavy-mask, round-corners, invert-alpha): нужен `field.checkbox("smoothing")` +
правка core (shapes.ts, alpha). Отложен — требует визуального тестирования.
- **Vignette**: выбор центра + цвет + feather — 3 новых параметра. Требует
normative решения по UI.
- **Remove-background**: выбор точки удаления — либо параметр, либо отдельный
инструмент.
- **Метод масштабирования в `resize`**: поля в схеме нет (только `size` и
`keepAspect`), поэтому это уже не правка селекта, а новое поле — нужен ли
выбор алгоритма (bilinear/bicubic/nearest)?
- **Placeholders**: "нет параметров" — какой текст/компонент показывать.
### 7.2 Операционные (тест-кейсы, описания)
- find-contour: расширять на толщину линии (требует правки core)
- make-thicker: расширять картинку на толщину линии
- make-thinner: уменьшать от краев, fix выступа
- feather-edges: расширять картинку на толщину линии
- clean-edges: найти тест-кейс, возможно увеличить максимум
- despeckle-alpha: найти более заметный тест-кейс
- close-holes: проверить поведение на больших полупрозрачных областях
- auto-contrast: найти тестовый кейс
- sharpen: добавить тест-кейс
- center-by-alpha: найти тест-кейс
- is-grayscale: улучшить видимость результата
### 7.3 Дизайн-вопросы
- add-padding vs add-border: разница неочевидна, нужен review
- change-aspect-ratio: не видно что меняется — рамка результата
- watermark-tile: заполняет не полностью
- sepia: нужна ли настройка силы эффекта?
- posterize: настройка для каждого канала отдельно?
- pixelate: разные алгоритмы?
- swap-channels: обдумать flow через более простые инструменты
### 7.4 Аудит полей (компоненты)
- Range slider: поле ввода + кнопки +/- + кнопка сброса
- Color picker: пипетка с картинки, прозрачность
- Width/height с keep-aspect-ratio: единый компонент
- Position offset: отдельные числовые поля X/Y (уже сделано в
`fields/schema/OffsetControl.svelte`)