diff --git a/AGENTS.md b/AGENTS.md index a8caaa7..43f9cd4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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) — известный техдолг, его НЕ чинить и не игнорировать правилами. diff --git a/docs/plan-composite-params.md b/docs/plan-composite-params.md index cf2f8a5..f0098d4 100644 --- a/docs/plan-composite-params.md +++ b/docs/plan-composite-params.md @@ -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

` с обязательным `schema`, типизированный `run`, **без** `ParamDef[]` и **без** связи со старым. Импортирует core-функции (`expandCanvas`, @@ -442,12 +443,50 @@ Typed field builders + `Field` + `toolSchema

()` + `ToolSchema

` — (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` + `toolSchema

()` + `ToolSchema

` — на `ParamDef[]` как раньше. - **Новый UI** (kit/`SchemaToolView` + `SchemaFields` + `SchemaPreview`, читает `ToolSchema

`) — сейчас рендерит поля по схеме (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` + `toolSchema

()` + `ToolSchema

` — | Миграция инструментов на 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` + `toolSchema

()` + `ToolSchema

` — отдельно. 5. **Фаза 4** — масштаб UI на остальные: составные виджеты, пер-инструмент layout (для каждого инструмента — UI-макет). -6. **Фаза 5** — поглощение `tool-views.ts`, затем удаление старого UI/`ParamDef[]` +6. **Фаза 5** — изоляция: старый UI на `(old)/`-маршруты (не удаляется, + остаётся для ревью); preview на `registry-new` полностью; + линтер-изоляция веток (плагин `isolation`), затем перенос старых + компонентов/библиотек в папки `old/`. ## Зависимости diff --git a/web/eslint-plugins/isolation/index.js b/web/eslint-plugins/isolation/index.js new file mode 100644 index 0000000..f527437 --- /dev/null +++ b/web/eslint-plugins/isolation/index.js @@ -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, + }, +}; diff --git a/web/eslint-plugins/isolation/no-mixed-imports.js b/web/eslint-plugins/isolation/no-mixed-imports.js new file mode 100644 index 0000000..c27a01c --- /dev/null +++ b/web/eslint-plugins/isolation/no-mixed-imports.js @@ -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, + }; + }, +}; diff --git a/web/eslint.config.js b/web/eslint.config.js index 7b5f600..4e64c75 100644 --- a/web/eslint.config.js +++ b/web/eslint.config.js @@ -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, ); diff --git a/web/src/lib/preview/tool-icons.ts b/web/src/lib/preview/tool-icons.ts new file mode 100644 index 0000000..30b7523 --- /dev/null +++ b/web/src/lib/preview/tool-icons.ts @@ -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 = { + "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, +}; diff --git a/web/src/lib/registry.ts b/web/src/lib/registry.ts index bee8d8a..b56fb78 100644 --- a/web/src/lib/registry.ts +++ b/web/src/lib/registry.ts @@ -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

> = { category: CategoryId; sourceMode?: SourceMode; params: ParamDef[]; - schema?: ToolSchema

; run?: (img: PixelImage, params: P) => Promise | PixelImage; generate?: (params: P) => Promise | PixelImage; toText?: (img: PixelImage, params: P) => Promise | string; diff --git a/web/src/lib/registry/alpha.ts b/web/src/lib/registry/alpha.ts index 6468dd3..03af73f 100644 --- a/web/src/lib/registry/alpha.ts +++ b/web/src/lib/registry/alpha.ts @@ -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({ - 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", diff --git a/web/src/lib/registry/geometry.ts b/web/src/lib/registry/geometry.ts index 98e2fa2..08ae9b7 100644 --- a/web/src/lib/registry/geometry.ts +++ b/web/src/lib/registry/geometry.ts @@ -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({ - 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", diff --git a/web/src/routes/preview/list-tools/+page.svelte b/web/src/routes/preview/list-tools/+page.svelte index b64d10a..2415530 100644 --- a/web/src/routes/preview/list-tools/+page.svelte +++ b/web/src/routes/preview/list-tools/+page.svelte @@ -1,6 +1,6 @@