feat: combine tool in single card

This commit is contained in:
2026-08-24 08:31:13 +05:00
parent 95ee598875
commit 702b96261d
10 changed files with 267 additions and 145 deletions
+77 -43
View File
@@ -11,50 +11,84 @@
values: Record<string, any>;
pipetteTargetId?: string | null;
onPipetteToggle?: (id: string) => void;
hasMask?: boolean;
showMask?: boolean;
}
let { params, values = $bindable(), pipetteTargetId = null, onPipetteToggle }: Props = $props();
let {
params,
values = $bindable(),
pipetteTargetId = null,
onPipetteToggle,
hasMask = false,
showMask = $bindable(false)
}: Props = $props();
</script>
{#each params as param (param.id)}
<div class="field">
{#if param.type === 'checkbox'}
<CheckboxField id={param.id} label={param.label} bind:checked={values[param.id]} />
{:else if param.type === 'number'}
<TextField
id={param.id}
label={param.label}
type="number"
min={param.min}
max={param.max}
step={param.step}
bind:value={values[param.id]}
/>
{:else if param.type === 'slider'}
<SliderField
id={param.id}
label={param.label}
min={param.min}
max={param.max}
step={param.step}
default={param.default}
bind:value={values[param.id]}
/>
{:else if param.type === 'select'}
<SelectField
id={param.id}
label={param.label}
options={param.options}
bind:value={values[param.id]}
/>
{:else if param.type === 'color'}
<ColorField
id={param.id}
label={param.label}
bind:value={values[param.id]}
pipetteActive={pipetteTargetId === param.id}
onPipetteToggle={() => onPipetteToggle?.(param.id)}
/>
{/if}
</div>
{/each}
<div class="params-grid">
{#if hasMask}
<CheckboxField id="show-mask" label="Показать маску" bind:checked={showMask} />
{/if}
{#each params as param (param.id)}
<div class="field">
{#if param.type === 'checkbox'}
<CheckboxField id={param.id} label={param.label} bind:checked={values[param.id]} />
{:else if param.type === 'number'}
<TextField
id={param.id}
label={param.label}
type="number"
min={param.min}
max={param.max}
step={param.step}
bind:value={values[param.id]}
/>
{:else if param.type === 'slider'}
<SliderField
id={param.id}
label={param.label}
min={param.min}
max={param.max}
step={param.step}
default={param.default}
bind:value={values[param.id]}
/>
{:else if param.type === 'select'}
<SelectField
id={param.id}
label={param.label}
options={param.options}
bind:value={values[param.id]}
/>
{:else if param.type === 'color'}
<ColorField
id={param.id}
label={param.label}
bind:value={values[param.id]}
pipetteActive={pipetteTargetId === param.id}
onPipetteToggle={() => onPipetteToggle?.(param.id)}
/>
{/if}
</div>
{/each}
</div>
<style>
.params-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
column-gap: var(--space-5);
}
@media (min-width: 75rem) {
.params-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
@media (max-width: 50rem) {
.params-grid {
grid-template-columns: 1fr;
}
}
</style>
+53 -29
View File
@@ -258,24 +258,29 @@
<div class="error-banner" role="alert">{errorText}</div>
{/if}
<div class="panel tool-stage" class:single={isSourceless}>
{#if !isSourceless}
<div class="panel tool-block">
<div class="tool-stage" class:single={isSourceless}>
{#if !isSourceless}
<span class="edge-legend source-legend" aria-hidden="true">Исходник</span>
<div class="cell">
{#if isTextSource && !source}
<TextInputCard onSubmit={handleTextSubmit} />
{:else}
<SourceCard
{source}
onFile={handleFile}
onError={(message) => (errorText = message)}
onReset={reset}
pipetteActive={!!pipetteTargetId}
onPickColor={handlePickColor}
/>
{/if}
</div>
{/if}
<span class="edge-legend result-legend" aria-hidden="true">
{isInfo ? 'Сводка' : 'Результат'}
</span>
<div class="cell">
{#if isTextSource && !source}
<TextInputCard onSubmit={handleTextSubmit} />
{:else}
<SourceCard
{source}
onFile={handleFile}
onError={(message) => (errorText = message)}
onReset={reset}
pipetteActive={!!pipetteTargetId}
onPickColor={handlePickColor}
/>
{/if}
</div>
{/if}
<div class="cell">
<ResultCard
{tool}
sourceLoaded={isSourceless ? true : !!source}
@@ -293,18 +298,20 @@
</div>
</div>
{#if (source || isSourceless) && !isInfo}
<div class="base-params">
<ParamsCard
params={tool.params}
bind:values
{pipetteTargetId}
onPipetteToggle={handlePipetteToggle}
hasMask={hasMask}
bind:showMask
/>
{#if (source || isSourceless) && !isInfo && (tool.params.length > 0 || hasMask)}
<div class="params-sep">
<span class="edge-legend" aria-hidden="true">Параметры</span>
</div>
<ParamsCard
params={tool.params}
bind:values
{pipetteTargetId}
onPipetteToggle={handlePipetteToggle}
hasMask={hasMask}
bind:showMask
/>
{/if}
</div>
<div class="chain-stack">
{#each chain as step, index (step.id)}
@@ -357,8 +364,25 @@
margin-bottom: var(--space-3);
}
.base-params {
margin-top: var(--space-4);
.tool-stage {
position: relative;
}
.source-legend {
left: 25%;
transform: translateX(-50%);
}
.result-legend {
left: 75%;
transform: translateX(-50%);
}
@media (max-width: 48rem) {
.source-legend,
.result-legend {
display: none;
}
}
.empty-slot {
@@ -40,28 +40,28 @@
</script>
<div class="block">
<header>
<h3 class="heading-section">Шаг {index + 1}: {tool.title}</h3>
<button
type="button"
class="remove"
aria-label="Убрать шаг"
title="Убрать шаг"
onclick={onRemove}
>
</button>
</header>
<div class="panel tool-stage">
<span class="edge-legend step-legend">
Шаг {index + 1}: {tool.title}
<button
type="button"
class="remove"
aria-label="Убрать шаг"
title="Убрать шаг"
onclick={onRemove}
>
</button>
</span>
<div class="tool-stage panel">
<div class="cell">
<h4 class="heading-section">Вход</h4>
<span class="edge-legend cell-legend" aria-hidden="true">Вход</span>
<div class="cell-media">
<Preview image={input} />
</div>
</div>
<div class="cell">
<h4 class="heading-section">Результат</h4>
<span class="edge-legend cell-legend" aria-hidden="true">Результат</span>
{#if busy && !result}
<div class="cell-media">
<EmptyState title="Обработка…" hint="Выполняется шаг цепочки" />
@@ -89,30 +89,38 @@
</div>
</div>
<ParamsCard params={tool.params} bind:values />
{#if tool.params.length > 0}
<div class="params-sep">
<span class="edge-legend" aria-hidden="true">Параметры</span>
</div>
<ParamsCard params={tool.params} bind:values />
{/if}
</div>
<style>
.block {
display: flex;
flex-direction: column;
gap: var(--space-2);
}
header {
display: flex;
.tool-stage {
position: relative;
padding-top: var(--space-3);
border-top: 0;
}
.step-legend {
left: var(--space-3);
display: inline-flex;
align-items: center;
justify-content: space-between;
gap: var(--space-2);
}
h3 {
margin: 0;
color: var(--text);
font-weight: 600;
}
.remove {
width: 1.6rem;
height: 1.6rem;
width: 1.4rem;
height: 1.4rem;
padding: 0;
border: 1px solid var(--border);
border-radius: var(--radius-s);
@@ -130,8 +138,14 @@
color: var(--danger);
}
h4 {
margin-bottom: var(--space-1);
.cell {
position: relative;
}
.cell-legend {
top: -0.9em;
left: 50%;
transform: translateX(-50%);
}
.recalc {
+12 -14
View File
@@ -1,5 +1,4 @@
<script lang="ts">
import CheckboxField from '../ui/CheckboxField.svelte';
import ParamForm from '../ParamForm.svelte';
import type { ParamDef } from '$lib/registry';
@@ -22,14 +21,17 @@
}: Props = $props();
</script>
<div class="panel root">
<h2 class="heading-section">Параметры</h2>
{#if hasMask}
<CheckboxField id="show-mask" label="Показать маску" bind:checked={showMask} />
{/if}
{#if params.length > 0}
<ParamForm {params} bind:values {pipetteTargetId} {onPipetteToggle} />
{:else if !hasMask}
<div class="root">
{#if params.length > 0 || hasMask}
<ParamForm
{params}
bind:values
{pipetteTargetId}
{onPipetteToggle}
{hasMask}
bind:showMask
/>
{:else}
<p class="hint text-caption text-muted">
У этого инструмента нет параметров — результат уже готов.
</p>
@@ -38,11 +40,7 @@
<style>
.root {
padding: var(--space-4);
}
h2 {
margin-bottom: var(--space-2);
padding: 0 var(--space-4) var(--space-3);
}
.hint {
@@ -43,7 +43,6 @@
</script>
<div class="container">
<h2 class="heading-section">{isInfo ? 'Сводка' : 'Результат'}</h2>
{#if !sourceLoaded}
<div class="media">
<EmptyState
@@ -103,10 +102,6 @@
height: 100%;
}
h2 {
margin-bottom: var(--space-1);
}
.media {
flex: 1;
display: flex;
@@ -18,7 +18,6 @@
</script>
<div class="container">
<h2 class="heading-section">Исходник</h2>
{#if !source}
<DropZone {onFile} {onError} />
{:else}
@@ -40,10 +39,6 @@
height: 100%;
}
h2 {
margin-bottom: var(--space-1);
}
.media {
flex: 1;
display: flex;
+25 -11
View File
@@ -9,34 +9,48 @@
let { id, label, checked = $bindable(false), hint }: Props = $props();
</script>
<div class="checkbox">
<input id={id} type="checkbox" bind:checked />
<label for={id}>{label}</label>
<div class="checkbox-row">
<label class="checkbox" for={id}>
<input id={id} type="checkbox" bind:checked />
{label}
</label>
{#if hint}
<p class="hint text-caption text-muted">{hint}</p>
{/if}
</div>
{#if hint}
<p class="hint text-caption text-muted">{hint}</p>
{/if}
<style>
.checkbox-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-3);
padding: calc(var(--space-1) + 2px) 0;
border-bottom: 1px dashed var(--border);
}
.checkbox {
display: flex;
align-items: center;
gap: var(--space-2);
margin-bottom: var(--space-3);
cursor: pointer;
font-size: var(--text-caption, var(--text-s));
color: var(--text-muted);
}
input[type='checkbox'] {
width: 1rem;
height: 1rem;
cursor: pointer;
accent-color: var(--accent);
}
label {
font-size: var(--text-m);
cursor: pointer;
label:hover {
color: var(--text);
}
.hint {
margin: calc(-1 * var(--space-3)) 0 var(--space-3);
margin: 0;
text-align: right;
}
</style>
+30 -5
View File
@@ -13,17 +13,42 @@
<div class="field">
<label class="text-caption text-muted" for={id}>{label}</label>
{@render children()}
{#if hint}
<p class="hint text-caption text-muted">{hint}</p>
{/if}
<div class="control-slot">
{@render children()}
{#if hint}
<p class="hint text-caption text-muted">{hint}</p>
{/if}
</div>
</div>
<style>
.field {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-3);
padding: calc(var(--space-1) + 2px) 0;
border-bottom: 1px dashed var(--border);
}
label {
flex: none;
max-width: 45%;
text-align: left;
}
.control-slot {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
align-items: flex-end;
gap: var(--space-1);
margin-bottom: var(--space-3);
}
.hint {
margin: 0;
align-self: flex-end;
text-align: right;
}
</style>
+7 -5
View File
@@ -63,19 +63,21 @@
display: flex;
align-items: center;
gap: var(--space-1);
max-width: var(--control-max-width);
width: 100%;
max-width: 22rem;
}
input[type='range'] {
flex: 1;
min-width: 0;
min-width: 3rem;
accent-color: var(--accent);
margin: 0;
}
.step {
flex: none;
width: 1.7rem;
height: 1.7rem;
width: 1.5rem;
height: 1.5rem;
display: inline-flex;
align-items: center;
justify-content: center;
@@ -84,7 +86,7 @@
border-radius: var(--radius-s);
background: var(--surface);
color: var(--text);
font-size: 0.9rem;
font-size: 0.85rem;
line-height: 1;
cursor: pointer;
transition: