feat: combine tool in single card
This commit is contained in:
@@ -198,6 +198,27 @@ input.control[type='number'] {
|
||||
margin-top: var(--space-4);
|
||||
}
|
||||
|
||||
.edge-legend {
|
||||
position: absolute;
|
||||
top: -0.65em;
|
||||
padding: 0 var(--space-2);
|
||||
background: var(--surface);
|
||||
color: var(--text-muted);
|
||||
font-size: var(--text-xs);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
line-height: 1.3;
|
||||
white-space: nowrap;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.params-sep {
|
||||
position: relative;
|
||||
border-top: 1px solid var(--border);
|
||||
margin: 0 var(--space-4);
|
||||
padding-bottom: var(--space-3);
|
||||
}
|
||||
|
||||
.actions-row {
|
||||
display: flex;
|
||||
gap: var(--space-1);
|
||||
|
||||
@@ -11,12 +11,25 @@
|
||||
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="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]} />
|
||||
@@ -57,4 +70,25 @@
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
{/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>
|
||||
|
||||
@@ -258,8 +258,10 @@
|
||||
<div class="error-banner" role="alert">{errorText}</div>
|
||||
{/if}
|
||||
|
||||
<div class="panel tool-stage" class:single={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} />
|
||||
@@ -275,6 +277,9 @@
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
<span class="edge-legend result-legend" aria-hidden="true">
|
||||
{isInfo ? 'Сводка' : 'Результат'}
|
||||
</span>
|
||||
<div class="cell">
|
||||
<ResultCard
|
||||
{tool}
|
||||
@@ -293,8 +298,10 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if (source || isSourceless) && !isInfo}
|
||||
<div class="base-params">
|
||||
{#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
|
||||
@@ -303,8 +310,8 @@
|
||||
hasMask={hasMask}
|
||||
bind:showMask
|
||||
/>
|
||||
</div>
|
||||
{/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,8 +40,9 @@
|
||||
</script>
|
||||
|
||||
<div class="block">
|
||||
<header>
|
||||
<h3 class="heading-section">Шаг {index + 1}: {tool.title}</h3>
|
||||
<div class="panel tool-stage">
|
||||
<span class="edge-legend step-legend">
|
||||
Шаг {index + 1}: {tool.title}
|
||||
<button
|
||||
type="button"
|
||||
class="remove"
|
||||
@@ -51,17 +52,16 @@
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</header>
|
||||
</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>
|
||||
|
||||
{#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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -9,34 +9,48 @@
|
||||
let { id, label, checked = $bindable(false), hint }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class="checkbox">
|
||||
<div class="checkbox-row">
|
||||
<label class="checkbox" for={id}>
|
||||
<input id={id} type="checkbox" bind:checked />
|
||||
<label for={id}>{label}</label>
|
||||
</div>
|
||||
{#if hint}
|
||||
{label}
|
||||
</label>
|
||||
{#if hint}
|
||||
<p class="hint text-caption text-muted">{hint}</p>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<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>
|
||||
|
||||
@@ -13,17 +13,42 @@
|
||||
|
||||
<div class="field">
|
||||
<label class="text-caption text-muted" for={id}>{label}</label>
|
||||
<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>
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user