fix: resolve always text input issue, separate to simple components

This commit is contained in:
2026-09-07 10:48:45 +05:00
parent 5884f4bd82
commit a1438e6f7f
5 changed files with 316 additions and 192 deletions
@@ -0,0 +1,77 @@
<script lang="ts">
import DownloadButton from "./ui/DownloadButton.svelte";
import { Sparkles, Upload } from "@lucide/svelte";
interface Props {
inputMode: "file" | "text" | "none";
canDownload: boolean;
running: boolean;
onupload: (file: File) => void;
ongenerate: () => void;
ondownload: () => void;
}
let {
inputMode,
canDownload,
running,
onupload,
ongenerate,
ondownload,
}: Props = $props();
</script>
<div class="actions">
{#if inputMode === "none"}
<button class="btn generate" onclick={ongenerate} disabled={running}>
<Sparkles size={14} />
{running ? "Generating…" : "Generate"}
</button>
{:else if inputMode === "file"}
<label class="btn upload">
<Upload size={14} /> Open image
<input
type="file"
accept="image/*"
onchange={(e) => {
const f = (e.target as HTMLInputElement).files?.[0];
if (f) onupload(f);
}}
/>
</label>
{/if}
{#if canDownload}
<DownloadButton label="Download result" onclick={ondownload} />
{/if}
</div>
<style>
.actions {
display: flex;
align-items: center;
gap: var(--space-l);
flex-wrap: wrap;
}
.btn {
display: inline-flex;
align-items: center;
gap: var(--space-m);
padding: var(--space-m) var(--space-l);
border: var(--size-border) solid var(--color-border);
border-radius: var(--radius-m);
font: var(--font-size-s) var(--font-mono);
color: var(--color-text-muted);
cursor: pointer;
}
.generate {
background: var(--color-main);
border-color: var(--color-main);
color: var(--color-background);
}
.generate:disabled {
opacity: 0.6;
cursor: default;
}
.upload input {
display: none;
}
</style>
+57 -191
View File
@@ -1,21 +1,19 @@
<script lang="ts"> <script lang="ts">
import MetaList from "$lib/components/kit/MetaList.svelte"; import MetaList from "$lib/components/kit/MetaList.svelte";
import DownloadButton from "$lib/components/kit/ui/DownloadButton.svelte"; import SchemaActions from "$lib/components/kit/SchemaActions.svelte";
import SchemaTextResult from "$lib/components/kit/SchemaTextResult.svelte"; import SchemaResultTile from "$lib/components/kit/SchemaResultTile.svelte";
import SchemaTextSource from "$lib/components/kit/SchemaTextSource.svelte"; import SchemaSourceTile from "$lib/components/kit/SchemaSourceTile.svelte";
import { toDataUrl } from "$lib/core/io";
import type { PixelImage } from "$lib/core/types"; import type { PixelImage } from "$lib/core/types";
import { Check, Sparkles, Upload } from "@lucide/svelte";
interface Props { interface Props {
inputMode?: "file" | "text" | "none";
resultKind?: "image" | "text" | "verdict";
source: PixelImage | null; source: PixelImage | null;
result: PixelImage | null; result: PixelImage | null;
textSource?: string; textSource?: string;
textResult?: string | null; textResult?: string | null;
resultKind?: "image" | "text" | "verdict"; running?: boolean;
running: boolean; error?: string;
error: string;
isGenerator?: boolean;
onupload: (file: File) => void; onupload: (file: File) => void;
ongenerate?: () => void; ongenerate?: () => void;
ontextsource?: (text: string) => void; ontextsource?: (text: string) => void;
@@ -25,14 +23,14 @@
ondownload: () => void; ondownload: () => void;
} }
let { let {
inputMode = "file",
resultKind = "image",
source, source,
result, result,
textSource = "", textSource = "",
textResult = null, textResult = null,
resultKind = "image", running = false,
running, error = "",
error,
isGenerator = false,
onupload, onupload,
ongenerate, ongenerate,
ontextsource, ontextsource,
@@ -42,44 +40,39 @@
ondownload, ondownload,
}: Props = $props(); }: Props = $props();
const isTextInput = $derived(ontextsource !== undefined && !isGenerator); const isGenerator = $derived(inputMode === "none");
const isImageDownload = $derived(resultKind === "image" && Boolean(result)); const sourceValue = $derived(
isGenerator
let sourceUrl = $derived(source ? toDataUrl(source) : null); ? "—"
let resultUrl = $derived(result ? toDataUrl(result) : null); : inputMode === "text"
? "text"
: source
? `${source.width} × ${source.height} px`
: "—",
);
const resultValue = $derived(
resultKind === "image"
? result
? `${result.width} × ${result.height} px`
: "—"
: textResult
? "text"
: "—",
);
</script> </script>
<div class="panel-head"> <div class="panel-head">
<span class="label" <span class="label"
>{isGenerator ? "GENERATOR / RESULT" : "SOURCE / RESULT"}</span >{isGenerator ? "GENERATOR / RESULT" : "SOURCE / RESULT"}</span
> >
<div class="head-actions"> <SchemaActions
{#if isGenerator} {inputMode}
<button canDownload={resultKind === "image" && Boolean(result)}
class="upload generate-btn" {running}
onclick={ongenerate} {onupload}
disabled={running} ongenerate={ongenerate ?? (() => {})}
> {ondownload}
<Sparkles size={14} /> />
{running ? "Generating…" : "Generate"}
</button>
{:else if !isTextInput}
<label class="upload">
<Upload size={14} /> Open image
<input
type="file"
accept="image/*"
onchange={(e) => {
const f = (e.target as HTMLInputElement).files?.[0];
if (f) onupload(f);
}}
/>
</label>
{/if}
{#if isImageDownload}
<DownloadButton label="Download result" onclick={ondownload} />
{/if}
</div>
</div> </div>
{#if error} {#if error}
@@ -88,85 +81,31 @@
<div class="pair"> <div class="pair">
{#if !isGenerator} {#if !isGenerator}
<figure class="tile"> <SchemaSourceTile
<figcaption><span>SOURCE</span></figcaption> mode={inputMode === "text" ? "text" : "file"}
<div class="canvas"> {source}
{#if isTextInput} {textSource}
<div class="text-source-wrap"> {running}
<SchemaTextSource ontextinput={ontextsource}
value={textSource} {onrendertext}
disabled={running} />
oninput={ontextsource ?? (() => {})}
onrender={onrendertext ?? (() => {})}
/>
</div>
{:else if sourceUrl}
<img src={sourceUrl} alt="source" />
{:else}
<span class="empty">choose an image</span>
{/if}
</div>
</figure>
{/if} {/if}
<figure class="tile" class:full={isGenerator || resultKind !== "image"}> <SchemaResultTile
<figcaption><span>RESULT {running ? "…" : ""}</span></figcaption> {resultKind}
<div class="canvas" class:checker={resultKind === "image"}> {result}
{#if resultKind === "text" && textResult} {textResult}
<div class="text-result-wrap"> wide={isGenerator || resultKind !== "image"}
<SchemaTextResult {running}
value={textResult} oncopy={oncopytext}
kind="text" {ondownloadtxt}
oncopy={oncopytext ?? (() => {})} />
ondownload={ondownloadtxt ?? (() => {})}
/>
</div>
{:else if resultKind === "verdict" && textResult}
<div class="text-result-wrap">
<SchemaTextResult
value={textResult}
kind="verdict"
oncopy={oncopytext ?? (() => {})}
ondownload={ondownloadtxt ?? (() => {})}
/>
</div>
{:else if resultKind === "image" && resultUrl}
<img src={resultUrl} alt="result" />
{:else if running}
<Check size={22} />
{:else}
<span class="empty">
{isGenerator ? "click Generate" : "no result yet"}
</span>
{/if}
</div>
</figure>
</div> </div>
<div class="meta"> <div class="meta">
<MetaList <MetaList
items={[ items={[
{ { caption: "SOURCE", value: sourceValue },
caption: "SOURCE", { caption: "RESULT", value: resultValue },
value:
!isGenerator && !isTextInput
? source
? `${source.width} × ${source.height} px`
: "—"
: isTextInput
? "text"
: "—",
},
{
caption: "RESULT",
value:
resultKind === "image"
? result
? `${result.width} × ${result.height} px`
: "—"
: textResult
? "text"
: "—",
},
{ caption: "FORMAT", value: "PNG" }, { caption: "FORMAT", value: "PNG" },
]} ]}
/> />
@@ -187,35 +126,6 @@
letter-spacing: var(--space-text-l); letter-spacing: var(--space-text-l);
color: var(--color-main); color: var(--color-main);
} }
.head-actions {
display: flex;
align-items: center;
gap: var(--space-l);
flex-wrap: wrap;
}
.upload {
display: inline-flex;
align-items: center;
gap: var(--space-m);
padding: var(--space-m) var(--space-l);
border: var(--size-border) solid var(--color-border);
border-radius: var(--radius-m);
font: var(--font-size-s) var(--font-mono);
color: var(--color-text-muted);
cursor: pointer;
}
.generate-btn {
background: var(--color-main);
border-color: var(--color-main);
color: var(--color-background);
}
.generate-btn:disabled {
opacity: 0.6;
cursor: default;
}
.upload input {
display: none;
}
.error { .error {
margin: 0 0 var(--space-l); margin: 0 0 var(--space-l);
color: var(--color-danger); color: var(--color-danger);
@@ -226,50 +136,6 @@
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
gap: var(--space-l); gap: var(--space-l);
} }
.pair > .full {
grid-column: 1 / -1;
}
.tile figcaption {
display: flex;
align-items: center;
gap: var(--space-m);
margin-bottom: var(--space-m);
font: var(--font-size-s) var(--font-mono);
color: var(--color-text-muted);
}
.canvas {
display: flex;
align-items: center;
justify-content: center;
min-height: clamp(var(--space-brand), 30vh, 60vh);
border: var(--size-border) solid var(--color-border);
border-radius: var(--radius-m);
overflow: hidden;
background: var(--color-background-muted);
color: var(--color-text-muted);
}
.canvas img {
width: 100%;
height: 100%;
object-fit: contain;
display: block;
}
.canvas.checker {
background: repeating-conic-gradient(
var(--color-checker-main) 0 25%,
var(--color-checker-alt) 0 50%
)
50% / 28px 28px;
}
.empty {
font: var(--font-size-s) var(--font-mono);
}
.text-source-wrap,
.text-result-wrap {
width: 100%;
padding: var(--space-l);
box-sizing: border-box;
}
.meta { .meta {
margin-top: var(--space-l); margin-top: var(--space-l);
} }
@@ -0,0 +1,101 @@
<script lang="ts">
import SchemaTextResult from "./SchemaTextResult.svelte";
import { toDataUrl } from "$lib/core/io";
import type { PixelImage } from "$lib/core/types";
import { Check } from "@lucide/svelte";
interface Props {
resultKind?: "image" | "text" | "verdict";
result: PixelImage | null;
textResult?: string | null;
wide?: boolean;
running?: boolean;
oncopy?: () => void;
ondownloadtxt?: () => void;
}
let {
resultKind = "image",
result,
textResult = null,
wide = false,
running = false,
oncopy,
ondownloadtxt,
}: Props = $props();
const resultUrl = $derived(result ? toDataUrl(result) : null);
</script>
<figure class="tile" style:grid-column={wide ? "1 / -1" : undefined}>
<figcaption><span>RESULT {running ? "…" : ""}</span></figcaption>
<div class="canvas" class:checker={resultKind === "image"}>
{#if resultKind === "text" && textResult}
<div class="text-result-wrap">
<SchemaTextResult
value={textResult}
kind="text"
oncopy={oncopy ?? (() => {})}
ondownload={ondownloadtxt ?? (() => {})}
/>
</div>
{:else if resultKind === "verdict" && textResult}
<div class="text-result-wrap">
<SchemaTextResult
value={textResult}
kind="verdict"
oncopy={oncopy ?? (() => {})}
ondownload={ondownloadtxt ?? (() => {})}
/>
</div>
{:else if resultKind === "image" && resultUrl}
<img src={resultUrl} alt="result" />
{:else if running}
<Check size={22} />
{:else}
<span class="empty">no result yet</span>
{/if}
</div>
</figure>
<style>
.tile figcaption {
display: flex;
align-items: center;
gap: var(--space-m);
margin-bottom: var(--space-m);
font: var(--font-size-s) var(--font-mono);
color: var(--color-text-muted);
}
.canvas {
display: flex;
align-items: center;
justify-content: center;
min-height: clamp(var(--space-brand), 30vh, 60vh);
border: var(--size-border) solid var(--color-border);
border-radius: var(--radius-m);
overflow: hidden;
background: var(--color-background-muted);
color: var(--color-text-muted);
}
.canvas img {
width: 100%;
height: 100%;
object-fit: contain;
display: block;
}
.canvas.checker {
background: repeating-conic-gradient(
var(--color-checker-main) 0 25%,
var(--color-checker-alt) 0 50%
)
50% / 28px 28px;
}
.empty {
font: var(--font-size-s) var(--font-mono);
}
.text-result-wrap {
width: 100%;
padding: var(--space-l);
box-sizing: border-box;
}
</style>
@@ -0,0 +1,80 @@
<script lang="ts">
import SchemaTextSource from "./SchemaTextSource.svelte";
import { toDataUrl } from "$lib/core/io";
import type { PixelImage } from "$lib/core/types";
interface Props {
mode: "file" | "text";
source: PixelImage | null;
textSource?: string;
running?: boolean;
ontextinput?: (text: string) => void;
onrendertext?: () => void;
}
let {
mode,
source,
textSource = "",
running = false,
ontextinput,
onrendertext,
}: Props = $props();
const sourceUrl = $derived(source ? toDataUrl(source) : null);
</script>
<figure class="tile">
<figcaption><span>SOURCE</span></figcaption>
<div class="canvas">
{#if mode === "text"}
<div class="text-source-wrap">
<SchemaTextSource
value={textSource}
disabled={running}
oninput={ontextinput ?? (() => {})}
onrender={onrendertext ?? (() => {})}
/>
</div>
{:else if sourceUrl}
<img src={sourceUrl} alt="source" />
{:else}
<span class="empty">choose an image</span>
{/if}
</div>
</figure>
<style>
.tile figcaption {
display: flex;
align-items: center;
gap: var(--space-m);
margin-bottom: var(--space-m);
font: var(--font-size-s) var(--font-mono);
color: var(--color-text-muted);
}
.canvas {
display: flex;
align-items: center;
justify-content: center;
min-height: clamp(var(--space-brand), 30vh, 60vh);
border: var(--size-border) solid var(--color-border);
border-radius: var(--radius-m);
overflow: hidden;
background: var(--color-background-muted);
color: var(--color-text-muted);
}
.canvas img {
width: 100%;
height: 100%;
object-fit: contain;
display: block;
}
.empty {
font: var(--font-size-s) var(--font-mono);
}
.text-source-wrap {
width: 100%;
padding: var(--space-l);
box-sizing: border-box;
}
</style>
@@ -196,10 +196,10 @@
{result} {result}
{textSource} {textSource}
{textResult} {textResult}
{inputMode}
{resultKind} {resultKind}
{running} {running}
{error} {error}
{isGenerator}
onupload={handleFile} onupload={handleFile}
ongenerate={runGenerate} ongenerate={runGenerate}
ontextsource={(t) => { ontextsource={(t) => {