mirror of
https://github.com/Ku6epXBOCTuK/easy-png-tools.git
synced 2026-11-13 17:08:20 +00:00
feat: update dnd tools in pipeline
This commit is contained in:
@@ -52,6 +52,8 @@
|
||||
"vitest": "^4.1.11"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dnd-kit/dom": "^0.3.2",
|
||||
"@dnd-kit/svelte": "^0.3.2",
|
||||
"@fontsource/ibm-plex-mono": "^5.3.0",
|
||||
"@fontsource/ibm-plex-sans": "^5.3.0",
|
||||
"@lucide/svelte": "^1.33.0",
|
||||
|
||||
Generated
+74
@@ -8,6 +8,12 @@ importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
'@dnd-kit/dom':
|
||||
specifier: ^0.3.2
|
||||
version: 0.3.2
|
||||
'@dnd-kit/svelte':
|
||||
specifier: ^0.3.2
|
||||
version: 0.3.2(svelte@5.56.9(@typescript-eslint/types@8.67.0))
|
||||
'@fontsource/ibm-plex-mono':
|
||||
specifier: ^5.3.0
|
||||
version: 5.3.0
|
||||
@@ -196,6 +202,26 @@ packages:
|
||||
peerDependencies:
|
||||
postcss-selector-parser: ^7.1.1
|
||||
|
||||
'@dnd-kit/abstract@0.3.2':
|
||||
resolution: {integrity: sha512-uvPVK+SZYD6Viddn9M0K0JQdXknuVSxA/EbMlFRanve3P/XTc18oLa5zGftKSGjfQGmuzkZ34E26DSbly1zi3Q==}
|
||||
|
||||
'@dnd-kit/collision@0.3.2':
|
||||
resolution: {integrity: sha512-pNmNSLCI8S9fNQ7QJ3fBCDjiT0sqBhUFcKgmyYaGvGCAU+kq0AP8OWlh0JSisc9k5mFyxmRpmFQcnJpILz/RPA==}
|
||||
|
||||
'@dnd-kit/dom@0.3.2':
|
||||
resolution: {integrity: sha512-cIUAVgt2szQyz6JRy7I+0r+xeyOAGH21Y15hb5bIyHoDEaZBvIDH+OOlD9eoLjCbsxDLN9WloU2CBi3OE6LYDg==}
|
||||
|
||||
'@dnd-kit/geometry@0.3.2':
|
||||
resolution: {integrity: sha512-3UBPuIS7E3oGiHxOE8h810QA+0pnrnCtGxl4Os1z3yy5YkC/BEYGY+TxWPTQaY1/OMV7GCX7ZNMlama2QN3n3w==}
|
||||
|
||||
'@dnd-kit/state@0.3.2':
|
||||
resolution: {integrity: sha512-dLUIkoYrIJhGXfF2wGLTfb46vUokEsO/OoE21TSfmahYrx7ysTmnwbePsznFaHlwgZhQEh6AlLvthLCeY21b1A==}
|
||||
|
||||
'@dnd-kit/svelte@0.3.2':
|
||||
resolution: {integrity: sha512-HkTnx/3GnxfVNqHSojxhxp5r+KymL9Q7UI2gonXN2+bmdObw03FSQOjicOoRzlV2u9o5DtK1sU4YUQAfkLCGGQ==}
|
||||
peerDependencies:
|
||||
svelte: ^5.29.0
|
||||
|
||||
'@eslint-community/eslint-utils@4.10.1':
|
||||
resolution: {integrity: sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==}
|
||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
@@ -313,6 +339,9 @@ packages:
|
||||
'@polka/url@1.0.0-next.29':
|
||||
resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==}
|
||||
|
||||
'@preact/signals-core@1.14.4':
|
||||
resolution: {integrity: sha512-HNB6HYeYKhQbJ1aKl+YRjrS4+QWHLKX6qKoUsfS/m0vqzsVaEBiZiaKbG/e+NKk2ch5ALQr/ihWaMHxiCuuWHA==}
|
||||
|
||||
'@rolldown/binding-android-arm64@1.2.4':
|
||||
resolution: {integrity: sha512-jHC2cnyKz5xU2fhECtFl8OZ83cYNt13GZQD+0uMJ/X3o+ijmd56okHhTUwxVSHPx1IRVIJEZ1/1pPzeLCU6XKA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
@@ -1518,6 +1547,9 @@ packages:
|
||||
peerDependencies:
|
||||
typescript: '>=4.8.4'
|
||||
|
||||
tslib@2.8.1:
|
||||
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
||||
|
||||
type-check@0.4.0:
|
||||
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
@@ -1744,6 +1776,44 @@ snapshots:
|
||||
dependencies:
|
||||
postcss-selector-parser: 7.1.5
|
||||
|
||||
'@dnd-kit/abstract@0.3.2':
|
||||
dependencies:
|
||||
'@dnd-kit/geometry': 0.3.2
|
||||
'@dnd-kit/state': 0.3.2
|
||||
tslib: 2.8.1
|
||||
|
||||
'@dnd-kit/collision@0.3.2':
|
||||
dependencies:
|
||||
'@dnd-kit/abstract': 0.3.2
|
||||
'@dnd-kit/geometry': 0.3.2
|
||||
tslib: 2.8.1
|
||||
|
||||
'@dnd-kit/dom@0.3.2':
|
||||
dependencies:
|
||||
'@dnd-kit/abstract': 0.3.2
|
||||
'@dnd-kit/collision': 0.3.2
|
||||
'@dnd-kit/geometry': 0.3.2
|
||||
'@dnd-kit/state': 0.3.2
|
||||
tslib: 2.8.1
|
||||
|
||||
'@dnd-kit/geometry@0.3.2':
|
||||
dependencies:
|
||||
'@dnd-kit/state': 0.3.2
|
||||
tslib: 2.8.1
|
||||
|
||||
'@dnd-kit/state@0.3.2':
|
||||
dependencies:
|
||||
'@preact/signals-core': 1.14.4
|
||||
tslib: 2.8.1
|
||||
|
||||
'@dnd-kit/svelte@0.3.2(svelte@5.56.9(@typescript-eslint/types@8.67.0))':
|
||||
dependencies:
|
||||
'@dnd-kit/abstract': 0.3.2
|
||||
'@dnd-kit/dom': 0.3.2
|
||||
'@dnd-kit/state': 0.3.2
|
||||
svelte: 5.56.9(@typescript-eslint/types@8.67.0)
|
||||
tslib: 2.8.1
|
||||
|
||||
'@eslint-community/eslint-utils@4.10.1(eslint@10.9.0(supports-color@10.2.2))':
|
||||
dependencies:
|
||||
eslint: 10.9.0(supports-color@10.2.2)
|
||||
@@ -1847,6 +1917,8 @@ snapshots:
|
||||
|
||||
'@polka/url@1.0.0-next.29': {}
|
||||
|
||||
'@preact/signals-core@1.14.4': {}
|
||||
|
||||
'@rolldown/binding-android-arm64@1.2.4':
|
||||
optional: true
|
||||
|
||||
@@ -2991,6 +3063,8 @@ snapshots:
|
||||
dependencies:
|
||||
typescript: 6.0.3
|
||||
|
||||
tslib@2.8.1: {}
|
||||
|
||||
type-check@0.4.0:
|
||||
dependencies:
|
||||
prelude-ls: 1.2.1
|
||||
|
||||
@@ -177,6 +177,7 @@
|
||||
--duration-l: 0.8s;
|
||||
|
||||
--z-dropdown: 10;
|
||||
--z-drag: 20;
|
||||
}
|
||||
|
||||
[data-theme="dark"] {
|
||||
|
||||
@@ -3,55 +3,44 @@
|
||||
import IconButton from "$lib/components/ui/IconButton.svelte";
|
||||
import { t } from "$lib/i18n/t";
|
||||
import type { Snippet } from "svelte";
|
||||
import type { Attachment } from "svelte/attachments";
|
||||
|
||||
interface Props {
|
||||
index: number;
|
||||
title?: string;
|
||||
type?: string;
|
||||
draggable?: boolean;
|
||||
collapsed?: boolean;
|
||||
tools?: Snippet;
|
||||
children?: Snippet;
|
||||
onremove?: () => void;
|
||||
ontoggle?: () => void;
|
||||
ondragstart?: (e: DragEvent) => void;
|
||||
ondragover?: (e: DragEvent) => void;
|
||||
ondrop?: (e: DragEvent) => void;
|
||||
ondragend?: () => void;
|
||||
// dnd-kit attachments wired by SortableStepCard; noop on static usage.
|
||||
elementAttachment?: Attachment<HTMLElement>;
|
||||
handleAttachment?: Attachment<HTMLElement>;
|
||||
}
|
||||
|
||||
const noopAttach: Attachment<HTMLElement> = () => {};
|
||||
|
||||
let {
|
||||
index,
|
||||
title,
|
||||
type,
|
||||
draggable = false,
|
||||
collapsed = false,
|
||||
tools,
|
||||
children,
|
||||
onremove,
|
||||
ontoggle,
|
||||
ondragstart,
|
||||
ondragover,
|
||||
ondrop,
|
||||
ondragend,
|
||||
elementAttachment = noopAttach,
|
||||
handleAttachment = noopAttach,
|
||||
}: Props = $props();
|
||||
</script>
|
||||
|
||||
<article
|
||||
class="step-card"
|
||||
ondragover={(e) => ondragover?.(e)}
|
||||
ondrop={(e) => ondrop?.(e)}
|
||||
>
|
||||
<header
|
||||
class="step-head"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
{draggable}
|
||||
ondragstart={(e) => ondragstart?.(e)}
|
||||
ondragend={() => ondragend?.()}
|
||||
>
|
||||
<article class="step-card" {@attach elementAttachment}>
|
||||
<header class="step-head" role="button" tabindex="0">
|
||||
<span class="step-index">{index.toString().padStart(2, "0")}</span>
|
||||
<GripVertical size={16} class="drag" aria-hidden="true" />
|
||||
<span class="drag-handle" {@attach handleAttachment}>
|
||||
<GripVertical size={16} class="drag" aria-hidden="true" />
|
||||
</span>
|
||||
<div class="step-heading-text">
|
||||
{#if type}<span class="step-type">{type}</span>{/if}
|
||||
{#if title}<h2 class="step-title">{title}</h2>{/if}
|
||||
@@ -101,6 +90,12 @@
|
||||
font: var(--font-size-s) var(--font-mono);
|
||||
color: var(--color-main);
|
||||
}
|
||||
.drag-handle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: grab;
|
||||
touch-action: none;
|
||||
}
|
||||
.step-heading-text {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
@@ -127,6 +122,5 @@
|
||||
}
|
||||
.step-card :global(.drag) {
|
||||
color: var(--color-border);
|
||||
cursor: grab;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -19,11 +19,14 @@
|
||||
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;
|
||||
@@ -65,6 +68,7 @@
|
||||
steps,
|
||||
stepCard,
|
||||
canExtend,
|
||||
stepDnd,
|
||||
aligned = $bindable(false),
|
||||
inputMode,
|
||||
source,
|
||||
@@ -146,13 +150,19 @@
|
||||
{#each warnings as w (w)}
|
||||
<p class="warn" role="status">{w}</p>
|
||||
{/each}
|
||||
{#if canExtend}
|
||||
{#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}
|
||||
@@ -241,7 +251,7 @@
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{#if canExtend}
|
||||
{#if canExtend && !stepDnd?.active}
|
||||
<div class="aligned-row">
|
||||
<AddStepButton onadd={(id) => onaddstep(i + 1, id)} />
|
||||
<div
|
||||
@@ -251,6 +261,15 @@
|
||||
</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>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { goto } from "$app/navigation";
|
||||
import { resolve } from "$app/paths";
|
||||
import StepCard from "$lib/components/display/StepCard.svelte";
|
||||
import Button from "$lib/components/ui/Button.svelte";
|
||||
import { ButtonVariantDefine } from "$lib/components/ui/define";
|
||||
import { isFav, toggleFav } from "$lib/favorites.svelte";
|
||||
import { Star } from "@lucide/svelte";
|
||||
import { GripVertical, Star } from "@lucide/svelte";
|
||||
import { DragDropProvider } from "@dnd-kit/svelte";
|
||||
import { hasTransparency } from "$lib/core/analyze";
|
||||
import { debounce } from "$lib/core/debounce";
|
||||
import { ToolError } from "$lib/core/errors";
|
||||
@@ -30,6 +30,7 @@
|
||||
import {
|
||||
createChain,
|
||||
isStructuralDefault,
|
||||
moveStep,
|
||||
type ChainStep,
|
||||
} from "$lib/pipeline.svelte";
|
||||
import {
|
||||
@@ -49,8 +50,11 @@
|
||||
import SchemaAlignedLayout from "./SchemaAlignedLayout.svelte";
|
||||
import SchemaFields from "./SchemaFields.svelte";
|
||||
import SchemaPreview from "./SchemaPreview.svelte";
|
||||
import SortableStepCard from "./SortableStepCard.svelte";
|
||||
import StepDropIndicator from "./StepDropIndicator.svelte";
|
||||
import ToolPickerButton from "./ToolPickerButton.svelte";
|
||||
import { createSchemaToolRunner } from "./schema-tool-runner.svelte";
|
||||
import { createStepDnd } from "./step-dnd.svelte";
|
||||
import {
|
||||
createSchemaToolState,
|
||||
toolSchemaOf,
|
||||
@@ -121,9 +125,13 @@
|
||||
removeStepAt,
|
||||
toggleStep,
|
||||
replaceStepTool,
|
||||
onStepDrop,
|
||||
} = stepState;
|
||||
const steps = $derived(stepState.steps);
|
||||
const stepDnd = createStepDnd({
|
||||
onMove: (from, to) => {
|
||||
stepState.steps = moveStep(stepState.steps, from, to);
|
||||
},
|
||||
});
|
||||
|
||||
const runner = createSchemaToolRunner({
|
||||
pageSlug: () => page.slug,
|
||||
@@ -440,26 +448,13 @@
|
||||
</header>
|
||||
|
||||
{#snippet stepCard(step: ChainStep, i: number)}
|
||||
<StepCard
|
||||
index={i + 1}
|
||||
<SortableStepCard
|
||||
{step}
|
||||
index={i}
|
||||
title={stepTitle(step.id)}
|
||||
draggable
|
||||
collapsed={step.collapsed ?? false}
|
||||
ontoggle={() => toggleStep(i)}
|
||||
onremove={steps.length > 1 ? () => removeStepAt(step.key) : undefined}
|
||||
ondragstart={(e) => {
|
||||
stepState.dragFrom = i;
|
||||
if (e.dataTransfer) {
|
||||
e.dataTransfer.effectAllowed = "move";
|
||||
e.dataTransfer.setData("text/plain", String(i));
|
||||
}
|
||||
}}
|
||||
ondragover={(e) => {
|
||||
e.preventDefault();
|
||||
if (e.dataTransfer) e.dataTransfer.dropEffect = "move";
|
||||
}}
|
||||
ondrop={(e) => onStepDrop(e, i)}
|
||||
ondragend={() => (stepState.dragFrom = null)}
|
||||
>
|
||||
{#snippet tools()}
|
||||
<ToolPickerButton
|
||||
@@ -479,122 +474,147 @@
|
||||
onreset={() => resetStep(i)}
|
||||
/>
|
||||
{/if}
|
||||
</StepCard>
|
||||
</SortableStepCard>
|
||||
{/snippet}
|
||||
|
||||
{#if !alignedMode}
|
||||
<div class="workspace">
|
||||
<section class="settings">
|
||||
{#if canExtend}
|
||||
<AddStepButton onadd={(id) => addStepAt(0, id)} />
|
||||
{/if}
|
||||
{#each steps as step, i (step.key)}
|
||||
{@render stepCard(step, i)}
|
||||
{#if canExtend}
|
||||
<AddStepButton onadd={(id) => addStepAt(i + 1, id)} />
|
||||
<DragDropProvider
|
||||
onDragStart={stepDnd.onDragStart}
|
||||
onDragMove={stepDnd.onDragOver}
|
||||
onDragOver={stepDnd.onDragOver}
|
||||
onDragEnd={stepDnd.onDragEnd}
|
||||
>
|
||||
{#if !alignedMode}
|
||||
<div class="workspace">
|
||||
<section class="settings">
|
||||
{#if canExtend && !stepDnd.active}
|
||||
<AddStepButton onadd={(id) => addStepAt(0, id)} />
|
||||
{/if}
|
||||
{/each}
|
||||
</section>
|
||||
{#each steps as step, i (step.key)}
|
||||
{#if stepDnd.isIndicatorAt(i)}
|
||||
<StepDropIndicator height={stepDnd.dragHeight} />
|
||||
{/if}
|
||||
{@render stepCard(step, i)}
|
||||
{#if canExtend && !stepDnd.active}
|
||||
<AddStepButton onadd={(id) => addStepAt(i + 1, id)} />
|
||||
{/if}
|
||||
{/each}
|
||||
{#if stepDnd.isIndicatorAt(steps.length)}
|
||||
<StepDropIndicator height={stepDnd.dragHeight} />
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<SchemaPreview
|
||||
toolId={lastTool.id}
|
||||
{inputMode}
|
||||
resultKind={displayResultKind}
|
||||
running={busy}
|
||||
error={errorText}
|
||||
bind:aligned
|
||||
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>
|
||||
</div>
|
||||
{#each allWarnings as w (warningText(w))}
|
||||
<p class="warn" role="status">{warningText(w)}</p>
|
||||
{/each}
|
||||
{:else}
|
||||
<SchemaAlignedLayout
|
||||
{steps}
|
||||
{stepCard}
|
||||
{canExtend}
|
||||
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}
|
||||
<section class="panel">
|
||||
<SchemaPreview
|
||||
toolId={lastTool.id}
|
||||
{inputMode}
|
||||
resultKind={displayResultKind}
|
||||
running={busy}
|
||||
error={errorText}
|
||||
bind:aligned
|
||||
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>
|
||||
</div>
|
||||
{#each allWarnings as w (warningText(w))}
|
||||
<p class="warn" role="status">{warningText(w)}</p>
|
||||
{/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}
|
||||
<div
|
||||
class="step-ghost"
|
||||
style:width="{stepDnd.ghostWidth}px"
|
||||
style:left="{stepDnd.pointer.x - stepDnd.grabOffset.x}px"
|
||||
style:top="{stepDnd.pointer.y - stepDnd.grabOffset.y}px"
|
||||
>
|
||||
<GripVertical size={16} aria-hidden="true" />
|
||||
<span class="step-ghost-title">{stepDnd.activeData.title}</span>
|
||||
</div>
|
||||
{/if}
|
||||
</DragDropProvider>
|
||||
</div>
|
||||
{:else}
|
||||
<p class="no-schema">{t("paramsCard.noSchema")}</p>
|
||||
@@ -606,6 +626,23 @@
|
||||
clamp(var(--space-m), 4vw, var(--space-xxxl));
|
||||
flex: 1;
|
||||
}
|
||||
.step-ghost {
|
||||
position: fixed;
|
||||
z-index: var(--z-drag);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-m);
|
||||
padding: var(--space-m) var(--space-xl);
|
||||
border: var(--size-border-thick) dashed var(--color-main);
|
||||
background: var(--color-panel);
|
||||
color: var(--color-border);
|
||||
opacity: 0.85;
|
||||
pointer-events: none;
|
||||
}
|
||||
.step-ghost-title {
|
||||
font: 600 var(--font-size-m) var(--font-mono);
|
||||
color: var(--color-text);
|
||||
}
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
<script lang="ts">
|
||||
import StepCard from "$lib/components/display/StepCard.svelte";
|
||||
import type { ChainStep } from "$lib/pipeline.svelte";
|
||||
import { createSortable } from "@dnd-kit/svelte/sortable";
|
||||
import type { Snippet } from "svelte";
|
||||
|
||||
interface Props {
|
||||
step: ChainStep;
|
||||
index: number;
|
||||
title: string;
|
||||
collapsed?: boolean;
|
||||
tools?: Snippet;
|
||||
children?: Snippet;
|
||||
onremove?: () => void;
|
||||
ontoggle?: () => void;
|
||||
}
|
||||
|
||||
let {
|
||||
step,
|
||||
index,
|
||||
title,
|
||||
collapsed = false,
|
||||
tools,
|
||||
children,
|
||||
onremove,
|
||||
ontoggle,
|
||||
}: Props = $props();
|
||||
|
||||
// feedback: "none" -- no transform shifting; step-dnd.svelte.ts hides the
|
||||
// source and renders a dashed drop indicator instead.
|
||||
const sortable = createSortable({
|
||||
get id() {
|
||||
return step.key;
|
||||
},
|
||||
get index() {
|
||||
return index;
|
||||
},
|
||||
group: "steps",
|
||||
type: "step",
|
||||
accept: "step",
|
||||
feedback: "none",
|
||||
get data() {
|
||||
return { title };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<StepCard
|
||||
index={index + 1}
|
||||
{title}
|
||||
{collapsed}
|
||||
{tools}
|
||||
{children}
|
||||
{onremove}
|
||||
{ontoggle}
|
||||
elementAttachment={sortable.attach}
|
||||
handleAttachment={sortable.attachHandle}
|
||||
/>
|
||||
@@ -0,0 +1,23 @@
|
||||
<script lang="ts">
|
||||
interface Props {
|
||||
height?: number;
|
||||
}
|
||||
|
||||
let { height = 0 }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="step-drop-indicator"
|
||||
style:height={height > 0 ? `${height}px` : null}
|
||||
></div>
|
||||
|
||||
<style>
|
||||
.step-drop-indicator {
|
||||
border: var(--size-border-thick) dashed var(--color-main);
|
||||
background: var(--color-panel);
|
||||
opacity: 0.6;
|
||||
min-height: var(--space-xxl);
|
||||
pointer-events: none;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -2,7 +2,6 @@ import type { PixelImage } from "$lib/core/types";
|
||||
import {
|
||||
createStep,
|
||||
insertStep,
|
||||
moveStep,
|
||||
removeStep,
|
||||
type ChainStep,
|
||||
} from "$lib/pipeline.svelte";
|
||||
@@ -23,7 +22,6 @@ export interface SchemaToolStateCtx {
|
||||
|
||||
interface StepsState {
|
||||
steps: ChainStep[];
|
||||
dragFrom: number | null;
|
||||
}
|
||||
|
||||
export function toolSchemaOf(step: ChainStep) {
|
||||
@@ -122,7 +120,7 @@ function touchedFor(marks: Marks, key: string): SvelteSet<string> {
|
||||
// Chain-step state of SchemaToolView: the steps array, per-step edit marks
|
||||
// and all mutations. Results/execution live in schema-tool-runner.svelte.ts.
|
||||
export function createSchemaToolState(ctx: SchemaToolStateCtx) {
|
||||
const s = $state<StepsState>({ steps: [], dragFrom: null });
|
||||
const s = $state<StepsState>({ steps: [] });
|
||||
const marks: Marks = {
|
||||
touchedByStep: new SvelteMap(),
|
||||
lastAxis: {},
|
||||
@@ -134,12 +132,6 @@ export function createSchemaToolState(ctx: SchemaToolStateCtx) {
|
||||
set steps(v: ChainStep[]) {
|
||||
s.steps = v;
|
||||
},
|
||||
get dragFrom() {
|
||||
return s.dragFrom;
|
||||
},
|
||||
set dragFrom(v: number | null) {
|
||||
s.dragFrom = v;
|
||||
},
|
||||
touchedFor: (key: string) => touchedFor(marks, key),
|
||||
// Marks are keyed by step.key from the previous host; keep them and
|
||||
// stale keys leak into the new chain's fields.
|
||||
@@ -181,10 +173,5 @@ export function createSchemaToolState(ctx: SchemaToolStateCtx) {
|
||||
collapsed: step.collapsed,
|
||||
});
|
||||
},
|
||||
onStepDrop: (e: DragEvent, to: number) => {
|
||||
e.preventDefault();
|
||||
if (s.dragFrom !== null) s.steps = moveStep(s.steps, s.dragFrom, to);
|
||||
s.dragFrom = null;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
import type { Draggable, Droppable } from "@dnd-kit/dom";
|
||||
import { isSortable, type SortableDraggable } from "@dnd-kit/dom/sortable";
|
||||
|
||||
export interface StepDragData {
|
||||
title: string;
|
||||
}
|
||||
|
||||
interface StepDndOptions {
|
||||
// Compressed-index semantics: `to` counts items with the source excluded,
|
||||
// matching moveStep's remove-then-insert.
|
||||
onMove: (from: number, to: number) => void;
|
||||
}
|
||||
|
||||
interface DndEvent {
|
||||
operation?: {
|
||||
source?: Draggable | null;
|
||||
position?: { current?: { x: number; y: number } } | null;
|
||||
};
|
||||
canceled?: boolean;
|
||||
}
|
||||
|
||||
// Count the slot the pointer is over: how many non-source cards have their
|
||||
// vertical midpoint above the pointer.
|
||||
function indexAtPosition(
|
||||
source: SortableDraggable<StepDragData>,
|
||||
pointerY: number,
|
||||
): number {
|
||||
const manager = source.sortable.manager;
|
||||
let index = 0;
|
||||
if (!manager) return index;
|
||||
for (const droppable of manager.registry.droppables) {
|
||||
if (!isSortable(droppable)) continue;
|
||||
if (droppable.sortable.id === source.id) continue;
|
||||
const el = (droppable as Droppable).element;
|
||||
if (!el) continue;
|
||||
const rect = el.getBoundingClientRect();
|
||||
if (rect.height === 0) continue;
|
||||
if (pointerY > rect.top + rect.height / 2) index++;
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
// Step-reorder state for SchemaToolView, single list. The dragged card is
|
||||
// hidden (DragOverlay renders the ghost) and a dashed indicator of the
|
||||
// dragged height marks the insertion slot between the remaining cards.
|
||||
export function createStepDnd({ onMove }: StepDndOptions) {
|
||||
let activeData = $state<StepDragData | null>(null);
|
||||
let indicatorIndex = $state<number | null>(null);
|
||||
let dragHeight = $state(0);
|
||||
let sourceIndex = $state<number | null>(null);
|
||||
let sourceElement: HTMLElement | null = null;
|
||||
// Ghost positioning: current pointer, grab offset inside the card and the
|
||||
// card width. DragOverlay is not used -- the Feedback plugin skips
|
||||
// draggables with feedback: "none", so the ghost follows the pointer here.
|
||||
let pointer = $state<{ x: number; y: number } | null>(null);
|
||||
let grabOffset = $state<{ x: number; y: number }>({ x: 0, y: 0 });
|
||||
let ghostWidth = $state(0);
|
||||
// X-range of the source card: dragging sideways over the preview panel
|
||||
// must not light up the indicator.
|
||||
let sourceX: { left: number; right: number } | null = null;
|
||||
|
||||
function computeIndicator(event: DndEvent): number | null {
|
||||
const source = event.operation?.source;
|
||||
if (!source || !isSortable(source)) return null;
|
||||
const position = event.operation?.position?.current;
|
||||
if (!position || !sourceX) return null;
|
||||
if (position.x < sourceX.left || position.x > sourceX.right) return null;
|
||||
return indexAtPosition(
|
||||
source as SortableDraggable<StepDragData>,
|
||||
position.y,
|
||||
);
|
||||
}
|
||||
|
||||
function onDragStart(event: DndEvent) {
|
||||
const source = event.operation?.source;
|
||||
if (!source) return;
|
||||
activeData = (source.data as StepDragData | undefined) ?? null;
|
||||
sourceElement = (source.element as HTMLElement | undefined) ?? null;
|
||||
const rect = sourceElement?.getBoundingClientRect();
|
||||
dragHeight = rect?.height ?? 0;
|
||||
ghostWidth = rect?.width ?? 0;
|
||||
sourceX = rect ? { left: rect.left, right: rect.right } : null;
|
||||
const start = event.operation?.position?.current;
|
||||
grabOffset =
|
||||
start && rect
|
||||
? { x: start.x - rect.left, y: start.y - rect.top }
|
||||
: { x: 0, y: 0 };
|
||||
pointer = start ?? null;
|
||||
if (isSortable(source)) sourceIndex = source.sortable.initialIndex;
|
||||
if (sourceElement) sourceElement.style.display = "none";
|
||||
}
|
||||
|
||||
function onDragOver(event: DndEvent) {
|
||||
pointer = event.operation?.position?.current ?? pointer;
|
||||
indicatorIndex = computeIndicator(event);
|
||||
}
|
||||
|
||||
function onDragEnd(event: DndEvent) {
|
||||
const drop = event.canceled ? null : computeIndicator(event);
|
||||
if (sourceElement) {
|
||||
sourceElement.style.display = "";
|
||||
sourceElement = null;
|
||||
}
|
||||
const from = sourceIndex;
|
||||
activeData = null;
|
||||
indicatorIndex = null;
|
||||
dragHeight = 0;
|
||||
sourceIndex = null;
|
||||
sourceX = null;
|
||||
pointer = null;
|
||||
if (from === null || drop === null || drop === from) return;
|
||||
onMove(from, drop);
|
||||
}
|
||||
|
||||
// Slots render in the full list where the hidden source still occupies
|
||||
// its DOM position: a compressed index at/past the source shifts by 1.
|
||||
function isIndicatorAt(slot: number): boolean {
|
||||
if (indicatorIndex === null) return false;
|
||||
let renderIndex = indicatorIndex;
|
||||
if (sourceIndex !== null && indicatorIndex >= sourceIndex) {
|
||||
renderIndex += 1;
|
||||
}
|
||||
return renderIndex === slot;
|
||||
}
|
||||
|
||||
return {
|
||||
get activeData() {
|
||||
return activeData;
|
||||
},
|
||||
get dragHeight() {
|
||||
return dragHeight;
|
||||
},
|
||||
get pointer() {
|
||||
return pointer;
|
||||
},
|
||||
get grabOffset() {
|
||||
return grabOffset;
|
||||
},
|
||||
get ghostWidth() {
|
||||
return ghostWidth;
|
||||
},
|
||||
get active() {
|
||||
return activeData !== null;
|
||||
},
|
||||
onDragStart,
|
||||
onDragOver,
|
||||
onDragEnd,
|
||||
isIndicatorAt,
|
||||
};
|
||||
}
|
||||
|
||||
export type StepDnd = ReturnType<typeof createStepDnd>;
|
||||
Reference in New Issue
Block a user