mirror of
https://github.com/Ku6epXBOCTuK/easy-png-tools.git
synced 2026-09-15 14:06:35 +00:00
Compare commits
6
Commits
0b107c7d93
...
5cd2a586ce
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5cd2a586ce | ||
|
|
ec585e1ee6 | ||
|
|
f2829de216 | ||
|
|
0c2de3f191 | ||
|
|
35df076732 | ||
|
|
05165031c3 |
+22
-7
@@ -16,7 +16,17 @@
|
||||
|
||||
## Мелочи всякие
|
||||
|
||||
- [ ] сгенерировать или найти файлы для ручной проверки `A. Краевые PNG-файлы`
|
||||
- [x] сгенерировать или найти файлы для ручной проверки `A. Краевые PNG-файлы`
|
||||
- [x] при загрузке нового изображения, результат не обновлятеся, только при
|
||||
изменении параметров инструмента
|
||||
- [ ] Пересмотреть прозрачность в инструментах - Create empty PNG - прозрачность
|
||||
должны быть настраиваемой 0-100%, а не переключателем
|
||||
- [ ] обдумать объединение пар `inputMode + source` и `resultKind + result` в
|
||||
единую структуру, позволит упростить некоторые вещи - например
|
||||
`sourceValue` \ `resultValue` в `SchemaPreview.svelte`
|
||||
- [ ] Рассмотреть необходимость всех этих пропсов в `SchemaPreview.svelte` они
|
||||
дублируют друг друга `ontextsource`, `onrendertext`, `oncopytext`,
|
||||
`ondownloadtxt`, `ondownload`,
|
||||
- [ ] Переделать внешний вид инструментов без настроек, например Remove alpha
|
||||
channel PNG "remove-alpha-channel-png". Сейчас выглядит слишком пустым.
|
||||
- [ ] Убрать описание формата в отображении исходник\результат - это должно быть
|
||||
@@ -31,14 +41,14 @@
|
||||
паспортом, семейства с кириллицей)
|
||||
- [ ] Посмотреть темную тему - слишком темная и слишком много оттенка (убавить c
|
||||
в hct?)
|
||||
- [ ] на картинке результате при работе дергается высота надписи (высота иконки)
|
||||
- [x] на картинке результате при работе дергается высота надписи (высота иконки)
|
||||
- [x] Глянуть что за ошибка (воспроизвелось на 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)
|
||||
- [x] не работают генераторы картинок (create-empty-png)
|
||||
- [ ] генераторы работают не автоматически - требуется нажать кнопку generate
|
||||
- [x] генераторы работают не автоматически - требуется нажать кнопку generate
|
||||
(create-empty-png)
|
||||
- [ ] square-mask-png - проверить работу - параметры неверно считаются. width -
|
||||
100, height - 100, x - 100, y - 100, обрезает 50% на 50% картинку
|
||||
@@ -56,9 +66,9 @@
|
||||
- [ ] придумать процесс редактирования и настройки pipeline - drag-n-drop,
|
||||
удаление, добавление инструментов. переход один инструмент - pipeline без
|
||||
перезагрузки
|
||||
- [ ] Слаги инструментов (`*-png`) — надо обдумать, возможно что ссылки\маршруты
|
||||
должны быть с png, а названия инструментов - без. Это упростит названия
|
||||
инструментов и сделает их более понятными
|
||||
- [ ] Слаги инструментов (`*-png`) — надо переименовть, они работают с любыми
|
||||
изображениями. Надо сделать отдельный интерфейс маппинга инструмент ->
|
||||
ссылка
|
||||
|
||||
## Переезд
|
||||
|
||||
@@ -184,7 +194,12 @@
|
||||
|
||||
9. **`png-info` (детальная информация о PNG)** — оценка M/L. Отложен из
|
||||
миграции. Отдельный райз: exif-теги, редактирование, структурированный вывод;
|
||||
нужен свой отдельный случай в UI, не «ещё один text/verdict».
|
||||
нужен свой отдельный случай в UI, не «ещё один text/verdict». возможно нужна
|
||||
вообще отдельная страница, несовместимая с другими инструментами: 1) она
|
||||
работает не с изображением, а с мета данными из файла, 2) она не может быть
|
||||
частью цепочки, т.к. результат не является изображением, 3) если надо
|
||||
изменить теги, никакого форматирования и изменения в изображение вносить не
|
||||
надо, только в мета информацияю
|
||||
|
||||
10. **Region-инструменты** (censor/erase/pixelate-area/blur-area/sharpen-area/
|
||||
reverse-area) — ждут UI выделения области на превью. Из
|
||||
|
||||
@@ -0,0 +1,291 @@
|
||||
# План: i18n в preview (новый UI)
|
||||
|
||||
> Статус: готов к реализации (вопросы §5 решены). Соответствует беклог-задаче
|
||||
> «добавить i18n в preview» (`docs/backlog.md`). Реализация не начата.
|
||||
|
||||
## 1. Что получается
|
||||
|
||||
Новый UI (`web/src/routes/**` кроме `v1`, `web/src/lib/components/**` кроме
|
||||
`v1`) переходит на существующий модуль `lib/i18n`: переключатель RU/EN в шапке
|
||||
работает по-настоящему, все строки локализованы, ошибки уже локализуются.
|
||||
Инфраструктура i18n уже есть и проверена (v1-ветка) — задача в том, чтобы
|
||||
подключить к ней preview и закрыть пробелы словарей.
|
||||
|
||||
До начала локализации **v1 изолируется**: у v1 появляется полная замороженная
|
||||
копия `lib/i18n` → `lib/v1/i18n`, у нового UI — своя независимая ветка
|
||||
`lib/i18n`, которую дальше меняем без оглядки на v1 (Фаза 0). Переключённая
|
||||
локаль (localStorage) остаётся общей для обеих версий — язык переживает переход
|
||||
между страницами.
|
||||
|
||||
## 2. Текущее состояние (исследование)
|
||||
|
||||
### Уже есть
|
||||
|
||||
- `lib/i18n/locale.svelte.ts` — состояние локали (runes), персист в
|
||||
localStorage, обновление `document.documentElement.lang`.
|
||||
- `lib/i18n/t.ts` — перевод по точечному ключу с фолбэком на базовую локаль и
|
||||
интерполяцией `{var}`.
|
||||
- `lib/i18n/dict.ts` — тип `Dict`, `BASE_LOCALE = "en"` (база — английский, ru —
|
||||
перевод), `LOCALES = ["ru", "en"]`.
|
||||
- `lib/i18n/ru.ts` (~1351 строка) и `en.ts` (188 строк) — полные переводы
|
||||
инструментов v1 по id (title/description/params/options/results).
|
||||
- `lib/i18n/tool-strings.ts` — хелперы для v1
|
||||
(`toolTitle/toolDescription/ paramLabel/optionLabel/searchDoc`).
|
||||
- `lib/i18n/matching.ts` — кросс-языковой поиск (ё→е, диакритика, скоринг).
|
||||
- Тесты: `i18n.test.ts`, `tools-i18n.test.ts`, `matching.test.ts`,
|
||||
`smoke-i18n.test.ts`, `search-coverage.test.ts`.
|
||||
- `+layout.svelte` уже вызывает `initLocale()` на клиенте.
|
||||
|
||||
### Пробелы в новом UI
|
||||
|
||||
- `LangToggle.svelte` — заглушка с локальным `let lang = $state("RU")`, не
|
||||
связана с `locale.svelte.ts`.
|
||||
- Новый UI почти целиком на хардкод-английском:
|
||||
- ~150+ строк по ~30 компонентам и маршрутам (заголовки, кнопки, аria,
|
||||
плейсхолдеры, подписи секций);
|
||||
- 122 инструмента с английскими `title`/`description` в `lib/registry/*.ts`;
|
||||
- ~60+ подписей опций select;
|
||||
- ~30 заголовков layout-групп (`title: "Canvas"`, `"Background"`, …);
|
||||
- вердикты анализаторов — raw English строки (`"Yes — valid PNG signature."`),
|
||||
при том что v1 возвращает ключи словаря (`"verifyYes"`) и рендерит их через
|
||||
`tools[id].results`;
|
||||
- `labelOf(id)` в `SchemaFields.svelte` генерирует подписи полей из field ID
|
||||
(keepAspect → "Keep Aspect");
|
||||
- категории в `catalog.ts` (`GROUP_LABELS`) — English uppercase, хотя в `dict`
|
||||
уже есть ключи `categories.X`.
|
||||
- Из всех новых компонентов `t()` вызывает только `SchemaToolView.svelte`, и
|
||||
только для ошибок (`ToolError.key`).
|
||||
|
||||
### Совпадение регистров
|
||||
|
||||
- Новый registry и старый v1 registry используют **одинаковые id** инструментов
|
||||
(resize-png, flip-png, …) — переводы title/description из v1 `ru.ts` служат
|
||||
исходником для новой ветки `lib/i18n/ru.ts` (копирование данных при наполнении
|
||||
Фазы 2, не общий модуль).
|
||||
- **Расхождение**: param ids в ru.ts (`width`/`height`/`keepAspect`) не равны
|
||||
field ids новоно schema (`size` dimension, `anchor`). Нужны новые переводы
|
||||
полей.
|
||||
- `ru.ts` tools[id].results для анализаторов уже содержит ключи (`verifyYes`,
|
||||
`grayscaleNo`, `orientationPortrait` и т.д.).
|
||||
|
||||
### Архитектурные ограничения
|
||||
|
||||
- Изоляция old↔new (`isolation/no-mixed-imports`): сейчас `lib/i18n/` — shared.
|
||||
По решению задачи (Фаза 0) i18n **форкается**: v1 получает полную копию
|
||||
`lib/v1/i18n`, а `lib/i18n` переходит в ветку «new» (правило изоляции запретит
|
||||
старому коду импортить новый i18n). До форка в общем модуле два скрытых
|
||||
v1-импорта: `dict.ts` тянет `CategoryId` из `../v1/categories`,
|
||||
`tool-strings.ts` — `$lib/v1/registry`; после форка оба живут только в копии.
|
||||
Новому UI нужен параллельный `schema-tool-strings.ts` поверх `$lib/registry`.
|
||||
- Tone detection в `SchemaTextResult.svelte`: `/^(yes|true)/i` по value —
|
||||
сломается при русских вердиктах («Да», «Нет»).
|
||||
- `tools-i18n.test.ts` покрывает только v1 TOOLS; нужен аналог для нового
|
||||
registry.
|
||||
- `known-issues.spec.ts`:
|
||||
`test.fixme("error message is localized, not a raw i18n key")`.
|
||||
|
||||
## 3. Как делаем
|
||||
|
||||
Принципы:
|
||||
|
||||
- Мелкие атомарные коммиты (правило AGENTS.md); самому не коммитить — коммитит
|
||||
разработчик после ревью.
|
||||
- Изменения «снизу вверх»: сначала общий слой i18n, потом словари, потом UI.
|
||||
- Каждый этап заканчивается зелёными `pnpm --dir web lint` +
|
||||
`pnpm --dir web check` + тесты.
|
||||
- Решения по §5 зафиксированы и учитываются с первых правок словарей.
|
||||
|
||||
## 4. Фазы
|
||||
|
||||
### Фаза 0: Форк i18n для v1 + подготовка общего слоя
|
||||
|
||||
Выполняется строго до остальных фаз, без изменения поведения UI. Цель — два
|
||||
независимых i18n-слоя: v1 получает замороженную копию, новый UI — свою ветку,
|
||||
которую дальше меняем свободно. На выходе фазы — зелёные `lint`, `check`,
|
||||
`test`, `test:rules`.
|
||||
|
||||
- **0.1** Форк: скопировать `web/src/lib/i18n/` → `web/src/lib/v1/i18n/` (полный
|
||||
снимок: `dict.ts`, `en.ts`, `ru.ts`, `t.ts`, `locale.svelte.ts`,
|
||||
`matching.ts`, `tool-strings.ts` + все 5 тестов). v1 работает дальше только с
|
||||
копией — поздние правки нового i18n её не затрагивают.
|
||||
- **0.2** Привести копию к v1-канону (относительные пути от нового места):
|
||||
- копия `dict.ts`: импорт `CategoryId` `../v1/categories` → `../categories` (=
|
||||
`lib/v1/categories`);
|
||||
- копии тестов (`tools-i18n.test.ts`, `search-coverage.test.ts`,
|
||||
`smoke-i18n.test.ts`): `../v1/registry` → `../registry` (=
|
||||
`lib/v1/registry`);
|
||||
- импорт `$lib/v1/registry` в копии `tool-strings.ts` остаётся валидным;
|
||||
- перепроверить остальные относительные и `$lib`-пути в копии.
|
||||
- **0.3** Переключить импорты старого UI: все `$lib/i18n/*` в `routes/v1/**` и
|
||||
`lib/v1/components/**` (39 импортов в 23 файлах) → `$lib/v1/i18n/*`.
|
||||
- **0.4** Очистить новый `lib/i18n/` от v1-специфики, иначе isolation поймает
|
||||
new→old:
|
||||
- удалить `tool-strings.ts` (завязан на v1-типы `ParamDef`/`ToolEntry`; для
|
||||
нового UI вместо него `schema-tool-strings.ts`, см. 0.8);
|
||||
- удалить v1-центричные тесты `tools-i18n.test.ts`, `search-coverage.test.ts`,
|
||||
`smoke-i18n.test.ts` (они импортируют `../v1/registry`; живут только в
|
||||
копии);
|
||||
- остаются: `dict.ts`, `en.ts`, `ru.ts`, `t.ts`, `locale.svelte.ts`,
|
||||
`matching.ts`, `i18n.test.ts`, `matching.test.ts`.
|
||||
- **0.5** Обновить правило изоляции и конфиги:
|
||||
- `isolation/no-mixed-imports.js`: добавить `lib/i18n/**` в `DEFAULT_NEW`
|
||||
(копия по-прежнему old через `lib/v1/**`);
|
||||
- `eslint-plugins/__tests__/no-mixed-imports.test.ts`: кейсы «old → i18n
|
||||
чисто» (строки 83–89) и «lib/i18n — shared» (строки 99–105) обновить под
|
||||
новую классификацию: old→новый i18n = нарушение; добавить фикстуру
|
||||
`__fixtures__/src/lib/v1/i18n/t.ts` для кейса old-копия;
|
||||
- `eslint.config.js`: в `allowDefaultProject` добавить фикстуру
|
||||
`eslint-plugins/__fixtures__/src/lib/v1/i18n/t.ts`; поправить комментарий
|
||||
изоляции «Общее (core/, i18n/, theme)» → «Общее (core/, theme)»;
|
||||
- `eslint-plugins/README.md`: убрать i18n из списка shared (§ Isolation).
|
||||
- **0.6** Новый `lib/i18n/dict.ts`: `CategoryId` переключить с
|
||||
`../v1/categories` на `../categories` (новый структурно-идентичный тип) —
|
||||
скрытых v1-зависимостей в новом i18n не остаётся.
|
||||
- **0.7** Проверка форка: `pnpm --dir web lint`, `check`, `test` (v1-копия
|
||||
тестов подхватывается `vitest include: src/**/*.test.ts`), `test:rules`;
|
||||
смоук: v1-страницы и оба переключателя RU/EN работают как раньше, языки обеих
|
||||
версий согласованы.
|
||||
- **0.8** Добавить `lib/i18n/schema-tool-strings.ts` поверх `$lib/registry`
|
||||
(новый тип `ToolEntry`):
|
||||
- `toolTitle(tool)`, `toolDescription(tool)` с фолбэком на registry English;
|
||||
- `fieldLabel(field, id)` — перевод по ключу из `fields`-секции словаря (см.
|
||||
решение C), фолбэк на `labelOf(id)`;
|
||||
- `groupLabel(title)` — перевод по ключу из `groups`-секции словаря (см.
|
||||
решение A);
|
||||
- `verdictText(toolId, key)` — рендер вердикта по ключу;
|
||||
- `verdictTone(key)` — `success`|`danger`|`info` по имени ключа
|
||||
(`*Yes`/`*No`/`*Portrait`/`*Landscape`) — не по строке.
|
||||
- **0.9** Расширить тип `Dict` (только новая ветка `lib/i18n`):
|
||||
- новая секция `preview` для UI-строк нового дизайна (только те, что остаются
|
||||
после чистки mono-лейблов, см. решение B);
|
||||
- новая глобальная секция `fields: Record<string, string>` — переводы подписей
|
||||
полей по ключу из схемы (решение C);
|
||||
- новая глобальная секция `groups: Record<string, string>` — переводы
|
||||
заголовков групп полей (решение A).
|
||||
|
||||
### Фаза 1: Словари — наполнение (только новая ветка `lib/i18n`, v1 не трогаем)
|
||||
|
||||
- **1.1** Заполнить `en.ts` и `ru.ts` секциями `preview`, `fields`, `groups`.
|
||||
- **1.2** Проинвентаризировать разницу между 122 инструментами нового registry и
|
||||
ключами `ru.tools`: для инструментов без перевода (нет в v1) добавить
|
||||
title/description в ru.ts новой ветки (по категориям — несколько коммитов).
|
||||
- **1.3** Добавить в `ru.ts` ключи `fields`/`groups` для используемых в схемах
|
||||
ключей полей и заголовков групп (по ходу проставления ключей в schema, Фаза
|
||||
5).
|
||||
- **1.4** Добавить `tools[id].options[fieldId][value]` для select-полей, чьи
|
||||
опции нужно локализовать.
|
||||
- **1.5** Тест полноты (задел на Фазу 6): для каждого ключа label в schema есть
|
||||
перевод в обоих словарях.
|
||||
|
||||
### Фаза 2: LangToggle → реальный locale
|
||||
|
||||
- **2.1** `LangToggle.svelte`: убрать локальный `lang`, подключить
|
||||
`getLocale()`, `setLocale()`, `LOCALES`. Модель — language switch из
|
||||
`routes/v1/+layout.svelte`.
|
||||
- **2.2** Проверить e2e `navigation.spec.ts` «language toggle marks the active
|
||||
button» — должен проходить без изменений.
|
||||
|
||||
### Фаза 3: Routes + layout
|
||||
|
||||
- **3.1** `+layout.svelte`: breadcrumbs через `t()`, динамический `lang`.
|
||||
- **3.2** `+page.svelte`, `list-tools/+page.svelte`: footer-строка →
|
||||
`t("header.footerNote")`.
|
||||
- **3.3** `tools/[id]/+page.svelte`: «Tool not found», fallback title → `t()`.
|
||||
|
||||
### Фаза 4: Компоненты — основной блок (по одному коммиту на компонент)
|
||||
|
||||
Каждый коммит: удаление декоративных mono-лейблов (решение B) + замена
|
||||
оставшихся строк на `t`/хелперы + lint/check. Список mono-лейблов, которые
|
||||
**удаляем**: eyebrow'ы (`PNG PROCESSING / SINGLE TOOL`, `KIT SHOWCASE`,
|
||||
`EASY-PNG-TOOLS / CATALOG`), статусы (`LIVE PREVIEW`, `rendering ok`), панельные
|
||||
подписи (`TOOL SETTINGS`), дублирующие капшены (`SOURCE / RESULT`,
|
||||
`GENERATOR / RESULT`). Оставляем и локализуем только информативные: категории
|
||||
каталога, счётчики инструментов, подписи данных (`FORMAT`, `parts`, `px`, «ZIP
|
||||
(PNG)»), alt-тексты, а также содержательные заголовки («Configure output»).
|
||||
|
||||
- `SchemaToolView.svelte`: убрать eyebrow/`LIVE PREVIEW`/`TOOL SETTINGS`,
|
||||
перевести «Configure output» и «This tool has no schema yet.».
|
||||
- `SchemaSourceTile.svelte`: убрать figcaption `SOURCE`, перевести «choose an
|
||||
image»/«no source…» и alt.
|
||||
- `SchemaResultTile.svelte`: убрать figcaption `RESULT`, перевести «no result
|
||||
yet»/«parts» и alt.
|
||||
- `SchemaPreview.svelte`: убрать панельные
|
||||
`SOURCE / RESULT`/`GENERATOR / RESULT`, перевести meta-капшены (`FORMAT` и
|
||||
т.п.).
|
||||
- `SchemaActions.svelte`: Generating…, Generate, Open image, Download result.
|
||||
- `SchemaFields.svelte`: Reset, «updates automatically», `labelOf(field.id)` →
|
||||
`fieldLabel(field)`.
|
||||
- `SchemaTextSource.svelte`: placeholder, Try sample, Render text.
|
||||
- `SchemaTextResult.svelte`: убрать лейбл `RESULT` у вердикта, перевести Copy /
|
||||
Download .txt; tone detection → `verdictTone()`.
|
||||
- `CatalogToolbar.svelte`: фильтры → `t("categories.all")` +
|
||||
`t("categories.X")`, поисковый placeholder.
|
||||
- `CatalogHeader.svelte`: убрать eyebrow, перевести heading/lead и счётчик
|
||||
«TOOLS AVAILABLE».
|
||||
- `CatalogGroup.svelte`: перевести суффикс счётчика «TOOLS».
|
||||
- `Dropzone.svelte`: текст зоны → `t("dropZone.pickDefault")`.
|
||||
- Schema field controls (по одному или группой): Width/Height, From/To, Backing
|
||||
plate, Opacity, Font/Size/Color/Bold, Sans/Serif/Mono, Angle, «+ Add color»,
|
||||
«Remove color».
|
||||
- `TopNav.svelte`, `ThemeToggle.svelte`, `CodeBlock.svelte`, `StepCard.svelte`,
|
||||
`Footer.svelte`.
|
||||
|
||||
### Фаза 5: Вердикты + ключи в schema
|
||||
|
||||
- **5.1** Анализаторы в `registry/analyze.ts`: возвращают ключи (`"verifyYes"`
|
||||
вместо raw English).
|
||||
- **5.2** `SchemaPreview.svelte`: рендер вердикта через
|
||||
`verdictText(tool.id, key)`.
|
||||
- **5.3** Layout groups в `registry/*.ts` (решение A): `title` → ключ
|
||||
(`title: "groups.canvas"`); в `SchemaFields.svelte` — `groupLabel(title)`. В
|
||||
`Dict` — глобальная секция `groups`.
|
||||
- **5.4** Лейблы полей в `registry/*.ts` (решение C): на поле в схеме
|
||||
проставляется `label`-ключ (`field.number({ label: "fields.thickness", … })`),
|
||||
вывод в UI — `fieldLabel(field)` = `t(label)`. В `Dict` — глобальная секция
|
||||
`fields`.
|
||||
- **5.5** Категории в `catalog.ts`: `GROUP_LABELS` → `t("categories.X")` (ключи
|
||||
уже есть в dict).
|
||||
|
||||
### Фаза 6: Тесты
|
||||
|
||||
- **6.1** `new-tools-i18n.test.ts` — полнота словарей для нового registry:
|
||||
title/description ru для всех инструментов, каждый `label`-ключ схемы есть в
|
||||
`fields`, каждый `groups`-ключ есть в `groups`, без лишних ключей.
|
||||
- **6.2** Фикс `known-issues.spec.ts` test.fixme — ошибки локализуются.
|
||||
- **6.3** В новой ветке `lib/i18n` появится собственный `smoke-i18n.test.ts`
|
||||
(аналог v1: переключение локали, `html lang`, контент без смеси языков) —
|
||||
v1-копия теста не трогается.
|
||||
|
||||
### Фаза 7: Кросс-языковой поиск
|
||||
|
||||
Переключить поиск в новом каталоге с `toLowerCase()` на
|
||||
`scoreDoc()`/`normalizeForSearch()` из `matching.ts` (входит в объём, см.
|
||||
решение D). Последний шаг, аккуратный отдельный коммит.
|
||||
|
||||
## 5. Решения
|
||||
|
||||
- **A. Layout groups**: заголовки групп храним i18n-ключами прямо в схеме
|
||||
(`title: "groups.canvas"`), перевод — в глобальной секции `groups` словаря
|
||||
(`Dict.groups: Record<string, string>`). В UI — `groupLabel(title)` =
|
||||
`t(title)` с фолбэком на сырой ключ.
|
||||
- **B. Mono-лейблы**: декоративные лейблы без полезной информации **удаляем**
|
||||
(eyebrow'ы, статусы, панельные капшены, дублирующие подписи секций) — они
|
||||
занимают место. Оставляем только там, где реально нужно, и локализуем:
|
||||
счётчики инструментов, категории каталога, подписи данных (FORMAT, parts, px,
|
||||
«ZIP (PNG)»), alt-тексты, содержательные headings.
|
||||
- **C. Лейблы полей**: в схеме задаём i18n-ключ
|
||||
(`field.number({ label: "fields.thickness", … })`), в UI выводится перевод —
|
||||
`fieldLabel(field)` = `t(label)`. Ключи — глобальная секция `fields` словаря,
|
||||
фолбэк на текущий `labelOf(id)` для несопоставленных.
|
||||
- **D. Объём**: переводим всё сразу — и каркас, и контент реестра, и вердикты, и
|
||||
кросс-языковой поиск (Фаза 7). Крупные изменения кода разбиваем на небольшие
|
||||
атомарные шаги/коммиты.
|
||||
|
||||
## 6. Проверки
|
||||
|
||||
- `pnpm --dir web lint`
|
||||
- `pnpm --dir web exec svelte-check --tsconfig ./tsconfig.json`
|
||||
- `pnpm --dir web test`
|
||||
- `pnpm --dir web test:rules` (если менялся eslint/isolation-конфиг)
|
||||
- Смоук: переключатель RU/EN на новом UI; страницы каталога/инструмента, ошибка,
|
||||
вердикт — без смеси языков; `html lang` следует за локалью.
|
||||
+11
-11
@@ -101,11 +101,11 @@
|
||||
--brand-main: #86efac;
|
||||
--brand-alt: hct(from var(--brand-main) calc(h + 90) c t);
|
||||
|
||||
--color-background: hct(from var(--brand-main) h 8 95);
|
||||
--color-background-muted: hct(from var(--brand-main) h 12 85);
|
||||
--color-panel: hct(from var(--brand-main) h 6.5 98);
|
||||
--color-checker-main: hct(from var(--brand-main) h 10.7 85.7);
|
||||
--color-checker-alt: hct(from var(--brand-main) h 13.2 76);
|
||||
--color-background: hct(from var(--brand-main) h 2 95);
|
||||
--color-background-muted: hct(from var(--brand-main) h 4 85);
|
||||
--color-panel: hct(from var(--brand-main) h 3 98);
|
||||
--color-checker-main: hct(from var(--brand-main) h 5 85.7);
|
||||
--color-checker-alt: hct(from var(--brand-main) h 7 76);
|
||||
|
||||
--color-border: hct(from var(--brand-main) h 13.1 69.4);
|
||||
|
||||
@@ -186,13 +186,13 @@
|
||||
--brand-main: #86efac;
|
||||
--brand-alt: hct(from var(--brand-main) calc(h + 90) c t);
|
||||
|
||||
--color-background: hct(from var(--brand-main) h 15 6);
|
||||
--color-background-muted: hct(from var(--brand-main) h 15 10);
|
||||
--color-panel: hct(from var(--brand-main) h 12 8);
|
||||
--color-checker-main: hct(from var(--brand-main) h 12 10);
|
||||
--color-checker-alt: hct(from var(--brand-main) h 14 14);
|
||||
--color-background: hct(from var(--brand-main) h 2 16);
|
||||
--color-background-muted: hct(from var(--brand-main) h 4 24);
|
||||
--color-panel: hct(from var(--brand-main) h 3 26);
|
||||
--color-checker-main: hct(from var(--brand-main) h 6 16);
|
||||
--color-checker-alt: hct(from var(--brand-main) h 8 24);
|
||||
|
||||
--color-border: hct(from var(--brand-main) h 15 25);
|
||||
--color-border: hct(from var(--brand-main) h 8 40);
|
||||
|
||||
--color-text: hct(from var(--brand-main) h 25 90);
|
||||
--color-text-muted: hct(from var(--brand-main) h 22 70);
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
|
||||
<div class="catalog-head">
|
||||
<div>
|
||||
<div class="eyebrow">EASY-PNG-TOOLS / CATALOG</div>
|
||||
<h1>Tool catalog</h1>
|
||||
<p>
|
||||
Focused utilities for working with PNG. Inspect, transform, and export —
|
||||
@@ -28,12 +27,6 @@
|
||||
gap: var(--space-xxl);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.eyebrow {
|
||||
font: var(--font-size-s) var(--font-mono);
|
||||
letter-spacing: var(--space-text-xl);
|
||||
color: var(--color-text-muted);
|
||||
margin-bottom: var(--space-m);
|
||||
}
|
||||
h1 {
|
||||
margin: 0;
|
||||
font-size: var(--font-size-2xl);
|
||||
|
||||
@@ -1,32 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { Upload } from "@lucide/svelte";
|
||||
import DownloadButton from "./ui/DownloadButton.svelte";
|
||||
import { Sparkles, Upload } from "@lucide/svelte";
|
||||
|
||||
interface Props {
|
||||
inputMode: "image" | "text" | "none";
|
||||
canDownload: boolean;
|
||||
running: boolean;
|
||||
onupload: (file: File) => void;
|
||||
ongenerate: () => void;
|
||||
ondownload: () => void;
|
||||
}
|
||||
let {
|
||||
inputMode,
|
||||
canDownload,
|
||||
running,
|
||||
onupload,
|
||||
ongenerate,
|
||||
ondownload,
|
||||
}: Props = $props();
|
||||
let { inputMode, canDownload, running, onupload, ondownload }: Props =
|
||||
$props();
|
||||
</script>
|
||||
|
||||
<div class="actions">
|
||||
{#if inputMode === "none"}
|
||||
<button class="btn generate" onclick={ongenerate} disabled={running}>
|
||||
<Sparkles size={14} />
|
||||
{running ? "Generating…" : "Generate"}
|
||||
</button>
|
||||
{:else if inputMode === "image"}
|
||||
{#if inputMode === "image"}
|
||||
<label class="btn upload">
|
||||
<Upload size={14} /> Open image
|
||||
<input
|
||||
@@ -40,7 +28,11 @@
|
||||
</label>
|
||||
{/if}
|
||||
{#if canDownload}
|
||||
<DownloadButton label="Download result" onclick={ondownload} />
|
||||
<DownloadButton
|
||||
label="Download result"
|
||||
onclick={ondownload}
|
||||
disabled={running}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -62,15 +54,6 @@
|
||||
color: var(--color-text-muted);
|
||||
cursor: pointer;
|
||||
}
|
||||
.generate {
|
||||
background: var(--color-main);
|
||||
border-color: var(--color-main);
|
||||
color: var(--color-background);
|
||||
}
|
||||
.generate:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: default;
|
||||
}
|
||||
.upload input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
running?: boolean;
|
||||
error?: string;
|
||||
onupload: (file: File) => void;
|
||||
ongenerate?: () => void;
|
||||
ontextsource?: (text: string) => void;
|
||||
onrendertext?: () => void;
|
||||
oncopytext?: () => void;
|
||||
@@ -35,7 +34,6 @@
|
||||
running = false,
|
||||
error = "",
|
||||
onupload,
|
||||
ongenerate,
|
||||
ontextsource,
|
||||
onrendertext,
|
||||
oncopytext,
|
||||
@@ -43,16 +41,16 @@
|
||||
ondownload,
|
||||
}: Props = $props();
|
||||
|
||||
const isGenerator = $derived(inputMode === "none");
|
||||
const sourceValue = $derived(
|
||||
isGenerator
|
||||
? "—"
|
||||
: inputMode === "text"
|
||||
? "text"
|
||||
: source
|
||||
? `${source.width} × ${source.height} px`
|
||||
: "—",
|
||||
);
|
||||
const sourceValue = $derived.by(() => {
|
||||
switch (inputMode) {
|
||||
case "none":
|
||||
return "—";
|
||||
case "text":
|
||||
return "text";
|
||||
case "image":
|
||||
return source ? `${source.width} × ${source.height} px` : "—";
|
||||
}
|
||||
});
|
||||
const resultValue = $derived(
|
||||
resultKind === "image"
|
||||
? result
|
||||
@@ -77,15 +75,12 @@
|
||||
</script>
|
||||
|
||||
<div class="panel-head">
|
||||
<span class="label"
|
||||
>{isGenerator ? "GENERATOR / RESULT" : "SOURCE / RESULT"}</span
|
||||
>
|
||||
<span class="label">PREVIEW PANEL</span>
|
||||
<SchemaActions
|
||||
{inputMode}
|
||||
canDownload={hasResult}
|
||||
{running}
|
||||
{onupload}
|
||||
ongenerate={ongenerate ?? (() => {})}
|
||||
{ondownload}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
<script lang="ts">
|
||||
import { toDataUrl } from "$lib/core/io";
|
||||
import type { PixelImage } from "$lib/core/types";
|
||||
import type { FileResult } from "$lib/registry";
|
||||
import { Check, RefreshCw } from "@lucide/svelte";
|
||||
import type { FileResult, ResultKind } from "$lib/registry";
|
||||
import { Check } from "@lucide/svelte";
|
||||
import SchemaTextResult from "./SchemaTextResult.svelte";
|
||||
import PreviewTile from "./layout/PreviewTile.svelte";
|
||||
|
||||
interface Props {
|
||||
resultKind?: "image" | "text" | "verdict" | "files";
|
||||
resultKind?: ResultKind;
|
||||
result: PixelImage | null;
|
||||
fileResult?: FileResult | null;
|
||||
textResult?: string | null;
|
||||
@@ -33,20 +34,13 @@
|
||||
}))
|
||||
: [],
|
||||
);
|
||||
|
||||
let parts = $derived(
|
||||
resultKind === "files" && fileResult ? fileResult.files.length : undefined,
|
||||
);
|
||||
</script>
|
||||
|
||||
<figure class="tile">
|
||||
<figcaption>
|
||||
<span>
|
||||
RESULT
|
||||
{#if resultKind === "files" && fileResult}
|
||||
<span class="count">{fileResult.files.length} parts</span>
|
||||
{/if}
|
||||
{#if running}
|
||||
<RefreshCw class="rotating" size="16" />
|
||||
{/if}
|
||||
</span>
|
||||
</figcaption>
|
||||
<PreviewTile label="RESULT" viewMode={resultKind} loading={running} {parts}>
|
||||
<div class="canvas" class:checker={resultKind === "image"}>
|
||||
{#if resultKind === "text" && textResult}
|
||||
<div class="text-result-wrap">
|
||||
@@ -83,20 +77,9 @@
|
||||
<span class="empty">no result yet</span>
|
||||
{/if}
|
||||
</div>
|
||||
</figure>
|
||||
</PreviewTile>
|
||||
|
||||
<style>
|
||||
.tile figcaption {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-m);
|
||||
margin-bottom: var(--space-m);
|
||||
font: var(--font-size-s) var(--font-mono);
|
||||
color: var(--color-text-muted);
|
||||
& :global(.rotating) {
|
||||
animation: rotate var(--duration-l) linear infinite;
|
||||
}
|
||||
}
|
||||
.canvas {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -109,8 +92,10 @@
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
.canvas img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
width: auto;
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
max-height: 100vh;
|
||||
object-fit: contain;
|
||||
display: block;
|
||||
}
|
||||
@@ -124,10 +109,6 @@
|
||||
.empty {
|
||||
font: var(--font-size-s) var(--font-mono);
|
||||
}
|
||||
.count {
|
||||
margin-left: var(--space-m);
|
||||
color: var(--color-main);
|
||||
}
|
||||
.parts-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(
|
||||
@@ -170,13 +151,4 @@
|
||||
padding: var(--space-l);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { toDataUrl } from "$lib/core/io";
|
||||
import type { PixelImage } from "$lib/core/types";
|
||||
import type { InputMode } from "$lib/registry";
|
||||
import PreviewTile from "./layout/PreviewTile.svelte";
|
||||
import SchemaTextSource from "./SchemaTextSource.svelte";
|
||||
|
||||
interface Props {
|
||||
@@ -24,9 +25,7 @@
|
||||
const sourceUrl = $derived(source ? toDataUrl(source) : null);
|
||||
</script>
|
||||
|
||||
<figure class="tile">
|
||||
<figcaption><span>SOURCE</span></figcaption>
|
||||
<div class="canvas" class:checker={mode === "image"}>
|
||||
<PreviewTile label="SOURCE" viewMode={mode}>
|
||||
{#if mode === "text"}
|
||||
<div class="text-source-wrap">
|
||||
<SchemaTextSource
|
||||
@@ -43,42 +42,9 @@
|
||||
{:else}
|
||||
<span class="empty">no source — configure the parameters</span>
|
||||
{/if}
|
||||
</div>
|
||||
</figure>
|
||||
</PreviewTile>
|
||||
|
||||
<style>
|
||||
.tile figcaption {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-m);
|
||||
margin-bottom: var(--space-m);
|
||||
font: var(--font-size-s) var(--font-mono);
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
.canvas {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: clamp(var(--space-brand), 30vh, 60vh);
|
||||
border: var(--size-border) solid var(--color-border);
|
||||
border-radius: var(--radius-s);
|
||||
overflow: hidden;
|
||||
background: var(--color-background-muted);
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
.canvas img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
display: block;
|
||||
}
|
||||
.canvas.checker {
|
||||
background: repeating-conic-gradient(
|
||||
var(--color-checker-main) 0 25%,
|
||||
var(--color-checker-alt) 0 50%
|
||||
)
|
||||
50% / 28px 28px;
|
||||
}
|
||||
.empty {
|
||||
font: var(--font-size-s) var(--font-mono);
|
||||
}
|
||||
|
||||
@@ -8,12 +8,12 @@
|
||||
import { execute } from "$lib/executor";
|
||||
import { t } from "$lib/i18n/t";
|
||||
import type { FileResult, ToolEntry } from "$lib/registry";
|
||||
import { downloadZip } from "$lib/zip";
|
||||
import {
|
||||
defaultSchemaParams,
|
||||
sanitizeSchemaParams,
|
||||
type ToolSchema,
|
||||
} from "$lib/registry-schema";
|
||||
import { downloadZip } from "$lib/zip";
|
||||
|
||||
interface Props {
|
||||
tool: ToolEntry;
|
||||
@@ -142,8 +142,10 @@
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (inputMode !== "image" || !source || !started) return;
|
||||
// TODO: can any edge case start infinite loop?
|
||||
if (!started) return;
|
||||
void values;
|
||||
void source;
|
||||
debouncedRun();
|
||||
return () => debouncedRun.cancel();
|
||||
});
|
||||
@@ -159,11 +161,9 @@
|
||||
<div class="schema-tool">
|
||||
<header class="header">
|
||||
<div class="title-block">
|
||||
<span class="eyebrow">PNG PROCESSING <span>/</span> SINGLE TOOL</span>
|
||||
<h1>{tool.title}</h1>
|
||||
<p class="lede">{tool.description}</p>
|
||||
</div>
|
||||
<span class="status"><i></i> LIVE PREVIEW</span>
|
||||
</header>
|
||||
|
||||
<div class="workspace">
|
||||
@@ -187,7 +187,6 @@
|
||||
{running}
|
||||
{error}
|
||||
onupload={handleFile}
|
||||
ongenerate={run}
|
||||
ontextsource={(textValue) => {
|
||||
textSource = textValue;
|
||||
}}
|
||||
@@ -221,14 +220,11 @@
|
||||
line-height: 1.1;
|
||||
color: var(--color-text);
|
||||
}
|
||||
.eyebrow,
|
||||
.label,
|
||||
.status {
|
||||
.label {
|
||||
font: var(--font-size-s) var(--font-mono);
|
||||
letter-spacing: var(--space-text-l);
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
.eyebrow span,
|
||||
.label {
|
||||
color: var(--color-main);
|
||||
}
|
||||
@@ -239,19 +235,6 @@
|
||||
font-size: var(--font-size-s);
|
||||
line-height: 1.5;
|
||||
}
|
||||
.status {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-m);
|
||||
align-self: flex-start;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.status i {
|
||||
width: var(--size-border-thick);
|
||||
height: var(--size-border-thick);
|
||||
border-radius: 50%;
|
||||
background: var(--color-success);
|
||||
}
|
||||
.workspace {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(var(--size-workspace-min), 1fr) minmax(
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
<script lang="ts">
|
||||
import type { InputMode, ResultKind } from "$lib/registry";
|
||||
import { RefreshCw } from "@lucide/svelte";
|
||||
import type { Snippet } from "svelte";
|
||||
|
||||
interface Props {
|
||||
label: string;
|
||||
viewMode: InputMode | ResultKind;
|
||||
children: Snippet;
|
||||
loading?: boolean;
|
||||
parts?: number;
|
||||
}
|
||||
let {
|
||||
label,
|
||||
viewMode: mode,
|
||||
children,
|
||||
loading = false,
|
||||
parts = undefined,
|
||||
}: Props = $props();
|
||||
|
||||
// TODO: make canvas aspect ratio auto, from 16:9 to 9:16 max
|
||||
</script>
|
||||
|
||||
<figure class="tile">
|
||||
<figcaption>
|
||||
<span>
|
||||
{label}
|
||||
{#if parts !== undefined}
|
||||
<span class="count">{parts} parts</span>
|
||||
{/if}
|
||||
{#if loading}
|
||||
<RefreshCw class="rotating" size="12" />
|
||||
{/if}
|
||||
</span>
|
||||
</figcaption>
|
||||
<div class="canvas" class:checker={mode === "image"}>
|
||||
{@render children()}
|
||||
</div>
|
||||
</figure>
|
||||
|
||||
<style>
|
||||
.tile figcaption {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-m);
|
||||
margin-bottom: var(--space-m);
|
||||
font: var(--font-size-s) var(--font-mono);
|
||||
color: var(--color-text-muted);
|
||||
& :global(.rotating) {
|
||||
animation: rotate var(--duration-l) linear infinite;
|
||||
}
|
||||
}
|
||||
.count {
|
||||
margin-left: var(--space-m);
|
||||
color: var(--color-main);
|
||||
}
|
||||
.canvas {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: clamp(var(--space-brand), 30vh, 60vh);
|
||||
border: var(--size-border) solid var(--color-border);
|
||||
border-radius: var(--radius-s);
|
||||
overflow: hidden;
|
||||
background: var(--color-background-muted);
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
.canvas :global(img) {
|
||||
width: auto;
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
max-height: 100vh;
|
||||
object-fit: contain;
|
||||
display: block;
|
||||
}
|
||||
.canvas.checker {
|
||||
background: repeating-conic-gradient(
|
||||
var(--color-checker-main) 0 25%,
|
||||
var(--color-checker-alt) 0 50%
|
||||
)
|
||||
50% / 28px 28px;
|
||||
}
|
||||
@keyframes rotate {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -5,8 +5,9 @@
|
||||
interface Props {
|
||||
label: string;
|
||||
onclick?: () => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
let { label, onclick }: Props = $props();
|
||||
let { label, onclick, disabled = false }: Props = $props();
|
||||
</script>
|
||||
|
||||
<Button icon={Download} {label} {onclick} />
|
||||
<Button icon={Download} {label} {onclick} {disabled} />
|
||||
|
||||
Reference in New Issue
Block a user