mirror of
https://github.com/Ku6epXBOCTuK/easy-png-tools.git
synced 2026-11-13 17:08:20 +00:00
fix: update dnd tools in pipeline
This commit is contained in:
@@ -1,374 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import ChipButton from "$lib/components/ui/ChipButton.svelte";
|
|
||||||
import Toggle from "$lib/components/ui/Toggle.svelte";
|
|
||||||
import { toDataUrl, type OutputMime } from "$lib/core/io";
|
|
||||||
import type { PixelImage } from "$lib/core/types";
|
|
||||||
import { t } from "$lib/i18n/t";
|
|
||||||
import type { ChainStep } from "$lib/pipeline.svelte";
|
|
||||||
import type {
|
|
||||||
FileResult,
|
|
||||||
InputMode,
|
|
||||||
ResultKind,
|
|
||||||
ResultNote,
|
|
||||||
ToolImageFile,
|
|
||||||
} from "$lib/registry";
|
|
||||||
import type { Snippet } from "svelte";
|
|
||||||
import AddStepButton from "./AddStepButton.svelte";
|
|
||||||
import PartsGrid from "./PartsGrid.svelte";
|
|
||||||
import PreviewTile from "./PreviewTile.svelte";
|
|
||||||
import SchemaActions from "./SchemaActions.svelte";
|
|
||||||
import SchemaResultTile from "./SchemaResultTile.svelte";
|
|
||||||
import SchemaSourceTile from "./SchemaSourceTile.svelte";
|
|
||||||
import StepDropIndicator from "./StepDropIndicator.svelte";
|
|
||||||
import type { StepDnd } from "./step-dnd.svelte";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
steps: ChainStep[];
|
|
||||||
stepCard: Snippet<[ChainStep, number]>;
|
|
||||||
canExtend: boolean;
|
|
||||||
stepDnd?: StepDnd;
|
|
||||||
aligned: boolean;
|
|
||||||
inputMode: InputMode;
|
|
||||||
source: PixelImage | null;
|
|
||||||
sourceFiles: ToolImageFile[];
|
|
||||||
textSource: string;
|
|
||||||
running: boolean;
|
|
||||||
resultKind: ResultKind;
|
|
||||||
result: PixelImage | null;
|
|
||||||
resultNote: ResultNote | null;
|
|
||||||
fileResult: FileResult | null;
|
|
||||||
textResult: string | null;
|
|
||||||
textVars: Record<string, string | number> | undefined;
|
|
||||||
toolId: string;
|
|
||||||
format: OutputMime;
|
|
||||||
quality: number | undefined;
|
|
||||||
limitKb: number | undefined;
|
|
||||||
alphaLoss: boolean;
|
|
||||||
errorText: string;
|
|
||||||
warnings: string[];
|
|
||||||
stepResults: (PixelImage | null)[];
|
|
||||||
stepFileSets: ToolImageFile[][];
|
|
||||||
stepMasks: (PixelImage | null)[];
|
|
||||||
stepMaskOn: boolean[];
|
|
||||||
stepMaskable: boolean[];
|
|
||||||
onaddstep: (index: number, toolId: string) => void;
|
|
||||||
ontogglestepmask: (index: number) => void;
|
|
||||||
ontextinput: (text: string) => void;
|
|
||||||
onrendertext: () => void;
|
|
||||||
onupload: (file: File) => void;
|
|
||||||
onuploadmany: (files: File[]) => void;
|
|
||||||
ondownload: () => void;
|
|
||||||
oncopy: () => void;
|
|
||||||
ondownloadtxt: () => void;
|
|
||||||
onformat: (mime: OutputMime) => void;
|
|
||||||
onquality: (value: number) => void;
|
|
||||||
onlimit: (kb: number | undefined) => void;
|
|
||||||
}
|
|
||||||
let {
|
|
||||||
steps,
|
|
||||||
stepCard,
|
|
||||||
canExtend,
|
|
||||||
stepDnd,
|
|
||||||
aligned = $bindable(false),
|
|
||||||
inputMode,
|
|
||||||
source,
|
|
||||||
sourceFiles,
|
|
||||||
textSource,
|
|
||||||
running,
|
|
||||||
resultKind,
|
|
||||||
result,
|
|
||||||
resultNote,
|
|
||||||
fileResult,
|
|
||||||
textResult,
|
|
||||||
textVars,
|
|
||||||
toolId,
|
|
||||||
format,
|
|
||||||
quality,
|
|
||||||
limitKb,
|
|
||||||
alphaLoss,
|
|
||||||
errorText,
|
|
||||||
warnings,
|
|
||||||
stepResults,
|
|
||||||
stepFileSets,
|
|
||||||
stepMasks,
|
|
||||||
stepMaskOn,
|
|
||||||
stepMaskable,
|
|
||||||
onaddstep,
|
|
||||||
ontogglestepmask,
|
|
||||||
ontextinput,
|
|
||||||
onrendertext,
|
|
||||||
onupload,
|
|
||||||
onuploadmany,
|
|
||||||
ondownload,
|
|
||||||
oncopy,
|
|
||||||
ondownloadtxt,
|
|
||||||
onformat,
|
|
||||||
onquality,
|
|
||||||
onlimit,
|
|
||||||
}: Props = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#snippet maskChip(i: number)}
|
|
||||||
{#if stepMaskable[i]}
|
|
||||||
<ChipButton
|
|
||||||
label={t("resultCard.maskToggle")}
|
|
||||||
active={stepMaskOn[i]}
|
|
||||||
onclick={() => ontogglestepmask(i)}
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
{/snippet}
|
|
||||||
|
|
||||||
<div class="aligned">
|
|
||||||
<div class="aligned-row">
|
|
||||||
<div></div>
|
|
||||||
<div class="panel-segment head-segment">
|
|
||||||
<span class="label">{t("resultCard.previewPanel")}</span>
|
|
||||||
<label class="align-toggle">
|
|
||||||
<Toggle bind:checked={aligned} label={t("chain.alignToggle")} />
|
|
||||||
<span>{t("chain.alignToggle")}</span>
|
|
||||||
</label>
|
|
||||||
<SchemaActions
|
|
||||||
{inputMode}
|
|
||||||
{resultKind}
|
|
||||||
canDownload={result !== null}
|
|
||||||
{running}
|
|
||||||
{format}
|
|
||||||
{quality}
|
|
||||||
{limitKb}
|
|
||||||
{alphaLoss}
|
|
||||||
{onupload}
|
|
||||||
{ondownload}
|
|
||||||
{onformat}
|
|
||||||
{onquality}
|
|
||||||
{onlimit}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{#if errorText}
|
|
||||||
<p class="error" role="alert">{errorText}</p>
|
|
||||||
{/if}
|
|
||||||
{#each warnings as w (w)}
|
|
||||||
<p class="warn" role="status">{w}</p>
|
|
||||||
{/each}
|
|
||||||
{#if canExtend && !stepDnd?.active}
|
|
||||||
<div class="aligned-row">
|
|
||||||
<AddStepButton onadd={(id) => onaddstep(0, id)} />
|
|
||||||
<div class="panel-segment connect-segment"></div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{#each steps as step, i (step.key)}
|
|
||||||
{#if stepDnd?.isIndicatorAt(i)}
|
|
||||||
<div class="aligned-row">
|
|
||||||
<StepDropIndicator height={stepDnd.dragHeight} />
|
|
||||||
<div class="panel-segment connect-segment"></div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{@const input = i === 0 ? null : stepResults[i - 1]}
|
|
||||||
{@const out = stepResults[i]}
|
|
||||||
{#if i > 0}
|
|
||||||
<div class="group-divider">
|
|
||||||
<div class="gd-cell"></div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
<div class="aligned-row">
|
|
||||||
{@render stepCard(step, i)}
|
|
||||||
<div
|
|
||||||
class="panel-segment pair-segment"
|
|
||||||
class:last-segment={i === steps.length - 1 && !canExtend}
|
|
||||||
>
|
|
||||||
{#if i === 0}
|
|
||||||
<SchemaSourceTile
|
|
||||||
mode={inputMode}
|
|
||||||
{source}
|
|
||||||
sources={sourceFiles}
|
|
||||||
{textSource}
|
|
||||||
{running}
|
|
||||||
fileCount={sourceFiles.length}
|
|
||||||
{ontextinput}
|
|
||||||
{onrendertext}
|
|
||||||
{onupload}
|
|
||||||
{onuploadmany}
|
|
||||||
/>
|
|
||||||
{:else}
|
|
||||||
{@const inSet = stepFileSets[i - 1] ?? []}
|
|
||||||
<PreviewTile
|
|
||||||
label={t("chain.inputLegend")}
|
|
||||||
viewMode="image"
|
|
||||||
dims={input ? `${input.width} × ${input.height}` : undefined}
|
|
||||||
parts={inSet.length > 1 ? inSet.length : undefined}
|
|
||||||
>
|
|
||||||
{#if inSet.length > 1}
|
|
||||||
<PartsGrid files={inSet} />
|
|
||||||
{:else if input}
|
|
||||||
<img src={toDataUrl(input)} alt="" />
|
|
||||||
{:else}
|
|
||||||
<span class="empty">{t("resultCard.noResult")}</span>
|
|
||||||
{/if}
|
|
||||||
</PreviewTile>
|
|
||||||
{/if}
|
|
||||||
{#if i === steps.length - 1}
|
|
||||||
<SchemaResultTile
|
|
||||||
{resultKind}
|
|
||||||
result={out}
|
|
||||||
{resultNote}
|
|
||||||
{fileResult}
|
|
||||||
{textResult}
|
|
||||||
{textVars}
|
|
||||||
{toolId}
|
|
||||||
{running}
|
|
||||||
mask={stepMasks[i] ?? null}
|
|
||||||
maskOn={stepMaskOn[i] ?? false}
|
|
||||||
ontogglemask={stepMaskable[i]
|
|
||||||
? () => ontogglestepmask(i)
|
|
||||||
: undefined}
|
|
||||||
{oncopy}
|
|
||||||
{ondownloadtxt}
|
|
||||||
/>
|
|
||||||
{:else}
|
|
||||||
{@const outSet = stepFileSets[i] ?? []}
|
|
||||||
<PreviewTile
|
|
||||||
label={t("chain.stepResult", { n: i + 1 })}
|
|
||||||
viewMode="image"
|
|
||||||
dims={out ? `${out.width} × ${out.height}` : undefined}
|
|
||||||
parts={outSet.length > 1 ? outSet.length : undefined}
|
|
||||||
>
|
|
||||||
{#snippet actions()}
|
|
||||||
{@render maskChip(i)}
|
|
||||||
{/snippet}
|
|
||||||
{#if outSet.length > 1 && !stepMaskOn[i]}
|
|
||||||
<PartsGrid files={outSet} />
|
|
||||||
{:else if out}
|
|
||||||
<img
|
|
||||||
src={toDataUrl(
|
|
||||||
stepMaskOn[i] && stepMasks[i] ? stepMasks[i]! : out,
|
|
||||||
)}
|
|
||||||
alt=""
|
|
||||||
/>
|
|
||||||
{:else}
|
|
||||||
<span class="empty">{t("resultCard.noResult")}</span>
|
|
||||||
{/if}
|
|
||||||
</PreviewTile>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{#if canExtend && !stepDnd?.active}
|
|
||||||
<div class="aligned-row">
|
|
||||||
<AddStepButton onadd={(id) => onaddstep(i + 1, id)} />
|
|
||||||
<div
|
|
||||||
class="panel-segment connect-segment"
|
|
||||||
class:add-last={i === steps.length - 1}
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{/each}
|
|
||||||
{#if stepDnd?.isIndicatorAt(steps.length)}
|
|
||||||
<div class="aligned-row">
|
|
||||||
<StepDropIndicator height={stepDnd.dragHeight} />
|
|
||||||
<div
|
|
||||||
class="panel-segment connect-segment"
|
|
||||||
class:add-last={!canExtend || stepDnd.active}
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.aligned {
|
|
||||||
display: grid;
|
|
||||||
}
|
|
||||||
.aligned-row {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: minmax(var(--size-workspace-min), 1fr) 2fr;
|
|
||||||
gap: var(--space-xxl);
|
|
||||||
align-items: start;
|
|
||||||
}
|
|
||||||
/* Сегменты справа образуют «одну панель»: общий фон, боковые границы,
|
|
||||||
верх у шапки и низ у последней строки. */
|
|
||||||
.panel-segment {
|
|
||||||
background: var(--color-panel);
|
|
||||||
border-left: var(--size-border) solid var(--color-border);
|
|
||||||
border-right: var(--size-border) solid var(--color-border);
|
|
||||||
padding: var(--space-xl);
|
|
||||||
}
|
|
||||||
.head-segment {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--space-xl);
|
|
||||||
flex-wrap: wrap;
|
|
||||||
border-top: var(--size-border) solid var(--color-border);
|
|
||||||
border-bottom: var(--size-border) solid var(--color-border);
|
|
||||||
border-radius: var(--radius-m) var(--radius-m) 0 0;
|
|
||||||
}
|
|
||||||
.head-segment .label {
|
|
||||||
font: var(--font-size-s) var(--font-mono);
|
|
||||||
letter-spacing: var(--space-text-l);
|
|
||||||
text-transform: uppercase;
|
|
||||||
color: var(--color-main);
|
|
||||||
}
|
|
||||||
.pair-segment {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
gap: var(--space-l);
|
|
||||||
}
|
|
||||||
/* Коннектор в строках «Add step»: продолжает боковые границы панели. */
|
|
||||||
.connect-segment {
|
|
||||||
align-self: stretch;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
.connect-segment.add-last {
|
|
||||||
border-bottom: var(--size-border) solid var(--color-border);
|
|
||||||
border-radius: 0 0 var(--radius-m) var(--radius-m);
|
|
||||||
}
|
|
||||||
/* Полоса-разделитель между группами: ячейки с фоном и боковыми границами
|
|
||||||
(как у карточки слева и панели справа), сверху — линия через всю ширину. */
|
|
||||||
.group-divider {
|
|
||||||
position: relative;
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: minmax(var(--size-workspace-min), 1fr) 2fr;
|
|
||||||
gap: var(--space-xxl);
|
|
||||||
}
|
|
||||||
.gd-cell {
|
|
||||||
grid-column: 2;
|
|
||||||
background: var(--color-panel);
|
|
||||||
border-left: var(--size-border) solid var(--color-border);
|
|
||||||
border-right: var(--size-border) solid var(--color-border);
|
|
||||||
padding: var(--space-s) 0;
|
|
||||||
}
|
|
||||||
.group-divider::after {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
inset: 0 0 auto;
|
|
||||||
border-top: var(--size-border) solid var(--color-border);
|
|
||||||
}
|
|
||||||
.last-segment {
|
|
||||||
border-bottom: var(--size-border) solid var(--color-border);
|
|
||||||
border-radius: 0 0 var(--radius-m) var(--radius-m);
|
|
||||||
}
|
|
||||||
.align-toggle {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--space-m);
|
|
||||||
margin-right: auto;
|
|
||||||
color: var(--color-text-muted);
|
|
||||||
font: var(--font-size-s) var(--font-mono);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.error {
|
|
||||||
margin: 0;
|
|
||||||
color: var(--color-danger);
|
|
||||||
font: var(--font-size-s) var(--font-mono);
|
|
||||||
}
|
|
||||||
.warn {
|
|
||||||
margin: 0;
|
|
||||||
color: var(--color-warning);
|
|
||||||
font: var(--font-size-s) var(--font-mono);
|
|
||||||
}
|
|
||||||
.empty {
|
|
||||||
font: var(--font-size-s) var(--font-mono);
|
|
||||||
}
|
|
||||||
@media (--bp-tablet) {
|
|
||||||
.aligned-row {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,178 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import {
|
|
||||||
hasPreviewResult,
|
|
||||||
type PreviewHeadModel,
|
|
||||||
type PreviewResultModel,
|
|
||||||
type PreviewSourceModel,
|
|
||||||
type PreviewStepsModel,
|
|
||||||
} from "./schema-preview-model";
|
|
||||||
import SchemaActions from "./SchemaActions.svelte";
|
|
||||||
import PartsGrid from "./PartsGrid.svelte";
|
|
||||||
import SchemaResultTile from "./SchemaResultTile.svelte";
|
|
||||||
import SchemaSourceTile from "./SchemaSourceTile.svelte";
|
|
||||||
import PreviewTile from "./PreviewTile.svelte";
|
|
||||||
import Toggle from "$lib/components/ui/Toggle.svelte";
|
|
||||||
import ChipButton from "$lib/components/ui/ChipButton.svelte";
|
|
||||||
import { toDataUrl } from "$lib/core/io";
|
|
||||||
import type { PixelImage } from "$lib/core/types";
|
|
||||||
import { t } from "$lib/i18n/t";
|
|
||||||
import type { InputMode, ResultKind } from "$lib/registry";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
inputMode: InputMode;
|
|
||||||
resultKind?: ResultKind;
|
|
||||||
toolId: string;
|
|
||||||
running?: boolean;
|
|
||||||
error?: string;
|
|
||||||
aligned?: boolean;
|
|
||||||
head: PreviewHeadModel;
|
|
||||||
source: PreviewSourceModel;
|
|
||||||
steps?: PreviewStepsModel;
|
|
||||||
result: PreviewResultModel;
|
|
||||||
}
|
|
||||||
let {
|
|
||||||
inputMode,
|
|
||||||
resultKind = "image",
|
|
||||||
toolId,
|
|
||||||
running = false,
|
|
||||||
error = "",
|
|
||||||
aligned = $bindable(false),
|
|
||||||
head,
|
|
||||||
source,
|
|
||||||
steps = {},
|
|
||||||
result,
|
|
||||||
}: Props = $props();
|
|
||||||
|
|
||||||
const stepResults = $derived(steps.results ?? []);
|
|
||||||
const stepFileSets = $derived(steps.fileSets ?? []);
|
|
||||||
const stepMaskable = $derived(steps.maskable ?? []);
|
|
||||||
const stepMaskOn = $derived(steps.maskOn ?? []);
|
|
||||||
const stepMasks = $derived(steps.masks ?? []);
|
|
||||||
|
|
||||||
const hasResult = $derived(
|
|
||||||
hasPreviewResult({
|
|
||||||
inputMode,
|
|
||||||
resultKind,
|
|
||||||
result: result.result,
|
|
||||||
fileResult: result.fileResult ?? null,
|
|
||||||
textResult: result.textResult ?? null,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="panel-head">
|
|
||||||
<span class="label">{t("resultCard.previewPanel")}</span>
|
|
||||||
{#if stepResults.length > 1}
|
|
||||||
<label class="align-toggle">
|
|
||||||
<Toggle bind:checked={aligned} label={t("chain.alignToggle")} />
|
|
||||||
<span>{t("chain.alignToggle")}</span>
|
|
||||||
</label>
|
|
||||||
{/if}
|
|
||||||
<SchemaActions
|
|
||||||
{inputMode}
|
|
||||||
{resultKind}
|
|
||||||
canDownload={hasResult}
|
|
||||||
{running}
|
|
||||||
{...head}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if error}
|
|
||||||
<p class="error" role="alert">{error}</p>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<div class="pair">
|
|
||||||
<SchemaSourceTile
|
|
||||||
mode={inputMode}
|
|
||||||
{running}
|
|
||||||
onupload={head.onupload}
|
|
||||||
{...source}
|
|
||||||
/>
|
|
||||||
{#each stepResults.slice(0, -1) as img, i (i)}
|
|
||||||
{@const set = stepFileSets[i] ?? []}
|
|
||||||
{#if set.length > 1 && !stepMaskOn[i]}
|
|
||||||
<PreviewTile
|
|
||||||
label={t("chain.stepResult", { n: i + 1 })}
|
|
||||||
viewMode="image"
|
|
||||||
parts={set.length}
|
|
||||||
partsDims={`${set[0].image.width} × ${set[0].image.height}`}
|
|
||||||
>
|
|
||||||
{#snippet actions()}
|
|
||||||
{#if stepMaskable[i]}
|
|
||||||
<ChipButton
|
|
||||||
label={t("resultCard.maskToggle")}
|
|
||||||
active={stepMaskOn[i] ?? false}
|
|
||||||
onclick={() => steps.ontogglestepmask?.(i)}
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
{/snippet}
|
|
||||||
<PartsGrid files={set} />
|
|
||||||
</PreviewTile>
|
|
||||||
{:else if img}
|
|
||||||
<PreviewTile
|
|
||||||
label={t("chain.stepResult", { n: i + 1 })}
|
|
||||||
viewMode="image"
|
|
||||||
dims={`${img.width} × ${img.height}`}
|
|
||||||
>
|
|
||||||
{#snippet actions()}
|
|
||||||
{#if stepMaskable[i]}
|
|
||||||
<ChipButton
|
|
||||||
label={t("resultCard.maskToggle")}
|
|
||||||
active={stepMaskOn[i] ?? false}
|
|
||||||
onclick={() => steps.ontogglestepmask?.(i)}
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
{/snippet}
|
|
||||||
<img
|
|
||||||
src={toDataUrl(
|
|
||||||
stepMaskOn[i] && stepMasks[i] ? (stepMasks[i] as PixelImage) : img,
|
|
||||||
)}
|
|
||||||
alt=""
|
|
||||||
/>
|
|
||||||
</PreviewTile>
|
|
||||||
{/if}
|
|
||||||
{/each}
|
|
||||||
<SchemaResultTile {resultKind} {toolId} {running} {...result} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.panel-head {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--space-m);
|
|
||||||
padding-bottom: var(--space-l);
|
|
||||||
border-bottom: var(--size-border) solid var(--color-border);
|
|
||||||
margin-bottom: var(--space-xl);
|
|
||||||
}
|
|
||||||
.label {
|
|
||||||
font: var(--font-size-s) var(--font-mono);
|
|
||||||
letter-spacing: var(--space-text-l);
|
|
||||||
text-transform: uppercase;
|
|
||||||
color: var(--color-main);
|
|
||||||
}
|
|
||||||
.align-toggle {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--space-m);
|
|
||||||
margin-right: auto;
|
|
||||||
color: var(--color-text-muted);
|
|
||||||
font: var(--font-size-s) var(--font-mono);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.error {
|
|
||||||
margin: 0 0 var(--space-l);
|
|
||||||
color: var(--color-danger);
|
|
||||||
font: var(--font-size-s) var(--font-mono);
|
|
||||||
}
|
|
||||||
.pair {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
gap: var(--space-l);
|
|
||||||
}
|
|
||||||
@media (--bp-tablet) {
|
|
||||||
.pair {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
import { hasTransparency } from "$lib/core/analyze";
|
import { hasTransparency } from "$lib/core/analyze";
|
||||||
import { debounce } from "$lib/core/debounce";
|
import { debounce } from "$lib/core/debounce";
|
||||||
import { ToolError } from "$lib/core/errors";
|
import { ToolError } from "$lib/core/errors";
|
||||||
import { decodeFile, type OutputMime } from "$lib/core/io";
|
import { decodeFile, toDataUrl, type OutputMime } from "$lib/core/io";
|
||||||
import { outputFormatByMime } from "$lib/output-formats";
|
import { outputFormatByMime } from "$lib/output-formats";
|
||||||
import type { PixelImage } from "$lib/core/types";
|
import type { PixelImage } from "$lib/core/types";
|
||||||
import { baseName, uniqueName, type ChainWarning } from "$lib/run-chain";
|
import { baseName, uniqueName, type ChainWarning } from "$lib/run-chain";
|
||||||
@@ -44,15 +44,21 @@
|
|||||||
clampSourceAwareMaxes,
|
clampSourceAwareMaxes,
|
||||||
sanitizeSchemaParams,
|
sanitizeSchemaParams,
|
||||||
} from "$lib/registry-schema";
|
} from "$lib/registry-schema";
|
||||||
|
import ChipButton from "$lib/components/ui/ChipButton.svelte";
|
||||||
|
import Toggle from "$lib/components/ui/Toggle.svelte";
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import { SvelteSet } from "svelte/reactivity";
|
import { SvelteSet } from "svelte/reactivity";
|
||||||
import AddStepButton from "./AddStepButton.svelte";
|
import AddStepButton from "./AddStepButton.svelte";
|
||||||
import SchemaAlignedLayout from "./SchemaAlignedLayout.svelte";
|
import PartsGrid from "./PartsGrid.svelte";
|
||||||
|
import PreviewTile from "./PreviewTile.svelte";
|
||||||
|
import SchemaActions from "./SchemaActions.svelte";
|
||||||
import SchemaFields from "./SchemaFields.svelte";
|
import SchemaFields from "./SchemaFields.svelte";
|
||||||
import SchemaPreview from "./SchemaPreview.svelte";
|
import SchemaResultTile from "./SchemaResultTile.svelte";
|
||||||
|
import SchemaSourceTile from "./SchemaSourceTile.svelte";
|
||||||
import SortableStepCard from "./SortableStepCard.svelte";
|
import SortableStepCard from "./SortableStepCard.svelte";
|
||||||
import StepDropIndicator from "./StepDropIndicator.svelte";
|
import StepDropIndicator from "./StepDropIndicator.svelte";
|
||||||
import ToolPickerButton from "./ToolPickerButton.svelte";
|
import ToolPickerButton from "./ToolPickerButton.svelte";
|
||||||
|
import { hasPreviewResult } from "./schema-preview-model";
|
||||||
import { createSchemaToolRunner } from "./schema-tool-runner.svelte";
|
import { createSchemaToolRunner } from "./schema-tool-runner.svelte";
|
||||||
import { createStepDnd } from "./step-dnd.svelte";
|
import { createStepDnd } from "./step-dnd.svelte";
|
||||||
import {
|
import {
|
||||||
@@ -175,6 +181,15 @@
|
|||||||
const alignedMode = $derived(
|
const alignedMode = $derived(
|
||||||
aligned && resultKind === "image" && steps.length > 1,
|
aligned && resultKind === "image" && steps.length > 1,
|
||||||
);
|
);
|
||||||
|
const hasResult = $derived(
|
||||||
|
hasPreviewResult({
|
||||||
|
inputMode,
|
||||||
|
resultKind: displayResultKind,
|
||||||
|
result,
|
||||||
|
fileResult,
|
||||||
|
textResult,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
const alphaLoss = $derived(
|
const alphaLoss = $derived(
|
||||||
resultKind === "image" &&
|
resultKind === "image" &&
|
||||||
@@ -477,132 +492,199 @@
|
|||||||
</SortableStepCard>
|
</SortableStepCard>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|
||||||
|
{#snippet maskChip(i: number)}
|
||||||
|
{#if stepMaskable[i]}
|
||||||
|
<ChipButton
|
||||||
|
label={t("resultCard.maskToggle")}
|
||||||
|
active={stepMaskOn[i] ?? false}
|
||||||
|
onclick={() => toggleStepMaskByIndex(i)}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
|
{#snippet inputTile(i: number)}
|
||||||
|
{#if i === 0}
|
||||||
|
<SchemaSourceTile
|
||||||
|
mode={inputMode}
|
||||||
|
{source}
|
||||||
|
sources={sourceFiles}
|
||||||
|
{textSource}
|
||||||
|
running={busy}
|
||||||
|
fileCount={sourceFiles.length}
|
||||||
|
ontextinput={(v) => (textSource = v)}
|
||||||
|
onrendertext={run}
|
||||||
|
onupload={handleFile}
|
||||||
|
onuploadmany={handleFiles}
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
{@const input = stepResults[i - 1]}
|
||||||
|
{@const inSet = stepFileSets[i - 1] ?? []}
|
||||||
|
<PreviewTile
|
||||||
|
label={t("chain.inputLegend")}
|
||||||
|
viewMode="image"
|
||||||
|
dims={input ? `${input.width} × ${input.height}` : undefined}
|
||||||
|
parts={inSet.length > 1 ? inSet.length : undefined}
|
||||||
|
>
|
||||||
|
{#if inSet.length > 1}
|
||||||
|
<PartsGrid files={inSet} />
|
||||||
|
{:else if input}
|
||||||
|
<img src={toDataUrl(input)} alt="" />
|
||||||
|
{:else}
|
||||||
|
<span class="empty">{t("resultCard.noResult")}</span>
|
||||||
|
{/if}
|
||||||
|
</PreviewTile>
|
||||||
|
{/if}
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
|
{#snippet outputTile(i: number)}
|
||||||
|
{@const out = stepResults[i]}
|
||||||
|
{#if i === steps.length - 1}
|
||||||
|
<SchemaResultTile
|
||||||
|
resultKind={displayResultKind}
|
||||||
|
result={out}
|
||||||
|
{resultNote}
|
||||||
|
{fileResult}
|
||||||
|
{textResult}
|
||||||
|
textVars={verdictVars}
|
||||||
|
toolId={lastTool.id}
|
||||||
|
running={busy}
|
||||||
|
mask={stepMasks[i] ?? null}
|
||||||
|
maskOn={stepMaskOn[i] ?? false}
|
||||||
|
ontogglemask={stepMaskable[i]
|
||||||
|
? () => toggleStepMaskByIndex(i)
|
||||||
|
: undefined}
|
||||||
|
oncopy={copyText}
|
||||||
|
ondownloadtxt={downloadText}
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
{@const outSet = stepFileSets[i] ?? []}
|
||||||
|
<PreviewTile
|
||||||
|
label={t("chain.stepResult", { n: i + 1 })}
|
||||||
|
viewMode="image"
|
||||||
|
dims={out ? `${out.width} × ${out.height}` : undefined}
|
||||||
|
parts={outSet.length > 1 ? outSet.length : undefined}
|
||||||
|
>
|
||||||
|
{#snippet actions()}
|
||||||
|
{@render maskChip(i)}
|
||||||
|
{/snippet}
|
||||||
|
{#if outSet.length > 1 && !stepMaskOn[i]}
|
||||||
|
<PartsGrid files={outSet} />
|
||||||
|
{:else if out}
|
||||||
|
<img
|
||||||
|
src={toDataUrl(
|
||||||
|
stepMaskOn[i] && stepMasks[i]
|
||||||
|
? (stepMasks[i] as PixelImage)
|
||||||
|
: out,
|
||||||
|
)}
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<span class="empty">{t("resultCard.noResult")}</span>
|
||||||
|
{/if}
|
||||||
|
</PreviewTile>
|
||||||
|
{/if}
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
|
{#snippet panelHead()}
|
||||||
|
<div class="panel-head">
|
||||||
|
<span class="label">{t("resultCard.previewPanel")}</span>
|
||||||
|
{#if resultKind === "image" && steps.length > 1}
|
||||||
|
<label class="align-toggle">
|
||||||
|
<Toggle bind:checked={aligned} label={t("chain.alignToggle")} />
|
||||||
|
<span>{t("chain.alignToggle")}</span>
|
||||||
|
</label>
|
||||||
|
{/if}
|
||||||
|
<SchemaActions
|
||||||
|
{inputMode}
|
||||||
|
resultKind={displayResultKind}
|
||||||
|
canDownload={hasResult}
|
||||||
|
running={busy}
|
||||||
|
{format}
|
||||||
|
quality={currentQuality}
|
||||||
|
{limitKb}
|
||||||
|
{alphaLoss}
|
||||||
|
onupload={handleFile}
|
||||||
|
ondownload={download}
|
||||||
|
onformat={(v) => (format = v)}
|
||||||
|
onquality={setFormatQuality}
|
||||||
|
onlimit={(v) => (limitKb = v)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
|
{#snippet addRow(index: number)}
|
||||||
|
{#if canExtend && !stepDnd.active}
|
||||||
|
<div class="add-row">
|
||||||
|
<AddStepButton onadd={(id) => addStepAt(index, id)} />
|
||||||
|
{#if alignedMode}
|
||||||
|
<div class="row-bridge" aria-hidden="true"></div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
|
{#snippet indicator(slot: number)}
|
||||||
|
{#if stepDnd.isIndicatorAt(slot)}
|
||||||
|
<div class="indicator-row">
|
||||||
|
<StepDropIndicator height={stepDnd.dragHeight} />
|
||||||
|
{#if alignedMode}
|
||||||
|
<div class="row-bridge" aria-hidden="true"></div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
<DragDropProvider
|
<DragDropProvider
|
||||||
onDragStart={stepDnd.onDragStart}
|
onDragStart={stepDnd.onDragStart}
|
||||||
onDragMove={stepDnd.onDragOver}
|
onDragMove={stepDnd.onDragOver}
|
||||||
onDragOver={stepDnd.onDragOver}
|
onDragOver={stepDnd.onDragOver}
|
||||||
onDragEnd={stepDnd.onDragEnd}
|
onDragEnd={stepDnd.onDragEnd}
|
||||||
>
|
>
|
||||||
{#if !alignedMode}
|
{#if alignedMode}
|
||||||
<div class="workspace">
|
<div class="workspace aligned" class:dnd-active={stepDnd.active}>
|
||||||
<section class="settings">
|
<div class="head-spacer" aria-hidden="true"></div>
|
||||||
{#if canExtend && !stepDnd.active}
|
{@render panelHead()}
|
||||||
<AddStepButton onadd={(id) => addStepAt(0, id)} />
|
{@render addRow(0)}
|
||||||
{/if}
|
|
||||||
{#each steps as step, i (step.key)}
|
{#each steps as step, i (step.key)}
|
||||||
{#if stepDnd.isIndicatorAt(i)}
|
{@render indicator(i)}
|
||||||
<StepDropIndicator height={stepDnd.dragHeight} />
|
|
||||||
{/if}
|
|
||||||
{@render stepCard(step, i)}
|
{@render stepCard(step, i)}
|
||||||
{#if canExtend && !stepDnd.active}
|
<div class="pair">
|
||||||
<AddStepButton onadd={(id) => addStepAt(i + 1, id)} />
|
{@render inputTile(i)}
|
||||||
{/if}
|
{@render outputTile(i)}
|
||||||
|
</div>
|
||||||
|
{@render addRow(i + 1)}
|
||||||
{/each}
|
{/each}
|
||||||
{#if stepDnd.isIndicatorAt(steps.length)}
|
{@render indicator(steps.length)}
|
||||||
<StepDropIndicator height={stepDnd.dragHeight} />
|
<div class="panel-foot" aria-hidden="true"></div>
|
||||||
{/if}
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="workspace" class:dnd-active={stepDnd.active}>
|
||||||
|
<section class="settings">
|
||||||
|
{@render addRow(0)}
|
||||||
|
{#each steps as step, i (step.key)}
|
||||||
|
{@render indicator(i)}
|
||||||
|
{@render stepCard(step, i)}
|
||||||
|
{@render addRow(i + 1)}
|
||||||
|
{/each}
|
||||||
|
{@render indicator(steps.length)}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="panel">
|
<section class="panel">
|
||||||
<SchemaPreview
|
{@render panelHead()}
|
||||||
toolId={lastTool.id}
|
<div class="pair-grid">
|
||||||
{inputMode}
|
<div class="cell">{@render inputTile(0)}</div>
|
||||||
resultKind={displayResultKind}
|
{#each steps as step, i (step.key)}
|
||||||
running={busy}
|
<div class="cell">{@render outputTile(i)}</div>
|
||||||
error={errorText}
|
{/each}
|
||||||
bind:aligned
|
</div>
|
||||||
head={{
|
|
||||||
format,
|
|
||||||
quality: currentQuality,
|
|
||||||
limitKb,
|
|
||||||
alphaLoss,
|
|
||||||
onupload: handleFile,
|
|
||||||
ondownload: download,
|
|
||||||
onformat: (v) => (format = v),
|
|
||||||
onquality: setFormatQuality,
|
|
||||||
onlimit: (v) => (limitKb = v),
|
|
||||||
}}
|
|
||||||
source={{
|
|
||||||
source,
|
|
||||||
sources: sourceFiles,
|
|
||||||
fileCount: sourceFiles.length,
|
|
||||||
textSource,
|
|
||||||
ontextinput: (v) => (textSource = v),
|
|
||||||
onrendertext: run,
|
|
||||||
onuploadmany: handleFiles,
|
|
||||||
}}
|
|
||||||
steps={{
|
|
||||||
results: stepResults,
|
|
||||||
fileSets: stepFileSets,
|
|
||||||
maskable: stepMaskable,
|
|
||||||
maskOn: stepMaskOn,
|
|
||||||
masks: stepMasks,
|
|
||||||
ontogglestepmask: toggleStepMaskByIndex,
|
|
||||||
}}
|
|
||||||
result={{
|
|
||||||
result,
|
|
||||||
resultNote,
|
|
||||||
fileResult,
|
|
||||||
textResult,
|
|
||||||
textVars: verdictVars,
|
|
||||||
mask: stepMasks.at(-1) ?? null,
|
|
||||||
maskOn: stepMaskOn.at(-1) ?? false,
|
|
||||||
ontogglemask: stepMaskable.at(-1)
|
|
||||||
? () => toggleStepMask(steps[steps.length - 1].key)
|
|
||||||
: undefined,
|
|
||||||
oncopy: copyText,
|
|
||||||
ondownloadtxt: downloadText,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if errorText}
|
||||||
|
<p class="error" role="alert">{errorText}</p>
|
||||||
|
{/if}
|
||||||
{#each allWarnings as w (warningText(w))}
|
{#each allWarnings as w (warningText(w))}
|
||||||
<p class="warn" role="status">{warningText(w)}</p>
|
<p class="warn" role="status">{warningText(w)}</p>
|
||||||
{/each}
|
{/each}
|
||||||
{:else}
|
|
||||||
<SchemaAlignedLayout
|
|
||||||
{steps}
|
|
||||||
{stepCard}
|
|
||||||
{canExtend}
|
|
||||||
{stepDnd}
|
|
||||||
bind:aligned
|
|
||||||
{inputMode}
|
|
||||||
{source}
|
|
||||||
{sourceFiles}
|
|
||||||
{textSource}
|
|
||||||
running={busy}
|
|
||||||
resultKind={displayResultKind}
|
|
||||||
{result}
|
|
||||||
{resultNote}
|
|
||||||
{fileResult}
|
|
||||||
{textResult}
|
|
||||||
textVars={verdictVars}
|
|
||||||
toolId={lastTool.id}
|
|
||||||
{format}
|
|
||||||
quality={currentQuality}
|
|
||||||
{limitKb}
|
|
||||||
{alphaLoss}
|
|
||||||
{errorText}
|
|
||||||
warnings={allWarnings.map(warningText)}
|
|
||||||
{stepResults}
|
|
||||||
{stepFileSets}
|
|
||||||
{stepMasks}
|
|
||||||
{stepMaskOn}
|
|
||||||
{stepMaskable}
|
|
||||||
onaddstep={addStepAt}
|
|
||||||
ontogglestepmask={toggleStepMaskByIndex}
|
|
||||||
ontextinput={(v) => (textSource = v)}
|
|
||||||
onrendertext={run}
|
|
||||||
onupload={handleFile}
|
|
||||||
onuploadmany={handleFiles}
|
|
||||||
ondownload={download}
|
|
||||||
oncopy={copyText}
|
|
||||||
ondownloadtxt={downloadText}
|
|
||||||
onformat={(v) => (format = v)}
|
|
||||||
onquality={setFormatQuality}
|
|
||||||
onlimit={(v) => (limitKb = v)}
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
{#if stepDnd.activeData && stepDnd.pointer}
|
{#if stepDnd.activeData && stepDnd.pointer}
|
||||||
<div
|
<div
|
||||||
class="step-ghost"
|
class="step-ghost"
|
||||||
@@ -678,6 +760,11 @@
|
|||||||
font-size: var(--font-size-s);
|
font-size: var(--font-size-s);
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
/* Two scaffoldings over shared snippets: normal mode keeps two independent
|
||||||
|
columns (cards pack under each other, tiles pack two per row inside one
|
||||||
|
panel); aligned mode is a flat 3-column grid where each card shares its
|
||||||
|
grid row with the input/output pair and the right side reads as one
|
||||||
|
continuous panel. */
|
||||||
.workspace {
|
.workspace {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(var(--size-workspace-min), 1fr) minmax(
|
grid-template-columns: minmax(var(--size-workspace-min), 1fr) minmax(
|
||||||
@@ -687,23 +774,157 @@
|
|||||||
gap: var(--space-xxl);
|
gap: var(--space-xxl);
|
||||||
align-items: start;
|
align-items: start;
|
||||||
}
|
}
|
||||||
|
.settings {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
.panel {
|
.panel {
|
||||||
|
padding: var(--space-xl);
|
||||||
border: var(--size-border) solid var(--color-border);
|
border: var(--size-border) solid var(--color-border);
|
||||||
border-radius: var(--radius-m);
|
border-radius: var(--radius-m);
|
||||||
background: var(--color-panel);
|
background: var(--color-panel);
|
||||||
|
}
|
||||||
|
.panel-head {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-m);
|
||||||
|
padding-bottom: var(--space-l);
|
||||||
|
border-bottom: var(--size-border) solid var(--color-border);
|
||||||
|
margin-bottom: var(--space-xl);
|
||||||
|
}
|
||||||
|
.label {
|
||||||
|
font: var(--font-size-s) var(--font-mono);
|
||||||
|
letter-spacing: var(--space-text-l);
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--color-main);
|
||||||
|
}
|
||||||
|
.align-toggle {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-m);
|
||||||
|
margin-right: auto;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
font: var(--font-size-s) var(--font-mono);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.pair-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: var(--space-l);
|
||||||
|
}
|
||||||
|
.cell {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Aligned mode: cards in column 1, each step's input/output pair spans
|
||||||
|
columns 2-3 on the card's row. The right side forms one panel: head and
|
||||||
|
foot carry the top/bottom borders, pairs and row bridges carry the side
|
||||||
|
borders, so rows have no vertical gaps. */
|
||||||
|
.workspace.aligned {
|
||||||
|
grid-template-columns: minmax(var(--size-workspace-min), 1fr) 1fr 1fr;
|
||||||
|
gap: 0 var(--space-xxl);
|
||||||
|
}
|
||||||
|
.aligned > :global(.step-card) {
|
||||||
|
grid-column: 1;
|
||||||
|
}
|
||||||
|
.aligned .head-spacer {
|
||||||
|
grid-column: 1;
|
||||||
|
}
|
||||||
|
.aligned .panel-head {
|
||||||
|
grid-column: 2 / -1;
|
||||||
|
padding: var(--space-l) var(--space-xl);
|
||||||
|
border: var(--size-border) solid var(--color-border);
|
||||||
|
border-bottom: none;
|
||||||
|
border-radius: var(--radius-m) var(--radius-m) 0 0;
|
||||||
|
background: var(--color-panel);
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
.aligned .pair {
|
||||||
|
grid-column: 2 / -1;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: var(--space-l);
|
||||||
padding: var(--space-xl);
|
padding: var(--space-xl);
|
||||||
|
border-right: var(--size-border) solid var(--color-border);
|
||||||
|
border-left: var(--size-border) solid var(--color-border);
|
||||||
|
background: var(--color-panel);
|
||||||
|
}
|
||||||
|
/* The workspace has align-items: start; panel pieces must stretch to the
|
||||||
|
row height or the side borders break when a card is taller than its
|
||||||
|
pair. */
|
||||||
|
.aligned .panel-head,
|
||||||
|
.aligned .pair,
|
||||||
|
.aligned .row-bridge,
|
||||||
|
.aligned .panel-foot {
|
||||||
|
align-self: stretch;
|
||||||
|
}
|
||||||
|
.aligned .add-row,
|
||||||
|
.aligned .indicator-row {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: subgrid;
|
||||||
|
}
|
||||||
|
.aligned .row-bridge {
|
||||||
|
grid-column: 2 / -1;
|
||||||
|
border-right: var(--size-border) solid var(--color-border);
|
||||||
|
border-left: var(--size-border) solid var(--color-border);
|
||||||
|
background: var(--color-panel);
|
||||||
|
}
|
||||||
|
.aligned .panel-foot {
|
||||||
|
grid-column: 2 / -1;
|
||||||
|
min-height: var(--space-xl);
|
||||||
|
border: var(--size-border) solid var(--color-border);
|
||||||
|
border-top: none;
|
||||||
|
border-radius: 0 0 var(--radius-m) var(--radius-m);
|
||||||
|
background: var(--color-panel);
|
||||||
|
}
|
||||||
|
/* Compact drag: while a step is dragged, cards collapse to headers so the
|
||||||
|
reorder happens in a tight list; aligned additionally drops the preview
|
||||||
|
panel (its rows are too tall to drag across). */
|
||||||
|
.dnd-active :global(.step-body) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.aligned.dnd-active .panel-head,
|
||||||
|
.aligned.dnd-active .pair,
|
||||||
|
.aligned.dnd-active .row-bridge,
|
||||||
|
.aligned.dnd-active .panel-foot,
|
||||||
|
.aligned.dnd-active .head-spacer {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.error {
|
||||||
|
margin: var(--space-l) 0 0;
|
||||||
|
color: var(--color-danger);
|
||||||
|
font: var(--font-size-s) var(--font-mono);
|
||||||
|
}
|
||||||
|
.empty {
|
||||||
|
font: var(--font-size-s) var(--font-mono);
|
||||||
}
|
}
|
||||||
.no-schema {
|
.no-schema {
|
||||||
font: var(--font-size-s) var(--font-mono);
|
font: var(--font-size-s) var(--font-mono);
|
||||||
}
|
}
|
||||||
.warn {
|
.warn {
|
||||||
margin: 0;
|
margin: var(--space-s) 0 0;
|
||||||
color: var(--color-warning);
|
color: var(--color-warning);
|
||||||
font: var(--font-size-s) var(--font-mono);
|
font: var(--font-size-s) var(--font-mono);
|
||||||
}
|
}
|
||||||
@media (--bp-tablet) {
|
@media (--bp-tablet) {
|
||||||
.workspace {
|
.workspace,
|
||||||
|
.workspace.aligned {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
.pair-grid,
|
||||||
|
.aligned .pair {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
.aligned .head-spacer,
|
||||||
|
.aligned .row-bridge {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.aligned .panel-head,
|
||||||
|
.aligned .pair,
|
||||||
|
.aligned .panel-foot {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -20,14 +20,17 @@ interface DndEvent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Count the slot the pointer is over: how many non-source cards have their
|
// Count the slot the pointer is over: how many non-source cards have their
|
||||||
// vertical midpoint above the pointer.
|
// vertical midpoint above the pointer. Also reports the height of the first
|
||||||
|
// visible card: during drag all cards collapse to headers, so the indicator
|
||||||
|
// takes that uniform height rather than the hidden source's stale height.
|
||||||
function indexAtPosition(
|
function indexAtPosition(
|
||||||
source: SortableDraggable<StepDragData>,
|
source: SortableDraggable<StepDragData>,
|
||||||
pointerY: number,
|
pointerY: number,
|
||||||
): number {
|
): { index: number; cardHeight: number } {
|
||||||
const manager = source.sortable.manager;
|
const manager = source.sortable.manager;
|
||||||
let index = 0;
|
let index = 0;
|
||||||
if (!manager) return index;
|
let cardHeight = 0;
|
||||||
|
if (!manager) return { index, cardHeight };
|
||||||
for (const droppable of manager.registry.droppables) {
|
for (const droppable of manager.registry.droppables) {
|
||||||
if (!isSortable(droppable)) continue;
|
if (!isSortable(droppable)) continue;
|
||||||
if (droppable.sortable.id === source.id) continue;
|
if (droppable.sortable.id === source.id) continue;
|
||||||
@@ -35,9 +38,10 @@ function indexAtPosition(
|
|||||||
if (!el) continue;
|
if (!el) continue;
|
||||||
const rect = el.getBoundingClientRect();
|
const rect = el.getBoundingClientRect();
|
||||||
if (rect.height === 0) continue;
|
if (rect.height === 0) continue;
|
||||||
|
if (!cardHeight) cardHeight = rect.height;
|
||||||
if (pointerY > rect.top + rect.height / 2) index++;
|
if (pointerY > rect.top + rect.height / 2) index++;
|
||||||
}
|
}
|
||||||
return index;
|
return { index, cardHeight };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step-reorder state for SchemaToolView, single list. The dragged card is
|
// Step-reorder state for SchemaToolView, single list. The dragged card is
|
||||||
@@ -65,10 +69,12 @@ export function createStepDnd({ onMove }: StepDndOptions) {
|
|||||||
const position = event.operation?.position?.current;
|
const position = event.operation?.position?.current;
|
||||||
if (!position || !sourceX) return null;
|
if (!position || !sourceX) return null;
|
||||||
if (position.x < sourceX.left || position.x > sourceX.right) return null;
|
if (position.x < sourceX.left || position.x > sourceX.right) return null;
|
||||||
return indexAtPosition(
|
const { index, cardHeight } = indexAtPosition(
|
||||||
source as SortableDraggable<StepDragData>,
|
source as SortableDraggable<StepDragData>,
|
||||||
position.y,
|
position.y,
|
||||||
);
|
);
|
||||||
|
if (cardHeight) dragHeight = cardHeight;
|
||||||
|
return index;
|
||||||
}
|
}
|
||||||
|
|
||||||
function onDragStart(event: DndEvent) {
|
function onDragStart(event: DndEvent) {
|
||||||
|
|||||||
Reference in New Issue
Block a user