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
|
- [ ] Переделать внешний вид инструментов без настроек, например Remove alpha
|
||||||
channel PNG "remove-alpha-channel-png". Сейчас выглядит слишком пустым.
|
channel PNG "remove-alpha-channel-png". Сейчас выглядит слишком пустым.
|
||||||
- [ ] Убрать описание формата в отображении исходник\результат - это должно быть
|
- [ ] Убрать описание формата в отображении исходник\результат - это должно быть
|
||||||
@@ -31,14 +41,14 @@
|
|||||||
паспортом, семейства с кириллицей)
|
паспортом, семейства с кириллицей)
|
||||||
- [ ] Посмотреть темную тему - слишком темная и слишком много оттенка (убавить c
|
- [ ] Посмотреть темную тему - слишком темная и слишком много оттенка (убавить c
|
||||||
в hct?)
|
в hct?)
|
||||||
- [ ] на картинке результате при работе дергается высота надписи (высота иконки)
|
- [x] на картинке результате при работе дергается высота надписи (высота иконки)
|
||||||
- [x] Глянуть что за ошибка (воспроизвелось на blur-png, в том числе и повторно)
|
- [x] Глянуть что за ошибка (воспроизвелось на blur-png, в том числе и повторно)
|
||||||
[баг в хроме](https://issues.chromium.org/issues/556160936)
|
[баг в хроме](https://issues.chromium.org/issues/556160936)
|
||||||
`Uncaught TypeError: Cannot read properties of undefined (reading 'startTime')`
|
`Uncaught TypeError: Cannot read properties of undefined (reading 'startTime')`
|
||||||
`at et.reportAllChanges (anonymous:2:19429)`
|
`at et.reportAllChanges (anonymous:2:19429)`
|
||||||
- [ ] сделать загрузку картинки исходника по ctrl+v и drag-and-drop (dropzone)
|
- [ ] сделать загрузку картинки исходника по ctrl+v и drag-and-drop (dropzone)
|
||||||
- [x] не работают генераторы картинок (create-empty-png)
|
- [x] не работают генераторы картинок (create-empty-png)
|
||||||
- [ ] генераторы работают не автоматически - требуется нажать кнопку generate
|
- [x] генераторы работают не автоматически - требуется нажать кнопку generate
|
||||||
(create-empty-png)
|
(create-empty-png)
|
||||||
- [ ] square-mask-png - проверить работу - параметры неверно считаются. width -
|
- [ ] square-mask-png - проверить работу - параметры неверно считаются. width -
|
||||||
100, height - 100, x - 100, y - 100, обрезает 50% на 50% картинку
|
100, height - 100, x - 100, y - 100, обрезает 50% на 50% картинку
|
||||||
@@ -56,9 +66,9 @@
|
|||||||
- [ ] придумать процесс редактирования и настройки pipeline - drag-n-drop,
|
- [ ] придумать процесс редактирования и настройки pipeline - drag-n-drop,
|
||||||
удаление, добавление инструментов. переход один инструмент - pipeline без
|
удаление, добавление инструментов. переход один инструмент - pipeline без
|
||||||
перезагрузки
|
перезагрузки
|
||||||
- [ ] Слаги инструментов (`*-png`) — надо обдумать, возможно что ссылки\маршруты
|
- [ ] Слаги инструментов (`*-png`) — надо переименовть, они работают с любыми
|
||||||
должны быть с png, а названия инструментов - без. Это упростит названия
|
изображениями. Надо сделать отдельный интерфейс маппинга инструмент ->
|
||||||
инструментов и сделает их более понятными
|
ссылка
|
||||||
|
|
||||||
## Переезд
|
## Переезд
|
||||||
|
|
||||||
@@ -184,7 +194,12 @@
|
|||||||
|
|
||||||
9. **`png-info` (детальная информация о PNG)** — оценка M/L. Отложен из
|
9. **`png-info` (детальная информация о PNG)** — оценка M/L. Отложен из
|
||||||
миграции. Отдельный райз: exif-теги, редактирование, структурированный вывод;
|
миграции. Отдельный райз: exif-теги, редактирование, структурированный вывод;
|
||||||
нужен свой отдельный случай в UI, не «ещё один text/verdict».
|
нужен свой отдельный случай в UI, не «ещё один text/verdict». возможно нужна
|
||||||
|
вообще отдельная страница, несовместимая с другими инструментами: 1) она
|
||||||
|
работает не с изображением, а с мета данными из файла, 2) она не может быть
|
||||||
|
частью цепочки, т.к. результат не является изображением, 3) если надо
|
||||||
|
изменить теги, никакого форматирования и изменения в изображение вносить не
|
||||||
|
надо, только в мета информацияю
|
||||||
|
|
||||||
10. **Region-инструменты** (censor/erase/pixelate-area/blur-area/sharpen-area/
|
10. **Region-инструменты** (censor/erase/pixelate-area/blur-area/sharpen-area/
|
||||||
reverse-area) — ждут UI выделения области на превью. Из
|
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-main: #86efac;
|
||||||
--brand-alt: hct(from var(--brand-main) calc(h + 90) c t);
|
--brand-alt: hct(from var(--brand-main) calc(h + 90) c t);
|
||||||
|
|
||||||
--color-background: hct(from var(--brand-main) h 8 95);
|
--color-background: hct(from var(--brand-main) h 2 95);
|
||||||
--color-background-muted: hct(from var(--brand-main) h 12 85);
|
--color-background-muted: hct(from var(--brand-main) h 4 85);
|
||||||
--color-panel: hct(from var(--brand-main) h 6.5 98);
|
--color-panel: hct(from var(--brand-main) h 3 98);
|
||||||
--color-checker-main: hct(from var(--brand-main) h 10.7 85.7);
|
--color-checker-main: hct(from var(--brand-main) h 5 85.7);
|
||||||
--color-checker-alt: hct(from var(--brand-main) h 13.2 76);
|
--color-checker-alt: hct(from var(--brand-main) h 7 76);
|
||||||
|
|
||||||
--color-border: hct(from var(--brand-main) h 13.1 69.4);
|
--color-border: hct(from var(--brand-main) h 13.1 69.4);
|
||||||
|
|
||||||
@@ -186,13 +186,13 @@
|
|||||||
--brand-main: #86efac;
|
--brand-main: #86efac;
|
||||||
--brand-alt: hct(from var(--brand-main) calc(h + 90) c t);
|
--brand-alt: hct(from var(--brand-main) calc(h + 90) c t);
|
||||||
|
|
||||||
--color-background: hct(from var(--brand-main) h 15 6);
|
--color-background: hct(from var(--brand-main) h 2 16);
|
||||||
--color-background-muted: hct(from var(--brand-main) h 15 10);
|
--color-background-muted: hct(from var(--brand-main) h 4 24);
|
||||||
--color-panel: hct(from var(--brand-main) h 12 8);
|
--color-panel: hct(from var(--brand-main) h 3 26);
|
||||||
--color-checker-main: hct(from var(--brand-main) h 12 10);
|
--color-checker-main: hct(from var(--brand-main) h 6 16);
|
||||||
--color-checker-alt: hct(from var(--brand-main) h 14 14);
|
--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: hct(from var(--brand-main) h 25 90);
|
||||||
--color-text-muted: hct(from var(--brand-main) h 22 70);
|
--color-text-muted: hct(from var(--brand-main) h 22 70);
|
||||||
|
|||||||
@@ -7,7 +7,6 @@
|
|||||||
|
|
||||||
<div class="catalog-head">
|
<div class="catalog-head">
|
||||||
<div>
|
<div>
|
||||||
<div class="eyebrow">EASY-PNG-TOOLS / CATALOG</div>
|
|
||||||
<h1>Tool catalog</h1>
|
<h1>Tool catalog</h1>
|
||||||
<p>
|
<p>
|
||||||
Focused utilities for working with PNG. Inspect, transform, and export —
|
Focused utilities for working with PNG. Inspect, transform, and export —
|
||||||
@@ -28,12 +27,6 @@
|
|||||||
gap: var(--space-xxl);
|
gap: var(--space-xxl);
|
||||||
flex-wrap: wrap;
|
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 {
|
h1 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: var(--font-size-2xl);
|
font-size: var(--font-size-2xl);
|
||||||
|
|||||||
@@ -1,32 +1,20 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { Upload } from "@lucide/svelte";
|
||||||
import DownloadButton from "./ui/DownloadButton.svelte";
|
import DownloadButton from "./ui/DownloadButton.svelte";
|
||||||
import { Sparkles, Upload } from "@lucide/svelte";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
inputMode: "image" | "text" | "none";
|
inputMode: "image" | "text" | "none";
|
||||||
canDownload: boolean;
|
canDownload: boolean;
|
||||||
running: boolean;
|
running: boolean;
|
||||||
onupload: (file: File) => void;
|
onupload: (file: File) => void;
|
||||||
ongenerate: () => void;
|
|
||||||
ondownload: () => void;
|
ondownload: () => void;
|
||||||
}
|
}
|
||||||
let {
|
let { inputMode, canDownload, running, onupload, ondownload }: Props =
|
||||||
inputMode,
|
$props();
|
||||||
canDownload,
|
|
||||||
running,
|
|
||||||
onupload,
|
|
||||||
ongenerate,
|
|
||||||
ondownload,
|
|
||||||
}: Props = $props();
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
{#if inputMode === "none"}
|
{#if inputMode === "image"}
|
||||||
<button class="btn generate" onclick={ongenerate} disabled={running}>
|
|
||||||
<Sparkles size={14} />
|
|
||||||
{running ? "Generating…" : "Generate"}
|
|
||||||
</button>
|
|
||||||
{:else if inputMode === "image"}
|
|
||||||
<label class="btn upload">
|
<label class="btn upload">
|
||||||
<Upload size={14} /> Open image
|
<Upload size={14} /> Open image
|
||||||
<input
|
<input
|
||||||
@@ -40,7 +28,11 @@
|
|||||||
</label>
|
</label>
|
||||||
{/if}
|
{/if}
|
||||||
{#if canDownload}
|
{#if canDownload}
|
||||||
<DownloadButton label="Download result" onclick={ondownload} />
|
<DownloadButton
|
||||||
|
label="Download result"
|
||||||
|
onclick={ondownload}
|
||||||
|
disabled={running}
|
||||||
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -62,15 +54,6 @@
|
|||||||
color: var(--color-text-muted);
|
color: var(--color-text-muted);
|
||||||
cursor: pointer;
|
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 {
|
.upload input {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,6 @@
|
|||||||
running?: boolean;
|
running?: boolean;
|
||||||
error?: string;
|
error?: string;
|
||||||
onupload: (file: File) => void;
|
onupload: (file: File) => void;
|
||||||
ongenerate?: () => void;
|
|
||||||
ontextsource?: (text: string) => void;
|
ontextsource?: (text: string) => void;
|
||||||
onrendertext?: () => void;
|
onrendertext?: () => void;
|
||||||
oncopytext?: () => void;
|
oncopytext?: () => void;
|
||||||
@@ -35,7 +34,6 @@
|
|||||||
running = false,
|
running = false,
|
||||||
error = "",
|
error = "",
|
||||||
onupload,
|
onupload,
|
||||||
ongenerate,
|
|
||||||
ontextsource,
|
ontextsource,
|
||||||
onrendertext,
|
onrendertext,
|
||||||
oncopytext,
|
oncopytext,
|
||||||
@@ -43,16 +41,16 @@
|
|||||||
ondownload,
|
ondownload,
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
const isGenerator = $derived(inputMode === "none");
|
const sourceValue = $derived.by(() => {
|
||||||
const sourceValue = $derived(
|
switch (inputMode) {
|
||||||
isGenerator
|
case "none":
|
||||||
? "—"
|
return "—";
|
||||||
: inputMode === "text"
|
case "text":
|
||||||
? "text"
|
return "text";
|
||||||
: source
|
case "image":
|
||||||
? `${source.width} × ${source.height} px`
|
return source ? `${source.width} × ${source.height} px` : "—";
|
||||||
: "—",
|
}
|
||||||
);
|
});
|
||||||
const resultValue = $derived(
|
const resultValue = $derived(
|
||||||
resultKind === "image"
|
resultKind === "image"
|
||||||
? result
|
? result
|
||||||
@@ -77,15 +75,12 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="panel-head">
|
<div class="panel-head">
|
||||||
<span class="label"
|
<span class="label">PREVIEW PANEL</span>
|
||||||
>{isGenerator ? "GENERATOR / RESULT" : "SOURCE / RESULT"}</span
|
|
||||||
>
|
|
||||||
<SchemaActions
|
<SchemaActions
|
||||||
{inputMode}
|
{inputMode}
|
||||||
canDownload={hasResult}
|
canDownload={hasResult}
|
||||||
{running}
|
{running}
|
||||||
{onupload}
|
{onupload}
|
||||||
ongenerate={ongenerate ?? (() => {})}
|
|
||||||
{ondownload}
|
{ondownload}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { toDataUrl } from "$lib/core/io";
|
import { toDataUrl } from "$lib/core/io";
|
||||||
import type { PixelImage } from "$lib/core/types";
|
import type { PixelImage } from "$lib/core/types";
|
||||||
import type { FileResult } from "$lib/registry";
|
import type { FileResult, ResultKind } from "$lib/registry";
|
||||||
import { Check, RefreshCw } from "@lucide/svelte";
|
import { Check } from "@lucide/svelte";
|
||||||
import SchemaTextResult from "./SchemaTextResult.svelte";
|
import SchemaTextResult from "./SchemaTextResult.svelte";
|
||||||
|
import PreviewTile from "./layout/PreviewTile.svelte";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
resultKind?: "image" | "text" | "verdict" | "files";
|
resultKind?: ResultKind;
|
||||||
result: PixelImage | null;
|
result: PixelImage | null;
|
||||||
fileResult?: FileResult | null;
|
fileResult?: FileResult | null;
|
||||||
textResult?: string | null;
|
textResult?: string | null;
|
||||||
@@ -33,20 +34,13 @@
|
|||||||
}))
|
}))
|
||||||
: [],
|
: [],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let parts = $derived(
|
||||||
|
resultKind === "files" && fileResult ? fileResult.files.length : undefined,
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<figure class="tile">
|
<PreviewTile label="RESULT" viewMode={resultKind} loading={running} {parts}>
|
||||||
<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>
|
|
||||||
<div class="canvas" class:checker={resultKind === "image"}>
|
<div class="canvas" class:checker={resultKind === "image"}>
|
||||||
{#if resultKind === "text" && textResult}
|
{#if resultKind === "text" && textResult}
|
||||||
<div class="text-result-wrap">
|
<div class="text-result-wrap">
|
||||||
@@ -83,20 +77,9 @@
|
|||||||
<span class="empty">no result yet</span>
|
<span class="empty">no result yet</span>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</figure>
|
</PreviewTile>
|
||||||
|
|
||||||
<style>
|
<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 {
|
.canvas {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -109,8 +92,10 @@
|
|||||||
color: var(--color-text-muted);
|
color: var(--color-text-muted);
|
||||||
}
|
}
|
||||||
.canvas img {
|
.canvas img {
|
||||||
width: 100%;
|
width: auto;
|
||||||
height: 100%;
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
max-height: 100vh;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
@@ -124,10 +109,6 @@
|
|||||||
.empty {
|
.empty {
|
||||||
font: var(--font-size-s) var(--font-mono);
|
font: var(--font-size-s) var(--font-mono);
|
||||||
}
|
}
|
||||||
.count {
|
|
||||||
margin-left: var(--space-m);
|
|
||||||
color: var(--color-main);
|
|
||||||
}
|
|
||||||
.parts-grid {
|
.parts-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(
|
grid-template-columns: repeat(
|
||||||
@@ -170,13 +151,4 @@
|
|||||||
padding: var(--space-l);
|
padding: var(--space-l);
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes rotate {
|
|
||||||
0% {
|
|
||||||
transform: rotate(0deg);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import { toDataUrl } from "$lib/core/io";
|
import { toDataUrl } from "$lib/core/io";
|
||||||
import type { PixelImage } from "$lib/core/types";
|
import type { PixelImage } from "$lib/core/types";
|
||||||
import type { InputMode } from "$lib/registry";
|
import type { InputMode } from "$lib/registry";
|
||||||
|
import PreviewTile from "./layout/PreviewTile.svelte";
|
||||||
import SchemaTextSource from "./SchemaTextSource.svelte";
|
import SchemaTextSource from "./SchemaTextSource.svelte";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -24,61 +25,26 @@
|
|||||||
const sourceUrl = $derived(source ? toDataUrl(source) : null);
|
const sourceUrl = $derived(source ? toDataUrl(source) : null);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<figure class="tile">
|
<PreviewTile label="SOURCE" viewMode={mode}>
|
||||||
<figcaption><span>SOURCE</span></figcaption>
|
{#if mode === "text"}
|
||||||
<div class="canvas" class:checker={mode === "image"}>
|
<div class="text-source-wrap">
|
||||||
{#if mode === "text"}
|
<SchemaTextSource
|
||||||
<div class="text-source-wrap">
|
value={textSource}
|
||||||
<SchemaTextSource
|
disabled={running}
|
||||||
value={textSource}
|
oninput={ontextinput ?? (() => {})}
|
||||||
disabled={running}
|
onrender={onrendertext ?? (() => {})}
|
||||||
oninput={ontextinput ?? (() => {})}
|
/>
|
||||||
onrender={onrendertext ?? (() => {})}
|
</div>
|
||||||
/>
|
{:else if mode === "image" && sourceUrl}
|
||||||
</div>
|
<img src={sourceUrl} alt="source" />
|
||||||
{:else if mode === "image" && sourceUrl}
|
{:else if mode === "image"}
|
||||||
<img src={sourceUrl} alt="source" />
|
<span class="empty">choose an image</span>
|
||||||
{:else if mode === "image"}
|
{:else}
|
||||||
<span class="empty">choose an image</span>
|
<span class="empty">no source — configure the parameters</span>
|
||||||
{:else}
|
{/if}
|
||||||
<span class="empty">no source — configure the parameters</span>
|
</PreviewTile>
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</figure>
|
|
||||||
|
|
||||||
<style>
|
<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 {
|
.empty {
|
||||||
font: var(--font-size-s) var(--font-mono);
|
font: var(--font-size-s) var(--font-mono);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,12 +8,12 @@
|
|||||||
import { execute } from "$lib/executor";
|
import { execute } from "$lib/executor";
|
||||||
import { t } from "$lib/i18n/t";
|
import { t } from "$lib/i18n/t";
|
||||||
import type { FileResult, ToolEntry } from "$lib/registry";
|
import type { FileResult, ToolEntry } from "$lib/registry";
|
||||||
import { downloadZip } from "$lib/zip";
|
|
||||||
import {
|
import {
|
||||||
defaultSchemaParams,
|
defaultSchemaParams,
|
||||||
sanitizeSchemaParams,
|
sanitizeSchemaParams,
|
||||||
type ToolSchema,
|
type ToolSchema,
|
||||||
} from "$lib/registry-schema";
|
} from "$lib/registry-schema";
|
||||||
|
import { downloadZip } from "$lib/zip";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
tool: ToolEntry;
|
tool: ToolEntry;
|
||||||
@@ -142,8 +142,10 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (inputMode !== "image" || !source || !started) return;
|
// TODO: can any edge case start infinite loop?
|
||||||
|
if (!started) return;
|
||||||
void values;
|
void values;
|
||||||
|
void source;
|
||||||
debouncedRun();
|
debouncedRun();
|
||||||
return () => debouncedRun.cancel();
|
return () => debouncedRun.cancel();
|
||||||
});
|
});
|
||||||
@@ -159,11 +161,9 @@
|
|||||||
<div class="schema-tool">
|
<div class="schema-tool">
|
||||||
<header class="header">
|
<header class="header">
|
||||||
<div class="title-block">
|
<div class="title-block">
|
||||||
<span class="eyebrow">PNG PROCESSING <span>/</span> SINGLE TOOL</span>
|
|
||||||
<h1>{tool.title}</h1>
|
<h1>{tool.title}</h1>
|
||||||
<p class="lede">{tool.description}</p>
|
<p class="lede">{tool.description}</p>
|
||||||
</div>
|
</div>
|
||||||
<span class="status"><i></i> LIVE PREVIEW</span>
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="workspace">
|
<div class="workspace">
|
||||||
@@ -187,7 +187,6 @@
|
|||||||
{running}
|
{running}
|
||||||
{error}
|
{error}
|
||||||
onupload={handleFile}
|
onupload={handleFile}
|
||||||
ongenerate={run}
|
|
||||||
ontextsource={(textValue) => {
|
ontextsource={(textValue) => {
|
||||||
textSource = textValue;
|
textSource = textValue;
|
||||||
}}
|
}}
|
||||||
@@ -221,14 +220,11 @@
|
|||||||
line-height: 1.1;
|
line-height: 1.1;
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
}
|
}
|
||||||
.eyebrow,
|
.label {
|
||||||
.label,
|
|
||||||
.status {
|
|
||||||
font: var(--font-size-s) var(--font-mono);
|
font: var(--font-size-s) var(--font-mono);
|
||||||
letter-spacing: var(--space-text-l);
|
letter-spacing: var(--space-text-l);
|
||||||
color: var(--color-text-muted);
|
color: var(--color-text-muted);
|
||||||
}
|
}
|
||||||
.eyebrow span,
|
|
||||||
.label {
|
.label {
|
||||||
color: var(--color-main);
|
color: var(--color-main);
|
||||||
}
|
}
|
||||||
@@ -239,19 +235,6 @@
|
|||||||
font-size: var(--font-size-s);
|
font-size: var(--font-size-s);
|
||||||
line-height: 1.5;
|
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 {
|
.workspace {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(var(--size-workspace-min), 1fr) minmax(
|
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 {
|
interface Props {
|
||||||
label: string;
|
label: string;
|
||||||
onclick?: () => void;
|
onclick?: () => void;
|
||||||
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
let { label, onclick }: Props = $props();
|
let { label, onclick, disabled = false }: Props = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Button icon={Download} {label} {onclick} />
|
<Button icon={Download} {label} {onclick} {disabled} />
|
||||||
|
|||||||
Reference in New Issue
Block a user