style: fix formatting via prettier

This commit is contained in:
2026-08-28 14:29:54 +05:00
parent 10278981ba
commit ada96b3534
124 changed files with 8211 additions and 5662 deletions
+13 -9
View File
@@ -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>
+5 -5
View File
@@ -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);
+9 -5
View File
@@ -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 = "";
}
}}
/>
+10 -8
View File
@@ -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>
+26 -18
View File
@@ -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)}
+39 -16
View File
@@ -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);
}
+137 -98
View File
@@ -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>
+6 -1
View File
@@ -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()}
+25 -19
View File
@@ -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)}
+77 -42
View File
@@ -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}
+4 -4
View File
@@ -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;
+12 -8
View File
@@ -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>
+27 -11
View File
@@ -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>
+23 -20
View File
@@ -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>
+18 -9
View File
@@ -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>
+15 -8
View File
@@ -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>
+8 -8
View File
@@ -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;
+14 -8
View File
@@ -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 -1
View File
@@ -1,5 +1,5 @@
<script lang="ts">
import type { Snippet } from 'svelte';
import type { Snippet } from "svelte";
interface Props {
title: string;
+1 -1
View File
@@ -1,5 +1,5 @@
<script lang="ts">
import type { Snippet } from 'svelte';
import type { Snippet } from "svelte";
interface Props {
id: string;
+3 -3
View File
@@ -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}
+21 -9
View File
@@ -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);
+23 -5
View File
@@ -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>