Compare commits
17
Commits
4794523084
...
d1a386f92f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d1a386f92f | ||
|
|
1813383724 | ||
|
|
1ae4b36c9c | ||
|
|
a57acd1ff2 | ||
|
|
6ec3e75df4 | ||
|
|
a24a182d18 | ||
|
|
1e63eaa03b | ||
|
|
e9de7a4bf8 | ||
|
|
7e94205866 | ||
|
|
2f758a88af | ||
|
|
991fe69381 | ||
|
|
11c4ccece4 | ||
|
|
8a6f87938d | ||
|
|
3d0f8ebb10 | ||
|
|
28ff9d3ee1 | ||
|
|
ef74cabf1e | ||
|
|
c590bdf91a |
@@ -9,3 +9,4 @@ refs/public/placeholder-logo.svg
|
|||||||
refs/public/placeholder-logo.png
|
refs/public/placeholder-logo.png
|
||||||
refs/public/placeholder.svg
|
refs/public/placeholder.svg
|
||||||
web/audit/cdp-audit*.txt
|
web/audit/cdp-audit*.txt
|
||||||
|
css-lint.txt
|
||||||
|
|||||||
@@ -0,0 +1,391 @@
|
|||||||
|
# План: составные типы параметров и полная типизация pipeline
|
||||||
|
|
||||||
|
> Статус: **в реализации.** Фаза 0 (фундамент) — сделана. Следующее: Фаза 1 —
|
||||||
|
> один полноценный рабочий инструмент в preview, затем масштабирование.
|
||||||
|
>
|
||||||
|
> Файлы: `web/src/lib/registry.ts` (3918 строк), `web/src/lib/preview/tool-views.ts` (193 строки),
|
||||||
|
> `web/src/lib/registry-schema.ts`.
|
||||||
|
|
||||||
|
## Ключевая стратегия: параллельная сборка, старый UI не трогаем
|
||||||
|
|
||||||
|
Старый UI (группа `(old)/`, `ParamForm.svelte` + старый pipeline) **продолжает
|
||||||
|
работать как сейчас, без рефакторинга**. Новый `ToolSchema<P>` строится
|
||||||
|
рядом и служит источником для **нового UI**. Инструменты переписываются под
|
||||||
|
новый registry по мере нужды — для нового UI.
|
||||||
|
|
||||||
|
Это та же логика, что в `plan-redesign.md` применена к дизайну: параллельная
|
||||||
|
сборка, изоляция от старого, затем новый становится основным и старый
|
||||||
|
удаляется вместе со старым дизайном.
|
||||||
|
|
||||||
|
**Разделение схем: две независимые схемы.**
|
||||||
|
|
||||||
|
| Схема | Источник | Использование |
|
||||||
|
| --------------------- | ------------------------------------------------- | ---------------------------------------------------------------- |
|
||||||
|
| Старый `ParamDef[]` | В `ToolEntry.params` — **остаётся без изменений** | Старый UI, старый pipeline (`ParamForm.svelte`, executor, chain) |
|
||||||
|
| Новый `ToolSchema<P>` | Новое поле `ToolEntry.schema` — добавляется рядом | Новый UI (kit/`ParamControl`, новый pipeline) |
|
||||||
|
|
||||||
|
- Старая и новая схемы **не выводятся друг из друга** — это два независимых
|
||||||
|
описания инструмента.
|
||||||
|
- Пока новый UI не готов — `schema` просто отсутствует у большинства
|
||||||
|
инструментов, старый UI ничего не замечает.
|
||||||
|
- Удаление старого UI (после перехода) попутно удаляет и старый `ParamDef[]`
|
||||||
|
|
||||||
|
## Проблема (текущая)
|
||||||
|
|
||||||
|
- `ParamDef` — плоский union из 6 примитивов (`number | slider | select |
|
||||||
|
checkbox | color | text`). Параметры инструмента — отдельные записи в плоском
|
||||||
|
массиве.
|
||||||
|
- `run()`/`generate()` получают `Record<string, unknown>`; значения достаются
|
||||||
|
через `num(p, "fromColor")` — runtime string lookup **без type safety**,
|
||||||
|
легко напутать id.
|
||||||
|
- Поля инструмента **слабо связаны** (например «ширина» и «цвет» рамки — два
|
||||||
|
независимых поля без общего контракта) → общий генерализованный рендер, нет
|
||||||
|
возможности сделать идеальный пер-инструмент UI.
|
||||||
|
- Preview-система (`tool-views.ts`) **дублирует** registry (FieldDef vs
|
||||||
|
ParamDef) — одни и те же инструменты описаны дважды.
|
||||||
|
|
||||||
|
## Решения (принятые в обсуждении)
|
||||||
|
|
||||||
|
1. **Полная типизация.** `interface Params` (runtime-тип) объявляется явно в
|
||||||
|
каждом инструменте. `run`/`generate` типизируются прямо на него, а не на
|
||||||
|
`Record<string, unknown>`.
|
||||||
|
2. **Отдельная типизированная схема для UI/дефолтов/валидации.** Два понятия
|
||||||
|
на инструмент: runtime-тип + схема. Схема проверяется компилятором на
|
||||||
|
соответствие `Params` (чтобы не рассинхронизировались).
|
||||||
|
3. **Общий рендер,** но с пер-инструмент layout: общие компоненты рисуют поля
|
||||||
|
по схеме; схема инструмента задаёт layout и группировку. Без отдельного
|
||||||
|
Svelte-компонента на каждый инструмент.
|
||||||
|
4. **Составные типы** (`gradient`, `color-pair`, `dimension`, `offset`,
|
||||||
|
`position9`, `font-style`, `plate`) остаются в плане — это способ выразить
|
||||||
|
в схеме связанную группу полей и переиспользовать её на нескольких
|
||||||
|
инструментах и в UI.
|
||||||
|
5. Рано или поздно `tool-views.ts` поглощается registry (preview/lede/layout
|
||||||
|
переезжают в meta инструмента) — но это отдельный шаг, см. ниже.
|
||||||
|
6. **Старый UI не рефакторим.** Две независимые схемы: старый `ParamDef[]`
|
||||||
|
остаётся источником для старого UI без изменений; новый `ToolSchema<P>`
|
||||||
|
строится рядом для нового UI (см. «Ключевая стратегия» выше).
|
||||||
|
|
||||||
|
## Целевая архитектура (на примере «добавить рамку»)
|
||||||
|
|
||||||
|
Цель: «добавить рамку: ширина и цвет» — **один интерфейс**, а не два
|
||||||
|
слабо связанных поля.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// 1. Runtime-тип — что получает run/generate
|
||||||
|
interface AddBorderParams {
|
||||||
|
thickness: number; // рамка: ширина
|
||||||
|
color: string; // рамка: цвет
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Типизированная UI-схема, проверяемая компилятором против Params
|
||||||
|
// Ключи схемы обязаны совпадать с полями Params; default совместим с типом.
|
||||||
|
const addBorderSchema = toolSchema<AddBorderParams>({
|
||||||
|
thickness: field.slider({ min: 1, max: 500, default: 5 }),
|
||||||
|
color: field.color({ default: "#000000" }),
|
||||||
|
layout: { group: "border", cols: 2 }, // пер-инструмент раскладка
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3. Инструмент в registry
|
||||||
|
const addBorder: ToolEntry<AddBorderParams> = {
|
||||||
|
id: "add-border-png",
|
||||||
|
title: "Add border to PNG",
|
||||||
|
category: "geometry",
|
||||||
|
schema: addBorderSchema,
|
||||||
|
run: (img, p) => expandCanvas(img, 4x..., p.color), // p.thickness — type safe
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Что даёт
|
||||||
|
|
||||||
|
- `p.thickness`, `p.color` — IDE autocomplete + compile-time ошибка при ошибке.
|
||||||
|
- `run()` нельзя передать лишний/чужой ключ.
|
||||||
|
- Схема — единый источник для дефолтов, валидации и рендера.
|
||||||
|
- Схема одного инструмента может задавать layout, которого нет в общем рендере.
|
||||||
|
|
||||||
|
## Типовая система (набросок)
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// ─── Поле схемы ───
|
||||||
|
interface NumberField { kind: "number"; default: number; min?: number; max?: number; step?: number }
|
||||||
|
interface SliderField { kind: "slider"; default: number; min: number; max: number; step?: number }
|
||||||
|
interface ColorField { kind: "color"; default: string }
|
||||||
|
interface SelectField { kind: "select"; default: string; options: { value: string; label: string }[] }
|
||||||
|
interface TextField { kind: "text"; default: string; placeholder?: string }
|
||||||
|
interface CheckboxField { kind: "checkbox"; default: boolean }
|
||||||
|
|
||||||
|
type FieldDef =
|
||||||
|
| NumberField | SliderField | ColorField | SelectField | TextField | CheckboxField;
|
||||||
|
|
||||||
|
// ─── «поле» описывает тип значения + схему вместе, чтобы не рассинхронизировать ───
|
||||||
|
interface Field<T> {
|
||||||
|
schema: FieldDef;
|
||||||
|
// T — ожидаемый runtime-тип этого поля (number|string|boolean)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Builder: field.slider({...}) возвращает Field<number> с типизированной схемой ───
|
||||||
|
const field = {
|
||||||
|
slider: (s: SliderField): Field<number> => ({ schema: s }),
|
||||||
|
number: (s: NumberField): Field<number> => ({ schema: s }),
|
||||||
|
color: (s: ColorField): Field<string> => ({ schema: s }),
|
||||||
|
select: <V extends string>(s: SelectField & { options: {value: V}[] }): Field<V> => ({ schema: s }),
|
||||||
|
text: (s: TextField): Field<string> => ({ schema: s }),
|
||||||
|
checkbox: (s: CheckboxField): Field<boolean> => ({ schema: s }),
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── toolSchema<P>: проверяет, что ключи схемы === полям P и типы сходятся ───
|
||||||
|
type FieldsOf<T> = { [K in keyof T]: Field<T[K]> } & {
|
||||||
|
layout?: { group?: string; cols?: number };
|
||||||
|
};
|
||||||
|
function toolSchema<P>(fields: FieldsOf<P>): ToolSchema<P> { ... }
|
||||||
|
// TS выдаст ошибку, если schema содержит ключ, отсутствующий в Params, или
|
||||||
|
// если type поля не совпадает с типом Params[K].
|
||||||
|
```
|
||||||
|
|
||||||
|
Ключевая идея связки: **builder `field.x<T>()` связывает runtime-тип поля с его
|
||||||
|
схемой на этапе компиляции**, а `toolSchema<P>` заставляет ключи схемы
|
||||||
|
совпадать с ключами `P`. Это и есть защита от рассинхрона.
|
||||||
|
|
||||||
|
## Составные типы (для переиспользования и группировки)
|
||||||
|
|
||||||
|
| Тип | Под-поля | Инструментов |
|
||||||
|
| ------------ | -------------------------- | ------------ |
|
||||||
|
| `dimension` | width + height | 12 |
|
||||||
|
| `color-pair` | from + to | 4 |
|
||||||
|
| `offset` | x + y | 4 |
|
||||||
|
| `gradient` | from + to + dir (+ type?) | 1 |
|
||||||
|
| `position9` | position (3×3 grid) | 3 |
|
||||||
|
| `font-style` | font + size + bold + color | 3-4 |
|
||||||
|
| `plate` | enabled + color + opacity | 2-3 |
|
||||||
|
|
||||||
|
Составной тип = группы полей, которые в интерфейсе `Params` могут разворачиваться
|
||||||
|
во вложенный объект или плоские ключи — решается на этапе реализации.
|
||||||
|
Пример для `gradient`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
interface GradientParams {
|
||||||
|
from: string;
|
||||||
|
to: string;
|
||||||
|
dir: string;
|
||||||
|
}
|
||||||
|
// схема: field.color(from), field.color(to), field.select(dir)
|
||||||
|
// плюс annotation в схеме: { group: "gradient" } → UI рисует как цветовой переход
|
||||||
|
```
|
||||||
|
|
||||||
|
## Инвентарь: какие инструменты мигрировать
|
||||||
|
|
||||||
|
### dimension (12)
|
||||||
|
|
||||||
|
resize-png*, crop-png*, fit-on-background-png, create-empty-png,
|
||||||
|
single-color-png, random-noise-png, linear-gradient-png, color-spectrum-png,
|
||||||
|
random-colors-png, draw-grid-png, placeholder-png, change-canvas-size-png
|
||||||
|
|
||||||
|
(\* у resize/crop — не просто dimension, есть специфика: keepAspect, x/y и т.д.)
|
||||||
|
|
||||||
|
### color-pair (4)
|
||||||
|
|
||||||
|
blend-two-png, step-colors-png, linear-gradient-png, two-colors-png
|
||||||
|
|
||||||
|
### offset (4)
|
||||||
|
|
||||||
|
circle-mask-png, square-mask-png, star-mask-png, wavy-mask-png
|
||||||
|
|
||||||
|
### position9 (3)
|
||||||
|
|
||||||
|
add-text-png, date-stamp-png, watermark-image-png
|
||||||
|
|
||||||
|
### font-style (3-4)
|
||||||
|
|
||||||
|
text-to-png, add-text-png, date-stamp-png
|
||||||
|
|
||||||
|
### plate (2-3)
|
||||||
|
|
||||||
|
add-text-png, date-stamp-png
|
||||||
|
|
||||||
|
## Пошаговый план перевода инструментов
|
||||||
|
|
||||||
|
> Каждый шаг — **атомарное, маленькое изменение** (в идеале 1 инструмент =
|
||||||
|
> 1 diff, укладывается в правило «коммиты < ~500 строк»). После каждого шага —
|
||||||
|
> ревью; параллельно можно делать UI нового инструмента.
|
||||||
|
>
|
||||||
|
> **Принцип малых шагов:** перевод инструмента не меняет поведение старой
|
||||||
|
> схемы (`params` остаётся), не ломает старый UI. Инструмент получает ДОПОЛНИТЕЛЬНО
|
||||||
|
> новое поле `schema` + `interface Params`, старое `run`/`params` не трогаем.
|
||||||
|
> Это гарантирует, что старый UI продолжает работать без регресса.
|
||||||
|
>
|
||||||
|
> **Новый подход (решение пользователя):** сначала делаем **один полноценный
|
||||||
|
> рабочий инструмент в preview** — не эталон «на бумаге», а реально юзабельный,
|
||||||
|
> чтобы руками оценить, насколько это рабочее решение и какие возникнут сложности.
|
||||||
|
> Оцениваем, при необходимости правим инфраструктуру, и только потом переводим
|
||||||
|
> остальные инструменты тем же проверенным паттерном.
|
||||||
|
|
||||||
|
### Фаза 0 — фундамент ✔ (сделано)
|
||||||
|
|
||||||
|
Typed field builders + `Field<T>` + `toolSchema<P>()` + `ToolSchema<P>` —
|
||||||
|
в `web/src/lib/registry-schema.ts`. `ToolEntry<P>` generic + опциональное
|
||||||
|
поле `schema`. Новые `defaultSchemaParams`/`sanitizeSchemaParams`.
|
||||||
|
Старый `ParamDef[]`/pipeline не тронуты. Проверено: compile-time-защита
|
||||||
|
(лишний/неверный/отсутствующий ключ ловит TS), тесты, svelte-check без новых
|
||||||
|
ошибок.
|
||||||
|
|
||||||
|
### Фаза 1 — ОДИН полноценный рабочий инструмент в preview (срез)
|
||||||
|
|
||||||
|
5. **Инструмент «добавить рамку» (`add-border-png`)** — сделать полностью
|
||||||
|
рабочим в preview:
|
||||||
|
- `interface AddBorderParams { thickness: number; color: string }`
|
||||||
|
- `addBorderSchema = toolSchema<AddBorderParams>(...)` + `schema: ...` в entry
|
||||||
|
(старый `params`/`run` остаются — старый UI не регрессит).
|
||||||
|
- **Новый рендер в preview** (`kit/ToolView` → `ParamControl` или новый
|
||||||
|
компонент схемы): читает `ToolSchema<P>`, рисует поля рамки, применение
|
||||||
|
реально работает (end-to-end до картинки).
|
||||||
|
- Дефолты/валидация/значения — из схемы (`defaultSchemaParams`,
|
||||||
|
`sanitizeSchemaParams`).
|
||||||
|
- **Ревью-оценка:** удобно ли это, что сломалось/усложнилось, что поправить
|
||||||
|
в инфраструктуре до масштабирования. Принимается макет/решение.
|
||||||
|
|
||||||
|
Результат: один реально рабочий инструмент на новой системе = доказываем паттерн,
|
||||||
|
прежде чем переводить остальных.
|
||||||
|
|
||||||
|
### Фаза 2 — простые инструменты без составных типов (1 инструмент = 1 шаг)
|
||||||
|
|
||||||
|
Одиночные/небольшие инструменты, где `interface Params` + `schema` не требуют
|
||||||
|
составных типов. Каждый — отдельный маленький diff (~15-30 строк), тем самым
|
||||||
|
проверенным в Фазе 1 паттерном:
|
||||||
|
|
||||||
|
6. add-stroke-png (color + slider)
|
||||||
|
7. find-contour-png (color + slider)
|
||||||
|
8. convert-png-to-jpg (color + slider)
|
||||||
|
9. convert-png-to-webp (slider)
|
||||||
|
10. remove-color-from-png (color + slider)
|
||||||
|
11. extract-color-from-png (color + slider)
|
||||||
|
12. add-noise-png (slider + select + number)
|
||||||
|
13. randomize-pixels-png (slider + number)
|
||||||
|
14. pixelate-png (slider)
|
||||||
|
15. vignette-png (slider)
|
||||||
|
16. gamma-png (slider)
|
||||||
|
17. temperature-png (slider)
|
||||||
|
18. tint-png (color + slider)
|
||||||
|
19. quantize-png (slider)
|
||||||
|
20. custom-palette-png (text)
|
||||||
|
21. dithering-png (slider + select)
|
||||||
|
22. jpeg-artifacts-png (slider)
|
||||||
|
|
||||||
|
(и т.п. — все одиночные инструменты в этом же ключе)
|
||||||
|
|
||||||
|
### Фаза 3 — инструменты с составными типами (по типу, затем по инструментам)
|
||||||
|
|
||||||
|
24. `dimension` — ввести составной тип + перевести **один** пилот
|
||||||
|
(create-empty-png), ревью, затем остальные 11 по 1:
|
||||||
|
create-empty → single-color → random-noise → fit-on-background →
|
||||||
|
change-canvas-size → placeholder → draw-grid → random-colors →
|
||||||
|
color-spectrum → linear-gradient → resize → crop
|
||||||
|
25. `color-pair` — пилот (blend-two-png), затем two-colors → step-colors →
|
||||||
|
linear-gradient (уже переведён в dimension, здесь добавляется к нему)
|
||||||
|
26. `offset` — пилот (circle-mask-png), затем square-mask → star-mask →
|
||||||
|
wavy-mask
|
||||||
|
27. `position9` — перевести add-text → date-stamp → watermark-image
|
||||||
|
28. `font-style` — перевести text-to-png → add-text (если ещё не) →
|
||||||
|
date-stamp
|
||||||
|
29. `plate` — перевести add-text → date-stamp
|
||||||
|
30. `gradient` — собрать из dimension + color-pair + direction на
|
||||||
|
linear-gradient (зависит от решения по gradient, см. план)
|
||||||
|
|
||||||
|
### Фаза 4 — масштаб UI на остальные инструменты
|
||||||
|
|
||||||
|
31. По мере перевода инструментов в Фазе 2-3 — рендер схемы (из Фазы 1)
|
||||||
|
покрывает их автоматически; составные виджеты (dimension, color-pair,
|
||||||
|
offset, position9, font-style, plate, gradient) — по одному, каждый с ревью.
|
||||||
|
32. Пер-инструмент layout (`schema.layout`): группировка полей рамки и т.п.
|
||||||
|
33. Для каждого переведённого инструмента — UI-макет, принимается отдельно.
|
||||||
|
|
||||||
|
### Фаза 5 — зачистка
|
||||||
|
|
||||||
|
34. Поглощение `tool-views.ts` (preview/lede/layout → meta инструмента).
|
||||||
|
35. После перехода на новый дизайн — удаление старого UI и старого `ParamDef[]`.
|
||||||
|
|
||||||
|
### Как ревьюить каждый шаг
|
||||||
|
|
||||||
|
- Diff **только один инструмент** (или фундамент без инструментов).
|
||||||
|
- Старый `params`/`run`/`defaultParams`/`sanitizeParams` — **не изменены** (кроме
|
||||||
|
добаление `schema`, если это пилот).
|
||||||
|
- Старый UI и старые тесты продолжают работать — запустить
|
||||||
|
`pnpm --dir web test` и `pnpm --dir web exec svelte-check`.
|
||||||
|
- Порядок полей в `interface Params` и в `schema` совпадает; дефолты в сeme
|
||||||
|
равны старым `default` из `params`.
|
||||||
|
- Пилот из Фазы 1 проверяется **руками в preview**: применяется рамка,
|
||||||
|
дефолты/валидация/смена значений работают end-to-end.
|
||||||
|
|
||||||
|
## Поток данных (runtime)
|
||||||
|
|
||||||
|
### Старый pipeline (не трогаем)
|
||||||
|
|
||||||
|
Всё как сейчас: старое `defaultParams`/`sanitizeParams`/executor/chain работают
|
||||||
|
на `ParamDef[]` и `Record<string, unknown>`. Никаких изменений.
|
||||||
|
|
||||||
|
### Новый pipeline (строим рядом)
|
||||||
|
|
||||||
|
- **Хранилище значений** — типизировано на схему; у нового UI значения полей
|
||||||
|
собираются в `Params` по `ToolSchema<P>`.
|
||||||
|
- **defaultParams / sanitizeParams (новые)** — выводятся из `ToolSchema<P>`,
|
||||||
|
дефолты лежат в схеме. Старые функции не трогаем, добавляем новые рядом.
|
||||||
|
- **Executor/worker (новые)** — получает `Record<string, unknown>`, собирает
|
||||||
|
`Params` по схеме, вызывает `run`. Остаётся serializable.
|
||||||
|
- **Pipeline/chain (новый)** — шаги хранят значения, типизируются по схеме.
|
||||||
|
|
||||||
|
## Изменения в UI-компонентах
|
||||||
|
|
||||||
|
- **Старый UI** (`ParamForm.svelte`, группа `(old)/`) — **не трогаем.** Работает
|
||||||
|
на `ParamDef[]` как раньше.
|
||||||
|
- **Новый UI** (kit/`ParamControl.svelte`, новый pipeline) — читает
|
||||||
|
`ToolSchema<P>`; поддержка составных типов и пер-инструмент layout
|
||||||
|
(`schema.layout`).
|
||||||
|
- `tool-views.ts` — со временем поглощается registry (preview/lede/layout →
|
||||||
|
meta инструмента). Отдельный шаг, НЕ блокирует типизацию params.
|
||||||
|
|
||||||
|
## Оценка трудозатрат
|
||||||
|
|
||||||
|
| Часть | Сложность | Время |
|
||||||
|
| ----------------------------------------------------- | ------------------------ | ---------------------- |
|
||||||
|
| Typed field builders + toolSchema\<P\> | Средняя | ~2ч |
|
||||||
|
| Новые defaultParams/sanitizeParams под схему | Средняя | ~2-3ч |
|
||||||
|
| ToolEntry\<P\> generic + поле schema (рядом с params) | Низкая-Средняя | ~1-2ч |
|
||||||
|
| Новый executor/worker/pipeline под схему | Средняя | ~2-3ч |
|
||||||
|
| Миграция инструментов на interface Params + схемы | Механическая, но крупная | ~8-12ч |
|
||||||
|
| Новый рендер (kit/ParamControl) + составные виджеты | Средняя | ~4-5ч |
|
||||||
|
| Поглощение tool-views.ts | Средняя | ~1-2ч |
|
||||||
|
| **Итого** | | **~20-29ч** (поэтапно) |
|
||||||
|
|
||||||
|
> Оценка выросла по сравнению с ранней версией плана, потому что принят путь
|
||||||
|
> «явный interface Params + схема + общий рендер с пер-инструмент layout» —
|
||||||
|
> это полный рефакторинг pipeline, а не только добавление составных типов.
|
||||||
|
>
|
||||||
|
> Старый UI/`ParamDef[]`/старый pipeline в смету **не входят** — они не
|
||||||
|
> рефакторятся, а продолжают работать до перехода (затем удаляются вместе со
|
||||||
|
> старым дизайном).
|
||||||
|
|
||||||
|
## Порядок реализации (кратко)
|
||||||
|
|
||||||
|
Детальный пошаговый план с атомарными изменениями и порядком ревью — в разделе
|
||||||
|
«Пошаговый план перевода инструментов» выше. Здесь краткая сводка фаз:
|
||||||
|
|
||||||
|
1. **Фаза 0** — фундамент: builders + `toolSchema<P>` + `ToolEntry<P>.schema` +
|
||||||
|
новые default/sanitize. Ни одного инструмента не изменено. ✔
|
||||||
|
2. **Фаза 1** — **один полноценный рабочий инструмент в preview**
|
||||||
|
(«добавить рамку»): schema + рендер + end-to-end применение. Ручная оценка
|
||||||
|
работоспособности решения/сложностей.
|
||||||
|
3. **Фаза 2** — простые/одиночные инструменты (проверенный паттерн Фазы 1),
|
||||||
|
1 инструмент = 1 диф.
|
||||||
|
4. **Фаза 3** — инструменты с составными типами (dimension → color-pair →
|
||||||
|
offset → position9 → font-style → plate → gradient), пилот каждого типа
|
||||||
|
отдельно.
|
||||||
|
5. **Фаза 4** — масштаб UI на остальные: составные виджеты, пер-инструмент
|
||||||
|
layout (для каждого инструмента — UI-макет).
|
||||||
|
6. **Фаза 5** — поглощение `tool-views.ts`, затем удаление старого UI/`ParamDef[]`
|
||||||
|
|
||||||
|
## Зависимости
|
||||||
|
|
||||||
|
- **UI сперва доделать** — текущая активная работа; типизация params ожидает
|
||||||
|
готовности нового дизайна.
|
||||||
|
- Ветка redesign (`plan-redesign.md`) — параллельная, учесть при миграции
|
||||||
|
kit/`ParamControl.svelte` и `overlay-store`.
|
||||||
|
- Старый pipeline остаётся рабочим и шippable на всём протяжении перехода
|
||||||
|
(аналогично стратегии `plan-redesign.md`).
|
||||||
@@ -15,6 +15,20 @@ export const COLOR_PROPS =
|
|||||||
export const SIZE_PROPS =
|
export const SIZE_PROPS =
|
||||||
/^(width|height|min-width|max-width|min-height|max-height|padding|padding-top|padding-right|padding-bottom|padding-left|margin|margin-top|margin-right|margin-bottom|margin-left|gap|column-gap|row-gap|top|right|bottom|left|inset|font-size|letter-spacing|word-spacing|line-height|border-radius|border-top-left-radius|border-top-right-radius|border-bottom-left-radius|border-bottom-right-radius|border-width|border-top-width|border-right-width|border-bottom-width|border-left-width|flex-basis|background-size|border-spacing)$/;
|
/^(width|height|min-width|max-width|min-height|max-height|padding|padding-top|padding-right|padding-bottom|padding-left|margin|margin-top|margin-right|margin-bottom|margin-left|gap|column-gap|row-gap|top|right|bottom|left|inset|font-size|letter-spacing|word-spacing|line-height|border-radius|border-top-left-radius|border-top-right-radius|border-bottom-left-radius|border-bottom-right-radius|border-width|border-top-width|border-right-width|border-bottom-width|border-left-width|flex-basis|background-size|border-spacing)$/;
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// SHORTHAND properties that accept BOTH a color and a size. The browser
|
||||||
|
// assigns their sub-properties by value type at runtime (length -> width,
|
||||||
|
// color -> ...-color), so they cannot be category-checked positionally.
|
||||||
|
// border -> border-width + border-color
|
||||||
|
// outline -> outline-width + outline-color
|
||||||
|
// text-decoration -> text-decoration-line/-color/...
|
||||||
|
// column-rule -> column-rule-width + column-rule-color
|
||||||
|
// The longhands they expand to (border-width, border-color, ...) are already
|
||||||
|
// covered individually by SIZE_PROPS / COLOR_PROPS.
|
||||||
|
// =====================================================================
|
||||||
|
export const MIXED_PROPS =
|
||||||
|
/^(border|border-top|border-right|border-bottom|border-left|outline|text-decoration|column-rule)$/;
|
||||||
|
|
||||||
// =====================================================================
|
// =====================================================================
|
||||||
// Properties that carry a DURATION (ms/s) — transitions/animations.
|
// Properties that carry a DURATION (ms/s) — transitions/animations.
|
||||||
// =====================================================================
|
// =====================================================================
|
||||||
|
|||||||
@@ -5,7 +5,13 @@
|
|||||||
// (--color-*, --brand-*). Crossing categories (e.g. padding: var(--color-x))
|
// (--color-*, --brand-*). Crossing categories (e.g. padding: var(--color-x))
|
||||||
// is a sign the wrong token is being reused.
|
// is a sign the wrong token is being reused.
|
||||||
|
|
||||||
import { COLOR_PROPS, COLOR_TOKEN, SIZE_PROPS, SIZE_TOKEN } from "./lists.js";
|
import {
|
||||||
|
COLOR_PROPS,
|
||||||
|
COLOR_TOKEN,
|
||||||
|
MIXED_PROPS,
|
||||||
|
SIZE_PROPS,
|
||||||
|
SIZE_TOKEN,
|
||||||
|
} from "./lists.js";
|
||||||
import { getStyleNodeLoc, getStyleRoot } from "./style-context.js";
|
import { getStyleNodeLoc, getStyleRoot } from "./style-context.js";
|
||||||
|
|
||||||
// Collect the token names referenced by var() in a value.
|
// Collect the token names referenced by var() in a value.
|
||||||
@@ -50,6 +56,14 @@ export default {
|
|||||||
const prop = decl.prop ?? "";
|
const prop = decl.prop ?? "";
|
||||||
const value = decl.value ?? "";
|
const value = decl.value ?? "";
|
||||||
|
|
||||||
|
// Mixed shorthands (border, outline, text-decoration,
|
||||||
|
// column-rule) legitimately take BOTH a size and a color.
|
||||||
|
// The browser assigns sub-properties by value type at runtime,
|
||||||
|
// not by position, so skip category checks for them.
|
||||||
|
// Their longhands (border-width, border-color, ...) are still
|
||||||
|
// covered individually.
|
||||||
|
if (MIXED_PROPS.test(prop)) return;
|
||||||
|
|
||||||
const isSizeProp = SIZE_PROPS.test(prop);
|
const isSizeProp = SIZE_PROPS.test(prop);
|
||||||
const isColorProp = COLOR_PROPS.test(prop);
|
const isColorProp = COLOR_PROPS.test(prop);
|
||||||
if (!isSizeProp && !isColorProp) return;
|
if (!isSizeProp && !isColorProp) return;
|
||||||
|
|||||||
@@ -1,69 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import type { Component, Snippet } from "svelte";
|
|
||||||
import Icon from "./Icon.svelte";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
children: Snippet;
|
|
||||||
onclick?: () => void;
|
|
||||||
type?: "button" | "submit";
|
|
||||||
variant?: "primary" | "ghost" | "accent" | "danger";
|
|
||||||
icon?: Component<{ size?: number; class?: string }>;
|
|
||||||
disabled?: boolean;
|
|
||||||
}
|
|
||||||
let {
|
|
||||||
children,
|
|
||||||
onclick,
|
|
||||||
type = "button",
|
|
||||||
variant = "primary",
|
|
||||||
icon,
|
|
||||||
disabled = false,
|
|
||||||
}: Props = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<button
|
|
||||||
{type}
|
|
||||||
class="btn btn--{variant}"
|
|
||||||
{disabled}
|
|
||||||
onclick={() => onclick?.()}
|
|
||||||
>
|
|
||||||
{#if icon}<Icon {icon} size={14} />{/if}
|
|
||||||
<span class="btn-label">{@render children()}</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.btn {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.4rem;
|
|
||||||
padding: 0.4rem 0.8rem;
|
|
||||||
border: 1px solid var(--line);
|
|
||||||
border-radius: var(--radius);
|
|
||||||
background: transparent;
|
|
||||||
color: var(--foreground);
|
|
||||||
font: inherit;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.btn--primary {
|
|
||||||
background: var(--blue);
|
|
||||||
color: #fff;
|
|
||||||
border-color: var(--blue);
|
|
||||||
}
|
|
||||||
.btn--ghost:hover:not(:disabled) {
|
|
||||||
border-color: var(--blue);
|
|
||||||
}
|
|
||||||
.btn--accent {
|
|
||||||
background: var(--cyan);
|
|
||||||
color: #042;
|
|
||||||
border-color: var(--cyan);
|
|
||||||
}
|
|
||||||
.btn--danger {
|
|
||||||
background: var(--danger);
|
|
||||||
color: #fff;
|
|
||||||
border-color: var(--danger);
|
|
||||||
}
|
|
||||||
.btn:disabled {
|
|
||||||
opacity: 0.5;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -14,7 +14,9 @@
|
|||||||
{ value: "color", label: "COLOR" },
|
{ value: "color", label: "COLOR" },
|
||||||
{ value: "geometry", label: "GEOMETRY" },
|
{ value: "geometry", label: "GEOMETRY" },
|
||||||
{ value: "filters", label: "FILTERS" },
|
{ value: "filters", label: "FILTERS" },
|
||||||
|
{ value: "text", label: "TEXT" },
|
||||||
{ value: "analyze", label: "ANALYZE" },
|
{ value: "analyze", label: "ANALYZE" },
|
||||||
|
{ value: "generate", label: "GENERATE" },
|
||||||
];
|
];
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Copy } from "@lucide/svelte";
|
import { Copy } from "@lucide/svelte";
|
||||||
import IconButton from "./IconButton.svelte";
|
import IconButton from "./ui/IconButton.svelte";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
code: string;
|
code: string;
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { ArrowDownToLine, Download } from "@lucide/svelte";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
label: string;
|
|
||||||
onclick?: () => void;
|
|
||||||
}
|
|
||||||
let { label, onclick }: Props = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<button class="download-btn" type="button" onclick={() => onclick?.()}>
|
|
||||||
<Download size={16} />
|
|
||||||
{label}
|
|
||||||
<ArrowDownToLine size={14} />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.download-btn {
|
|
||||||
background: var(--blue);
|
|
||||||
color: #fff;
|
|
||||||
width: 100%;
|
|
||||||
height: 42px;
|
|
||||||
font: 11px var(--font-mono);
|
|
||||||
border: 0;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
gap: 9px;
|
|
||||||
margin-top: 18px;
|
|
||||||
display: flex;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
.download-btn:hover {
|
|
||||||
background: color-mix(in srgb, var(--blue) 88%, #000);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { Component, Snippet } from "svelte";
|
import type { Component, Snippet } from "svelte";
|
||||||
import Icon from "./Icon.svelte";
|
import Icon from "./ui/Icon.svelte";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
title: string;
|
title: string;
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
<footer class="preview-footer">
|
|
||||||
<span>easy-png-tools <b class="version">v2.4.0</b></span>
|
|
||||||
<span>© 2024</span>
|
|
||||||
</footer>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.preview-footer {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 16px clamp(20px, 4vw, 72px);
|
|
||||||
border-top: 1px solid var(--line);
|
|
||||||
font: 10px var(--font-mono);
|
|
||||||
color: var(--muted);
|
|
||||||
& span {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 7px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.preview-footer .version {
|
|
||||||
font-weight: 400;
|
|
||||||
color: var(--blue);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import type { Component } from "svelte";
|
|
||||||
import Icon from "./Icon.svelte";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
icon: Component<{ size?: number; class?: string }>;
|
|
||||||
label: string;
|
|
||||||
onclick?: () => void;
|
|
||||||
variant?: "ghost" | "solid" | "accent" | "bare";
|
|
||||||
size?: number;
|
|
||||||
disabled?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
let {
|
|
||||||
icon,
|
|
||||||
label,
|
|
||||||
onclick,
|
|
||||||
variant = "ghost",
|
|
||||||
size = 16,
|
|
||||||
disabled = false,
|
|
||||||
}: Props = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="icon-btn icon-btn--{variant}"
|
|
||||||
aria-label={label}
|
|
||||||
{disabled}
|
|
||||||
onclick={() => onclick?.()}
|
|
||||||
>
|
|
||||||
<Icon {icon} {size} />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.icon-btn {
|
|
||||||
display: grid;
|
|
||||||
align-items: center;
|
|
||||||
border: 1px solid var(--line);
|
|
||||||
border-radius: var(--radius);
|
|
||||||
background: transparent;
|
|
||||||
color: var(--muted);
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: inherit;
|
|
||||||
line-height: inherit;
|
|
||||||
border: 0;
|
|
||||||
background: 0 0;
|
|
||||||
border: 0;
|
|
||||||
place-items: center;
|
|
||||||
padding: 6px;
|
|
||||||
display: grid;
|
|
||||||
}
|
|
||||||
.icon-btn:hover:not(:disabled) {
|
|
||||||
color: var(--foreground);
|
|
||||||
border-color: var(--blue);
|
|
||||||
}
|
|
||||||
.icon-btn--solid {
|
|
||||||
background: var(--blue);
|
|
||||||
color: #fff;
|
|
||||||
border-color: var(--blue);
|
|
||||||
}
|
|
||||||
.icon-btn--accent {
|
|
||||||
background: var(--cyan);
|
|
||||||
color: #042;
|
|
||||||
border-color: var(--cyan);
|
|
||||||
}
|
|
||||||
.icon-btn--bare {
|
|
||||||
padding: 6px;
|
|
||||||
border: 0;
|
|
||||||
border-radius: 0;
|
|
||||||
color: var(--muted);
|
|
||||||
}
|
|
||||||
.icon-btn--bare:hover:not(:disabled) {
|
|
||||||
color: var(--foreground);
|
|
||||||
border-color: transparent;
|
|
||||||
}
|
|
||||||
.icon-btn:disabled {
|
|
||||||
opacity: 0.5;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-btn--ghost {
|
|
||||||
padding: 6px;
|
|
||||||
border: 0;
|
|
||||||
border-radius: 0;
|
|
||||||
color: var(--muted);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -17,15 +17,13 @@
|
|||||||
|
|
||||||
<style>
|
<style>
|
||||||
.mono-label {
|
.mono-label {
|
||||||
font-family: var(--font-mono);
|
font: normal var(--font-size-s) var(--font-mono);
|
||||||
font-size: 10px;
|
letter-spacing: var(--space-text-l);
|
||||||
font-weight: 400;
|
color: var(--color-text-muted);
|
||||||
letter-spacing: 0.12em;
|
|
||||||
color: var(--muted);
|
|
||||||
line-height: normal;
|
line-height: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
.accent {
|
.accent {
|
||||||
color: var(--blue);
|
color: var(--color-accent);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import type { Snippet } from "svelte";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
children: Snippet;
|
|
||||||
}
|
|
||||||
let { children }: Props = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<section class="panel">
|
|
||||||
{@render children()}
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.panel {
|
|
||||||
background: var(--panel);
|
|
||||||
border: 1px solid var(--line);
|
|
||||||
border-radius: var(--radius);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
+8
-6
@@ -1,11 +1,11 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import ColorField from "$lib/components/kit/fields/ColorField.svelte";
|
||||||
|
import NumberField from "$lib/components/kit/fields/NumberField.svelte";
|
||||||
|
import SelectField from "$lib/components/kit/fields/SelectField.svelte";
|
||||||
|
import SliderField from "$lib/components/kit/fields/SliderField.svelte";
|
||||||
|
import TextField from "$lib/components/kit/fields/TextField.svelte";
|
||||||
|
import ToggleRow from "$lib/components/kit/fields/ToggleRow.svelte";
|
||||||
import type { ParamDef } from "$lib/registry";
|
import type { ParamDef } from "$lib/registry";
|
||||||
import ColorField from "$lib/components/kit/ColorField.svelte";
|
|
||||||
import SliderField from "$lib/components/kit/SliderField.svelte";
|
|
||||||
import SelectField from "$lib/components/kit/SelectField.svelte";
|
|
||||||
import ToggleRow from "$lib/components/kit/ToggleRow.svelte";
|
|
||||||
import TextField from "$lib/components/kit/TextField.svelte";
|
|
||||||
import NumberField from "$lib/components/kit/NumberField.svelte";
|
|
||||||
|
|
||||||
type ParamValue = string | number | boolean;
|
type ParamValue = string | number | boolean;
|
||||||
|
|
||||||
@@ -16,6 +16,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
let { param, value, onInput }: Props = $props();
|
let { param, value, onInput }: Props = $props();
|
||||||
|
|
||||||
|
// TODO: investigate - may be better move param fields switch to field component?
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if param.type === "color"}
|
{#if param.type === "color"}
|
||||||
+11
-12
@@ -1,15 +1,14 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Button from "$lib/components/kit/Button.svelte";
|
|
||||||
import CheckerCanvas from "$lib/components/kit/CheckerCanvas.svelte";
|
import CheckerCanvas from "$lib/components/kit/CheckerCanvas.svelte";
|
||||||
import DownloadButton from "$lib/components/kit/DownloadButton.svelte";
|
|
||||||
import Dropzone from "$lib/components/kit/Dropzone.svelte";
|
import Dropzone from "$lib/components/kit/Dropzone.svelte";
|
||||||
import EmptyState from "$lib/components/kit/EmptyState.svelte";
|
import EmptyState from "$lib/components/kit/EmptyState.svelte";
|
||||||
|
import Panel from "$lib/components/kit/layout/Panel.svelte";
|
||||||
import MetaList from "$lib/components/kit/MetaList.svelte";
|
import MetaList from "$lib/components/kit/MetaList.svelte";
|
||||||
import Panel from "$lib/components/kit/Panel.svelte";
|
|
||||||
import PanelHeading from "$lib/components/kit/PanelHeading.svelte";
|
|
||||||
import PreviewStack from "$lib/components/kit/PreviewStack.svelte";
|
import PreviewStack from "$lib/components/kit/PreviewStack.svelte";
|
||||||
import PreviewTile from "$lib/components/kit/PreviewTile.svelte";
|
import PreviewTile from "$lib/components/kit/PreviewTile.svelte";
|
||||||
import StatusLine from "$lib/components/kit/StatusLine.svelte";
|
import StatusLine from "$lib/components/kit/StatusLine.svelte";
|
||||||
|
import Button from "$lib/components/kit/ui/Button.svelte";
|
||||||
|
import DownloadButton from "$lib/components/kit/ui/DownloadButton.svelte";
|
||||||
import type { PixelImage } from "$lib/core/types";
|
import type { PixelImage } from "$lib/core/types";
|
||||||
|
|
||||||
interface PreviewItem {
|
interface PreviewItem {
|
||||||
@@ -52,14 +51,11 @@
|
|||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Panel>
|
<Panel title="Pipeline output" eyebrow="RESULT">
|
||||||
<PanelHeading title="Pipeline output" eyebrow="RESULT">
|
|
||||||
{#snippet actions()}
|
|
||||||
{#if resultUrl}
|
{#if resultUrl}
|
||||||
<DownloadButton label="Download result" onclick={onDownload} />
|
<DownloadButton label="Download result" onclick={onDownload} />
|
||||||
{/if}
|
{/if}
|
||||||
{/snippet}
|
|
||||||
</PanelHeading>
|
|
||||||
<div class="preview-body">
|
<div class="preview-body">
|
||||||
{#if chainError}
|
{#if chainError}
|
||||||
<EmptyState title="Pipeline failed" description={chainError} />
|
<EmptyState title="Pipeline failed" description={chainError} />
|
||||||
@@ -67,18 +63,21 @@
|
|||||||
{#if needsSource && !sourceImg}
|
{#if needsSource && !sourceImg}
|
||||||
<Dropzone onfile={onFile} />
|
<Dropzone onfile={onFile} />
|
||||||
{:else if needsSource && sourceImg}
|
{:else if needsSource && sourceImg}
|
||||||
<Button variant="ghost" onclick={onClearSource}>Change image</Button>
|
<Button variant="outline" onclick={onClearSource} label="Change image" />
|
||||||
{/if}
|
{/if}
|
||||||
<PreviewStack>
|
<PreviewStack>
|
||||||
{#if needsSource && sourceImg}
|
{#if needsSource && sourceImg}
|
||||||
<PreviewTile label="SOURCE">
|
<PreviewTile label="SOURCE" caption="">
|
||||||
<CheckerCanvas size="sm">
|
<CheckerCanvas size="sm">
|
||||||
<img class="tile-img" src={sourceUrl} alt="source" />
|
<img class="tile-img" src={sourceUrl} alt="source" />
|
||||||
</CheckerCanvas>
|
</CheckerCanvas>
|
||||||
</PreviewTile>
|
</PreviewTile>
|
||||||
{/if}
|
{/if}
|
||||||
{#each stepOutputs as out, i (out.toolId + i)}
|
{#each stepOutputs as out, i (out.toolId + i)}
|
||||||
<PreviewTile label={`STEP ${String(i + 1).padStart(2, "0")}`}>
|
<PreviewTile
|
||||||
|
label={`STEP ${String(i + 1).padStart(2, "0")}`}
|
||||||
|
caption=""
|
||||||
|
>
|
||||||
<CheckerCanvas size="sm">
|
<CheckerCanvas size="sm">
|
||||||
<img class="tile-img" src={out.url} alt={out.title} />
|
<img class="tile-img" src={out.url} alt={out.title} />
|
||||||
</CheckerCanvas>
|
</CheckerCanvas>
|
||||||
@@ -4,11 +4,11 @@
|
|||||||
interface Props {
|
interface Props {
|
||||||
label: string;
|
label: string;
|
||||||
caption: string;
|
caption: string;
|
||||||
active: boolean;
|
active?: boolean;
|
||||||
children: Snippet;
|
children: Snippet;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { label, caption, active, children }: Props = $props();
|
let { label, caption, active = false, children }: Props = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="preview-tile" class:active>
|
<div class="preview-tile" class:active>
|
||||||
|
|||||||
+5
-12
@@ -1,12 +1,10 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Button from "$lib/components/kit/Button.svelte";
|
|
||||||
import EmptyState from "$lib/components/kit/EmptyState.svelte";
|
import EmptyState from "$lib/components/kit/EmptyState.svelte";
|
||||||
import Panel from "$lib/components/kit/Panel.svelte";
|
import SelectField from "$lib/components/kit/fields/SelectField.svelte";
|
||||||
import PanelHeading from "$lib/components/kit/PanelHeading.svelte";
|
import Panel from "$lib/components/kit/layout/Panel.svelte";
|
||||||
import SelectField from "$lib/components/kit/SelectField.svelte";
|
|
||||||
import SettingsFooter from "$lib/components/kit/SettingsFooter.svelte";
|
import SettingsFooter from "$lib/components/kit/SettingsFooter.svelte";
|
||||||
import StepCard from "$lib/components/kit/StepCard.svelte";
|
import StepCard from "$lib/components/kit/StepCard.svelte";
|
||||||
import Badge from "$lib/components/kit/ui/Badge.svelte";
|
import Button from "$lib/components/kit/ui/Button.svelte";
|
||||||
import { getTool } from "$lib/registry";
|
import { getTool } from "$lib/registry";
|
||||||
import ParamControl from "./ParamControl.svelte";
|
import ParamControl from "./ParamControl.svelte";
|
||||||
|
|
||||||
@@ -38,12 +36,7 @@
|
|||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Panel>
|
<Panel title="Pipeline" eyebrow="STEPS">
|
||||||
<PanelHeading title="Pipeline" eyebrow="STEPS">
|
|
||||||
{#snippet actions()}
|
|
||||||
<Badge tone="accent">AUTO</Badge>
|
|
||||||
{/snippet}
|
|
||||||
</PanelHeading>
|
|
||||||
<div class="pipeline-body">
|
<div class="pipeline-body">
|
||||||
{#if steps.length === 0}
|
{#if steps.length === 0}
|
||||||
<EmptyState title="Pipeline empty" description="Add a tool step below." />
|
<EmptyState title="Pipeline empty" description="Add a tool step below." />
|
||||||
@@ -78,7 +71,7 @@
|
|||||||
options={chainOptions}
|
options={chainOptions}
|
||||||
onchange={(v) => onSetAddId(v)}
|
onchange={(v) => onSetAddId(v)}
|
||||||
/>
|
/>
|
||||||
<Button variant="ghost" onclick={onAddStep}>Add step</Button>
|
<Button variant="outline" onclick={onAddStep} label="Add step" />
|
||||||
</div>
|
</div>
|
||||||
</SettingsFooter>
|
</SettingsFooter>
|
||||||
</Panel>
|
</Panel>
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
import { resolve } from "$app/paths";
|
import { resolve } from "$app/paths";
|
||||||
import { ArrowUpRight } from "@lucide/svelte";
|
import { ArrowUpRight } from "@lucide/svelte";
|
||||||
import type { Component } from "svelte";
|
import type { Component } from "svelte";
|
||||||
import Icon from "./Icon.svelte";
|
import Icon from "./ui/Icon.svelte";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -73,15 +73,4 @@
|
|||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
align-self: start;
|
align-self: start;
|
||||||
}
|
}
|
||||||
.tool-arrow {
|
|
||||||
color: var(--blue);
|
|
||||||
opacity: 0;
|
|
||||||
transition:
|
|
||||||
opacity 0.12s ease,
|
|
||||||
transform 0.12s ease;
|
|
||||||
}
|
|
||||||
.tool-card:hover .tool-arrow {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translate(2px, -2px);
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,146 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { resolve } from "$app/paths";
|
|
||||||
import { CircleQuestionMark, Moon, Sun } from "@lucide/svelte";
|
|
||||||
import IconButton from "./IconButton.svelte";
|
|
||||||
import StatusDot from "./StatusDot.svelte";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
theme: "light" | "dark";
|
|
||||||
crumb?: string;
|
|
||||||
status?: string;
|
|
||||||
ontoggle: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
let { theme, crumb, status = "AUTO PIPELINE", ontoggle }: Props = $props();
|
|
||||||
|
|
||||||
let lang = $state("RU");
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<header class="topbar">
|
|
||||||
<a class="brand" href={resolve("/")}>
|
|
||||||
<span class="brand-mark">EP</span>
|
|
||||||
<span>easy-png-tools</span>
|
|
||||||
{#if crumb}<span class="version">{crumb}</span>{/if}
|
|
||||||
</a>
|
|
||||||
<nav class="nav">
|
|
||||||
<a href={resolve("/preview/demo")}>Workspace</a>
|
|
||||||
<a href={resolve("/preview/list-tools")}>Catalog</a>
|
|
||||||
<a href={resolve("/preview/tools/linear-gradient-png")}>Gradient</a>
|
|
||||||
<a href={resolve("/preview/tools/remove-background-png")}
|
|
||||||
>Background remover</a
|
|
||||||
>
|
|
||||||
</nav>
|
|
||||||
<div class="top-actions">
|
|
||||||
<span class="status"><StatusDot /> {status}</span>
|
|
||||||
<IconButton
|
|
||||||
icon={CircleQuestionMark}
|
|
||||||
label="Help"
|
|
||||||
variant="bare"
|
|
||||||
onclick={() => {}}
|
|
||||||
/>
|
|
||||||
<IconButton
|
|
||||||
icon={theme === "light" ? Moon : Sun}
|
|
||||||
label="Toggle theme"
|
|
||||||
variant="bare"
|
|
||||||
onclick={ontoggle}
|
|
||||||
/>
|
|
||||||
<div class="lang" role="group" aria-label="Language">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="lang-btn"
|
|
||||||
class:active={lang === "RU"}
|
|
||||||
onclick={() => (lang = "RU")}>RU</button
|
|
||||||
><span class="lang-div">/</span>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="lang-btn"
|
|
||||||
class:active={lang === "EN"}
|
|
||||||
onclick={() => (lang = "EN")}>EN</button
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.topbar {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
height: 64px;
|
|
||||||
padding: 0 clamp(20px, 4vw, 64px);
|
|
||||||
background: var(--panel);
|
|
||||||
border-bottom: 1px solid var(--line);
|
|
||||||
}
|
|
||||||
.brand {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
font: 600 14px var(--font-mono);
|
|
||||||
color: var(--foreground);
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
.brand-mark {
|
|
||||||
background: var(--blue);
|
|
||||||
color: #fff;
|
|
||||||
width: 30px;
|
|
||||||
height: 30px;
|
|
||||||
font-size: 11px;
|
|
||||||
display: grid;
|
|
||||||
place-items: center;
|
|
||||||
}
|
|
||||||
.version {
|
|
||||||
font: 10px var(--font-mono);
|
|
||||||
letter-spacing: 0.12em;
|
|
||||||
color: var(--blue);
|
|
||||||
}
|
|
||||||
.nav {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 22px;
|
|
||||||
margin-right: 28px;
|
|
||||||
margin-left: auto;
|
|
||||||
}
|
|
||||||
.nav a {
|
|
||||||
color: var(--muted);
|
|
||||||
font: 10px var(--font-mono);
|
|
||||||
white-space: nowrap;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
.nav a:hover {
|
|
||||||
color: var(--foreground);
|
|
||||||
}
|
|
||||||
.top-actions {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
.status {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 7px;
|
|
||||||
color: var(--muted);
|
|
||||||
font: 10px var(--font-mono);
|
|
||||||
letter-spacing: 0.08em;
|
|
||||||
}
|
|
||||||
.lang {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
border: 1px solid var(--line);
|
|
||||||
}
|
|
||||||
.lang-btn {
|
|
||||||
border: 0;
|
|
||||||
background: transparent;
|
|
||||||
color: var(--muted);
|
|
||||||
font: 10px var(--font-mono);
|
|
||||||
padding: 6px 9px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.lang-div {
|
|
||||||
color: var(--foreground);
|
|
||||||
font-family: var(--font-sans);
|
|
||||||
}
|
|
||||||
.lang-btn.active {
|
|
||||||
background: var(--foreground);
|
|
||||||
color: var(--background);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
export const ButtonVariantDefine = {
|
||||||
|
PRIMARY: "primary",
|
||||||
|
OUTLINE: "outline",
|
||||||
|
ACCENT: "accent",
|
||||||
|
DANGER: "danger",
|
||||||
|
CLEAR: "clear",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type ButtonVariant =
|
||||||
|
(typeof ButtonVariantDefine)[keyof typeof ButtonVariantDefine];
|
||||||
+5
-5
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { Snippet } from "svelte";
|
import type { Snippet } from "svelte";
|
||||||
import MonoLabel from "./MonoLabel.svelte";
|
import MonoLabel from "../MonoLabel.svelte";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
label: string;
|
label: string;
|
||||||
@@ -20,16 +20,16 @@
|
|||||||
.field {
|
.field {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.3rem;
|
gap: var(--space-s);
|
||||||
}
|
}
|
||||||
.field-control {
|
.field-control {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: var(--space-m);
|
||||||
}
|
}
|
||||||
.field-hint {
|
.field-hint {
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
font-size: 10px;
|
font-size: var(--font-size-s);
|
||||||
color: var(--muted);
|
color: var(--color-text-muted);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Toggle from "./Toggle.svelte";
|
import Toggle from "../ui/Toggle.svelte";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
label: string;
|
label: string;
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
// TODO: insert version from package.json
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<footer class="preview-footer">
|
||||||
|
<span>easy-png-tools <b class="version">v0.1.0</b></span>
|
||||||
|
<span>© 2026</span>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.preview-footer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: var(--space-xl);
|
||||||
|
border-top: var(--size-border) solid var(--color-border);
|
||||||
|
font: var(--font-size-s) var(--font-mono);
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
& span {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-m);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.preview-footer .version {
|
||||||
|
font-weight: normal;
|
||||||
|
color: var(--color-text-accent);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { Snippet } from "svelte";
|
||||||
|
import PanelHeading from "../PanelHeading.svelte";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
title: string;
|
||||||
|
eyebrow: string;
|
||||||
|
children: Snippet;
|
||||||
|
}
|
||||||
|
let { title, eyebrow, children }: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<section class="panel">
|
||||||
|
<PanelHeading {title} {eyebrow} />
|
||||||
|
{@render children()}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.panel {
|
||||||
|
background: var(--color-panel);
|
||||||
|
border: var(--size-border) solid var(--color-border);
|
||||||
|
border-radius: var(--radius-s);
|
||||||
|
padding: var(--space-l);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { resolve } from "$app/paths";
|
||||||
|
import LangToggle from "../ui/LangToggle.svelte";
|
||||||
|
import ThemeToggle from "../ui/ThemeToggle.svelte";
|
||||||
|
import TopNav from "../ui/TopNav.svelte";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
theme: "light" | "dark";
|
||||||
|
crumb?: string;
|
||||||
|
ontoggle: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { theme, crumb, ontoggle }: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<header class="topbar">
|
||||||
|
<a class="brand" href={resolve("/")}>
|
||||||
|
<span class="brand-mark">EP</span>
|
||||||
|
<span>easy-png-tools</span>
|
||||||
|
{#if crumb}
|
||||||
|
<span class="crumb">{crumb}</span>
|
||||||
|
{/if}
|
||||||
|
</a>
|
||||||
|
<TopNav />
|
||||||
|
<div class="top-actions">
|
||||||
|
<ThemeToggle {theme} {ontoggle} />
|
||||||
|
<LangToggle />
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.topbar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: var(--space-xl);
|
||||||
|
background: var(--color-background);
|
||||||
|
border-bottom: var(--size-border) solid var(--color-border);
|
||||||
|
}
|
||||||
|
.brand {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-m);
|
||||||
|
font: bold var(--font-size-l) var(--font-mono);
|
||||||
|
color: var(--color-text);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.brand-mark {
|
||||||
|
background: var(--color-main);
|
||||||
|
color: var(--color-background);
|
||||||
|
width: var(--space-brand);
|
||||||
|
height: var(--space-brand);
|
||||||
|
font-size: var(--font-size-m);
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
}
|
||||||
|
.crumb {
|
||||||
|
font: var(--font-size-s) var(--font-mono);
|
||||||
|
letter-spacing: var(--space-text-l);
|
||||||
|
color: var(--color-main);
|
||||||
|
}
|
||||||
|
.top-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-xl);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Check } from "@lucide/svelte";
|
import { Check } from "@lucide/svelte";
|
||||||
|
|
||||||
|
// TODO: сделать эти типы общими для кнопок \ бейджей \ етц???
|
||||||
const ToneVariantDefine = {
|
const ToneVariantDefine = {
|
||||||
DEFAULT: "default",
|
DEFAULT: "default",
|
||||||
ACCENT: "accent",
|
ACCENT: "accent",
|
||||||
@@ -46,7 +47,7 @@
|
|||||||
gap: var(--space-s);
|
gap: var(--space-s);
|
||||||
line-height: normal;
|
line-height: normal;
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
font-size: var(--font-text);
|
font-size: var(--font-size-s);
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
border-width: var(--size-border-thick);
|
border-width: var(--size-border-thick);
|
||||||
padding: var(--space-s) var(--space-m);
|
padding: var(--space-s) var(--space-m);
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { Component } from "svelte";
|
||||||
|
import { ButtonVariantDefine, type ButtonVariant } from "../define";
|
||||||
|
import Icon from "./Icon.svelte";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
label: string;
|
||||||
|
ariaLabel?: string;
|
||||||
|
onclick?: () => void;
|
||||||
|
variant?: ButtonVariant;
|
||||||
|
icon?: Component<{ size?: number; class?: string }>;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
let {
|
||||||
|
label,
|
||||||
|
ariaLabel = label,
|
||||||
|
onclick,
|
||||||
|
variant = ButtonVariantDefine.PRIMARY,
|
||||||
|
icon,
|
||||||
|
disabled = false,
|
||||||
|
}: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="btn btn--{variant}"
|
||||||
|
{disabled}
|
||||||
|
aria-label={ariaLabel}
|
||||||
|
onclick={() => onclick?.()}
|
||||||
|
>
|
||||||
|
{#if icon}
|
||||||
|
<Icon {icon} size={14} />
|
||||||
|
{/if}
|
||||||
|
{#if label}
|
||||||
|
<span class="btn-label">{label}</span>
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-m);
|
||||||
|
padding: var(--space-m) var(--space-xl);
|
||||||
|
border-color: var(--color-main);
|
||||||
|
border-width: var(--size-border-thick);
|
||||||
|
border-radius: var(--radius-s);
|
||||||
|
font-size: var(--font-size-l);
|
||||||
|
font-weight: bold;
|
||||||
|
cursor: pointer;
|
||||||
|
background: var(--color-main);
|
||||||
|
color: var(--color-background);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn--outline {
|
||||||
|
background: transparent;
|
||||||
|
color: var(--color-main-tint);
|
||||||
|
border-color: var(--color-main-tint);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn--accent {
|
||||||
|
background: var(--color-accent);
|
||||||
|
color: var(--color-background);
|
||||||
|
border-color: var(--color-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn--danger {
|
||||||
|
background: var(--color-danger);
|
||||||
|
color: var(--color-background);
|
||||||
|
border-color: var(--color-danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Download } from "@lucide/svelte";
|
||||||
|
import Button from "./Button.svelte";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
label: string;
|
||||||
|
onclick?: () => void;
|
||||||
|
}
|
||||||
|
let { label, onclick }: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Button icon={Download} {label} {onclick} />
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { Component } from "svelte";
|
||||||
|
import { ButtonVariantDefine, type ButtonVariant } from "../define";
|
||||||
|
import Button from "./Button.svelte";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
icon: Component<{ size?: number; class?: string }>;
|
||||||
|
label: string;
|
||||||
|
onclick?: () => void;
|
||||||
|
variant?: ButtonVariant;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
let {
|
||||||
|
icon,
|
||||||
|
label,
|
||||||
|
onclick,
|
||||||
|
variant = ButtonVariantDefine.PRIMARY,
|
||||||
|
disabled = false,
|
||||||
|
}: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Button label="" {onclick} {variant} {disabled} {icon} ariaLabel={label} />
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
let lang = $state("RU");
|
||||||
|
|
||||||
|
// TODO: change to sectioned dual buttons
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="lang" role="group" aria-label="Language">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="lang-btn"
|
||||||
|
class:active={lang === "RU"}
|
||||||
|
onclick={() => (lang = "RU")}>RU</button
|
||||||
|
><span class="lang-div">/</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="lang-btn"
|
||||||
|
class:active={lang === "EN"}
|
||||||
|
onclick={() => (lang = "EN")}>EN</button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.lang {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
}
|
||||||
|
.lang-btn {
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--muted);
|
||||||
|
font: 10px var(--font-mono);
|
||||||
|
padding: 6px 9px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.lang-div {
|
||||||
|
color: var(--foreground);
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
}
|
||||||
|
.lang-btn.active {
|
||||||
|
background: var(--foreground);
|
||||||
|
color: var(--background);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
+20
-10
@@ -7,9 +7,15 @@
|
|||||||
interface Props {
|
interface Props {
|
||||||
options: Option[];
|
options: Option[];
|
||||||
value?: string;
|
value?: string;
|
||||||
|
wide?: boolean;
|
||||||
onchange?: (value: string) => void;
|
onchange?: (value: string) => void;
|
||||||
}
|
}
|
||||||
let { options, value = $bindable(), onchange }: Props = $props();
|
let {
|
||||||
|
options,
|
||||||
|
wide = false,
|
||||||
|
value = $bindable(),
|
||||||
|
onchange,
|
||||||
|
}: Props = $props();
|
||||||
|
|
||||||
function select(v: string) {
|
function select(v: string) {
|
||||||
value = v;
|
value = v;
|
||||||
@@ -17,7 +23,7 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="segmented" role="group">
|
<div class="segmented" class:wide role="group">
|
||||||
{#each options as opt (opt.value)}
|
{#each options as opt (opt.value)}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -33,23 +39,27 @@
|
|||||||
|
|
||||||
<style>
|
<style>
|
||||||
.segmented {
|
.segmented {
|
||||||
display: flex;
|
display: inline-flex;
|
||||||
height: 32px;
|
border: var(--size-border) solid var(--color-border);
|
||||||
border: 1px solid var(--line);
|
cursor: pointer;
|
||||||
|
&.wide {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.segment {
|
.segment {
|
||||||
border: 0;
|
border: 0;
|
||||||
border-right: 1px solid var(--line);
|
border-right: var(--size-border) solid var(--color-border);
|
||||||
color: var(--muted);
|
color: var(--color-text-muted);
|
||||||
font: 10px var(--font-mono);
|
font: var(--font-size-s) var(--font-mono);
|
||||||
background: 0 0;
|
background: 0 0;
|
||||||
|
padding: var(--space-m);
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
.segment:last-child {
|
.segment:last-child {
|
||||||
border-right: 0px;
|
border-right: 0px;
|
||||||
}
|
}
|
||||||
.segment.selected {
|
.segment.selected {
|
||||||
background: var(--blue);
|
background: var(--color-main);
|
||||||
color: #fff;
|
color: var(--color-background);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Moon, Sun } from "@lucide/svelte";
|
||||||
|
import IconButton from "../ui/IconButton.svelte";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
theme: "light" | "dark";
|
||||||
|
ontoggle: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { theme, ontoggle }: Props = $props();
|
||||||
|
|
||||||
|
// TODO: change icon animations - custom button instead of IconButton
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<IconButton
|
||||||
|
icon={theme === "light" ? Moon : Sun}
|
||||||
|
label="Toggle theme"
|
||||||
|
variant="clear"
|
||||||
|
onclick={ontoggle}
|
||||||
|
/>
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { resolve } from "$app/paths";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<nav class="nav">
|
||||||
|
<a href={resolve("/preview/demo")}>Workspace</a>
|
||||||
|
<a href={resolve("/preview/list-tools")}>Catalog</a>
|
||||||
|
<a href={resolve("/preview/tools/linear-gradient-png")}>Gradient</a>
|
||||||
|
<a href={resolve("/preview/tools/remove-background-png")}>
|
||||||
|
Background remover
|
||||||
|
</a>
|
||||||
|
<a href={resolve("/preview/kit")}> UI Kit </a>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.nav {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-xxl);
|
||||||
|
margin-right: var(--space-xxxl);
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
.nav a {
|
||||||
|
color: var(--color-text);
|
||||||
|
font: bold var(--font-size-s) var(--font-mono);
|
||||||
|
white-space: nowrap;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.nav a:hover {
|
||||||
|
color: var(--color-main);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
+21
-224
@@ -1,3 +1,4 @@
|
|||||||
|
import { CATEGORIES } from "../categories";
|
||||||
import { TOOLS, type ToolEntry } from "../registry";
|
import { TOOLS, type ToolEntry } from "../registry";
|
||||||
|
|
||||||
export type PreviewGroup = {
|
export type PreviewGroup = {
|
||||||
@@ -6,230 +7,26 @@ export type PreviewGroup = {
|
|||||||
tools: ToolEntry[];
|
tools: ToolEntry[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Uppercase labels для всех категорий каталога (стиль нового дизайна). */
|
||||||
|
const GROUP_LABELS: Record<string, string> = {
|
||||||
|
convert: "CONVERT",
|
||||||
|
alpha: "TRANSPARENCY",
|
||||||
|
color: "COLOR",
|
||||||
|
geometry: "GEOMETRY",
|
||||||
|
filters: "FILTERS",
|
||||||
|
text: "TEXT",
|
||||||
|
analyze: "ANALYZE",
|
||||||
|
generate: "GENERATE",
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Preview-only catalog: a trimmed view of the registry that mirrors
|
* Preview-каталог: полный реестр инструментов, сгруппированный по категориям.
|
||||||
* refs-html/list-tools.html (28 tools in 6 groups). Used exclusively by
|
* Больше не урезаем до референс-набора — показываем все инструменты из TOOLS.
|
||||||
* /preview and /kit so the working site (old) and /ui keep the full registry.
|
|
||||||
*
|
|
||||||
* Titles/descriptions are overridden with the ref's exact copy where they
|
|
||||||
* diverge from the registry. The underlying ToolEntry (params, run, …) is
|
|
||||||
* reused so navigation and tool pages stay functional.
|
|
||||||
*/
|
*/
|
||||||
const REF: {
|
export const PREVIEW_GROUPS: PreviewGroup[] = CATEGORIES.map((category) => ({
|
||||||
group: string;
|
id: category,
|
||||||
label: string;
|
label: GROUP_LABELS[category] ?? category.toUpperCase(),
|
||||||
tools: { id: string; title: string; description: string }[];
|
tools: TOOLS.filter((tool) => tool.category === category),
|
||||||
}[] = [
|
})).filter((group) => group.tools.length > 0);
|
||||||
{
|
|
||||||
group: "convert",
|
|
||||||
label: "CONVERT",
|
|
||||||
tools: [
|
|
||||||
{
|
|
||||||
id: "jpg-to-png",
|
|
||||||
title: "Convert JPG to PNG",
|
|
||||||
description:
|
|
||||||
"Re-encode JPEG files as lossless PNG while preserving transparency.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "webp-to-png",
|
|
||||||
title: "Convert WebP to PNG",
|
|
||||||
description:
|
|
||||||
"Turn WebP images into a universal PNG format for any workflow.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "png-to-base64",
|
|
||||||
title: "PNG to Base64",
|
|
||||||
description:
|
|
||||||
"Encode an image as a base64 string for embedding in code or styles.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "png-to-data-uri",
|
|
||||||
title: "PNG to Data URI",
|
|
||||||
description: "Build a complete data URI ready for HTML and CSS.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "convert-png-to-jpg",
|
|
||||||
title: "Convert PNG to JPG",
|
|
||||||
description:
|
|
||||||
"Composite transparency over a selected backdrop and export JPEG.",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
group: "alpha",
|
|
||||||
label: "TRANSPARENCY",
|
|
||||||
tools: [
|
|
||||||
{
|
|
||||||
id: "remove-background-png",
|
|
||||||
title: "Remove background PNG",
|
|
||||||
description:
|
|
||||||
"Remove a solid background by color, tolerance, or edge-connected regions.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "extract-alpha-mask-png",
|
|
||||||
title: "Extract alpha mask",
|
|
||||||
description:
|
|
||||||
"Turn the alpha channel into a clean black-and-white mask.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "round-corners-png",
|
|
||||||
title: "Round corners PNG",
|
|
||||||
description: "Clip the image corners by a precise radius percentage.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "add-stroke-png",
|
|
||||||
title: "Outline PNG",
|
|
||||||
description:
|
|
||||||
"Add a colored ring around opaque content with adjustable thickness.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "change-png-opacity",
|
|
||||||
title: "Change PNG opacity",
|
|
||||||
description:
|
|
||||||
"Multiply the alpha channel while keeping the original colors unchanged.",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
group: "color",
|
|
||||||
label: "COLOR",
|
|
||||||
tools: [
|
|
||||||
{
|
|
||||||
id: "linear-gradient-png",
|
|
||||||
title: "Create gradient PNG",
|
|
||||||
description:
|
|
||||||
"Generate a smooth transition between two colors with direction controls.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "grayscale-png",
|
|
||||||
title: "Grayscale PNG",
|
|
||||||
description: "Convert the image to luminance-based shades of gray.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "invert-colors-png",
|
|
||||||
title: "Invert colors PNG",
|
|
||||||
description:
|
|
||||||
"Invert every color channel while leaving alpha untouched.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "adjust-brightness-contrast-png",
|
|
||||||
title: "Brightness & contrast",
|
|
||||||
description:
|
|
||||||
"Adjust brightness and contrast across a controlled range.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "temperature-png",
|
|
||||||
title: "Temperature PNG",
|
|
||||||
description:
|
|
||||||
"Make an image warmer or cooler with a single precise control.",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
group: "geometry",
|
|
||||||
label: "GEOMETRY",
|
|
||||||
tools: [
|
|
||||||
{
|
|
||||||
id: "resize-png",
|
|
||||||
title: "Resize PNG",
|
|
||||||
description:
|
|
||||||
"Scale an image with bilinear interpolation and optional aspect lock.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "crop-png",
|
|
||||||
title: "Crop PNG",
|
|
||||||
description:
|
|
||||||
"Cut a rectangular area with exact coordinates and dimensions.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "rotate-png",
|
|
||||||
title: "Rotate PNG",
|
|
||||||
description: "Rotate by 90, 180, or 270 degrees without quality loss.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "flip-png",
|
|
||||||
title: "Flip PNG",
|
|
||||||
description: "Mirror the image horizontally or vertically.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "add-padding-png",
|
|
||||||
title: "Add padding to PNG",
|
|
||||||
description:
|
|
||||||
"Expand the canvas on all sides by a chosen number of pixels.",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
group: "filters",
|
|
||||||
label: "FILTERS",
|
|
||||||
tools: [
|
|
||||||
{
|
|
||||||
id: "blur-png",
|
|
||||||
title: "Blur PNG",
|
|
||||||
description:
|
|
||||||
"Apply a fast Gaussian-style blur with transparent edge handling.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "sharpen-png",
|
|
||||||
title: "Sharpen PNG",
|
|
||||||
description: "Emphasize edges with an adjustable sharpening kernel.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "vignette-png",
|
|
||||||
title: "Vignette PNG",
|
|
||||||
description:
|
|
||||||
"Smoothly darken the image edges while preserving the center.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "jpeg-artifacts-png",
|
|
||||||
title: "JPEG artifacts",
|
|
||||||
description: "Simulate low-quality JPEG recompression for testing.",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
group: "analyze",
|
|
||||||
label: "ANALYZE",
|
|
||||||
tools: [
|
|
||||||
{
|
|
||||||
id: "png-info",
|
|
||||||
title: "PNG info",
|
|
||||||
description:
|
|
||||||
"Inspect dimensions, alpha presence, and unique color count.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "png-is-grayscale",
|
|
||||||
title: "Check grayscale",
|
|
||||||
description: "Report whether the image contains only shades of gray.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "png-is-transparent",
|
|
||||||
title: "Check transparency",
|
|
||||||
description: "Detect transparent and semi-transparent pixels.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "png-orientation",
|
|
||||||
title: "PNG orientation",
|
|
||||||
description: "Classify the image as portrait, landscape, or square.",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const TOOL_BY_ID = new Map(TOOLS.map((t) => [t.id, t]));
|
export const PREVIEW_TOTAL = TOOLS.length;
|
||||||
|
|
||||||
export const PREVIEW_GROUPS: PreviewGroup[] = REF.map((g) => ({
|
|
||||||
id: g.group,
|
|
||||||
label: g.label,
|
|
||||||
tools: g.tools.map((spec) => {
|
|
||||||
const base = TOOL_BY_ID.get(spec.id);
|
|
||||||
if (!base) {
|
|
||||||
throw new Error(`preview catalog: missing tool ${spec.id}`);
|
|
||||||
}
|
|
||||||
return { ...base, title: spec.title, description: spec.description };
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
|
|
||||||
// The ref header statically reads 32 (its own count); the body lists 28 cards.
|
|
||||||
// Mirroring the ref exactly keeps the audit at 0/0.
|
|
||||||
export const PREVIEW_TOTAL = 32;
|
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import {
|
||||||
|
defaultSchemaParams,
|
||||||
|
field,
|
||||||
|
sanitizeSchemaParams,
|
||||||
|
toolSchema,
|
||||||
|
} from "./registry-schema";
|
||||||
|
|
||||||
|
interface FrameParams {
|
||||||
|
thickness: number;
|
||||||
|
color: string;
|
||||||
|
enabled: boolean;
|
||||||
|
count: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const frameSchema = toolSchema<FrameParams>(
|
||||||
|
{
|
||||||
|
thickness: field.slider({ min: 1, max: 500, default: 5 }),
|
||||||
|
color: field.color({ default: "#000000" }),
|
||||||
|
enabled: field.checkbox({ default: true }),
|
||||||
|
count: field.select({
|
||||||
|
default: "two",
|
||||||
|
options: [
|
||||||
|
{ value: "one", label: "One" },
|
||||||
|
{ value: "two", label: "Two" },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{ layout: { group: "frame", cols: 2 }, label: "Frame" },
|
||||||
|
);
|
||||||
|
|
||||||
|
describe("registry-schema: дефолты из схемы", () => {
|
||||||
|
it("собирает дефолты всех полей", () => {
|
||||||
|
expect(defaultSchemaParams(frameSchema)).toEqual({
|
||||||
|
thickness: 5,
|
||||||
|
color: "#000000",
|
||||||
|
enabled: true,
|
||||||
|
count: "two",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("registry-schema: санитайз", () => {
|
||||||
|
it("пропускает валидные значения", () => {
|
||||||
|
const out = sanitizeSchemaParams(frameSchema, {
|
||||||
|
thickness: 40,
|
||||||
|
color: "#ff0000",
|
||||||
|
enabled: false,
|
||||||
|
count: "one",
|
||||||
|
});
|
||||||
|
expect(out).toEqual({
|
||||||
|
thickness: 40,
|
||||||
|
color: "#ff0000",
|
||||||
|
enabled: false,
|
||||||
|
count: "one",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("заменяет мусор дефолтами", () => {
|
||||||
|
const out = sanitizeSchemaParams(frameSchema, {
|
||||||
|
thickness: "abc",
|
||||||
|
color: "not-a-color",
|
||||||
|
enabled: "yes",
|
||||||
|
count: "wat",
|
||||||
|
extra: 123,
|
||||||
|
});
|
||||||
|
expect(out).toEqual({
|
||||||
|
thickness: 5,
|
||||||
|
color: "#000000",
|
||||||
|
enabled: true,
|
||||||
|
count: "two",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("clamp-ит числовые поля к min/max", () => {
|
||||||
|
const out = sanitizeSchemaParams(frameSchema, {
|
||||||
|
thickness: 9999,
|
||||||
|
color: "#000000",
|
||||||
|
enabled: true,
|
||||||
|
count: "two",
|
||||||
|
});
|
||||||
|
expect(out.thickness).toBe(500);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
// Новая типизированная схема параметров (для нового UI, старый UI не трогаем).
|
||||||
|
//
|
||||||
|
// Параллельный источник правды к старому `ParamDef[]`: старый pipeline
|
||||||
|
// продолжает работать на `ParamDef[]` как сейчас, а новый строит UI/deфолты/
|
||||||
|
// валидацию поверх `ToolSchema<P>`.
|
||||||
|
//
|
||||||
|
// Связка типов на этапе компиляции (защита от рассинхрона):
|
||||||
|
// - `field.x<T>()` возвращает `Field<T>` — phantom `__fieldT` связывает
|
||||||
|
// runtime-спецификацию поля с ожидаемым типом значения (`number`|`string`|`boolean`).
|
||||||
|
// - `toolSchema<P>(fields)` требует, чтобы ключи `fields` в точности совпадали
|
||||||
|
// с полями `P`, а `Field<P[K]>` тип-проверялся на соответствие `P[K]`.
|
||||||
|
// Ошибки компилятора ловят расхождения между интерфейсом Params и схемой.
|
||||||
|
|
||||||
|
export interface NumberSpec {
|
||||||
|
kind: "number";
|
||||||
|
default: number;
|
||||||
|
min?: number;
|
||||||
|
max?: number;
|
||||||
|
step?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SliderSpec {
|
||||||
|
kind: "slider";
|
||||||
|
default: number;
|
||||||
|
min: number;
|
||||||
|
max: number;
|
||||||
|
step?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ColorSpec {
|
||||||
|
kind: "color";
|
||||||
|
default: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SelectSpec<V extends string = string> {
|
||||||
|
kind: "select";
|
||||||
|
default: V;
|
||||||
|
options: { value: V; label: string }[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TextSpec {
|
||||||
|
kind: "text";
|
||||||
|
default: string;
|
||||||
|
placeholder?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CheckboxSpec {
|
||||||
|
kind: "checkbox";
|
||||||
|
default: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FieldSpec =
|
||||||
|
NumberSpec | SliderSpec | ColorSpec | SelectSpec | TextSpec | CheckboxSpec;
|
||||||
|
|
||||||
|
/** `Field<T>`: runtime-спека поля + phantom-тип ожидаемого значения (number|string|boolean). */
|
||||||
|
export interface Field<T> {
|
||||||
|
spec: FieldSpec;
|
||||||
|
/** Phantom, только для типобезопасности — не задаётся и не читается в рантайме. */
|
||||||
|
__fieldT?: T;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ToolSchemaMeta {
|
||||||
|
/** Пер-инструмент раскладка полей (группировка/колонки). */
|
||||||
|
layout?: { group?: string; cols?: number };
|
||||||
|
/** Короткая подпись инструмента для нового UI (необязательно). */
|
||||||
|
label?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ToolSchema<P> {
|
||||||
|
fields: { [K in keyof P]: Field<P[K]> };
|
||||||
|
layout?: { group?: string; cols?: number };
|
||||||
|
label?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const field = {
|
||||||
|
number: (s: Omit<NumberSpec, "kind">): Field<number> => ({
|
||||||
|
spec: { kind: "number", ...s },
|
||||||
|
}),
|
||||||
|
slider: (s: Omit<SliderSpec, "kind">): Field<number> => ({
|
||||||
|
spec: { kind: "slider", ...s },
|
||||||
|
}),
|
||||||
|
color: (s: Omit<ColorSpec, "kind">): Field<string> => ({
|
||||||
|
spec: { kind: "color", ...s },
|
||||||
|
}),
|
||||||
|
select: <V extends string>(s: Omit<SelectSpec<V>, "kind">): Field<V> => ({
|
||||||
|
spec: { kind: "select", ...s },
|
||||||
|
}),
|
||||||
|
text: (s: Omit<TextSpec, "kind">): Field<string> => ({
|
||||||
|
spec: { kind: "text", ...s },
|
||||||
|
}),
|
||||||
|
checkbox: (s: Omit<CheckboxSpec, "kind">): Field<boolean> => ({
|
||||||
|
spec: { kind: "checkbox", ...s },
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Собирает `ToolSchema<P>` из объявленных полей.
|
||||||
|
* Компилятор проверяет: ключи `fields` === полям `P`, типы полей совместимы с `P[K]`.
|
||||||
|
*/
|
||||||
|
export function toolSchema<P>(
|
||||||
|
fields: { [K in keyof P]: Field<P[K]> },
|
||||||
|
meta?: ToolSchemaMeta,
|
||||||
|
): ToolSchema<P> {
|
||||||
|
return { fields, ...(meta ?? {}) };
|
||||||
|
}
|
||||||
|
|
||||||
|
function clamp(value: number, min?: number, max?: number): number {
|
||||||
|
if (min !== undefined && value < min) return min;
|
||||||
|
if (max !== undefined && value > max) return max;
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Дефолты из схемы — единый источник значений по умолчанию для нового UI. */
|
||||||
|
export function defaultSchemaParams<P>(
|
||||||
|
schema: ToolSchema<P>,
|
||||||
|
): Record<keyof P, unknown> {
|
||||||
|
const out = {} as Record<keyof P, unknown>;
|
||||||
|
for (const key of Object.keys(schema.fields) as (keyof P)[]) {
|
||||||
|
out[key] = schema.fields[key].spec.default;
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Валидация/нормализация значений по схеме. Аналог старого `sanitizeParams`. */
|
||||||
|
export function sanitizeSchemaParams<P>(
|
||||||
|
schema: ToolSchema<P>,
|
||||||
|
values: Record<string, unknown>,
|
||||||
|
): Record<keyof P, unknown> {
|
||||||
|
const out = {} as Record<keyof P, unknown>;
|
||||||
|
for (const key of Object.keys(schema.fields) as (keyof P)[]) {
|
||||||
|
const spec = schema.fields[key].spec;
|
||||||
|
const raw = values[key as string];
|
||||||
|
switch (spec.kind) {
|
||||||
|
case "number":
|
||||||
|
case "slider": {
|
||||||
|
const n =
|
||||||
|
typeof raw === "number" && Number.isFinite(raw) ? raw : spec.default;
|
||||||
|
out[key] = clamp(n, spec.min, spec.max);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "select":
|
||||||
|
out[key] =
|
||||||
|
typeof raw === "string" && spec.options.some((o) => o.value === raw)
|
||||||
|
? raw
|
||||||
|
: spec.default;
|
||||||
|
break;
|
||||||
|
case "checkbox":
|
||||||
|
out[key] = typeof raw === "boolean" ? raw : spec.default;
|
||||||
|
break;
|
||||||
|
case "color":
|
||||||
|
out[key] =
|
||||||
|
typeof raw === "string" && /^#[0-9a-f]{6}$/i.test(raw)
|
||||||
|
? raw
|
||||||
|
: spec.default;
|
||||||
|
break;
|
||||||
|
case "text":
|
||||||
|
out[key] = typeof raw === "string" ? raw : spec.default;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
+20
-20
@@ -1,4 +1,5 @@
|
|||||||
import type { CategoryId } from "./categories";
|
import type { CategoryId } from "./categories";
|
||||||
|
import { field, toolSchema, type ToolSchema } from "./registry-schema";
|
||||||
import { ToolError } from "./core/errors";
|
import { ToolError } from "./core/errors";
|
||||||
import {
|
import {
|
||||||
colorMask,
|
colorMask,
|
||||||
@@ -189,34 +190,22 @@ export type OutputFormat = {
|
|||||||
|
|
||||||
export type SourceMode = "file" | "none" | "text";
|
export type SourceMode = "file" | "none" | "text";
|
||||||
|
|
||||||
export type ToolEntry = {
|
export type ToolEntry<P = Record<string, unknown>> = {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
category: CategoryId;
|
category: CategoryId;
|
||||||
sourceMode?: SourceMode;
|
sourceMode?: SourceMode;
|
||||||
params: ParamDef[];
|
params: ParamDef[];
|
||||||
run?: (
|
/** Новая типизированная схема для нового UI. Опциональна — старый UI её не использует. */
|
||||||
img: PixelImage,
|
schema?: ToolSchema<P>;
|
||||||
params: Record<string, unknown>,
|
run?: (img: PixelImage, params: P) => Promise<PixelImage> | PixelImage;
|
||||||
) => Promise<PixelImage> | PixelImage;
|
generate?: (params: P) => Promise<PixelImage> | PixelImage;
|
||||||
generate?: (
|
toText?: (img: PixelImage, params: P) => Promise<string> | string;
|
||||||
params: Record<string, unknown>,
|
runFromText?: (text: string, params: P) => Promise<PixelImage> | PixelImage;
|
||||||
) => Promise<PixelImage> | PixelImage;
|
|
||||||
toText?: (
|
|
||||||
img: PixelImage,
|
|
||||||
params: Record<string, unknown>,
|
|
||||||
) => Promise<string> | string;
|
|
||||||
runFromText?: (
|
|
||||||
text: string,
|
|
||||||
params: Record<string, unknown>,
|
|
||||||
) => Promise<PixelImage> | PixelImage;
|
|
||||||
/** Текстовый источник + текстовый результат без промежуточного изображения. */
|
/** Текстовый источник + текстовый результат без промежуточного изображения. */
|
||||||
textToText?: (text: string) => Promise<string> | string;
|
textToText?: (text: string) => Promise<string> | string;
|
||||||
preview?: (
|
preview?: (img: PixelImage, params: P) => Promise<PixelImage> | PixelImage;
|
||||||
img: PixelImage,
|
|
||||||
params: Record<string, unknown>,
|
|
||||||
) => Promise<PixelImage> | PixelImage;
|
|
||||||
popularity?: number;
|
popularity?: number;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
resultType?: "image" | "info" | "text";
|
resultType?: "image" | "info" | "text";
|
||||||
@@ -575,6 +564,16 @@ function hexToRgba(hex: string, alpha = 255): [number, number, number, number] {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface AddBorderParams {
|
||||||
|
thickness: number;
|
||||||
|
color: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const addBorderSchema = toolSchema<AddBorderParams>({
|
||||||
|
thickness: field.number({ min: 1, max: 500, step: 1, default: 5 }),
|
||||||
|
color: field.color({ default: "#000000" }),
|
||||||
|
});
|
||||||
|
|
||||||
export const TOOLS: ToolEntry[] = [
|
export const TOOLS: ToolEntry[] = [
|
||||||
...channelEntries(),
|
...channelEntries(),
|
||||||
...maskEntries(),
|
...maskEntries(),
|
||||||
@@ -944,6 +943,7 @@ export const TOOLS: ToolEntry[] = [
|
|||||||
description:
|
description:
|
||||||
"Draws a colored frame of the chosen thickness around the image.",
|
"Draws a colored frame of the chosen thickness around the image.",
|
||||||
category: "geometry",
|
category: "geometry",
|
||||||
|
schema: addBorderSchema,
|
||||||
params: [
|
params: [
|
||||||
{
|
{
|
||||||
id: "thickness",
|
id: "thickness",
|
||||||
|
|||||||
+14
-2
@@ -61,6 +61,7 @@
|
|||||||
button,
|
button,
|
||||||
input:where([type="button"], [type="reset"], [type="submit"]) {
|
input:where([type="button"], [type="reset"], [type="submit"]) {
|
||||||
appearance: auto;
|
appearance: auto;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
a,
|
a,
|
||||||
@@ -126,12 +127,23 @@
|
|||||||
--space-s: 4px;
|
--space-s: 4px;
|
||||||
--space-m: 8px;
|
--space-m: 8px;
|
||||||
--space-l: 12px;
|
--space-l: 12px;
|
||||||
--space-xl: 12px;
|
--space-xl: 16px;
|
||||||
|
--space-xxl: 24px;
|
||||||
|
--space-xxxl: 32px;
|
||||||
|
|
||||||
|
--space-brand: 32px;
|
||||||
|
|
||||||
|
--space-text-s: 0;
|
||||||
|
--space-text-m: 0.04em;
|
||||||
|
--space-text-l: 0.08em;
|
||||||
|
--space-text-xl: 0.12em;
|
||||||
|
|
||||||
--size-border: 1px;
|
--size-border: 1px;
|
||||||
--size-border-thick: 2px;
|
--size-border-thick: 2px;
|
||||||
|
|
||||||
--font-text: 10px;
|
--font-size-s: 12px;
|
||||||
|
--font-size-m: 14px;
|
||||||
|
--font-size-l: 16px;
|
||||||
|
|
||||||
--radius-s: 0px;
|
--radius-s: 0px;
|
||||||
--radius-m: 4px;
|
--radius-m: 4px;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { page } from "$app/state";
|
import { page } from "$app/state";
|
||||||
import Footer from "$lib/components/kit/Footer.svelte";
|
import Footer from "$lib/components/kit/layout/Footer.svelte";
|
||||||
import TopBar from "$lib/components/kit/TopBar.svelte";
|
import TopBar from "$lib/components/kit/layout/TopBar.svelte";
|
||||||
import type { Snippet } from "svelte";
|
import type { Snippet } from "svelte";
|
||||||
import "../../preview.css";
|
import "../../preview.css";
|
||||||
|
|
||||||
@@ -63,7 +63,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<main class="preview-root" data-theme={theme}>
|
<main class="preview-root" data-theme={theme}>
|
||||||
<TopBar {theme} {crumb} {status} ontoggle={toggle} />
|
<TopBar {theme} {crumb} ontoggle={toggle} />
|
||||||
{@render children()}
|
{@render children()}
|
||||||
<Footer />
|
<Footer />
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { TOOLS } from "$lib/registry";
|
import TextField from "$lib/components/kit/fields/TextField.svelte";
|
||||||
import Panel from "$lib/components/kit/Panel.svelte";
|
import Panel from "$lib/components/kit/layout/Panel.svelte";
|
||||||
import PanelHeading from "$lib/components/kit/PanelHeading.svelte";
|
|
||||||
import SettingsFooter from "$lib/components/kit/SettingsFooter.svelte";
|
import SettingsFooter from "$lib/components/kit/SettingsFooter.svelte";
|
||||||
import TextField from "$lib/components/kit/TextField.svelte";
|
|
||||||
import ToolCard from "$lib/components/kit/ToolCard.svelte";
|
import ToolCard from "$lib/components/kit/ToolCard.svelte";
|
||||||
import Button from "$lib/components/kit/Button.svelte";
|
import Button from "$lib/components/kit/ui/Button.svelte";
|
||||||
|
import { TOOLS } from "$lib/registry";
|
||||||
import { Search } from "@lucide/svelte";
|
import { Search } from "@lucide/svelte";
|
||||||
|
|
||||||
let query = $state("");
|
let query = $state("");
|
||||||
@@ -28,8 +27,7 @@
|
|||||||
</p>
|
</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<Panel>
|
<Panel title="Search" eyebrow="tools">
|
||||||
<PanelHeading title="Search" eyebrow="tools" />
|
|
||||||
<div class="ws-search">
|
<div class="ws-search">
|
||||||
<div class="ws-field">
|
<div class="ws-field">
|
||||||
<TextField
|
<TextField
|
||||||
@@ -38,7 +36,12 @@
|
|||||||
placeholder="Search tools…"
|
placeholder="Search tools…"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Button icon={Search} variant="primary" onclick={() => {}}>Search</Button>
|
<Button
|
||||||
|
icon={Search}
|
||||||
|
variant="primary"
|
||||||
|
onclick={() => {}}
|
||||||
|
label="Search"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="ws-results">
|
<div class="ws-results">
|
||||||
{#each results as tool (tool.id)}
|
{#each results as tool (tool.id)}
|
||||||
@@ -50,7 +53,7 @@
|
|||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
<SettingsFooter>
|
<SettingsFooter>
|
||||||
<Button onclick={() => {}}>Open last project</Button>
|
<Button onclick={() => {}} label="Open last project" />
|
||||||
</SettingsFooter>
|
</SettingsFooter>
|
||||||
</Panel>
|
</Panel>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -1,26 +1,26 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import ColorField from "$lib/components/kit/ColorField.svelte";
|
import ColorField from "$lib/components/kit/fields/ColorField.svelte";
|
||||||
import DownloadButton from "$lib/components/kit/DownloadButton.svelte";
|
import FieldGrid from "$lib/components/kit/fields/FieldGrid.svelte";
|
||||||
import FieldGrid from "$lib/components/kit/FieldGrid.svelte";
|
import SliderField from "$lib/components/kit/fields/SliderField.svelte";
|
||||||
import IconButton from "$lib/components/kit/IconButton.svelte";
|
import ToggleRow from "$lib/components/kit/fields/ToggleRow.svelte";
|
||||||
import PageGrid from "$lib/components/kit/layout/PageGrid.svelte";
|
import PageGrid from "$lib/components/kit/layout/PageGrid.svelte";
|
||||||
import MetaList from "$lib/components/kit/MetaList.svelte";
|
import MetaList from "$lib/components/kit/MetaList.svelte";
|
||||||
import PipelineFooter from "$lib/components/kit/PipelineFooter.svelte";
|
import PipelineFooter from "$lib/components/kit/PipelineFooter.svelte";
|
||||||
import PreviewStack from "$lib/components/kit/PreviewStack.svelte";
|
import PreviewStack from "$lib/components/kit/PreviewStack.svelte";
|
||||||
import PreviewTile from "$lib/components/kit/PreviewTile.svelte";
|
import PreviewTile from "$lib/components/kit/PreviewTile.svelte";
|
||||||
import SectionLabel from "$lib/components/kit/SectionLabel.svelte";
|
import SectionLabel from "$lib/components/kit/SectionLabel.svelte";
|
||||||
import Segmented from "$lib/components/kit/Segmented.svelte";
|
|
||||||
import SliderField from "$lib/components/kit/SliderField.svelte";
|
|
||||||
import StepCard from "$lib/components/kit/StepCard.svelte";
|
import StepCard from "$lib/components/kit/StepCard.svelte";
|
||||||
import ToggleRow from "$lib/components/kit/ToggleRow.svelte";
|
|
||||||
import Badge from "$lib/components/kit/ui/Badge.svelte";
|
import Badge from "$lib/components/kit/ui/Badge.svelte";
|
||||||
|
import DownloadButton from "$lib/components/kit/ui/DownloadButton.svelte";
|
||||||
|
import IconButton from "$lib/components/kit/ui/IconButton.svelte";
|
||||||
|
import Segmented from "$lib/components/kit/ui/Segmented.svelte";
|
||||||
|
import WorkspaceHeader from "$lib/components/kit/WorkspaceHeader.svelte";
|
||||||
import {
|
import {
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
Plus,
|
Plus,
|
||||||
Settings2,
|
Settings2,
|
||||||
SlidersHorizontal,
|
SlidersHorizontal,
|
||||||
} from "@lucide/svelte";
|
} from "@lucide/svelte";
|
||||||
import WorkspaceHeader from "./WorkspaceHeader.svelte";
|
|
||||||
|
|
||||||
let gradColor = $state("#DCEBFF");
|
let gradColor = $state("#DCEBFF");
|
||||||
let direction = $state(135);
|
let direction = $state(135);
|
||||||
@@ -95,7 +95,7 @@
|
|||||||
onremove={() => {}}
|
onremove={() => {}}
|
||||||
>
|
>
|
||||||
{#snippet tools()}
|
{#snippet tools()}
|
||||||
<Badge tone="success" check>AUTO</Badge>
|
<Badge tone="success" check label="AUTO" />
|
||||||
{/snippet}
|
{/snippet}
|
||||||
<FieldGrid>
|
<FieldGrid>
|
||||||
<ColorField label="COLOR" chevron bind:value={gradColor} />
|
<ColorField label="COLOR" chevron bind:value={gradColor} />
|
||||||
@@ -128,7 +128,7 @@
|
|||||||
onremove={() => {}}
|
onremove={() => {}}
|
||||||
>
|
>
|
||||||
{#snippet tools()}
|
{#snippet tools()}
|
||||||
<Badge tone="success" check>AUTO</Badge>
|
<Badge tone="success" check label="AUTO" />
|
||||||
{/snippet}
|
{/snippet}
|
||||||
<div class="transform-note">
|
<div class="transform-note">
|
||||||
<SlidersHorizontal size={15} /> Automatic subject detection enabled
|
<SlidersHorizontal size={15} /> Automatic subject detection enabled
|
||||||
@@ -137,7 +137,7 @@
|
|||||||
|
|
||||||
<StepCard index={3} type="STYLE" title="Add outline" onremove={() => {}}>
|
<StepCard index={3} type="STYLE" title="Add outline" onremove={() => {}}>
|
||||||
{#snippet tools()}
|
{#snippet tools()}
|
||||||
<Badge tone="success" check>AUTO</Badge>
|
<Badge tone="success" check label="AUTO" />
|
||||||
{/snippet}
|
{/snippet}
|
||||||
<FieldGrid compact>
|
<FieldGrid compact>
|
||||||
<SliderField
|
<SliderField
|
||||||
@@ -158,7 +158,7 @@
|
|||||||
onremove={() => {}}
|
onremove={() => {}}
|
||||||
>
|
>
|
||||||
{#snippet tools()}
|
{#snippet tools()}
|
||||||
<Badge tone="success" check>AUTO</Badge>
|
<Badge tone="success" check label="AUTO" />
|
||||||
{/snippet}
|
{/snippet}
|
||||||
<FieldGrid compact>
|
<FieldGrid compact>
|
||||||
<SliderField
|
<SliderField
|
||||||
@@ -296,9 +296,6 @@
|
|||||||
gap: 5px;
|
gap: 5px;
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
.image-preview.active {
|
|
||||||
border-color: var(--blue);
|
|
||||||
}
|
|
||||||
.sample-icon {
|
.sample-icon {
|
||||||
border: 1px solid;
|
border: 1px solid;
|
||||||
padding: 3px 5px;
|
padding: 3px 5px;
|
||||||
|
|||||||
@@ -1,28 +1,27 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Button from "$lib/components/kit/Button.svelte";
|
|
||||||
import CheckboxField from "$lib/components/kit/CheckboxField.svelte";
|
|
||||||
import CheckerCanvas from "$lib/components/kit/CheckerCanvas.svelte";
|
import CheckerCanvas from "$lib/components/kit/CheckerCanvas.svelte";
|
||||||
import CodeBlock from "$lib/components/kit/CodeBlock.svelte";
|
import CodeBlock from "$lib/components/kit/CodeBlock.svelte";
|
||||||
import ColorField from "$lib/components/kit/ColorField.svelte";
|
|
||||||
import DownloadButton from "$lib/components/kit/DownloadButton.svelte";
|
|
||||||
import EmptyState from "$lib/components/kit/EmptyState.svelte";
|
import EmptyState from "$lib/components/kit/EmptyState.svelte";
|
||||||
import IconButton from "$lib/components/kit/IconButton.svelte";
|
import CheckboxField from "$lib/components/kit/fields/CheckboxField.svelte";
|
||||||
|
import ColorField from "$lib/components/kit/fields/ColorField.svelte";
|
||||||
|
import SelectField from "$lib/components/kit/fields/SelectField.svelte";
|
||||||
|
import SliderField from "$lib/components/kit/fields/SliderField.svelte";
|
||||||
|
import TextField from "$lib/components/kit/fields/TextField.svelte";
|
||||||
import ImageCard from "$lib/components/kit/ImageCard.svelte";
|
import ImageCard from "$lib/components/kit/ImageCard.svelte";
|
||||||
|
import Panel from "$lib/components/kit/layout/Panel.svelte";
|
||||||
import MetaList from "$lib/components/kit/MetaList.svelte";
|
import MetaList from "$lib/components/kit/MetaList.svelte";
|
||||||
import Panel from "$lib/components/kit/Panel.svelte";
|
|
||||||
import PanelHeading from "$lib/components/kit/PanelHeading.svelte";
|
|
||||||
import PreviewTile from "$lib/components/kit/PreviewTile.svelte";
|
import PreviewTile from "$lib/components/kit/PreviewTile.svelte";
|
||||||
import Segmented from "$lib/components/kit/Segmented.svelte";
|
|
||||||
import SelectField from "$lib/components/kit/SelectField.svelte";
|
|
||||||
import SettingGroup from "$lib/components/kit/SettingGroup.svelte";
|
import SettingGroup from "$lib/components/kit/SettingGroup.svelte";
|
||||||
import SettingsFooter from "$lib/components/kit/SettingsFooter.svelte";
|
import SettingsFooter from "$lib/components/kit/SettingsFooter.svelte";
|
||||||
import SliderField from "$lib/components/kit/SliderField.svelte";
|
|
||||||
import StatusLine from "$lib/components/kit/StatusLine.svelte";
|
import StatusLine from "$lib/components/kit/StatusLine.svelte";
|
||||||
import StepCard from "$lib/components/kit/StepCard.svelte";
|
import StepCard from "$lib/components/kit/StepCard.svelte";
|
||||||
import TextField from "$lib/components/kit/TextField.svelte";
|
|
||||||
import Toggle from "$lib/components/kit/Toggle.svelte";
|
|
||||||
import ToolCard from "$lib/components/kit/ToolCard.svelte";
|
import ToolCard from "$lib/components/kit/ToolCard.svelte";
|
||||||
import Badge from "$lib/components/kit/ui/Badge.svelte";
|
import Badge from "$lib/components/kit/ui/Badge.svelte";
|
||||||
|
import Button from "$lib/components/kit/ui/Button.svelte";
|
||||||
|
import DownloadButton from "$lib/components/kit/ui/DownloadButton.svelte";
|
||||||
|
import IconButton from "$lib/components/kit/ui/IconButton.svelte";
|
||||||
|
import Segmented from "$lib/components/kit/ui/Segmented.svelte";
|
||||||
|
import Toggle from "$lib/components/kit/ui/Toggle.svelte";
|
||||||
import { Download, ImageOff, Palette, Plus, Trash2 } from "@lucide/svelte";
|
import { Download, ImageOff, Palette, Plus, Trash2 } from "@lucide/svelte";
|
||||||
|
|
||||||
let mode = $state("preview");
|
let mode = $state("preview");
|
||||||
@@ -58,22 +57,21 @@
|
|||||||
<StatusLine label="rendering ok" />
|
<StatusLine label="rendering ok" />
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<Panel>
|
<Panel title="Buttons" eyebrow="controls">
|
||||||
<PanelHeading title="Buttons" eyebrow="controls" />
|
|
||||||
<div class="stack">
|
<div class="stack">
|
||||||
<SettingGroup label="variants">
|
<SettingGroup label="variants">
|
||||||
<Button onclick={() => {}}>Primary</Button>
|
<Button onclick={() => {}} label="Primary" />
|
||||||
<Button variant="ghost" onclick={() => {}}>Ghost</Button>
|
<Button variant="outline" onclick={() => {}} label="Outline" />
|
||||||
<Button variant="accent" onclick={() => {}}>Accent</Button>
|
<Button variant="accent" onclick={() => {}} label="Accent" />
|
||||||
<Button variant="danger" onclick={() => {}}>Danger</Button>
|
<Button variant="danger" onclick={() => {}} label="Danger" />
|
||||||
</SettingGroup>
|
</SettingGroup>
|
||||||
<SettingGroup label="states">
|
<SettingGroup label="states">
|
||||||
<Button disabled onclick={() => {}}>Disabled</Button>
|
<Button disabled onclick={() => {}} label="Disabled" />
|
||||||
<IconButton icon={Plus} label="Add" onclick={() => {}} />
|
<IconButton icon={Plus} label="Add" onclick={() => {}} />
|
||||||
<IconButton
|
<IconButton
|
||||||
icon={Trash2}
|
icon={Trash2}
|
||||||
label="Delete"
|
label="Delete"
|
||||||
variant="solid"
|
variant="primary"
|
||||||
onclick={() => {}}
|
onclick={() => {}}
|
||||||
/>
|
/>
|
||||||
<IconButton
|
<IconButton
|
||||||
@@ -89,8 +87,7 @@
|
|||||||
</SettingsFooter>
|
</SettingsFooter>
|
||||||
</Panel>
|
</Panel>
|
||||||
|
|
||||||
<Panel>
|
<Panel title="Inputs" eyebrow="controls">
|
||||||
<PanelHeading title="Inputs" eyebrow="controls" />
|
|
||||||
<div class="stack">
|
<div class="stack">
|
||||||
<SliderField
|
<SliderField
|
||||||
label="corner radius"
|
label="corner radius"
|
||||||
@@ -121,8 +118,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</Panel>
|
</Panel>
|
||||||
|
|
||||||
<Panel>
|
<Panel title="Segmented & badges" eyebrow="controls">
|
||||||
<PanelHeading title="Segmented & badges" eyebrow="controls" />
|
|
||||||
<div class="stack">
|
<div class="stack">
|
||||||
<Segmented
|
<Segmented
|
||||||
bind:value={mode}
|
bind:value={mode}
|
||||||
@@ -133,16 +129,17 @@
|
|||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<SettingGroup label="tones">
|
<SettingGroup label="tones">
|
||||||
<Badge>default</Badge>
|
<Badge label="default" />
|
||||||
<Badge tone="accent">accent</Badge>
|
<Badge tone="accent" label="accent" />
|
||||||
<Badge tone="success">success</Badge>
|
<Badge tone="success" label="success" />
|
||||||
<Badge tone="amber">amber</Badge>
|
<Badge tone="danger" label="default" />
|
||||||
|
<Badge tone="warning" label="warning" />
|
||||||
|
<Badge tone="info" label="info" />
|
||||||
</SettingGroup>
|
</SettingGroup>
|
||||||
</div>
|
</div>
|
||||||
</Panel>
|
</Panel>
|
||||||
|
|
||||||
<Panel>
|
<Panel title="Steps" eyebrow="pipeline">
|
||||||
<PanelHeading title="Steps" eyebrow="pipeline" />
|
|
||||||
<div class="stack">
|
<div class="stack">
|
||||||
<StepCard index={1} type="BACKGROUND" title="Gradient fill">
|
<StepCard index={1} type="BACKGROUND" title="Gradient fill">
|
||||||
<ColorField label="color" bind:value={tint} />
|
<ColorField label="color" bind:value={tint} />
|
||||||
@@ -153,8 +150,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</Panel>
|
</Panel>
|
||||||
|
|
||||||
<Panel>
|
<Panel title="Tools" eyebrow="catalog">
|
||||||
<PanelHeading title="Tools" eyebrow="catalog" />
|
|
||||||
<div class="tool-grid">
|
<div class="tool-grid">
|
||||||
{#each tools as tool, i (tool.title)}
|
{#each tools as tool, i (tool.title)}
|
||||||
<ToolCard
|
<ToolCard
|
||||||
@@ -168,8 +164,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</Panel>
|
</Panel>
|
||||||
|
|
||||||
<Panel>
|
<Panel title="Preview" eyebrow="canvas">
|
||||||
<PanelHeading title="Preview" eyebrow="canvas" />
|
|
||||||
<div class="preview-grid">
|
<div class="preview-grid">
|
||||||
<ImageCard label="result.png · 512×512">
|
<ImageCard label="result.png · 512×512">
|
||||||
<CheckerCanvas size="sm" />
|
<CheckerCanvas size="sm" />
|
||||||
@@ -192,15 +187,14 @@
|
|||||||
</SettingsFooter>
|
</SettingsFooter>
|
||||||
</Panel>
|
</Panel>
|
||||||
|
|
||||||
<Panel>
|
<Panel title="States & code" eyebrow="misc">
|
||||||
<PanelHeading title="States & code" eyebrow="misc" />
|
|
||||||
<div class="stack">
|
<div class="stack">
|
||||||
<EmptyState
|
<EmptyState
|
||||||
icon={ImageOff}
|
icon={ImageOff}
|
||||||
title="No image loaded"
|
title="No image loaded"
|
||||||
description="Drop a PNG to start processing"
|
description="Drop a PNG to start processing"
|
||||||
>
|
>
|
||||||
<Button onclick={() => {}}>Open file</Button>
|
<Button onclick={() => {}} label="Open file" />
|
||||||
</EmptyState>
|
</EmptyState>
|
||||||
<CodeBlock
|
<CodeBlock
|
||||||
code={'export const config = {\n radius: 8,\n tint: "#3b82f6",\n};'}
|
code={'export const config = {\n radius: 8,\n tint: "#3b82f6",\n};'}
|
||||||
|
|||||||
Reference in New Issue
Block a user