feat: add step picker\list\editor

This commit is contained in:
2026-08-23 20:12:43 +05:00
parent 23c985bde9
commit 8532eb98c3
5 changed files with 506 additions and 0 deletions
@@ -0,0 +1,41 @@
<script lang="ts">
import ParamForm from '../ParamForm.svelte';
import type { PipelineStep } from '$lib/tools/pipeline';
import { getTool } from '$lib/registry';
interface Props {
step: PipelineStep;
}
let { step }: Props = $props();
const tool = $derived(getTool(step.toolId));
</script>
{#if tool}
<div class="editor panel">
<h3 class="heading-section">{tool.title}</h3>
<p class="description text-caption text-muted">{tool.description}</p>
{#if tool.params.length > 0}
<ParamForm params={tool.params} bind:values={step.values} />
{:else}
<p class="hint text-caption text-muted">У этого шага нет параметров.</p>
{/if}
</div>
{/if}
<style>
.editor {
padding: var(--space-3);
}
h3 {
margin-bottom: var(--space-1);
font-size: var(--text-m);
}
.description,
.hint {
margin: 0 0 var(--space-2);
}
</style>
@@ -0,0 +1,128 @@
<script lang="ts">
import type { PipelineStep } from '$lib/tools/pipeline';
import { getTool } from '$lib/registry';
interface Props {
steps: PipelineStep[];
selectedIndex: number;
onSelect: (index: number) => void;
onMoveUp: (index: number) => void;
onMoveDown: (index: number) => void;
onRemove: (index: number) => void;
}
let { steps, selectedIndex, onSelect, onMoveUp, onMoveDown, onRemove }: Props = $props();
function toolTitle(step: PipelineStep): string {
return getTool(step.toolId)?.title ?? step.toolId;
}
</script>
{#if steps.length === 0}
<p class="empty text-caption text-muted">Цепочка пуста — добавьте первый инструмент.</p>
{:else}
<ol>
{#each steps as step, index (step.id)}
<li>
<button type="button" class="label" class:selected={index === selectedIndex} onclick={() => onSelect(index)}>
<span class="num">{index + 1}</span>
{toolTitle(step)}
</button>
<span class="controls">
<button type="button" aria-label="Выше" disabled={index === 0} onclick={() => onMoveUp(index)}></button>
<button
type="button"
aria-label="Ниже"
disabled={index === steps.length - 1}
onclick={() => onMoveDown(index)}
></button>
<button type="button" class="remove" aria-label="Удалить шаг" onclick={() => onRemove(index)}>✕</button>
</span>
</li>
{/each}
</ol>
{/if}
<style>
ol {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: var(--space-1);
}
li {
display: flex;
align-items: center;
gap: var(--space-1);
}
.label {
flex: 1;
min-width: 0;
text-align: left;
padding: var(--space-1) var(--space-2);
border: 1px solid transparent;
border-radius: var(--radius-s);
background: none;
color: var(--text);
cursor: pointer;
font-size: var(--text-m);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.label:hover,
.label.selected {
background: color-mix(in srgb, var(--accent) 8%, var(--surface));
border-color: var(--border);
}
.num {
display: inline-block;
min-width: 1.4ch;
margin-right: var(--space-1);
color: var(--text-muted);
font-family: var(--font-mono);
font-size: var(--text-xs);
}
.controls {
display: flex;
gap: 2px;
}
.controls button {
width: 1.6rem;
height: 1.6rem;
padding: 0;
border: 1px solid var(--border);
border-radius: var(--radius-s);
background: var(--surface);
color: var(--text-muted);
line-height: 1;
cursor: pointer;
}
.controls button:hover:not(:disabled) {
border-color: var(--accent);
color: var(--accent);
}
.controls .remove:hover:not(:disabled) {
border-color: var(--danger);
color: var(--danger);
}
.controls button:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.empty {
margin: 0;
}
</style>
@@ -0,0 +1,50 @@
<script lang="ts">
import Button from '../ui/Button.svelte';
import { CATEGORIES } from '$lib/categories';
import { TOOLS } from '$lib/registry';
interface Props {
onAdd: (toolId: string) => void;
}
let { onAdd }: Props = $props();
const steppable = TOOLS.filter(
(tool) => (tool.resultType ?? 'image') === 'image' && (!tool.sourceMode || tool.sourceMode === 'file')
);
let selected = $state(steppable[0]?.id ?? '');
</script>
<div class="picker">
<select bind:value={selected} aria-label="Инструмент для нового шага">
{#each CATEGORIES as category (category.id)}
{@const categoryTools = steppable.filter((tool) => tool.category === category.id)}
{#if categoryTools.length > 0}
<optgroup label={category.label}>
{#each categoryTools as tool (tool.id)}
<option value={tool.id}>{tool.title}</option>
{/each}
</optgroup>
{/if}
{/each}
</select>
<Button variant="secondary" onclick={() => onAdd(selected)}>Добавить шаг</Button>
</div>
<style>
.picker {
display: flex;
gap: var(--space-2);
align-items: center;
}
select {
flex: 1;
min-width: 0;
padding: var(--space-1) var(--space-2);
border: 1px solid var(--border);
border-radius: var(--radius-s);
background: var(--surface);
}
</style>