mirror of
https://github.com/Ku6epXBOCTuK/easy-png-tools.git
synced 2026-09-14 21:46:35 +00:00
style: fix formatting via prettier
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
<script lang="ts">
|
||||
import Button from './ui/Button.svelte';
|
||||
import { downloadBlob, encode } from '$lib/core/io';
|
||||
import type { PixelImage } from '$lib/core/types';
|
||||
import type { OutputFormat } from '$lib/registry';
|
||||
import { t } from '$lib/i18n/t';
|
||||
import Button from "./ui/Button.svelte";
|
||||
import { downloadBlob, encode } from "$lib/core/io";
|
||||
import type { PixelImage } from "$lib/core/types";
|
||||
import type { OutputFormat } from "$lib/registry";
|
||||
import { t } from "$lib/i18n/t";
|
||||
|
||||
interface Props {
|
||||
image: PixelImage | null;
|
||||
@@ -21,9 +21,13 @@
|
||||
if (!image || !format || busy) return;
|
||||
busy = true;
|
||||
try {
|
||||
const raw = format.qualityParamId ? params[format.qualityParamId] : undefined;
|
||||
const raw = format.qualityParamId
|
||||
? params[format.qualityParamId]
|
||||
: undefined;
|
||||
const quality =
|
||||
typeof raw === 'number' ? Math.min(Math.max(raw, 1), 100) / 100 : undefined;
|
||||
typeof raw === "number"
|
||||
? Math.min(Math.max(raw, 1), 100) / 100
|
||||
: undefined;
|
||||
const blob = await encode(image, format.mime, quality);
|
||||
downloadBlob(blob, `${baseName}.${format.ext}`);
|
||||
} catch (e) {
|
||||
@@ -39,8 +43,8 @@
|
||||
fullWidth
|
||||
disabled={!image || !format}
|
||||
{busy}
|
||||
busyText={t('download.busy')}
|
||||
busyText={t("download.busy")}
|
||||
onclick={download}
|
||||
>
|
||||
{t('download.file', { ext: format?.ext ?? 'png' })}
|
||||
{t("download.file", { ext: format?.ext ?? "png" })}
|
||||
</Button>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
import { isSupportedImage, unsupportedImageError } from '$lib/core/io';
|
||||
import { t } from '$lib/i18n/t';
|
||||
import type { Snippet } from "svelte";
|
||||
import { isSupportedImage, unsupportedImageError } from "$lib/core/io";
|
||||
import { t } from "$lib/i18n/t";
|
||||
|
||||
interface Props {
|
||||
onFile: (file: File) => void;
|
||||
@@ -13,8 +13,8 @@
|
||||
let {
|
||||
onFile,
|
||||
onError,
|
||||
label = t('dropZone.overlayDefault'),
|
||||
children
|
||||
label = t("dropZone.overlayDefault"),
|
||||
children,
|
||||
}: Props = $props();
|
||||
|
||||
let depth = $state(0);
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { ACCEPTED_IMAGE_TYPES, isSupportedImage, unsupportedImageError } from '$lib/core/io';
|
||||
import { t } from '$lib/i18n/t';
|
||||
import {
|
||||
ACCEPTED_IMAGE_TYPES,
|
||||
isSupportedImage,
|
||||
unsupportedImageError,
|
||||
} from "$lib/core/io";
|
||||
import { t } from "$lib/i18n/t";
|
||||
|
||||
interface Props {
|
||||
onFile: (file: File) => void;
|
||||
@@ -8,7 +12,7 @@
|
||||
label?: string;
|
||||
}
|
||||
|
||||
let { onFile, onError, label = t('dropZone.pickDefault') }: Props = $props();
|
||||
let { onFile, onError, label = t("dropZone.pickDefault") }: Props = $props();
|
||||
|
||||
let input = $state<HTMLInputElement | undefined>();
|
||||
let depth = $state(0);
|
||||
@@ -52,7 +56,7 @@
|
||||
aria-label={label}
|
||||
onclick={openPicker}
|
||||
onkeydown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.preventDefault();
|
||||
openPicker();
|
||||
}
|
||||
@@ -78,7 +82,7 @@
|
||||
onchange={() => {
|
||||
accept(input?.files?.[0]);
|
||||
if (input) {
|
||||
input.value = '';
|
||||
input.value = "";
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script lang="ts">
|
||||
import type { ImageInfo } from '$lib/core/analyze';
|
||||
import { LOCALE_TAGS } from '$lib/i18n/dict';
|
||||
import { getLocale } from '$lib/i18n/locale.svelte';
|
||||
import { t } from '$lib/i18n/t';
|
||||
import type { ImageInfo } from "$lib/core/analyze";
|
||||
import { LOCALE_TAGS } from "$lib/i18n/dict";
|
||||
import { getLocale } from "$lib/i18n/locale.svelte";
|
||||
import { t } from "$lib/i18n/t";
|
||||
|
||||
interface Props {
|
||||
info: ImageInfo | null;
|
||||
@@ -14,15 +14,17 @@
|
||||
{#if info}
|
||||
<dl>
|
||||
<div class="panel">
|
||||
<dt>{t('infoPanel.dimensions')}</dt>
|
||||
<dt>{t("infoPanel.dimensions")}</dt>
|
||||
<dd>{info.width} × {info.height} px</dd>
|
||||
</div>
|
||||
<div class="panel">
|
||||
<dt>{t('infoPanel.alpha')}</dt>
|
||||
<dd>{info.hasAlpha ? t('infoPanel.alphaYes') : t('infoPanel.alphaNo')}</dd>
|
||||
<dt>{t("infoPanel.alpha")}</dt>
|
||||
<dd>
|
||||
{info.hasAlpha ? t("infoPanel.alphaYes") : t("infoPanel.alphaNo")}
|
||||
</dd>
|
||||
</div>
|
||||
<div class="panel">
|
||||
<dt>{t('infoPanel.colorCount')}</dt>
|
||||
<dt>{t("infoPanel.colorCount")}</dt>
|
||||
<dd>{info.colorCount.toLocaleString(LOCALE_TAGS[getLocale()])}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<script lang="ts">
|
||||
import CheckboxField from './ui/CheckboxField.svelte';
|
||||
import ColorField from './ui/ColorField.svelte';
|
||||
import SelectField from './ui/SelectField.svelte';
|
||||
import SliderField from './ui/SliderField.svelte';
|
||||
import TextField from './ui/TextField.svelte';
|
||||
import type { ParamDef, ToolEntry } from '$lib/registry';
|
||||
import { optionLabel, paramLabel } from '$lib/i18n/tool-strings';
|
||||
import { t } from '$lib/i18n/t';
|
||||
import CheckboxField from "./ui/CheckboxField.svelte";
|
||||
import ColorField from "./ui/ColorField.svelte";
|
||||
import SelectField from "./ui/SelectField.svelte";
|
||||
import SliderField from "./ui/SliderField.svelte";
|
||||
import TextField from "./ui/TextField.svelte";
|
||||
import type { ParamDef, ToolEntry } from "$lib/registry";
|
||||
import { optionLabel, paramLabel } from "$lib/i18n/tool-strings";
|
||||
import { t } from "$lib/i18n/t";
|
||||
|
||||
interface Props {
|
||||
tool: ToolEntry;
|
||||
@@ -25,19 +25,27 @@
|
||||
pipetteTargetId = null,
|
||||
onPipetteToggle,
|
||||
hasMask = false,
|
||||
showMask = $bindable(false)
|
||||
showMask = $bindable(false),
|
||||
}: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class="params-grid">
|
||||
{#if hasMask}
|
||||
<CheckboxField id="show-mask" label={t('ui.showMask')} bind:checked={showMask} />
|
||||
<CheckboxField
|
||||
id="show-mask"
|
||||
label={t("ui.showMask")}
|
||||
bind:checked={showMask}
|
||||
/>
|
||||
{/if}
|
||||
{#each params as param (param.id)}
|
||||
<div class="field">
|
||||
{#if param.type === 'checkbox'}
|
||||
<CheckboxField id={param.id} label={paramLabel(tool, param)} bind:checked={values[param.id]} />
|
||||
{:else if param.type === 'number'}
|
||||
{#if param.type === "checkbox"}
|
||||
<CheckboxField
|
||||
id={param.id}
|
||||
label={paramLabel(tool, param)}
|
||||
bind:checked={values[param.id]}
|
||||
/>
|
||||
{:else if param.type === "number"}
|
||||
<TextField
|
||||
id={param.id}
|
||||
label={paramLabel(tool, param)}
|
||||
@@ -47,7 +55,7 @@
|
||||
step={param.step}
|
||||
bind:value={values[param.id]}
|
||||
/>
|
||||
{:else if param.type === 'slider'}
|
||||
{:else if param.type === "slider"}
|
||||
<SliderField
|
||||
id={param.id}
|
||||
label={paramLabel(tool, param)}
|
||||
@@ -57,17 +65,17 @@
|
||||
default={param.default}
|
||||
bind:value={values[param.id]}
|
||||
/>
|
||||
{:else if param.type === 'select'}
|
||||
{:else if param.type === "select"}
|
||||
<SelectField
|
||||
id={param.id}
|
||||
label={paramLabel(tool, param)}
|
||||
options={param.options.map((o) => ({
|
||||
value: o.value,
|
||||
label: optionLabel(tool, param, o.value)
|
||||
label: optionLabel(tool, param, o.value),
|
||||
}))}
|
||||
bind:value={values[param.id]}
|
||||
/>
|
||||
{:else if param.type === 'color'}
|
||||
{:else if param.type === "color"}
|
||||
<ColorField
|
||||
id={param.id}
|
||||
label={paramLabel(tool, param)}
|
||||
@@ -75,7 +83,7 @@
|
||||
pipetteActive={pipetteTargetId === param.id}
|
||||
onPipetteToggle={() => onPipetteToggle?.(param.id)}
|
||||
/>
|
||||
{:else if param.type === 'text'}
|
||||
{:else if param.type === "text"}
|
||||
<TextField
|
||||
id={param.id}
|
||||
label={paramLabel(tool, param)}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { rgbToHex } from '$lib/core/color';
|
||||
import type { PixelImage } from '$lib/core/types';
|
||||
import PipetteLoupe from './tool/PipetteLoupe.svelte';
|
||||
import { rgbToHex } from "$lib/core/color";
|
||||
import type { PixelImage } from "$lib/core/types";
|
||||
import PipetteLoupe from "./tool/PipetteLoupe.svelte";
|
||||
|
||||
interface Props {
|
||||
image: PixelImage | null;
|
||||
@@ -12,27 +12,42 @@
|
||||
let { image, pipetteActive = false, onPickColor }: Props = $props();
|
||||
|
||||
let canvas = $state<HTMLCanvasElement | undefined>();
|
||||
let hover = $state<{ clientX: number; clientY: number; px: number; py: number; hex: string } | null>(
|
||||
null
|
||||
);
|
||||
let hover = $state<{
|
||||
clientX: number;
|
||||
clientY: number;
|
||||
px: number;
|
||||
py: number;
|
||||
hex: string;
|
||||
} | null>(null);
|
||||
|
||||
$effect(() => {
|
||||
if (!canvas || !image) return;
|
||||
canvas.width = image.width;
|
||||
canvas.height = image.height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) return;
|
||||
ctx.putImageData(new ImageData(image.data, image.width, image.height), 0, 0);
|
||||
ctx.putImageData(
|
||||
new ImageData(image.data, image.width, image.height),
|
||||
0,
|
||||
0,
|
||||
);
|
||||
});
|
||||
|
||||
function pixelAt(event: MouseEvent): { px: number; py: number; hex: string } | null {
|
||||
function pixelAt(
|
||||
event: MouseEvent,
|
||||
): { px: number; py: number; hex: string } | null {
|
||||
if (!canvas || !image) return null;
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
if (rect.width === 0 || rect.height === 0) return null;
|
||||
const px = Math.floor((event.clientX - rect.left) * (canvas.width / rect.width));
|
||||
const py = Math.floor((event.clientY - rect.top) * (canvas.height / rect.height));
|
||||
if (px < 0 || py < 0 || px >= canvas.width || py >= canvas.height) return null;
|
||||
const ctx = canvas.getContext('2d');
|
||||
const px = Math.floor(
|
||||
(event.clientX - rect.left) * (canvas.width / rect.width),
|
||||
);
|
||||
const py = Math.floor(
|
||||
(event.clientY - rect.top) * (canvas.height / rect.height),
|
||||
);
|
||||
if (px < 0 || py < 0 || px >= canvas.width || py >= canvas.height)
|
||||
return null;
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) return null;
|
||||
const [r, g, b] = ctx.getImageData(px, py, 1, 1).data;
|
||||
return { px, py, hex: rgbToHex(r, g, b) };
|
||||
@@ -53,7 +68,13 @@
|
||||
}
|
||||
const pixel = pixelAt(event);
|
||||
hover = pixel
|
||||
? { clientX: event.clientX, clientY: event.clientY, px: pixel.px, py: pixel.py, hex: pixel.hex }
|
||||
? {
|
||||
clientX: event.clientX,
|
||||
clientY: event.clientY,
|
||||
px: pixel.px,
|
||||
py: pixel.py,
|
||||
hex: pixel.hex,
|
||||
}
|
||||
: null;
|
||||
}
|
||||
|
||||
@@ -90,8 +111,10 @@
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
max-height: 32rem;
|
||||
background:
|
||||
repeating-conic-gradient(var(--check-a) 0% 25%, var(--check-b) 0% 50%);
|
||||
background: repeating-conic-gradient(
|
||||
var(--check-a) 0% 25%,
|
||||
var(--check-b) 0% 50%
|
||||
);
|
||||
background-size: 16px 16px;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
@@ -1,60 +1,81 @@
|
||||
<script lang="ts">
|
||||
import { imageInfo, type ImageInfo } from '$lib/core/analyze';
|
||||
import { ToolError } from '$lib/core/errors';
|
||||
import { decodeFile, isSupportedImage, unsupportedImageError } from '$lib/core/io';
|
||||
import type { PixelImage } from '$lib/core/types';
|
||||
import { defaultParams, getTool, outputOf, sanitizeParams, type ToolEntry } from '$lib/registry';
|
||||
import { loadStoredSteps, newStepId, saveSteps, type PipelineStep } from '$lib/tools/pipeline';
|
||||
import { TOOL_ICONS } from '$lib/tools/tool-icons';
|
||||
import DownloadButton from './DownloadButton.svelte';
|
||||
import ParamForm from './ParamForm.svelte';
|
||||
import Preview from './Preview.svelte';
|
||||
import ToolSearch from './search/ToolSearch.svelte';
|
||||
import { createAutoRunner } from '$lib/tools/auto-run';
|
||||
import { executeStep } from '$lib/tools/executor';
|
||||
import { clearOverlay, setOverlay } from '$lib/tools/overlay-store.svelte';
|
||||
import { t } from '$lib/i18n/t';
|
||||
import { toolDescription, toolTitle } from '$lib/i18n/tool-strings';
|
||||
import type { StageStatus } from './stage/stage-props';
|
||||
import ChainToolBlock from './chain/ChainToolBlock.svelte';
|
||||
import ToolStage from './stage/ToolStage.svelte';
|
||||
import ToolStageClassic from './stage/ToolStageClassic.svelte';
|
||||
import { imageInfo, type ImageInfo } from "$lib/core/analyze";
|
||||
import { ToolError } from "$lib/core/errors";
|
||||
import {
|
||||
decodeFile,
|
||||
isSupportedImage,
|
||||
unsupportedImageError,
|
||||
} from "$lib/core/io";
|
||||
import type { PixelImage } from "$lib/core/types";
|
||||
import {
|
||||
defaultParams,
|
||||
getTool,
|
||||
outputOf,
|
||||
sanitizeParams,
|
||||
type ToolEntry,
|
||||
} from "$lib/registry";
|
||||
import {
|
||||
loadStoredSteps,
|
||||
newStepId,
|
||||
saveSteps,
|
||||
type PipelineStep,
|
||||
} from "$lib/tools/pipeline";
|
||||
import { TOOL_ICONS } from "$lib/tools/tool-icons";
|
||||
import DownloadButton from "./DownloadButton.svelte";
|
||||
import ParamForm from "./ParamForm.svelte";
|
||||
import Preview from "./Preview.svelte";
|
||||
import ToolSearch from "./search/ToolSearch.svelte";
|
||||
import { createAutoRunner } from "$lib/tools/auto-run";
|
||||
import { executeStep } from "$lib/tools/executor";
|
||||
import { clearOverlay, setOverlay } from "$lib/tools/overlay-store.svelte";
|
||||
import { t } from "$lib/i18n/t";
|
||||
import { toolDescription, toolTitle } from "$lib/i18n/tool-strings";
|
||||
import type { StageStatus } from "./stage/stage-props";
|
||||
import ChainToolBlock from "./chain/ChainToolBlock.svelte";
|
||||
import ToolStage from "./stage/ToolStage.svelte";
|
||||
import ToolStageClassic from "./stage/ToolStageClassic.svelte";
|
||||
|
||||
export type PresetStep = { toolId: string; values?: Record<string, unknown> };
|
||||
|
||||
let {
|
||||
tool,
|
||||
restoreChain = false,
|
||||
stageVariant = 'inline',
|
||||
stageVariant = "inline",
|
||||
presetBaseValues,
|
||||
presetChain
|
||||
presetChain,
|
||||
}: {
|
||||
tool: ToolEntry;
|
||||
restoreChain?: boolean;
|
||||
stageVariant?: 'classic' | 'inline';
|
||||
stageVariant?: "classic" | "inline";
|
||||
presetBaseValues?: Record<string, unknown>;
|
||||
presetChain?: PresetStep[];
|
||||
} = $props();
|
||||
|
||||
const StageComponent = $derived(stageVariant === 'classic' ? ToolStageClassic : ToolStage);
|
||||
const StageComponent = $derived(
|
||||
stageVariant === "classic" ? ToolStageClassic : ToolStage,
|
||||
);
|
||||
|
||||
type Status = StageStatus;
|
||||
|
||||
const isPreset = $derived(
|
||||
(presetChain?.length ?? 0) > 0 || Object.keys(presetBaseValues ?? {}).length > 0
|
||||
(presetChain?.length ?? 0) > 0 ||
|
||||
Object.keys(presetBaseValues ?? {}).length > 0,
|
||||
);
|
||||
|
||||
let status = $state<Status>('idle');
|
||||
let status = $state<Status>("idle");
|
||||
let source = $state<PixelImage | null>(null);
|
||||
let result = $state<PixelImage | null>(null);
|
||||
let previewResult = $state<PixelImage | null>(null);
|
||||
let textResult = $state<string | null>(null);
|
||||
let showMask = $state(false);
|
||||
let info = $state<ImageInfo | null>(null);
|
||||
let errorText = $state('');
|
||||
let errorText = $state("");
|
||||
let overlayImage = $state<PixelImage | null>(null);
|
||||
// svelte-ignore state_referenced_locally
|
||||
let values = $state<Record<string, any>>({ ...defaultParams(tool), ...presetBaseValues });
|
||||
let values = $state<Record<string, any>>({
|
||||
...defaultParams(tool),
|
||||
...presetBaseValues,
|
||||
});
|
||||
// svelte-ignore state_referenced_locally
|
||||
let chain = $state<PipelineStep[]>(
|
||||
restoreChain
|
||||
@@ -66,28 +87,30 @@
|
||||
{
|
||||
id: newStepId(),
|
||||
toolId: preset.toolId,
|
||||
values: { ...defaultParams(stepTool), ...preset.values }
|
||||
}
|
||||
values: { ...defaultParams(stepTool), ...preset.values },
|
||||
},
|
||||
]
|
||||
: [];
|
||||
})
|
||||
}),
|
||||
);
|
||||
let chainResults = $state<(PixelImage | null)[]>([]);
|
||||
let lastRunChainJson = '';
|
||||
let lastRunChainJson = "";
|
||||
|
||||
const isInfo = $derived(tool.resultType === 'info');
|
||||
const isSourceless = $derived(tool.sourceMode === 'none');
|
||||
const isTextSource = $derived(tool.sourceMode === 'text');
|
||||
const isInfo = $derived(tool.resultType === "info");
|
||||
const isSourceless = $derived(tool.sourceMode === "none");
|
||||
const isTextSource = $derived(tool.sourceMode === "text");
|
||||
const sanitized = $derived(sanitizeParams(tool, values));
|
||||
const canChainBase = $derived(
|
||||
(tool.resultType ?? 'image') === 'image' && tool.sourceMode !== 'text'
|
||||
(tool.resultType ?? "image") === "image" && tool.sourceMode !== "text",
|
||||
);
|
||||
const hasMask = $derived(typeof tool.preview === 'function');
|
||||
const shownBase = $derived(showMask && previewResult ? previewResult : result);
|
||||
const hasFilledSteps = $derived(chain.some((step) => step.toolId !== ''));
|
||||
const hasMask = $derived(typeof tool.preview === "function");
|
||||
const shownBase = $derived(
|
||||
showMask && previewResult ? previewResult : result,
|
||||
);
|
||||
const hasFilledSteps = $derived(chain.some((step) => step.toolId !== ""));
|
||||
|
||||
function addChainStep() {
|
||||
chain.push({ id: newStepId(), toolId: '', values: {} });
|
||||
chain.push({ id: newStepId(), toolId: "", values: {} });
|
||||
}
|
||||
|
||||
function removeChainStep(index: number) {
|
||||
@@ -118,7 +141,7 @@
|
||||
const runner = createAutoRunner();
|
||||
let hasLastRun = false;
|
||||
let lastRunSource: PixelImage | null = null;
|
||||
let lastRunValuesJson = '';
|
||||
let lastRunValuesJson = "";
|
||||
let pipetteTargetId = $state<string | null>(null);
|
||||
|
||||
function handlePipetteToggle(id: string) {
|
||||
@@ -132,8 +155,8 @@
|
||||
}
|
||||
|
||||
async function handleFile(file: File) {
|
||||
errorText = '';
|
||||
status = 'processing';
|
||||
errorText = "";
|
||||
status = "processing";
|
||||
try {
|
||||
source = await decodeFile(file);
|
||||
values = defaultParams(tool);
|
||||
@@ -143,7 +166,7 @@
|
||||
pipetteTargetId = null;
|
||||
info = isInfo ? imageInfo(source) : null;
|
||||
if (isInfo) {
|
||||
status = 'loaded';
|
||||
status = "loaded";
|
||||
} else {
|
||||
await runTool();
|
||||
}
|
||||
@@ -154,15 +177,15 @@
|
||||
|
||||
async function handleTextSubmit(text: string) {
|
||||
if (!isTextSource) return;
|
||||
errorText = '';
|
||||
status = 'processing';
|
||||
errorText = "";
|
||||
status = "processing";
|
||||
if (tool.textToText) {
|
||||
try {
|
||||
result = null;
|
||||
previewResult = null;
|
||||
info = null;
|
||||
textResult = await tool.textToText(text);
|
||||
status = 'loaded';
|
||||
status = "loaded";
|
||||
} catch (e) {
|
||||
showError(e);
|
||||
}
|
||||
@@ -196,7 +219,7 @@
|
||||
let next: PixelImage | null = null;
|
||||
let nextText: string | null = null;
|
||||
|
||||
if (tool.resultType === 'text') {
|
||||
if (tool.resultType === "text") {
|
||||
nextText = await tool.toText!(source!, sanitized);
|
||||
} else if (isSourceless) {
|
||||
next = await tool.generate!(sanitized);
|
||||
@@ -221,23 +244,31 @@
|
||||
let current: PixelImage | null = next ?? source;
|
||||
for (let i = 0; i < chain.length; i++) {
|
||||
const step = chain[i];
|
||||
const stepTool = step.toolId === '' ? undefined : getTool(step.toolId);
|
||||
const stepTool = step.toolId === "" ? undefined : getTool(step.toolId);
|
||||
if (!current || !stepTool?.run) {
|
||||
collected.push(null);
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
current = await executeStep(stepTool, current, sanitizeParams(stepTool, step.values));
|
||||
current = await executeStep(
|
||||
stepTool,
|
||||
current,
|
||||
sanitizeParams(stepTool, step.values),
|
||||
);
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
t('toolPage.stepError', { n: i + 2, title: toolTitle(stepTool), msg: errorMessage(e) })
|
||||
t("toolPage.stepError", {
|
||||
n: i + 2,
|
||||
title: toolTitle(stepTool),
|
||||
msg: errorMessage(e),
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (!runner.isCurrent(token)) return;
|
||||
collected.push(current);
|
||||
}
|
||||
chainResults = collected;
|
||||
status = 'loaded';
|
||||
status = "loaded";
|
||||
} catch (e) {
|
||||
if (!runner.isCurrent(token)) return;
|
||||
showError(e);
|
||||
@@ -262,7 +293,7 @@
|
||||
|
||||
$effect(() => {
|
||||
if (isPreset) return;
|
||||
const filled = chain.filter((step) => step.toolId !== '');
|
||||
const filled = chain.filter((step) => step.toolId !== "");
|
||||
if (filled.length === 0 && !hasLastRun) return;
|
||||
saveSteps(filled);
|
||||
});
|
||||
@@ -273,7 +304,7 @@
|
||||
}
|
||||
|
||||
function showError(e: unknown) {
|
||||
status = source ? 'loaded' : 'idle';
|
||||
status = source ? "loaded" : "idle";
|
||||
errorText = errorMessage(e);
|
||||
}
|
||||
|
||||
@@ -285,8 +316,8 @@
|
||||
showMask = false;
|
||||
pipetteTargetId = null;
|
||||
info = null;
|
||||
errorText = '';
|
||||
status = 'idle';
|
||||
errorText = "";
|
||||
status = "idle";
|
||||
clearOverlay();
|
||||
values = { ...defaultParams(tool), ...presetBaseValues };
|
||||
}
|
||||
@@ -303,7 +334,7 @@
|
||||
const items = event.clipboardData?.items;
|
||||
if (!items) return;
|
||||
for (const item of items) {
|
||||
if (!item.type.startsWith('image/')) continue;
|
||||
if (!item.type.startsWith("image/")) continue;
|
||||
const file = item.getAsFile();
|
||||
if (file) {
|
||||
event.preventDefault();
|
||||
@@ -331,32 +362,32 @@
|
||||
<StageComponent
|
||||
mode="base"
|
||||
{tool}
|
||||
source={source}
|
||||
result={result}
|
||||
{source}
|
||||
{result}
|
||||
displayImage={shownBase}
|
||||
info={info}
|
||||
status={status}
|
||||
isInfo={isInfo}
|
||||
isSourceless={isSourceless}
|
||||
isTextSource={isTextSource}
|
||||
textResult={textResult}
|
||||
sanitized={sanitized}
|
||||
canChainBase={canChainBase}
|
||||
{info}
|
||||
{status}
|
||||
{isInfo}
|
||||
{isSourceless}
|
||||
{isTextSource}
|
||||
{textResult}
|
||||
{sanitized}
|
||||
{canChainBase}
|
||||
hasChain={chain.length > 0}
|
||||
hasMask={hasMask}
|
||||
{hasMask}
|
||||
bind:showMask
|
||||
bind:values
|
||||
pipetteTargetId={pipetteTargetId}
|
||||
handleFile={handleFile}
|
||||
{pipetteTargetId}
|
||||
{handleFile}
|
||||
onTextSubmit={handleTextSubmit}
|
||||
onSourceError={(e) => (errorText = errorMessage(e))}
|
||||
reset={reset}
|
||||
toggleChain={toggleChain}
|
||||
handlePipetteToggle={handlePipetteToggle}
|
||||
handlePickColor={handlePickColor}
|
||||
showError={showError}
|
||||
errorMessage={errorMessage}
|
||||
overlayImage={overlayImage}
|
||||
{reset}
|
||||
{toggleChain}
|
||||
{handlePipetteToggle}
|
||||
{handlePickColor}
|
||||
{showError}
|
||||
{errorMessage}
|
||||
{overlayImage}
|
||||
onOverlayFile={(f) => void handleOverlayFile(f)}
|
||||
onOverlayError={(e) => (errorText = errorMessage(e))}
|
||||
onOverlayClear={clearOverlay}
|
||||
@@ -364,33 +395,38 @@
|
||||
|
||||
<div class="chain-stack">
|
||||
{#each chain as step, index (step.id)}
|
||||
{#if step.toolId === ''}
|
||||
{#if step.toolId === ""}
|
||||
<div class="panel empty-slot">
|
||||
<header>
|
||||
<h3 class="heading-section">{t('toolPage.stepHeading', { n: index + 2 })}</h3>
|
||||
<button
|
||||
type="button"
|
||||
class="remove-step"
|
||||
aria-label={t('toolPage.removeStepAria')}
|
||||
onclick={() => removeChainStep(index)}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</header>
|
||||
<ToolSearch onSelect={(id) => applyChainTool(index, id)} chainableOnly />
|
||||
<header>
|
||||
<h3 class="heading-section">
|
||||
{t("toolPage.stepHeading", { n: index + 2 })}
|
||||
</h3>
|
||||
<button
|
||||
type="button"
|
||||
class="remove-step"
|
||||
aria-label={t("toolPage.removeStepAria")}
|
||||
onclick={() => removeChainStep(index)}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</header>
|
||||
<ToolSearch
|
||||
onSelect={(id) => applyChainTool(index, id)}
|
||||
chainableOnly
|
||||
/>
|
||||
</div>
|
||||
{:else if getTool(step.toolId)}
|
||||
{@const stepTool = getTool(step.toolId)!}
|
||||
{#if stageVariant === 'inline'}
|
||||
{#if stageVariant === "inline"}
|
||||
{@const StepIcon = TOOL_ICONS[stepTool.id]}
|
||||
<ToolStage
|
||||
mode="chain"
|
||||
index={index}
|
||||
{index}
|
||||
tool={stepTool}
|
||||
bind:values={step.values}
|
||||
input={index === 0 ? result : (chainResults[index - 1] ?? null)}
|
||||
result={chainResults[index] ?? null}
|
||||
busy={status === 'processing'}
|
||||
busy={status === "processing"}
|
||||
isLast={index === chain.length - 1}
|
||||
onRemove={() => removeChainStep(index)}
|
||||
onError={showError}
|
||||
@@ -404,12 +440,15 @@
|
||||
<StepIcon size={14} strokeWidth={2} />
|
||||
</span>
|
||||
{/if}
|
||||
{t('chain.stepLabel', { n: index + 2, title: toolTitle(stepTool) })}
|
||||
{t("chain.stepLabel", {
|
||||
n: index + 2,
|
||||
title: toolTitle(stepTool),
|
||||
})}
|
||||
<button
|
||||
type="button"
|
||||
class="remove-step"
|
||||
aria-label={t('toolPage.removeStepAria')}
|
||||
title={t('toolPage.removeStepAria')}
|
||||
aria-label={t("toolPage.removeStepAria")}
|
||||
title={t("toolPage.removeStepAria")}
|
||||
onclick={() => removeChainStep(index)}
|
||||
>
|
||||
✕
|
||||
@@ -419,12 +458,12 @@
|
||||
</ToolStage>
|
||||
{:else}
|
||||
<ChainToolBlock
|
||||
index={index}
|
||||
{index}
|
||||
tool={stepTool}
|
||||
bind:values={step.values}
|
||||
input={index === 0 ? result : (chainResults[index - 1] ?? null)}
|
||||
result={chainResults[index] ?? null}
|
||||
busy={status === 'processing'}
|
||||
busy={status === "processing"}
|
||||
isLast={index === chain.length - 1}
|
||||
onRemove={() => removeChainStep(index)}
|
||||
onError={showError}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<script lang="ts">
|
||||
import { outputOf, sanitizeParams, type ToolEntry } from '$lib/registry';
|
||||
import type { PixelImage } from '$lib/core/types';
|
||||
import { t } from '$lib/i18n/t';
|
||||
import { toolTitle } from '$lib/i18n/tool-strings';
|
||||
import DownloadButton from '../DownloadButton.svelte';
|
||||
import Button from '../ui/Button.svelte';
|
||||
import EmptyState from '../ui/EmptyState.svelte';
|
||||
import ParamsCard from '../tool/ParamsCard.svelte';
|
||||
import Preview from '../Preview.svelte';
|
||||
import { TOOL_ICONS } from '$lib/tools/tool-icons';
|
||||
import { outputOf, sanitizeParams, type ToolEntry } from "$lib/registry";
|
||||
import type { PixelImage } from "$lib/core/types";
|
||||
import { t } from "$lib/i18n/t";
|
||||
import { toolTitle } from "$lib/i18n/tool-strings";
|
||||
import DownloadButton from "../DownloadButton.svelte";
|
||||
import Button from "../ui/Button.svelte";
|
||||
import EmptyState from "../ui/EmptyState.svelte";
|
||||
import ParamsCard from "../tool/ParamsCard.svelte";
|
||||
import Preview from "../Preview.svelte";
|
||||
import { TOOL_ICONS } from "$lib/tools/tool-icons";
|
||||
|
||||
interface Props {
|
||||
index: number;
|
||||
@@ -35,7 +35,7 @@
|
||||
onRemove,
|
||||
onError,
|
||||
onAddStep,
|
||||
onRemoveChain
|
||||
onRemoveChain,
|
||||
}: Props = $props();
|
||||
|
||||
const format = $derived(outputOf(tool));
|
||||
@@ -47,14 +47,16 @@
|
||||
<div class="panel tool-stage">
|
||||
<span class="edge-legend step-legend">
|
||||
{#if StepIcon}
|
||||
<span class="step-icon" aria-hidden="true"><StepIcon size={14} strokeWidth={2} /></span>
|
||||
<span class="step-icon" aria-hidden="true"
|
||||
><StepIcon size={14} strokeWidth={2} /></span
|
||||
>
|
||||
{/if}
|
||||
{t('chain.stepLabel', { n: index + 2, title: toolTitle(tool) })}
|
||||
{t("chain.stepLabel", { n: index + 2, title: toolTitle(tool) })}
|
||||
<button
|
||||
type="button"
|
||||
class="remove"
|
||||
aria-label={t('chain.removeStepAria')}
|
||||
title={t('chain.removeStepAria')}
|
||||
aria-label={t("chain.removeStepAria")}
|
||||
title={t("chain.removeStepAria")}
|
||||
onclick={onRemove}
|
||||
>
|
||||
✕
|
||||
@@ -62,34 +64,44 @@
|
||||
</span>
|
||||
|
||||
<div class="cell">
|
||||
<span class="edge-legend cell-legend" aria-hidden="true">{t('chain.inputLegend')}</span>
|
||||
<span class="edge-legend cell-legend" aria-hidden="true"
|
||||
>{t("chain.inputLegend")}</span
|
||||
>
|
||||
<div class="cell-media">
|
||||
<Preview image={input} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="cell">
|
||||
<span class="edge-legend cell-legend" aria-hidden="true">{t('chain.resultLegend')}</span>
|
||||
<span class="edge-legend cell-legend" aria-hidden="true"
|
||||
>{t("chain.resultLegend")}</span
|
||||
>
|
||||
{#if busy && !result}
|
||||
<div class="cell-media">
|
||||
<EmptyState title={t('chain.busyTitle')} hint={t('chain.busyHint')} />
|
||||
<EmptyState title={t("chain.busyTitle")} hint={t("chain.busyHint")} />
|
||||
</div>
|
||||
{:else}
|
||||
<div class="cell-media">
|
||||
<Preview image={result} />
|
||||
{#if busy}
|
||||
<span class="recalc" aria-live="polite">{t('resultCard.recalc')}</span>
|
||||
<span class="recalc" aria-live="polite"
|
||||
>{t("resultCard.recalc")}</span
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="actions-row">
|
||||
<DownloadButton
|
||||
image={result}
|
||||
format={format}
|
||||
{format}
|
||||
baseName="{index + 2}-{tool.id}"
|
||||
params={safeParams}
|
||||
{onError}
|
||||
/>
|
||||
<Button variant="secondary" fullWidth onclick={isLast ? onAddStep : onRemoveChain}>
|
||||
{isLast ? t('resultCard.nextTool') : t('resultCard.breakChain')}
|
||||
<Button
|
||||
variant="secondary"
|
||||
fullWidth
|
||||
onclick={isLast ? onAddStep : onRemoveChain}
|
||||
>
|
||||
{isLast ? t("resultCard.nextTool") : t("resultCard.breakChain")}
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -98,9 +110,11 @@
|
||||
|
||||
{#if tool.params.length > 0}
|
||||
<div class="params-sep">
|
||||
<span class="edge-legend" aria-hidden="true">{t('chain.paramsLegend')}</span>
|
||||
<span class="edge-legend" aria-hidden="true"
|
||||
>{t("chain.paramsLegend")}</span
|
||||
>
|
||||
</div>
|
||||
<ParamsCard tool={tool} params={tool.params} bind:values />
|
||||
<ParamsCard {tool} params={tool.params} bind:values />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -22,7 +22,12 @@
|
||||
</div>
|
||||
<div class="top-actions">
|
||||
<span class="status"><StatusDot /> AUTO PIPELINE</span>
|
||||
<IconButton icon={CircleHelp} label="Help" variant="bare" onclick={() => {}} />
|
||||
<IconButton
|
||||
icon={CircleHelp}
|
||||
label="Help"
|
||||
variant="bare"
|
||||
onclick={() => {}}
|
||||
/>
|
||||
<IconButton
|
||||
icon={theme === "light" ? Moon : Sun}
|
||||
label="Toggle theme"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { TOOL_ICONS } from '$lib/tools/tool-icons';
|
||||
import { TOOL_ICONS } from "$lib/tools/tool-icons";
|
||||
|
||||
interface Props {
|
||||
toolId: string;
|
||||
@@ -18,7 +18,7 @@
|
||||
selected = false,
|
||||
href,
|
||||
onActivate,
|
||||
onHover
|
||||
onHover,
|
||||
}: Props = $props();
|
||||
|
||||
const Icon = $derived(TOOL_ICONS[toolId]);
|
||||
@@ -35,12 +35,7 @@
|
||||
{/snippet}
|
||||
|
||||
{#if href}
|
||||
<a
|
||||
{href}
|
||||
class="card"
|
||||
class:selected
|
||||
onmousemove={onHover}
|
||||
>
|
||||
<a {href} class="card" class:selected onmousemove={onHover}>
|
||||
{@render content()}
|
||||
</a>
|
||||
{:else}
|
||||
@@ -53,7 +48,7 @@
|
||||
onclick={onActivate}
|
||||
onmousemove={onHover}
|
||||
onkeydown={(e) => {
|
||||
if (e.key === 'Enter') onActivate?.();
|
||||
if (e.key === "Enter") onActivate?.();
|
||||
}}
|
||||
>
|
||||
{@render content()}
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
<script lang="ts">
|
||||
import { isChainable, TOOLS } from '$lib/registry';
|
||||
import { LOCALE_TAGS } from '$lib/i18n/dict';
|
||||
import { getLocale } from '$lib/i18n/locale.svelte';
|
||||
import { normalizeForSearch, scoreDoc } from '$lib/i18n/matching';
|
||||
import { t } from '$lib/i18n/t';
|
||||
import { toolDescription, toolSearchDoc, toolTitle } from '$lib/i18n/tool-strings';
|
||||
import ToolCard from './ToolCard.svelte';
|
||||
import { isChainable, TOOLS } from "$lib/registry";
|
||||
import { LOCALE_TAGS } from "$lib/i18n/dict";
|
||||
import { getLocale } from "$lib/i18n/locale.svelte";
|
||||
import { normalizeForSearch, scoreDoc } from "$lib/i18n/matching";
|
||||
import { t } from "$lib/i18n/t";
|
||||
import {
|
||||
toolDescription,
|
||||
toolSearchDoc,
|
||||
toolTitle,
|
||||
} from "$lib/i18n/tool-strings";
|
||||
import ToolCard from "./ToolCard.svelte";
|
||||
|
||||
interface Props {
|
||||
onSelect: (toolId: string) => void;
|
||||
@@ -15,9 +19,11 @@
|
||||
|
||||
let { onSelect, chainableOnly = false }: Props = $props();
|
||||
|
||||
const candidates = $derived(chainableOnly ? TOOLS.filter(isChainable) : TOOLS);
|
||||
const candidates = $derived(
|
||||
chainableOnly ? TOOLS.filter(isChainable) : TOOLS,
|
||||
);
|
||||
|
||||
let query = $state('');
|
||||
let query = $state("");
|
||||
let activeIndex = $state(0);
|
||||
let listOpen = $state(false);
|
||||
|
||||
@@ -40,7 +46,7 @@
|
||||
title: toolTitle(tool),
|
||||
description: toolDescription(tool),
|
||||
popularity: tool.popularity ?? 50,
|
||||
score: s
|
||||
score: s,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -49,31 +55,31 @@
|
||||
(a, b) =>
|
||||
b.score - a.score ||
|
||||
b.popularity - a.popularity ||
|
||||
collator.compare(a.title, b.title)
|
||||
collator.compare(a.title, b.title),
|
||||
);
|
||||
return found.slice(0, 12);
|
||||
});
|
||||
|
||||
function choose(id: string) {
|
||||
listOpen = false;
|
||||
query = '';
|
||||
query = "";
|
||||
activeIndex = 0;
|
||||
onSelect(id);
|
||||
}
|
||||
|
||||
function onKeydown(event: KeyboardEvent) {
|
||||
if (!listOpen || matches.length === 0) return;
|
||||
if (event.key === 'ArrowDown') {
|
||||
if (event.key === "ArrowDown") {
|
||||
event.preventDefault();
|
||||
activeIndex = (activeIndex + 1) % matches.length;
|
||||
} else if (event.key === 'ArrowUp') {
|
||||
} else if (event.key === "ArrowUp") {
|
||||
event.preventDefault();
|
||||
activeIndex = (activeIndex - 1 + matches.length) % matches.length;
|
||||
} else if (event.key === 'Enter') {
|
||||
} else if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
const match = matches[Math.min(activeIndex, matches.length - 1)];
|
||||
if (match) choose(match.id);
|
||||
} else if (event.key === 'Escape') {
|
||||
} else if (event.key === "Escape") {
|
||||
listOpen = false;
|
||||
}
|
||||
}
|
||||
@@ -89,14 +95,14 @@
|
||||
activeIndex = 0;
|
||||
}}
|
||||
onkeydown={onKeydown}
|
||||
placeholder={t('search.placeholder')}
|
||||
aria-label={t('search.aria')}
|
||||
placeholder={t("search.placeholder")}
|
||||
aria-label={t("search.aria")}
|
||||
role="combobox"
|
||||
aria-expanded={listOpen}
|
||||
aria-controls="tool-search-list"
|
||||
/>
|
||||
{#if listOpen && query.trim().length > 0 && matches.length === 0}
|
||||
<p class="none text-muted">{t('search.nothingFound')}</p>
|
||||
<p class="none text-muted">{t("search.nothingFound")}</p>
|
||||
{:else if listOpen && matches.length > 0}
|
||||
<div id="tool-search-list" class="cards" role="listbox">
|
||||
{#each matches as match, index (match.id)}
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
import { outputOf, sanitizeParams, type ToolEntry } from '$lib/registry';
|
||||
import type { ImageInfo } from '$lib/core/analyze';
|
||||
import type { PixelImage } from '$lib/core/types';
|
||||
import { t } from '$lib/i18n/t';
|
||||
import { toolTitle } from '$lib/i18n/tool-strings';
|
||||
import { TOOL_ICONS } from '$lib/tools/tool-icons';
|
||||
import DownloadButton from '../DownloadButton.svelte';
|
||||
import Button from '../ui/Button.svelte';
|
||||
import EmptyState from '../ui/EmptyState.svelte';
|
||||
import OverlayCard from '../tool/OverlayCard.svelte';
|
||||
import ParamsCard from '../tool/ParamsCard.svelte';
|
||||
import ResultCard from '../tool/ResultCard.svelte';
|
||||
import SourceCard from '../tool/SourceCard.svelte';
|
||||
import TextInputCard from '../tool/TextInputCard.svelte';
|
||||
import Preview from '../Preview.svelte';
|
||||
import type { StageStatus } from './stage-props';
|
||||
import type { Snippet } from "svelte";
|
||||
import { outputOf, sanitizeParams, type ToolEntry } from "$lib/registry";
|
||||
import type { ImageInfo } from "$lib/core/analyze";
|
||||
import type { PixelImage } from "$lib/core/types";
|
||||
import { t } from "$lib/i18n/t";
|
||||
import { toolTitle } from "$lib/i18n/tool-strings";
|
||||
import { TOOL_ICONS } from "$lib/tools/tool-icons";
|
||||
import DownloadButton from "../DownloadButton.svelte";
|
||||
import Button from "../ui/Button.svelte";
|
||||
import EmptyState from "../ui/EmptyState.svelte";
|
||||
import OverlayCard from "../tool/OverlayCard.svelte";
|
||||
import ParamsCard from "../tool/ParamsCard.svelte";
|
||||
import ResultCard from "../tool/ResultCard.svelte";
|
||||
import SourceCard from "../tool/SourceCard.svelte";
|
||||
import TextInputCard from "../tool/TextInputCard.svelte";
|
||||
import Preview from "../Preview.svelte";
|
||||
import type { StageStatus } from "./stage-props";
|
||||
|
||||
interface Props {
|
||||
mode: 'base' | 'chain';
|
||||
mode: "base" | "chain";
|
||||
header?: Snippet;
|
||||
tool: ToolEntry;
|
||||
source?: PixelImage | null;
|
||||
@@ -66,7 +66,7 @@
|
||||
source = null,
|
||||
displayImage = null,
|
||||
info = null,
|
||||
status = 'idle',
|
||||
status = "idle",
|
||||
isInfo = false,
|
||||
isSourceless = false,
|
||||
isTextSource = false,
|
||||
@@ -98,43 +98,59 @@
|
||||
onRemove,
|
||||
onError,
|
||||
onAddStep,
|
||||
onRemoveChain
|
||||
onRemoveChain,
|
||||
}: Props = $props();
|
||||
|
||||
const isChain = $derived(mode === 'chain');
|
||||
const isChain = $derived(mode === "chain");
|
||||
const format = $derived(outputOf(tool));
|
||||
const safeParams = $derived(sanitizeParams(tool, values ?? {}));
|
||||
const StepIcon = $derived(TOOL_ICONS[tool.id]);
|
||||
const showParams = $derived(
|
||||
mode === 'chain'
|
||||
mode === "chain"
|
||||
? tool.params.length > 0
|
||||
: (source !== null || isSourceless) &&
|
||||
!isInfo &&
|
||||
(tool.params.length > 0 || hasMask)
|
||||
!isInfo &&
|
||||
(tool.params.length > 0 || hasMask),
|
||||
);
|
||||
|
||||
const leftLegend = $derived(isChain ? t('chain.inputLegend') : t('toolPage.legendSource'));
|
||||
const midLegend = $derived(isChain ? t('chain.paramsLegend') : t('toolPage.legendParams'));
|
||||
const leftLegend = $derived(
|
||||
isChain ? t("chain.inputLegend") : t("toolPage.legendSource"),
|
||||
);
|
||||
const midLegend = $derived(
|
||||
isChain ? t("chain.paramsLegend") : t("toolPage.legendParams"),
|
||||
);
|
||||
const rightLegend = $derived(
|
||||
isChain ? t('chain.resultLegend') : isInfo ? t('toolPage.legendSummary') : t('toolPage.legendResult')
|
||||
isChain
|
||||
? t("chain.resultLegend")
|
||||
: isInfo
|
||||
? t("toolPage.legendSummary")
|
||||
: t("toolPage.legendResult"),
|
||||
);
|
||||
</script>
|
||||
|
||||
<div class="panel stage-grid" class:no-params={!showParams} class:single={!isChain && !!isSourceless}>
|
||||
<div
|
||||
class="panel stage-grid"
|
||||
class:no-params={!showParams}
|
||||
class:single={!isChain && !!isSourceless}
|
||||
>
|
||||
{#if header}
|
||||
{@render header()}
|
||||
{/if}
|
||||
|
||||
{#if isChain}
|
||||
<section class="pane">
|
||||
<span class="edge-legend pane-legend" aria-hidden="true">{leftLegend}</span>
|
||||
<span class="edge-legend pane-legend" aria-hidden="true"
|
||||
>{leftLegend}</span
|
||||
>
|
||||
<div class="pane-body">
|
||||
<Preview image={input} />
|
||||
</div>
|
||||
</section>
|
||||
{:else if !isSourceless}
|
||||
<section class="pane">
|
||||
<span class="edge-legend pane-legend" aria-hidden="true">{leftLegend}</span>
|
||||
<span class="edge-legend pane-legend" aria-hidden="true"
|
||||
>{leftLegend}</span
|
||||
>
|
||||
<div class="pane-body">
|
||||
{#if isTextSource && !source}
|
||||
<TextInputCard onSubmit={(text) => onTextSubmit?.(text)} />
|
||||
@@ -162,34 +178,50 @@
|
||||
|
||||
{#if showParams}
|
||||
<section class="pane pane-params">
|
||||
<span class="edge-legend pane-legend" aria-hidden="true">{midLegend}</span>
|
||||
<span class="edge-legend pane-legend" aria-hidden="true">{midLegend}</span
|
||||
>
|
||||
<div class="pane-body">
|
||||
<ParamsCard {tool} params={tool.params} bind:values {pipetteTargetId} onPipetteToggle={(id) => handlePipetteToggle?.(id)} {hasMask} bind:showMask />
|
||||
<ParamsCard
|
||||
{tool}
|
||||
params={tool.params}
|
||||
bind:values
|
||||
{pipetteTargetId}
|
||||
onPipetteToggle={(id) => handlePipetteToggle?.(id)}
|
||||
{hasMask}
|
||||
bind:showMask
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
<section class="pane">
|
||||
<span class="edge-legend pane-legend" aria-hidden="true">{rightLegend}</span>
|
||||
<span class="edge-legend pane-legend" aria-hidden="true">{rightLegend}</span
|
||||
>
|
||||
<div class="pane-body">
|
||||
{#if isChain}
|
||||
{#if busy && !result}
|
||||
<EmptyState title={t('chain.busyTitle')} hint={t('chain.busyHint')} />
|
||||
<EmptyState title={t("chain.busyTitle")} hint={t("chain.busyHint")} />
|
||||
{:else}
|
||||
<Preview image={result} />
|
||||
{#if busy}
|
||||
<span class="recalc" aria-live="polite">{t('resultCard.recalc')}</span>
|
||||
<span class="recalc" aria-live="polite"
|
||||
>{t("resultCard.recalc")}</span
|
||||
>
|
||||
{/if}
|
||||
<div class="actions-row">
|
||||
<DownloadButton
|
||||
image={result}
|
||||
format={format}
|
||||
{format}
|
||||
baseName="{index + 2}-{tool.id}"
|
||||
params={safeParams}
|
||||
onError={(e) => showError?.(e)}
|
||||
/>
|
||||
<Button variant="secondary" fullWidth onclick={isLast ? onAddStep : onRemoveChain}>
|
||||
{isLast ? t('resultCard.nextTool') : t('resultCard.breakChain')}
|
||||
<Button
|
||||
variant="secondary"
|
||||
fullWidth
|
||||
onclick={isLast ? onAddStep : onRemoveChain}
|
||||
>
|
||||
{isLast ? t("resultCard.nextTool") : t("resultCard.breakChain")}
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -199,11 +231,11 @@
|
||||
sourceLoaded={isSourceless ? true : !!source}
|
||||
{status}
|
||||
{result}
|
||||
displayImage={displayImage}
|
||||
{displayImage}
|
||||
{info}
|
||||
{isInfo}
|
||||
params={sanitized}
|
||||
textResult={textResult}
|
||||
{textResult}
|
||||
onChainToggle={canChainBase ? toggleChain : undefined}
|
||||
{hasChain}
|
||||
onDownloadError={(e) => showError?.(e)}
|
||||
@@ -276,7 +308,7 @@
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.pane-params :global(input[type='range']) {
|
||||
.pane-params :global(input[type="range"]) {
|
||||
order: -1;
|
||||
flex: 1 1 100%;
|
||||
min-width: 0;
|
||||
@@ -306,7 +338,10 @@
|
||||
|
||||
@media (min-width: 75rem) {
|
||||
.stage-grid:not(.no-params):not(.single) {
|
||||
grid-template-columns: minmax(0, 1fr) clamp(13rem, 17vw, 18rem) minmax(0, 1fr);
|
||||
grid-template-columns: minmax(0, 1fr) clamp(13rem, 17vw, 18rem) minmax(
|
||||
0,
|
||||
1fr
|
||||
);
|
||||
}
|
||||
|
||||
.stage-grid.no-params:not(.single),
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { t } from '$lib/i18n/t';
|
||||
import ParamsCard from '../tool/ParamsCard.svelte';
|
||||
import ResultCard from '../tool/ResultCard.svelte';
|
||||
import SourceCard from '../tool/SourceCard.svelte';
|
||||
import TextInputCard from '../tool/TextInputCard.svelte';
|
||||
import type { StageProps } from './stage-props';
|
||||
import { t } from "$lib/i18n/t";
|
||||
import ParamsCard from "../tool/ParamsCard.svelte";
|
||||
import ResultCard from "../tool/ResultCard.svelte";
|
||||
import SourceCard from "../tool/SourceCard.svelte";
|
||||
import TextInputCard from "../tool/TextInputCard.svelte";
|
||||
import type { StageProps } from "./stage-props";
|
||||
|
||||
let {
|
||||
tool,
|
||||
@@ -32,14 +32,16 @@
|
||||
handlePipetteToggle,
|
||||
handlePickColor,
|
||||
showError,
|
||||
errorMessage
|
||||
errorMessage,
|
||||
}: StageProps = $props();
|
||||
</script>
|
||||
|
||||
<div class="panel tool-block">
|
||||
<div class="tool-stage" class:single={isSourceless}>
|
||||
{#if !isSourceless}
|
||||
<span class="edge-legend source-legend" aria-hidden="true">{t('toolPage.legendSource')}</span>
|
||||
<span class="edge-legend source-legend" aria-hidden="true"
|
||||
>{t("toolPage.legendSource")}</span
|
||||
>
|
||||
<div class="cell">
|
||||
{#if isTextSource && !source}
|
||||
<TextInputCard onSubmit={onTextSubmit} />
|
||||
@@ -56,7 +58,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
<span class="edge-legend result-legend" aria-hidden="true">
|
||||
{isInfo ? t('toolPage.legendSummary') : t('toolPage.legendResult')}
|
||||
{isInfo ? t("toolPage.legendSummary") : t("toolPage.legendResult")}
|
||||
</span>
|
||||
<div class="cell">
|
||||
<ResultCard
|
||||
@@ -64,11 +66,11 @@
|
||||
sourceLoaded={isSourceless ? true : !!source}
|
||||
{status}
|
||||
{result}
|
||||
displayImage={displayImage}
|
||||
{displayImage}
|
||||
{info}
|
||||
{isInfo}
|
||||
params={sanitized}
|
||||
textResult={textResult}
|
||||
{textResult}
|
||||
onChainToggle={canChainBase ? toggleChain : undefined}
|
||||
{hasChain}
|
||||
onDownloadError={showError}
|
||||
@@ -78,7 +80,9 @@
|
||||
|
||||
{#if (source || isSourceless) && !isInfo && (tool.params.length > 0 || hasMask)}
|
||||
<div class="params-sep">
|
||||
<span class="edge-legend" aria-hidden="true">{t('toolPage.legendParams')}</span>
|
||||
<span class="edge-legend" aria-hidden="true"
|
||||
>{t("toolPage.legendParams")}</span
|
||||
>
|
||||
</div>
|
||||
<ParamsCard
|
||||
{tool}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { ImageInfo } from '$lib/core/analyze';
|
||||
import type { PixelImage } from '$lib/core/types';
|
||||
import type { ToolEntry } from '$lib/registry';
|
||||
import type { ImageInfo } from "$lib/core/analyze";
|
||||
import type { PixelImage } from "$lib/core/types";
|
||||
import type { ToolEntry } from "$lib/registry";
|
||||
|
||||
export type StageStatus = 'idle' | 'loaded' | 'processing' | 'error';
|
||||
export type StageStatus = "idle" | "loaded" | "processing" | "error";
|
||||
|
||||
export interface StageProps {
|
||||
tool: ToolEntry;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { t } from '$lib/i18n/t';
|
||||
import type { PixelImage } from '$lib/core/types';
|
||||
import DropZone from '../DropZone.svelte';
|
||||
import Preview from '../Preview.svelte';
|
||||
import Button from '../ui/Button.svelte';
|
||||
import { t } from "$lib/i18n/t";
|
||||
import type { PixelImage } from "$lib/core/types";
|
||||
import DropZone from "../DropZone.svelte";
|
||||
import Preview from "../Preview.svelte";
|
||||
import Button from "../ui/Button.svelte";
|
||||
|
||||
interface Props {
|
||||
overlay: PixelImage | null;
|
||||
@@ -16,13 +16,17 @@
|
||||
</script>
|
||||
|
||||
<div class="overlay-card">
|
||||
<span class="edge-legend overlay-legend" aria-hidden="true">{t('ui.overlayTitle')}</span>
|
||||
<span class="edge-legend overlay-legend" aria-hidden="true"
|
||||
>{t("ui.overlayTitle")}</span
|
||||
>
|
||||
{#if !overlay}
|
||||
<DropZone {onFile} {onError} label={t('ui.overlayDrop')} />
|
||||
<DropZone {onFile} {onError} label={t("ui.overlayDrop")} />
|
||||
{:else}
|
||||
<div class="preview-wrap">
|
||||
<Preview image={overlay} />
|
||||
<Button variant="secondary" onclick={onClear}>{t('ui.overlayRemove')}</Button>
|
||||
<Button variant="secondary" onclick={onClear}
|
||||
>{t("ui.overlayRemove")}</Button
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import ParamForm from '../ParamForm.svelte';
|
||||
import type { ParamDef, ToolEntry } from '$lib/registry';
|
||||
import { t } from '$lib/i18n/t';
|
||||
import ParamForm from "../ParamForm.svelte";
|
||||
import type { ParamDef, ToolEntry } from "$lib/registry";
|
||||
import { t } from "$lib/i18n/t";
|
||||
|
||||
interface Props {
|
||||
tool: ToolEntry;
|
||||
@@ -20,7 +20,7 @@
|
||||
pipetteTargetId = null,
|
||||
onPipetteToggle,
|
||||
hasMask = false,
|
||||
showMask = $bindable(false)
|
||||
showMask = $bindable(false),
|
||||
}: Props = $props();
|
||||
</script>
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
bind:showMask
|
||||
/>
|
||||
{:else}
|
||||
<p class="hint text-caption text-muted">{t('paramsCard.noParams')}</p>
|
||||
<p class="hint text-caption text-muted">{t("paramsCard.noParams")}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import type { PixelImage } from '$lib/core/types';
|
||||
import type { PixelImage } from "$lib/core/types";
|
||||
|
||||
interface Props {
|
||||
image: PixelImage;
|
||||
@@ -25,10 +25,16 @@
|
||||
|
||||
function ensureSource(): HTMLCanvasElement | null {
|
||||
if (!srcCanvas || srcKey !== image) {
|
||||
srcCanvas = document.createElement('canvas');
|
||||
srcCanvas = document.createElement("canvas");
|
||||
srcCanvas.width = image.width;
|
||||
srcCanvas.height = image.height;
|
||||
srcCanvas.getContext('2d')?.putImageData(new ImageData(image.data, image.width, image.height), 0, 0);
|
||||
srcCanvas
|
||||
.getContext("2d")
|
||||
?.putImageData(
|
||||
new ImageData(image.data, image.width, image.height),
|
||||
0,
|
||||
0,
|
||||
);
|
||||
srcKey = image;
|
||||
}
|
||||
return srcCanvas;
|
||||
@@ -38,8 +44,12 @@
|
||||
return Math.min(max, Math.max(min, v));
|
||||
}
|
||||
|
||||
const blockX = $derived(clamp(px - HALF, 0, Math.max(0, image.width - BLOCK)));
|
||||
const blockY = $derived(clamp(py - HALF, 0, Math.max(0, image.height - BLOCK)));
|
||||
const blockX = $derived(
|
||||
clamp(px - HALF, 0, Math.max(0, image.width - BLOCK)),
|
||||
);
|
||||
const blockY = $derived(
|
||||
clamp(py - HALF, 0, Math.max(0, image.height - BLOCK)),
|
||||
);
|
||||
const flipBelow = $derived(clientY < SIZE + 24);
|
||||
|
||||
$effect(() => {
|
||||
@@ -47,12 +57,12 @@
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
loupeCanvas.width = SIZE * dpr;
|
||||
loupeCanvas.height = SIZE * dpr;
|
||||
const ctx = loupeCanvas.getContext('2d');
|
||||
const ctx = loupeCanvas.getContext("2d");
|
||||
const src = ensureSource();
|
||||
if (!ctx || !src) return;
|
||||
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||
ctx.imageSmoothingEnabled = false;
|
||||
ctx.fillStyle = '#111318';
|
||||
ctx.fillStyle = "#111318";
|
||||
ctx.fillRect(0, 0, SIZE, SIZE);
|
||||
ctx.drawImage(
|
||||
src,
|
||||
@@ -63,11 +73,16 @@
|
||||
CENTER + (blockX - px) * ZOOM,
|
||||
CENTER + (blockY - py) * ZOOM,
|
||||
BLOCK * ZOOM,
|
||||
BLOCK * ZOOM
|
||||
BLOCK * ZOOM,
|
||||
);
|
||||
ctx.strokeStyle = 'rgba(255,255,255,0.9)';
|
||||
ctx.strokeStyle = "rgba(255,255,255,0.9)";
|
||||
ctx.lineWidth = 1;
|
||||
ctx.strokeRect(CENTER - ZOOM / 2 + 0.5, CENTER - ZOOM / 2 + 0.5, ZOOM, ZOOM);
|
||||
ctx.strokeRect(
|
||||
CENTER - ZOOM / 2 + 0.5,
|
||||
CENTER - ZOOM / 2 + 0.5,
|
||||
ZOOM,
|
||||
ZOOM,
|
||||
);
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -77,7 +92,8 @@
|
||||
style="left:{clientX}px;top:{clientY}px"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<canvas bind:this={loupeCanvas} style="width:{SIZE}px;height:{SIZE}px"></canvas>
|
||||
<canvas bind:this={loupeCanvas} style="width:{SIZE}px;height:{SIZE}px"
|
||||
></canvas>
|
||||
<p class="hex">{hex}</p>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
<script lang="ts">
|
||||
import Button from '../ui/Button.svelte';
|
||||
import DownloadButton from '../DownloadButton.svelte';
|
||||
import EmptyState from '../ui/EmptyState.svelte';
|
||||
import InfoPanel from '../InfoPanel.svelte';
|
||||
import Preview from '../Preview.svelte';
|
||||
import TextResult from './TextResult.svelte';
|
||||
import type { ImageInfo } from '$lib/core/analyze';
|
||||
import type { PixelImage } from '$lib/core/types';
|
||||
import { outputOf, type ToolEntry } from '$lib/registry';
|
||||
import { t } from '$lib/i18n/t';
|
||||
import Button from "../ui/Button.svelte";
|
||||
import DownloadButton from "../DownloadButton.svelte";
|
||||
import EmptyState from "../ui/EmptyState.svelte";
|
||||
import InfoPanel from "../InfoPanel.svelte";
|
||||
import Preview from "../Preview.svelte";
|
||||
import TextResult from "./TextResult.svelte";
|
||||
import type { ImageInfo } from "$lib/core/analyze";
|
||||
import type { PixelImage } from "$lib/core/types";
|
||||
import { outputOf, type ToolEntry } from "$lib/registry";
|
||||
import { t } from "$lib/i18n/t";
|
||||
|
||||
type Status = 'idle' | 'loaded' | 'processing' | 'error';
|
||||
type Status = "idle" | "loaded" | "processing" | "error";
|
||||
|
||||
interface Props {
|
||||
tool: ToolEntry;
|
||||
@@ -39,20 +39,23 @@
|
||||
textResult,
|
||||
onChainToggle,
|
||||
hasChain = false,
|
||||
onDownloadError
|
||||
onDownloadError,
|
||||
}: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class="container">
|
||||
{#if !sourceLoaded}
|
||||
<div class="media">
|
||||
<EmptyState title={t('resultCard.emptyTitle')} hint={t('resultCard.emptyHint')} />
|
||||
<EmptyState
|
||||
title={t("resultCard.emptyTitle")}
|
||||
hint={t("resultCard.emptyHint")}
|
||||
/>
|
||||
</div>
|
||||
{:else if !isInfo && status === 'processing' && !result}
|
||||
{:else if !isInfo && status === "processing" && !result}
|
||||
<div class="media">
|
||||
<EmptyState
|
||||
title={t('resultCard.processingTitle')}
|
||||
hint={t('resultCard.processingHint')}
|
||||
title={t("resultCard.processingTitle")}
|
||||
hint={t("resultCard.processingHint")}
|
||||
/>
|
||||
</div>
|
||||
{:else if isInfo}
|
||||
@@ -61,7 +64,7 @@
|
||||
<InfoPanel {info} />
|
||||
{/if}
|
||||
</div>
|
||||
{:else if tool.resultType === 'text'}
|
||||
{:else if tool.resultType === "text"}
|
||||
<div class="media">
|
||||
{#if textResult !== null}
|
||||
<TextResult text={textResult} filename={tool.id} toolId={tool.id} />
|
||||
@@ -70,8 +73,8 @@
|
||||
{:else}
|
||||
<div class="media">
|
||||
<Preview image={displayImage} />
|
||||
{#if status === 'processing'}
|
||||
<span class="recalc" aria-live="polite">{t('resultCard.recalc')}</span>
|
||||
{#if status === "processing"}
|
||||
<span class="recalc" aria-live="polite">{t("resultCard.recalc")}</span>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="actions-row">
|
||||
@@ -84,7 +87,7 @@
|
||||
/>
|
||||
{#if onChainToggle}
|
||||
<Button variant="secondary" fullWidth onclick={onChainToggle}>
|
||||
{hasChain ? t('resultCard.breakChain') : t('resultCard.nextTool')}
|
||||
{hasChain ? t("resultCard.breakChain") : t("resultCard.nextTool")}
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<script lang="ts">
|
||||
import type { PixelImage } from '$lib/core/types';
|
||||
import { t } from '$lib/i18n/t';
|
||||
import DropOverlay from '../DropOverlay.svelte';
|
||||
import DropZone from '../DropZone.svelte';
|
||||
import Preview from '../Preview.svelte';
|
||||
import Button from '../ui/Button.svelte';
|
||||
import type { PixelImage } from "$lib/core/types";
|
||||
import { t } from "$lib/i18n/t";
|
||||
import DropOverlay from "../DropOverlay.svelte";
|
||||
import DropZone from "../DropZone.svelte";
|
||||
import Preview from "../Preview.svelte";
|
||||
import Button from "../ui/Button.svelte";
|
||||
|
||||
interface Props {
|
||||
source: PixelImage | null;
|
||||
@@ -15,7 +15,14 @@
|
||||
onPickColor?: (hex: string) => void;
|
||||
}
|
||||
|
||||
let { source, onFile, onError, onReset, pipetteActive = false, onPickColor }: Props = $props();
|
||||
let {
|
||||
source,
|
||||
onFile,
|
||||
onError,
|
||||
onReset,
|
||||
pipetteActive = false,
|
||||
onPickColor,
|
||||
}: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class="container">
|
||||
@@ -24,9 +31,11 @@
|
||||
{:else}
|
||||
<DropOverlay {onFile} {onError}>
|
||||
<div class="media">
|
||||
<Preview image={source} pipetteActive={pipetteActive} onPickColor={onPickColor} />
|
||||
<Preview image={source} {pipetteActive} {onPickColor} />
|
||||
</div>
|
||||
<Button variant="secondary" onclick={onReset}>{t('sourceCard.replaceImage')}</Button>
|
||||
<Button variant="secondary" onclick={onReset}
|
||||
>{t("sourceCard.replaceImage")}</Button
|
||||
>
|
||||
</DropOverlay>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import Button from '../ui/Button.svelte';
|
||||
import { t } from '$lib/i18n/t';
|
||||
import Button from "../ui/Button.svelte";
|
||||
import { t } from "$lib/i18n/t";
|
||||
|
||||
interface Props {
|
||||
onSubmit: (text: string) => void;
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
let { onSubmit }: Props = $props();
|
||||
|
||||
let text = $state('');
|
||||
let text = $state("");
|
||||
|
||||
function submit() {
|
||||
if (text.trim().length === 0) return;
|
||||
@@ -17,16 +17,19 @@
|
||||
</script>
|
||||
|
||||
<div class="container">
|
||||
<h2 class="heading-section">{t('textInput.heading')}</h2>
|
||||
<h2 class="heading-section">{t("textInput.heading")}</h2>
|
||||
<textarea
|
||||
class="input"
|
||||
rows="8"
|
||||
bind:value={text}
|
||||
placeholder={t('textInput.placeholder')}
|
||||
aria-label={t('textInput.aria')}
|
||||
></textarea>
|
||||
<Button variant="secondary" onclick={submit} disabled={text.trim().length === 0}>
|
||||
{t('textInput.decode')}
|
||||
placeholder={t("textInput.placeholder")}
|
||||
aria-label={t("textInput.aria")}></textarea>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onclick={submit}
|
||||
disabled={text.trim().length === 0}
|
||||
>
|
||||
{t("textInput.decode")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { downloadBlob } from '$lib/core/io';
|
||||
import { t } from '$lib/i18n/t';
|
||||
import { getMergedDict } from '$lib/i18n/locale.svelte';
|
||||
import { downloadBlob } from "$lib/core/io";
|
||||
import { t } from "$lib/i18n/t";
|
||||
import { getMergedDict } from "$lib/i18n/locale.svelte";
|
||||
|
||||
interface Props {
|
||||
text: string;
|
||||
@@ -12,7 +12,7 @@
|
||||
let { text, filename, toolId }: Props = $props();
|
||||
|
||||
const shown = $derived(
|
||||
toolId ? (getMergedDict().tools[toolId]?.results?.[text] ?? text) : text
|
||||
toolId ? (getMergedDict().tools[toolId]?.results?.[text] ?? text) : text,
|
||||
);
|
||||
|
||||
let copied = $state(false);
|
||||
@@ -24,17 +24,24 @@
|
||||
}
|
||||
|
||||
function download() {
|
||||
downloadBlob(new Blob([shown], { type: 'text/plain' }), `${filename}.txt`);
|
||||
downloadBlob(new Blob([shown], { type: "text/plain" }), `${filename}.txt`);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="panel text-result">
|
||||
<textarea class="output" rows="10" readonly value={shown} aria-label={t('textResult.outputAria')}></textarea>
|
||||
<textarea
|
||||
class="output"
|
||||
rows="10"
|
||||
readonly
|
||||
value={shown}
|
||||
aria-label={t("textResult.outputAria")}></textarea>
|
||||
<div class="actions">
|
||||
<button type="button" class="secondary" onclick={copy}>
|
||||
{copied ? t('textResult.copied') : t('textResult.copy')}
|
||||
{copied ? t("textResult.copied") : t("textResult.copy")}
|
||||
</button>
|
||||
<button type="button" class="primary" onclick={download}>{t('textResult.downloadTxt')}</button>
|
||||
<button type="button" class="primary" onclick={download}
|
||||
>{t("textResult.downloadTxt")}</button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
import type { Snippet } from "svelte";
|
||||
|
||||
interface Props {
|
||||
variant?: 'primary' | 'secondary';
|
||||
type?: 'button' | 'submit';
|
||||
variant?: "primary" | "secondary";
|
||||
type?: "button" | "submit";
|
||||
disabled?: boolean;
|
||||
busy?: boolean;
|
||||
busyText?: string;
|
||||
@@ -13,14 +13,14 @@
|
||||
}
|
||||
|
||||
let {
|
||||
variant = 'primary',
|
||||
type = 'button',
|
||||
variant = "primary",
|
||||
type = "button",
|
||||
disabled = false,
|
||||
busy = false,
|
||||
busyText = '',
|
||||
busyText = "",
|
||||
fullWidth = false,
|
||||
onclick,
|
||||
children
|
||||
children,
|
||||
}: Props = $props();
|
||||
</script>
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
class={fullWidth ? `${variant} fullwidth` : variant}
|
||||
aria-busy={busy}
|
||||
disabled={disabled || busy}
|
||||
onclick={onclick}
|
||||
{onclick}
|
||||
>
|
||||
{#if busy && busyText}
|
||||
{busyText}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
<div class="checkbox-row">
|
||||
<label class="checkbox" for={id}>
|
||||
<input id={id} type="checkbox" bind:checked />
|
||||
<input {id} type="checkbox" bind:checked />
|
||||
{label}
|
||||
</label>
|
||||
{#if hint}
|
||||
@@ -38,7 +38,7 @@
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
input[type='checkbox'] {
|
||||
input[type="checkbox"] {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
cursor: pointer;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import Field from './Field.svelte';
|
||||
import { t } from '$lib/i18n/t';
|
||||
import Field from "./Field.svelte";
|
||||
import { t } from "$lib/i18n/t";
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
@@ -11,20 +11,26 @@
|
||||
onPipetteToggle?: () => void;
|
||||
}
|
||||
|
||||
let { id, label, value = $bindable('#000000'), hint, pipetteActive = false, onPipetteToggle }:
|
||||
Props = $props();
|
||||
let {
|
||||
id,
|
||||
label,
|
||||
value = $bindable("#000000"),
|
||||
hint,
|
||||
pipetteActive = false,
|
||||
onPipetteToggle,
|
||||
}: Props = $props();
|
||||
</script>
|
||||
|
||||
<Field {id} {label} {hint}>
|
||||
<div class="row">
|
||||
<input id={id} class="control swatch" type="color" bind:value />
|
||||
<input {id} class="control swatch" type="color" bind:value />
|
||||
{#if onPipetteToggle}
|
||||
<button
|
||||
type="button"
|
||||
class="pipette"
|
||||
aria-label={t('ui.pipette')}
|
||||
aria-label={t("ui.pipette")}
|
||||
aria-pressed={pipetteActive}
|
||||
title={t('ui.pipette')}
|
||||
title={t("ui.pipette")}
|
||||
onclick={onPipetteToggle}
|
||||
>
|
||||
◎
|
||||
@@ -73,7 +79,7 @@
|
||||
color: var(--link);
|
||||
}
|
||||
|
||||
.pipette[aria-pressed='true'] {
|
||||
.pipette[aria-pressed="true"] {
|
||||
border-color: var(--link);
|
||||
color: var(--link);
|
||||
background: color-mix(in srgb, var(--accent) 10%, var(--surface));
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
import type { Snippet } from "svelte";
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
import type { Snippet } from "svelte";
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import Field from './Field.svelte';
|
||||
import Field from "./Field.svelte";
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
@@ -9,11 +9,11 @@
|
||||
hint?: string;
|
||||
}
|
||||
|
||||
let { id, label, value = $bindable(''), options, hint }: Props = $props();
|
||||
let { id, label, value = $bindable(""), options, hint }: Props = $props();
|
||||
</script>
|
||||
|
||||
<Field {id} {label} {hint}>
|
||||
<select id={id} class="control" bind:value>
|
||||
<select {id} class="control" bind:value>
|
||||
{#each options as option (option.value)}
|
||||
<option value={option.value}>{option.label}</option>
|
||||
{/each}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import Field from './Field.svelte';
|
||||
import { t } from '$lib/i18n/t';
|
||||
import Field from "./Field.svelte";
|
||||
import { t } from "$lib/i18n/t";
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
@@ -21,7 +21,7 @@
|
||||
max,
|
||||
step,
|
||||
default: defaultValue,
|
||||
hint
|
||||
hint,
|
||||
}: Props = $props();
|
||||
|
||||
function decrement() {
|
||||
@@ -38,18 +38,30 @@
|
||||
if (defaultValue !== undefined) value = defaultValue;
|
||||
}
|
||||
|
||||
const resetDisabled = $derived(defaultValue === undefined || value === defaultValue);
|
||||
const resetDisabled = $derived(
|
||||
defaultValue === undefined || value === defaultValue,
|
||||
);
|
||||
</script>
|
||||
|
||||
<Field {id} {label} {hint}>
|
||||
<div class="row">
|
||||
<button type="button" class="step" aria-label={t('ui.decrease')} onclick={decrement}>−</button>
|
||||
<input id={id} type="range" min={min} max={max} step={step} bind:value />
|
||||
<button type="button" class="step" aria-label={t('ui.increase')} onclick={increment}>+</button>
|
||||
<button
|
||||
type="button"
|
||||
class="step"
|
||||
aria-label={t('ui.reset')}
|
||||
aria-label={t("ui.decrease")}
|
||||
onclick={decrement}>−</button
|
||||
>
|
||||
<input {id} type="range" {min} {max} {step} bind:value />
|
||||
<button
|
||||
type="button"
|
||||
class="step"
|
||||
aria-label={t("ui.increase")}
|
||||
onclick={increment}>+</button
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="step"
|
||||
aria-label={t("ui.reset")}
|
||||
disabled={resetDisabled}
|
||||
onclick={reset}
|
||||
>
|
||||
@@ -68,7 +80,7 @@
|
||||
max-width: 22rem;
|
||||
}
|
||||
|
||||
input[type='range'] {
|
||||
input[type="range"] {
|
||||
flex: 1;
|
||||
min-width: 3rem;
|
||||
accent-color: var(--link);
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<script lang="ts">
|
||||
import Field from './Field.svelte';
|
||||
import Field from "./Field.svelte";
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
label: string;
|
||||
value?: string | number;
|
||||
type?: 'number' | 'text';
|
||||
type?: "number" | "text";
|
||||
min?: number;
|
||||
max?: number;
|
||||
step?: number;
|
||||
@@ -13,12 +13,30 @@
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
let { id, label, value = $bindable(''), type = 'text', min, max, step, hint, placeholder }: Props =
|
||||
$props();
|
||||
let {
|
||||
id,
|
||||
label,
|
||||
value = $bindable(""),
|
||||
type = "text",
|
||||
min,
|
||||
max,
|
||||
step,
|
||||
hint,
|
||||
placeholder,
|
||||
}: Props = $props();
|
||||
</script>
|
||||
|
||||
<Field {id} {label} {hint}>
|
||||
<input class="control" {id} {type} min={min} max={max} step={step} {placeholder} bind:value />
|
||||
<input
|
||||
class="control"
|
||||
{id}
|
||||
{type}
|
||||
{min}
|
||||
{max}
|
||||
{step}
|
||||
{placeholder}
|
||||
bind:value
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<style>
|
||||
|
||||
Reference in New Issue
Block a user