refactor: isolate old and preview versions, add lint rule
This commit is contained in:
@@ -138,6 +138,29 @@ scoped-путям (`kit/**`, `preview/**`).
|
||||
определён в `src/preview.css` (словарь токенов) или локально в компоненте.
|
||||
Файл читается один раз и кэшируется (не `glob`-зависим).
|
||||
|
||||
### Изоляция веток old ↔ preview
|
||||
|
||||
`isolation/no-mixed-imports` (`web/eslint-plugins/isolation/no-mixed-imports.js`,
|
||||
включён на весь `**/*.{ts,svelte}`) — **полная взаимная изоляция** старого UI и
|
||||
новой preview-ветки. В отличие от `no-restricted-imports`, правило **резолвит**
|
||||
каждый импорт (и `$lib/...`, и относительные `./`/`../`) до реального файла и
|
||||
классифицирует стороны по фактическому пути, поэтому относительным импортом
|
||||
правило не обойти.
|
||||
|
||||
- Старое: `routes/(old)/**`, `lib/registry.ts`, `lib/registry/**`,
|
||||
`lib/registry-helpers.ts`, `lib/categories.ts`, `lib/tools/**`,
|
||||
`lib/components/**` (кроме `kit/`).
|
||||
- Новое: `routes/preview/**`, `lib/registry-new/**`, `lib/preview/**`,
|
||||
`lib/registry-schema.ts`, `lib/registry-schema.test.ts`, `lib/components/kit/**`.
|
||||
- Общее (разрешено обоим): `core/`, `i18n/`, `theme`, `assets/`, корневой `lib`.
|
||||
- Плагин **конфигурируем** (опции `old`/`new` + `root`/`alias` в
|
||||
`eslint.config.js`): перенос старых файлов в папки `old/` — это правка
|
||||
glob-паттернов в настройке, а не код правила.
|
||||
- Изоляция уже достигнута: старый `registry/` не тянет `registry-schema`
|
||||
(пилоты add-border/add-stroke работают через `params`), `registry.ts` не
|
||||
импортирует `ToolSchema`; preview-`list-tools` использует копию
|
||||
`preview/tool-icons.ts`, а не старый `tools/tool-icons.ts`.
|
||||
|
||||
Линтер только показывает ошибки, старый код (непрефиксованные токены, hex в
|
||||
preview.css) — известный техдолг, его НЕ чинить и не игнорировать правилами.
|
||||
|
||||
|
||||
@@ -31,7 +31,8 @@
|
||||
> step-colors, placeholder, text-to-png), text (add-text, date-stamp),
|
||||
> filters (randomize-pixels, add-noise) — везде, где канвас отделён от
|
||||
> параметров эффекта либо фигура от позиции.
|
||||
> Следующее: шаг 34 — поглощение `tool-views.ts` (Фаза 5, зачистка).
|
||||
> Следующее: шаг 37 — перенос всех старых компонентов/библиотек в папки `old/`
|
||||
> (линтер-изоляция уже на месте, переезд — просто дописывание glob-паттерна).
|
||||
>
|
||||
> Ключевые файлы нового registry: `web/src/lib/registry-new/{types,index,*}.ts`
|
||||
> (по файлу на категорию: geometry/alpha/convert/analyze/filters/color/generate)
|
||||
@@ -61,7 +62,7 @@
|
||||
`lib/preview/categories.ts` (object as const) копирует и заменяет собой
|
||||
`../categories` для нового кода, старый `categories.ts` обслуживает `(old)` и
|
||||
остаётся без изменений. После перехода (Фаза 5) копия становится основной,
|
||||
а исходник удаляется вместе со старым UI.
|
||||
а исходник продолжает обслуживать `(old)` UI (он не удаляется — см. шаг 35).
|
||||
|
||||
**Разделение схем: две независимые схемы.**
|
||||
|
||||
@@ -81,8 +82,8 @@
|
||||
Помимо двух схем, registry тоже разделён по UI (по факту миграции):
|
||||
|
||||
- **Старый registry** (`web/src/lib/registry.ts` + `registry/` +
|
||||
`registry-helpers.ts`) — работает на старом UI, использует `ParamDef[]` и
|
||||
`tool-views.ts`. **Не трогаем**; идёт под удаление вместе со старым UI.
|
||||
`registry-helpers.ts`) — работает на старом UI, использует `ParamDef[]`.
|
||||
**Не трогаем**; остаётся обслуживать `(old)/` маршруты (см. шаг 35).
|
||||
- **Новый registry** (`web/src/lib/registry-new/`) — строится **с нуля «как надо»**:
|
||||
`ToolEntry<P>` с обязательным `schema`, типизированный `run`, **без** `ParamDef[]`
|
||||
и **без** связи со старым. Импортирует core-функции (`expandCanvas`,
|
||||
@@ -442,12 +443,50 @@ Typed field builders + `Field<T>` + `toolSchema<P>()` + `ToolSchema<P>` —
|
||||
(Canvas/Colors/Text), text-to-png (Text/Background/Padding);
|
||||
- text: add-text, date-stamp (Text/Placement/Plate);
|
||||
- filters: randomize-pixels (Blocks), add-noise (Noise/Seed).
|
||||
Инструменты с 1–3 простыми полями остались без layout (одна общая группа).
|
||||
Инструменты с 1–3 простыми полями остались без layout (одна общая группа).
|
||||
|
||||
### Фаза 5 — зачистка
|
||||
### Фаза 5 — изоляция старого UI
|
||||
|
||||
34. Поглощение `tool-views.ts` (preview/lede/layout → meta инструмента).
|
||||
35. После перехода на новый дизайн — удаление старого UI и старого `ParamDef[]`.
|
||||
34. Поглощение `tool-views.ts` (preview/lede/layout → meta инструмента) ✔.
|
||||
Отдельного `tool-views.ts` в репо нет: preview сразу строился на
|
||||
`registry-new`. Meta инструмента живёт в `ToolEntry` (`title`, `description`,
|
||||
`category`, `run`/`generate`), layout — в `schema.layout` (шаг 32), рендер —
|
||||
`SchemaToolView`/`SchemaFields`/`SchemaPreview`. Дублирующей системы нет.
|
||||
35. Старый UI **не удаляется**, а выносится в `(old)/`-маршруты и остаётся там
|
||||
временно (посмотреть, как работает, сравнить с новым; старые тесты
|
||||
продолжают проходить):
|
||||
- маршруты `(old)/{+page,demo,list-tools,tools/[id]}` — старый дизайн,
|
||||
тянет `old.css` (не `design2.css`), старый header/footer;
|
||||
- старый `registry.ts`/`registry/` + `ParamDef[]` обслуживают только
|
||||
`(old)/`-инструменты — не удаляются, не рефакторятся;
|
||||
- удаление происходит позже, отдельным решением (когда новый UI покроет
|
||||
все инструменты и ревью завершено).
|
||||
36. **Линтер-изоляция веток** (гарантия, что old и preview не смешиваются) ✔.
|
||||
Кастомный ESLint-плагин `web/eslint-plugins/isolation/no-mixed-imports`
|
||||
Резолвит каждый импорт (и `$lib/...`, и относительные `./`/`../`) до
|
||||
реального файла, классифицирует источник и цель по фактическому пути и
|
||||
ругается на old→new и new→old. Конфигурация (`old`/`new` glob-паттерны,
|
||||
`root`, `alias`) вынесена в настройки правила — единая точка правды:
|
||||
- старое: `routes/(old)/**`, `lib/registry.ts`, `lib/registry/**`,
|
||||
`lib/registry-helpers.ts`, `lib/categories.ts`, `lib/tools/**`,
|
||||
`lib/components/**` (кроме `kit/`);
|
||||
- новое: `routes/preview/**`, `lib/registry-new/**`, `lib/preview/**`,
|
||||
`lib/registry-schema.ts`, `lib/registry-schema.test.ts`,
|
||||
`lib/components/kit/**`;
|
||||
- общее (разрешено обоим): всё прочее — `core/`, `i18n/`, `theme`,
|
||||
`assets/`, корневой `lib` (`index.ts`, тесты).
|
||||
Достигнутая полная изоляция (одиночные пересечения устранены):
|
||||
- старые пилоты `registry/geometry.ts` (`add-border`) и `registry/alpha.ts`
|
||||
(`add-stroke`) получали `schema` из нового `registry-schema` — убрано;
|
||||
оба инструмента работают в старом UI через `params: ParamDef[]`,
|
||||
в preview — через свои schema-версии в `registry-new/`;
|
||||
- `registry.ts` больше не импортирует `ToolSchema` из `registry-schema`;
|
||||
- preview `list-tools` тянул `TOOL_ICONS` из старого `lib/tools/tool-icons`
|
||||
→ создана копия `lib/preview/tool-icons.ts` (правило копий).
|
||||
37. Перенос всех старых компонентов/библиотек в папки `old/` (**следующее**):
|
||||
переезд не трогает плагин — достаточно дописать один glob-паттерн в
|
||||
настройку правила (например `lib/old/**`, `lib/components/old/**`), а сама
|
||||
проверка работает по фактическим путям автоматически.
|
||||
|
||||
### Как ревьюить каждый шаг
|
||||
|
||||
@@ -484,8 +523,8 @@ Typed field builders + `Field<T>` + `toolSchema<P>()` + `ToolSchema<P>` —
|
||||
на `ParamDef[]` как раньше.
|
||||
- **Новый UI** (kit/`SchemaToolView` + `SchemaFields` + `SchemaPreview`, читает
|
||||
`ToolSchema<P>`) — сейчас рендерит поля по схеме (number/slider/color).
|
||||
- `tool-views.ts` — со временем поглощается registry (preview/lede/layout →
|
||||
meta инструмента). Отдельный шаг, НЕ блокирует типизацию params.
|
||||
- `tool-views.ts` отсутствует — preview/lede/layout уже живут в meta
|
||||
инструмента (`ToolEntry` + `schema.layout`), дублирования нет.
|
||||
|
||||
## Оценка трудозатрат
|
||||
|
||||
@@ -498,15 +537,16 @@ Typed field builders + `Field<T>` + `toolSchema<P>()` + `ToolSchema<P>` —
|
||||
| Миграция инструментов на interface Params + схемы | Механическая, но крупная | ~8-12ч |
|
||||
| Новый рендер (kit/ParamControl) + составные виджеты | Средняя | ~4-5ч |
|
||||
| Поглощение tool-views.ts | Средняя | ~1-2ч |
|
||||
| **Итого** | | **~20-29ч** (поэтапно) |
|
||||
| Вынос старого UI в (old)/ + адаптация маршрутов | Средняя-Низкая | ~2-3ч |
|
||||
| **Итого** | | **~22-32ч** (поэтапно) |
|
||||
|
||||
> Оценка выросла по сравнению с ранней версией плана, потому что принят путь
|
||||
> «явный interface Params + схема + общий рендер с пер-инструмент layout» —
|
||||
> это полный рефакторинг pipeline, а не только добавление составных типов.
|
||||
>
|
||||
> Старый UI/`ParamDef[]`/старый pipeline в смету **не входят** — они не
|
||||
> рефакторятся, а продолжают работать до перехода (затем удаляются вместе со
|
||||
> старым дизайном).
|
||||
> рефакторятся, а продолжают работать на `(old)/`-маршрутах до перехода
|
||||
> (затем убираются отдельным решением — см. шаг 35).
|
||||
|
||||
## Порядок реализации (кратко)
|
||||
|
||||
@@ -525,7 +565,10 @@ Typed field builders + `Field<T>` + `toolSchema<P>()` + `ToolSchema<P>` —
|
||||
отдельно.
|
||||
5. **Фаза 4** — масштаб UI на остальные: составные виджеты, пер-инструмент
|
||||
layout (для каждого инструмента — UI-макет).
|
||||
6. **Фаза 5** — поглощение `tool-views.ts`, затем удаление старого UI/`ParamDef[]`
|
||||
6. **Фаза 5** — изоляция: старый UI на `(old)/`-маршруты (не удаляется,
|
||||
остаётся для ревью); preview на `registry-new` полностью;
|
||||
линтер-изоляция веток (плагин `isolation`), затем перенос старых
|
||||
компонентов/библиотек в папки `old/`.
|
||||
|
||||
## Зависимости
|
||||
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Local ESLint plugin "isolation".
|
||||
*
|
||||
* Guarantees full isolation between the old UI branch and the new (preview)
|
||||
* branch. Unlike no-restricted-imports (which matches the literal import
|
||||
* specifier string only), these rules RESOLVE the specifier to a real file
|
||||
* (supporting both `$lib/...` aliases and relative `./`/`../` paths) and then
|
||||
* classify both sides by their actual location on disk.
|
||||
*
|
||||
* Why this is needed:
|
||||
* - old code imports new things via RELATIVE paths (e.g. `../registry-schema`),
|
||||
* which `$lib/...` glob patterns cannot catch;
|
||||
* - the same target can be written many ways (`$lib/x`, `../x`, `../../x`),
|
||||
* so enumerating literal strings is fragile.
|
||||
*
|
||||
* Configuration (per rule options):
|
||||
* - `root`: path from the lint cwd to the source root (default "src");
|
||||
* - `alias`: mapping from import aliases to dirs relative to root
|
||||
* (default { "$lib": "lib" });
|
||||
* - `old`, `new`: glob patterns (gitignore-style, `!` = negation) for the two
|
||||
* branches. Files matched by neither are "shared" and unrestricted.
|
||||
*/
|
||||
import noMixedImports from "./no-mixed-imports.js";
|
||||
|
||||
export default {
|
||||
meta: {
|
||||
name: "isolation",
|
||||
version: "1.0.0",
|
||||
},
|
||||
rules: {
|
||||
"no-mixed-imports": noMixedImports,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,201 @@
|
||||
/**
|
||||
* Isolation rule: forbid mixing the "old" and the "new" (preview) branches.
|
||||
*
|
||||
* The rule RESOLVES every import specifier to a real file (handles both the
|
||||
* `$lib/...` alias and relative `./` / `../` paths), classifies the source
|
||||
* file and each import target by their actual location, and reports whenever an
|
||||
* "old" file imports a "new" one or vice versa. Files that match neither side
|
||||
* are "shared" (core/, i18n/, theme, assets, tests, ...) and can import freely.
|
||||
*
|
||||
* Options (object, all optional):
|
||||
* - root: path from lint cwd to the source dir (default "src");
|
||||
* - alias: { $lib: "lib" } — alias -> dir relative to root;
|
||||
* - old, new: gitignore-style glob lists for the two branches. Both default
|
||||
* to the current layout so that moving a module between branches is a
|
||||
* one-line config change (e.g. everything old will live under `old/`).
|
||||
*/
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
const DEFAULT_OLD = [
|
||||
"routes/(old)/**",
|
||||
"lib/registry/**",
|
||||
"lib/registry.ts",
|
||||
"lib/registry-helpers.ts",
|
||||
"lib/categories.ts",
|
||||
"lib/tools/**",
|
||||
"lib/components/**",
|
||||
"!lib/components/kit/**",
|
||||
];
|
||||
|
||||
const DEFAULT_NEW = [
|
||||
"routes/preview/**",
|
||||
"lib/components/kit/**",
|
||||
"lib/preview/**",
|
||||
"lib/registry-new/**",
|
||||
"lib/registry-schema.ts",
|
||||
"lib/registry-schema.test.ts",
|
||||
];
|
||||
|
||||
/** Escape everything except glob metacharacters, then handle **, *, ?. */
|
||||
function globToRegExp(glob) {
|
||||
let out = "^";
|
||||
for (let i = 0; i < glob.length; i++) {
|
||||
const ch = glob[i];
|
||||
if (ch === "*" && glob[i + 1] === "*") {
|
||||
out += ".*";
|
||||
i++;
|
||||
} else if (ch === "*") {
|
||||
out += "[^/]*";
|
||||
} else if (ch === "?") {
|
||||
out += "[^/]";
|
||||
} else if ("\\^$.|?*+()[]{}".includes(ch)) {
|
||||
out += `\\${ch}`;
|
||||
} else {
|
||||
out += ch;
|
||||
}
|
||||
}
|
||||
return new RegExp(out + "$");
|
||||
}
|
||||
|
||||
/** gitignore-style: last-match-wins, `!` negates earlier positives. */
|
||||
function matchesGlobList(patterns, rel) {
|
||||
let included = false;
|
||||
for (const raw of patterns) {
|
||||
const negated = raw.startsWith("!");
|
||||
const pattern = negated ? raw.slice(1) : raw;
|
||||
if (globToRegExp(pattern).test(rel)) {
|
||||
if (negated) return false;
|
||||
included = true;
|
||||
}
|
||||
}
|
||||
return included;
|
||||
}
|
||||
|
||||
/** First existing candidate when resolving a bare path (no extension known). */
|
||||
function firstExisting(base) {
|
||||
const candidates = [
|
||||
base,
|
||||
`${base}.ts`,
|
||||
`${base}.svelte`,
|
||||
`${base}.svelte.ts`,
|
||||
`${base}/index.ts`,
|
||||
`${base}/index.svelte`,
|
||||
];
|
||||
for (const c of candidates) {
|
||||
try {
|
||||
if (fs.statSync(c).isFile()) return c;
|
||||
} catch {
|
||||
// keep trying
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function toPortable(p) {
|
||||
return p.replace(/\\/g, "/");
|
||||
}
|
||||
|
||||
export default {
|
||||
meta: {
|
||||
type: "problem",
|
||||
docs: {
|
||||
description:
|
||||
"Forbid imports between the old UI branch and the new (preview) branch.",
|
||||
category: "Best Practices",
|
||||
},
|
||||
schema: [
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
root: { type: "string" },
|
||||
alias: { type: "object", additionalProperties: { type: "string" } },
|
||||
old: { type: "array", items: { type: "string" } },
|
||||
new: { type: "array", items: { type: "string" } },
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
],
|
||||
messages: {
|
||||
noMixed:
|
||||
'Isolation violation: "{{side}}" file imports "{{target}}" ({{targetRel}}).',
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
const options = context.options[0] || {};
|
||||
const cwd = context.cwd;
|
||||
const srcRoot = path.resolve(cwd, options.root || "src");
|
||||
const alias = options.alias || { $lib: "lib" };
|
||||
const oldPatterns = options.old || DEFAULT_OLD;
|
||||
const newPatterns = options.new || DEFAULT_NEW;
|
||||
|
||||
/** Classify a path (already absolute or portable rel to srcRoot). */
|
||||
function classify(absOrRel, relativeToRoot) {
|
||||
const rel = relativeToRoot
|
||||
? absOrRel
|
||||
: toPortable(path.relative(srcRoot, absOrRel));
|
||||
// new wins over old if a path is matched by both (e.g. components/kit
|
||||
// is != lib/components/** via negation, but keep precedence safe).
|
||||
if (matchesGlobList(newPatterns, rel)) return "new";
|
||||
if (matchesGlobList(oldPatterns, rel)) return "old";
|
||||
return "shared";
|
||||
}
|
||||
|
||||
/** Resolve a specifier to a portable rel path from srcRoot, or null. */
|
||||
function resolveToRel(spec, currentDir) {
|
||||
if (spec.startsWith("$")) {
|
||||
const dot = spec.indexOf("/");
|
||||
const mapKey = spec.slice(0, dot === -1 ? spec.length : dot);
|
||||
const dir = alias[mapKey];
|
||||
if (!dir) return null;
|
||||
const rest = dot === -1 ? "" : spec.slice(dot + 1);
|
||||
const base = path.resolve(srcRoot, dir, rest);
|
||||
const found = firstExisting(base);
|
||||
if (!found) return null;
|
||||
return toPortable(path.relative(srcRoot, found));
|
||||
}
|
||||
if (spec.startsWith("./") || spec.startsWith("../")) {
|
||||
const base = path.resolve(currentDir, spec);
|
||||
const found = firstExisting(base);
|
||||
if (!found) return null;
|
||||
const rel = path.relative(srcRoot, found);
|
||||
if (rel.startsWith("..")) return null; // outside srcRoot
|
||||
return toPortable(rel);
|
||||
}
|
||||
return null; // package import (svelte, vitest, @lucide, ...)
|
||||
}
|
||||
|
||||
const filename = context.filename || context.physicalFilename;
|
||||
const currentAbs = path.resolve(filename);
|
||||
const currentRel = toPortable(path.relative(srcRoot, currentAbs));
|
||||
const currentSide = classify(currentRel, true);
|
||||
const currentDir = path.dirname(currentAbs);
|
||||
|
||||
if (currentSide === "shared") return {};
|
||||
|
||||
function checkSource(node) {
|
||||
const spec = node.source?.value;
|
||||
if (typeof spec !== "string") return;
|
||||
const targetRel = resolveToRel(spec, currentDir);
|
||||
if (!targetRel) return;
|
||||
const targetSide = classify(targetRel, true);
|
||||
if (
|
||||
(currentSide === "old" && targetSide === "new") ||
|
||||
(currentSide === "new" && targetSide === "old")
|
||||
) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: "noMixed",
|
||||
data: { side: currentSide, target: targetSide, targetRel },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
ImportDeclaration: checkSource,
|
||||
ImportExpression: checkSource,
|
||||
ExportNamedDeclaration: checkSource,
|
||||
ExportAllDeclaration: checkSource,
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -2,6 +2,7 @@ import js from "@eslint/js";
|
||||
import prettier from "eslint-config-prettier";
|
||||
import svelte from "eslint-plugin-svelte";
|
||||
import designTokens from "./eslint-plugins/index.js";
|
||||
import isolationPlugin from "./eslint-plugins/isolation/index.js";
|
||||
import globals from "globals";
|
||||
import svelteParser from "svelte-eslint-parser";
|
||||
import tseslint from "typescript-eslint";
|
||||
@@ -114,6 +115,20 @@ export default tseslint.config(
|
||||
"prefer-const": "off",
|
||||
},
|
||||
},
|
||||
// ===== Изоляция старого UI (old) и нового preview =====================
|
||||
// Полная взаимная изоляция веток (см. plan-composite-params, Фаза 5).
|
||||
// Кастомный плагин isolation/no-mixed-imports резолвит импорты по реальному
|
||||
// пути (и $lib, и относительные) и ругается на old→new / new→old.
|
||||
// Общее (core/, i18n/, theme) разрешено обоим.
|
||||
{
|
||||
files: ["**/*.{ts,svelte}"],
|
||||
plugins: {
|
||||
isolation: isolationPlugin,
|
||||
},
|
||||
rules: {
|
||||
"isolation/no-mixed-imports": "error",
|
||||
},
|
||||
},
|
||||
// prettier — последним, чтобы гасить форматирующие правила из recommended.
|
||||
prettier,
|
||||
);
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
import {
|
||||
AppWindow,
|
||||
Binary,
|
||||
Blend,
|
||||
CalendarDays,
|
||||
ClipboardPaste,
|
||||
Circle,
|
||||
Contrast,
|
||||
Crop,
|
||||
Crosshair,
|
||||
Dices,
|
||||
Droplet,
|
||||
Droplets,
|
||||
Eye,
|
||||
FileImage,
|
||||
FileOutput,
|
||||
FilePlus2,
|
||||
Film,
|
||||
FlipHorizontal2,
|
||||
Focus,
|
||||
Frame,
|
||||
Grid3x3,
|
||||
Hammer,
|
||||
Hash,
|
||||
Image as ImageIcon,
|
||||
Info,
|
||||
Layers,
|
||||
Link2,
|
||||
Maximize2,
|
||||
Minimize2,
|
||||
Moon,
|
||||
PaintBucket,
|
||||
Palette,
|
||||
Rainbow,
|
||||
Repeat2,
|
||||
RotateCw,
|
||||
Ruler,
|
||||
Scaling,
|
||||
Scan,
|
||||
Scissors,
|
||||
SearchCheck,
|
||||
FaceSlightlySmiling,
|
||||
Square,
|
||||
Stamp,
|
||||
Star,
|
||||
Sun,
|
||||
Type,
|
||||
WavesHorizontal,
|
||||
ZoomIn,
|
||||
ZoomOut,
|
||||
} from "@lucide/svelte";
|
||||
|
||||
export const TOOL_ICONS: Record<string, typeof AppWindow> = {
|
||||
"resize-png": Scaling,
|
||||
"crop-png": Crop,
|
||||
"rotate-png": RotateCw,
|
||||
"flip-png": FlipHorizontal2,
|
||||
"grayscale-png": Contrast,
|
||||
"invert-colors-png": Blend,
|
||||
"adjust-brightness-contrast-png": Sun,
|
||||
"convert-png-to-jpg": FileImage,
|
||||
"convert-png-to-webp": FileImage,
|
||||
"remove-color-from-png": Scissors,
|
||||
"png-info": Info,
|
||||
"jpg-to-png": FileImage,
|
||||
"webp-to-png": FileImage,
|
||||
"gif-to-png": Film,
|
||||
"bmp-to-png": FileImage,
|
||||
"ico-to-png": AppWindow,
|
||||
"png-to-bmp": FileOutput,
|
||||
"png-to-base64": Binary,
|
||||
"base64-to-png": ClipboardPaste,
|
||||
"png-to-data-uri": Link2,
|
||||
"data-uri-to-png": ClipboardPaste,
|
||||
"png-to-hex": Hash,
|
||||
"hex-to-png": Hash,
|
||||
"change-png-opacity": Droplet,
|
||||
"sepia-png": Palette,
|
||||
"change-png-hue": Rainbow,
|
||||
"extract-channel-png": Layers,
|
||||
"swap-channels-png": Repeat2,
|
||||
"black-and-white-png": Contrast,
|
||||
"posterize-png": Layers,
|
||||
"two-colors-png": Blend,
|
||||
"invert-alpha-png": Droplets,
|
||||
"remove-alpha-channel-png": Square,
|
||||
"set-alpha-channel-png": Droplet,
|
||||
"extract-alpha-mask-png": Layers,
|
||||
"round-corners-png": Frame,
|
||||
"add-padding-png": Maximize2,
|
||||
"add-border-png": Frame,
|
||||
"fit-on-background-png": ImageIcon,
|
||||
"tile-png": Grid3x3,
|
||||
"center-by-alpha-png": Crosshair,
|
||||
"create-empty-png": FilePlus2,
|
||||
"single-color-png": PaintBucket,
|
||||
"random-noise-png": Dices,
|
||||
"linear-gradient-png": Sun,
|
||||
"png-is-grayscale": SearchCheck,
|
||||
"png-is-transparent": Eye,
|
||||
"png-orientation": Ruler,
|
||||
"blur-png": Droplets,
|
||||
"sharpen-png": Focus,
|
||||
"remove-background-png": Scissors,
|
||||
"add-stroke-png": Square,
|
||||
"add-text-png": Type,
|
||||
"date-stamp-png": CalendarDays,
|
||||
"png-to-hsl": Blend,
|
||||
"png-to-hsv": Droplet,
|
||||
"png-to-hsi": Focus,
|
||||
"png-to-cmyk": PaintBucket,
|
||||
"png-to-ycbcr": Layers,
|
||||
"png-to-lab": Ruler,
|
||||
"show-transparent-png": Eye,
|
||||
"show-grayscale-pixels-png": Contrast,
|
||||
"show-color-pixels-png": Rainbow,
|
||||
"light-pixel-mask-png": Sun,
|
||||
"dark-pixel-mask-png": Moon,
|
||||
"unique-color-mask-png": Dices,
|
||||
"extract-color-from-png": Focus,
|
||||
"quantize-png": Contrast,
|
||||
"decrease-color-count-png": Scaling,
|
||||
"custom-palette-png": Palette,
|
||||
"dithering-png": Dices,
|
||||
"feather-edges-png": Droplets,
|
||||
"clean-edges-png": Scissors,
|
||||
"pixelate-png": Grid3x3,
|
||||
"randomize-pixels-png": Dices,
|
||||
"add-noise-png": Hash,
|
||||
"silhouette-png": Contrast,
|
||||
"trim-empty-space-png": Crop,
|
||||
"change-canvas-size-png": Frame,
|
||||
"change-aspect-ratio-png": Scaling,
|
||||
"swap-orientation-png": RotateCw,
|
||||
"symmetric-copy-png": FlipHorizontal2,
|
||||
"compress-png": Minimize2,
|
||||
"reduce-to-size-png": Scaling,
|
||||
"png-file-size": Info,
|
||||
"png-to-bytes": Binary,
|
||||
"bytes-to-png": Binary,
|
||||
"png-to-rgb-values": Hash,
|
||||
"rgb-values-to-png": Hash,
|
||||
"verify-is-png": SearchCheck,
|
||||
"text-to-png": Type,
|
||||
"emoji-to-png": FaceSlightlySmiling,
|
||||
"placeholder-png": Frame,
|
||||
"color-spectrum-png": Rainbow,
|
||||
"random-colors-png": Dices,
|
||||
"draw-grid-png": Grid3x3,
|
||||
"circle-mask-png": Circle,
|
||||
"square-mask-png": Square,
|
||||
"star-mask-png": Star,
|
||||
"wavy-mask-png": WavesHorizontal,
|
||||
"watermark-tile-png": Stamp,
|
||||
"watermark-image-png": FileImage,
|
||||
"color-wheel-png": Rainbow,
|
||||
"complementary-png": Contrast,
|
||||
"triadic-png": Hash,
|
||||
"tetradic-png": Grid3x3,
|
||||
"analogous-png": Blend,
|
||||
"monochromatic-png": Droplets,
|
||||
"shades-png": Sun,
|
||||
"mix-colors-png": PaintBucket,
|
||||
"blend-two-png": Layers,
|
||||
"step-colors-png": Scaling,
|
||||
"sort-colors-png": SearchCheck,
|
||||
"find-contour-png": Scan,
|
||||
"make-thicker-png": ZoomIn,
|
||||
"make-thinner-png": ZoomOut,
|
||||
"harden-alpha-png": Sun,
|
||||
"despeckle-alpha-png": SearchCheck,
|
||||
"close-holes-png": Hammer,
|
||||
"skew-png": Repeat2,
|
||||
"rotate-free-png": RotateCw,
|
||||
"zoom-png": ZoomIn,
|
||||
"shift-png": Crosshair,
|
||||
"vignette-png": Sun,
|
||||
"jpeg-artifacts-png": Layers,
|
||||
"gamma-png": Contrast,
|
||||
"auto-contrast-png": Contrast,
|
||||
"temperature-png": Sun,
|
||||
"tint-png": Droplet,
|
||||
"svg-to-png": FileImage,
|
||||
};
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { CategoryId } from "./categories";
|
||||
import type { ToolSchema } from "./registry-schema";
|
||||
import type { CategoryId } from "./categories";
|
||||
import type { OutputMime } from "./core/io";
|
||||
import type { PixelImage } from "./core/types";
|
||||
|
||||
@@ -63,7 +62,6 @@ export type ToolEntry<P = Record<string, unknown>> = {
|
||||
category: CategoryId;
|
||||
sourceMode?: SourceMode;
|
||||
params: ParamDef[];
|
||||
schema?: ToolSchema<P>;
|
||||
run?: (img: PixelImage, params: P) => Promise<PixelImage> | PixelImage;
|
||||
generate?: (params: P) => Promise<PixelImage> | PixelImage;
|
||||
toText?: (img: PixelImage, params: P) => Promise<string> | string;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { ToolEntry } from "../registry";
|
||||
import { ToolError } from "../core/errors";
|
||||
import { field, toolSchema } from "../registry-schema";
|
||||
import {
|
||||
colorMask,
|
||||
extractAlphaMask,
|
||||
@@ -33,16 +32,6 @@ import {
|
||||
import type { Position9 } from "../core/textdraw";
|
||||
import { num, str } from "../registry-helpers";
|
||||
|
||||
interface AddStrokeParams {
|
||||
color: string;
|
||||
thickness: number;
|
||||
}
|
||||
|
||||
const addStrokeSchema = toolSchema<AddStrokeParams>({
|
||||
color: field.color({ default: "#ff0000" }),
|
||||
thickness: field.slider({ min: 1, max: 10, step: 1, default: 3 }),
|
||||
});
|
||||
|
||||
export function alphaEntries(): ToolEntry[] {
|
||||
return [
|
||||
{
|
||||
@@ -412,7 +401,6 @@ export function alphaEntries(): ToolEntry[] {
|
||||
description:
|
||||
"Adds a colored ring outline around the opaque content with the chosen thickness.",
|
||||
category: "alpha",
|
||||
schema: addStrokeSchema,
|
||||
params: [
|
||||
{
|
||||
id: "color",
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { ToolEntry } from "../registry";
|
||||
import { ToolError } from "../core/errors";
|
||||
import { field, toolSchema } from "../registry-schema";
|
||||
import {
|
||||
crop,
|
||||
expandCanvas,
|
||||
@@ -25,16 +24,6 @@ import {
|
||||
} from "../core/affine";
|
||||
import { num, str } from "../registry-helpers";
|
||||
|
||||
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 function geometryEntries(): ToolEntry[] {
|
||||
return [
|
||||
{
|
||||
@@ -241,7 +230,6 @@ export function geometryEntries(): ToolEntry[] {
|
||||
description:
|
||||
"Draws a colored frame of the chosen thickness around the image.",
|
||||
category: "geometry",
|
||||
schema: addBorderSchema,
|
||||
params: [
|
||||
{
|
||||
id: "thickness",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { PREVIEW_GROUPS, PREVIEW_TOTAL } from "$lib/preview/catalog";
|
||||
import { TOOL_ICONS } from "$lib/tools/tool-icons";
|
||||
import { TOOL_ICONS } from "$lib/preview/tool-icons";
|
||||
import CatalogHeader from "$lib/components/kit/CatalogHeader.svelte";
|
||||
import CatalogToolbar from "$lib/components/kit/CatalogToolbar.svelte";
|
||||
import CatalogGroup from "$lib/components/kit/CatalogGroup.svelte";
|
||||
|
||||
Reference in New Issue
Block a user