From 6e0647dfbf9eb68d980ae8d76d6affdc9f1965d6 Mon Sep 17 00:00:00 2001 From: Ku6epXBOCTuK Date: Tue, 25 Aug 2026 18:44:03 +0500 Subject: [PATCH] feat: add demo page --- docs/plan-inline-params.md | 3 +- web/src/app.css | 14 + web/src/lib/components/ToolPage.svelte | 144 ++++++-- .../components/chain/ChainToolBlock.svelte | 4 +- web/src/lib/components/stage/ToolStage.svelte | 311 ++++++++++++++++++ .../components/stage/ToolStageInline.svelte | 208 ------------ web/src/lib/components/stage/stage-props.ts | 14 + web/src/routes/demo/+page.svelte | 40 +++ 8 files changed, 504 insertions(+), 234 deletions(-) create mode 100644 web/src/lib/components/stage/ToolStage.svelte delete mode 100644 web/src/lib/components/stage/ToolStageInline.svelte create mode 100644 web/src/routes/demo/+page.svelte diff --git a/docs/plan-inline-params.md b/docs/plan-inline-params.md index e63a84e..1d9e7ad 100644 --- a/docs/plan-inline-params.md +++ b/docs/plan-inline-params.md @@ -10,7 +10,7 @@ - Два взаимозаменяемых компонента этапа с одинаковым набором пропсов; `ToolPage` рендерит один тег. Попробовать новое — поменять компонент в этом теге; вернуть старое — поменять обратно. Никаких флагов и конфигов. - Общая типизация пропсов в одном файле рядом с компонентами; сами карточки (`SourceCard`, `ResultCard`, `TextInputCard`, `ParamsCard`) переиспользуются как есть — вся логика исполнения, маски, пипетки и автозапуска остаётся в `ToolPage` и приходит сверху. -- Chain-звенья не затрагиваются: у них своя раскладка «Вход | Результат», эксперимент только про базовый этап. +- Chain-звенья используют ту же панельную систему «Вход | Параметры | Результат» — общие классы `.pane` / `.pane-legend` / `.pane-params` вынесены в app.css, вертикальные поля в узкой колонке заданы один раз глобально. - Один порог отзывчивости: три колонки от ~75rem, ниже — стек «исходник → параметры → результат». Промежуточные перестроения не придумываем до фидбека дизайнера. ## 3. Компоненты @@ -46,5 +46,4 @@ src/lib/components/stage/ ## 6. Что сознательно не делаем - Редизайн визуального языка (цвета, типографика, формы) — материал для дизайнера, не для этого плана. -- Перестройка chain-звеньев под новую сетку. - Новая логика: исполнение, автосохранение, пресеты — раскладка ничего не знает про исполнение. diff --git a/web/src/app.css b/web/src/app.css index f6bb4e4..6c65077 100644 --- a/web/src/app.css +++ b/web/src/app.css @@ -240,6 +240,20 @@ input.control[type='number'] { padding-bottom: var(--space-3); } +/* Панель этапа для инлайн-раскладки: исходник | параметры | результат */ +.pane { + position: relative; + display: flex; + flex-direction: column; + min-width: 0; +} + +.pane-legend { + top: -0.65em; + left: 50%; + transform: translateX(-50%); +} + .actions-row { display: flex; gap: var(--space-1); diff --git a/web/src/lib/components/ToolPage.svelte b/web/src/lib/components/ToolPage.svelte index 67dc3be..88059f2 100644 --- a/web/src/lib/components/ToolPage.svelte +++ b/web/src/lib/components/ToolPage.svelte @@ -5,22 +5,44 @@ import type { PixelImage } from '$lib/core/types'; import { defaultParams, getTool, outputOf, sanitizeParams, type ToolEntry } from '$lib/registry'; import { loadStoredSteps, newStepId, saveSteps, type PipelineStep } from '$lib/tools/pipeline'; + import { TOOL_ICONS } from '$lib/tools/tool-icons'; import DownloadButton from './DownloadButton.svelte'; import ParamForm from './ParamForm.svelte'; import Preview from './Preview.svelte'; import ToolSearch from './search/ToolSearch.svelte'; - import ChainToolBlock from './chain/ChainToolBlock.svelte'; import { createAutoRunner } from '$lib/tools/auto-run'; import { executeStep } from '$lib/tools/executor'; import { t } from '$lib/i18n/t'; import { toolDescription, toolTitle } from '$lib/i18n/tool-strings'; import type { StageStatus } from './stage/stage-props'; - import ToolStageInline from './stage/ToolStageInline.svelte'; + import ChainToolBlock from './chain/ChainToolBlock.svelte'; + import ToolStage from './stage/ToolStage.svelte'; + import ToolStageClassic from './stage/ToolStageClassic.svelte'; - let { tool, restoreChain = false }: { tool: ToolEntry; restoreChain?: boolean } = $props(); + export type PresetStep = { toolId: string; values?: Record }; + + let { + tool, + restoreChain = false, + stageVariant = 'inline', + presetBaseValues, + presetChain + }: { + tool: ToolEntry; + restoreChain?: boolean; + stageVariant?: 'classic' | 'inline'; + presetBaseValues?: Record; + presetChain?: PresetStep[]; + } = $props(); + + const StageComponent = $derived(stageVariant === 'classic' ? ToolStageClassic : ToolStage); type Status = StageStatus; + const isPreset = $derived( + (presetChain?.length ?? 0) > 0 || Object.keys(presetBaseValues ?? {}).length > 0 + ); + let status = $state('idle'); let source = $state(null); let result = $state(null); @@ -29,9 +51,25 @@ let showMask = $state(false); let info = $state(null); let errorText = $state(''); - let values = $state>({}); // svelte-ignore state_referenced_locally - let chain = $state(restoreChain ? loadStoredSteps() : []); + let values = $state>({ ...defaultParams(tool), ...presetBaseValues }); + // svelte-ignore state_referenced_locally + let chain = $state( + restoreChain + ? loadStoredSteps() + : (presetChain ?? []).flatMap((preset) => { + const stepTool = getTool(preset.toolId); + return stepTool + ? [ + { + id: newStepId(), + toolId: preset.toolId, + values: { ...defaultParams(stepTool), ...preset.values } + } + ] + : []; + }) + ); let chainResults = $state<(PixelImage | null)[]>([]); let lastRunChainJson = ''; @@ -177,7 +215,7 @@ current = await executeStep(stepTool, current, sanitizeParams(stepTool, step.values)); } catch (e) { throw new Error( - t('toolPage.stepError', { n: i + 1, title: toolTitle(stepTool), msg: errorMessage(e) }) + t('toolPage.stepError', { n: i + 2, title: toolTitle(stepTool), msg: errorMessage(e) }) ); } if (!runner.isCurrent(token)) return; @@ -208,6 +246,7 @@ }); $effect(() => { + if (isPreset) return; const filled = chain.filter((step) => step.toolId !== ''); if (filled.length === 0 && !hasLastRun) return; saveSteps(filled); @@ -265,8 +304,9 @@ {/if} -
-

{t('toolPage.stepHeading', { n: index + 1 })}

+

{t('toolPage.stepHeading', { n: index + 2 })}

+ + {/snippet} + + {:else} + removeChainStep(index)} + onError={showError} + onAddStep={addChainStep} + onRemoveChain={removeChain} + /> + {/if} {/if} {/each} @@ -345,6 +423,28 @@ margin-bottom: var(--space-3); } + .step-legend { + left: var(--space-3); + top: -0.75em; + display: inline-flex; + align-items: center; + gap: var(--space-2); + color: var(--text); + font-weight: 600; + z-index: 2; + } + + .step-icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 1.4rem; + height: 1.4rem; + border-radius: var(--radius-s); + background: color-mix(in srgb, var(--accent) 10%, var(--surface)); + color: var(--link); + } + .empty-slot { padding: var(--space-3); } diff --git a/web/src/lib/components/chain/ChainToolBlock.svelte b/web/src/lib/components/chain/ChainToolBlock.svelte index 3735e55..b2bec75 100644 --- a/web/src/lib/components/chain/ChainToolBlock.svelte +++ b/web/src/lib/components/chain/ChainToolBlock.svelte @@ -49,7 +49,7 @@ {#if StepIcon} {/if} - {t('chain.stepLabel', { n: index + 1, title: toolTitle(tool) })} + {t('chain.stepLabel', { n: index + 2, title: toolTitle(tool) })} + + {/if} + {:else} + showError?.(e)} + /> + {/if} + + + + + diff --git a/web/src/lib/components/stage/ToolStageInline.svelte b/web/src/lib/components/stage/ToolStageInline.svelte deleted file mode 100644 index e621088..0000000 --- a/web/src/lib/components/stage/ToolStageInline.svelte +++ /dev/null @@ -1,208 +0,0 @@ - - -
- {#if !isSourceless} -
- -
- {#if isTextSource && !source} - - {:else} - - {/if} -
-
- {/if} - - {#if showParams} -
- -
- -
-
- {/if} - -
- -
- -
-
-
- - diff --git a/web/src/lib/components/stage/stage-props.ts b/web/src/lib/components/stage/stage-props.ts index f76cf55..821890e 100644 --- a/web/src/lib/components/stage/stage-props.ts +++ b/web/src/lib/components/stage/stage-props.ts @@ -32,3 +32,17 @@ export interface StageProps { showError: (e: unknown) => void; errorMessage: (e: unknown) => string; } + +export interface ChainStageProps { + tool: ToolEntry; + index: number; + input: PixelImage | null; + result: PixelImage | null; + busy: boolean; + isLast: boolean; + values: Record; + onRemove: () => void; + onError: (e: unknown) => void; + onAddStep: () => void; + onRemoveChain: () => void; +} diff --git a/web/src/routes/demo/+page.svelte b/web/src/routes/demo/+page.svelte new file mode 100644 index 0000000..a8f2ab6 --- /dev/null +++ b/web/src/routes/demo/+page.svelte @@ -0,0 +1,40 @@ + + + + Mockup: цепочка инструментов — easy-png-tools + + + +
+ +
+ +