mirror of
https://github.com/Ku6epXBOCTuK/easy-png-tools.git
synced 2026-11-13 17:08:20 +00:00
feat: add tooltip for errors\warnings
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
import { RefreshCw } from "@lucide/svelte";
|
||||
import { t } from "$lib/i18n/t";
|
||||
import Badge from "$lib/components/ui/Badge.svelte";
|
||||
import DetailBadge from "$lib/components/ui/DetailBadge.svelte";
|
||||
import type { Snippet } from "svelte";
|
||||
|
||||
interface Props {
|
||||
@@ -15,11 +16,11 @@
|
||||
dims?: string;
|
||||
note?: string;
|
||||
noteTone?: "warning" | "info";
|
||||
// Runtime warnings (chain/download events): shown as badges next to the
|
||||
// derived result note, but from a different source.
|
||||
warnings?: string[];
|
||||
// Runtime errors: danger badges; detail goes to the native tooltip until
|
||||
// a popover with full text exists.
|
||||
// Runtime warnings (chain/source/download events): shown as badges next
|
||||
// to the derived result note, full message in the "?" popover.
|
||||
warnings?: { label: string; detail?: string }[];
|
||||
// Runtime errors: danger badges; the full message lives in the
|
||||
// DetailBadge popover behind the "?" button.
|
||||
errors?: { label: string; detail?: string }[];
|
||||
actions?: Snippet;
|
||||
}
|
||||
@@ -61,15 +62,20 @@
|
||||
<Badge tone={noteTone} label={note} />
|
||||
{/if}
|
||||
{#each errors as error (error.label + (error.detail ?? ""))}
|
||||
<Badge
|
||||
<DetailBadge
|
||||
tone="danger"
|
||||
label={error.label}
|
||||
title={error.detail}
|
||||
detail={error.detail}
|
||||
role="alert"
|
||||
/>
|
||||
{/each}
|
||||
{#each warnings as warning (warning)}
|
||||
<Badge tone="warning" label={warning} role="status" />
|
||||
{#each warnings as warning (warning.label + (warning.detail ?? ""))}
|
||||
<DetailBadge
|
||||
tone="warning"
|
||||
label={warning.label}
|
||||
detail={warning.detail}
|
||||
role="status"
|
||||
/>
|
||||
{/each}
|
||||
</span>
|
||||
</figcaption>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import DownloadButton from "$lib/components/ui/DownloadButton.svelte";
|
||||
import UploadButton from "$lib/components/ui/UploadButton.svelte";
|
||||
import Badge from "$lib/components/ui/Badge.svelte";
|
||||
import DetailBadge from "$lib/components/ui/DetailBadge.svelte";
|
||||
import type { OutputMime } from "$lib/core/io";
|
||||
import { outputFormatByMime } from "$lib/output-formats";
|
||||
import { t } from "$lib/i18n/t";
|
||||
@@ -18,7 +18,7 @@
|
||||
limitKb?: number;
|
||||
alphaLoss?: boolean;
|
||||
// Download-zone feedback: size-limit misses and action failures.
|
||||
warnings?: string[];
|
||||
warnings?: { label: string; detail?: string }[];
|
||||
error?: { label: string; detail?: string } | null;
|
||||
onupload: (file: File) => void;
|
||||
ondownload: () => void;
|
||||
@@ -67,14 +67,19 @@
|
||||
disabled={running}
|
||||
/>
|
||||
{/if}
|
||||
{#each warnings as warning (warning)}
|
||||
<Badge tone="warning" label={warning} role="status" />
|
||||
{#each warnings as warning (warning.label + (warning.detail ?? ""))}
|
||||
<DetailBadge
|
||||
tone="warning"
|
||||
label={warning.label}
|
||||
detail={warning.detail}
|
||||
role="status"
|
||||
/>
|
||||
{/each}
|
||||
{#if error}
|
||||
<Badge
|
||||
<DetailBadge
|
||||
tone="danger"
|
||||
label={error.label}
|
||||
title={error.detail}
|
||||
detail={error.detail}
|
||||
role="alert"
|
||||
/>
|
||||
{/if}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
resultKind?: ResultKind;
|
||||
result: PixelImage | null;
|
||||
resultNote?: ResultNote | null;
|
||||
warnings?: string[];
|
||||
warnings?: { label: string; detail?: string }[];
|
||||
errors?: { label: string; detail?: string }[];
|
||||
fileResult?: FileResult | null;
|
||||
textResult?: string | null;
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
textSource?: string;
|
||||
running?: boolean;
|
||||
fileCount?: number;
|
||||
warnings?: string[];
|
||||
warnings?: { label: string; detail?: string }[];
|
||||
errors?: { label: string; detail?: string }[];
|
||||
ontextinput?: (text: string) => void;
|
||||
onrendertext?: () => void;
|
||||
|
||||
@@ -9,9 +9,10 @@
|
||||
import type { PixelImage } from "$lib/core/types";
|
||||
import {
|
||||
chainStepTitle,
|
||||
chainWarningText,
|
||||
chainWarningNotice,
|
||||
pageDescription,
|
||||
pageTitle,
|
||||
type WarningNotice,
|
||||
} from "$lib/i18n/schema-tool-strings";
|
||||
import { t } from "$lib/i18n/t";
|
||||
import { moveStep, type ChainStep } from "$lib/pipeline.svelte";
|
||||
@@ -124,20 +125,18 @@
|
||||
const stepFileSets = $derived(runner.stepFileSets);
|
||||
const runWarnings = $derived(runner.runWarnings);
|
||||
// Feedback zones: source tile / step tiles / download button area.
|
||||
const sourceWarningTexts = $derived(
|
||||
upload.sourceWarnings.map(chainWarningText),
|
||||
);
|
||||
const sourceNotices = $derived(upload.sourceWarnings.map(chainWarningNotice));
|
||||
const warningsByStep = $derived.by(() => {
|
||||
const byStep: string[][] = steps.map(() => []);
|
||||
const byStep: WarningNotice[][] = steps.map(() => []);
|
||||
for (const w of runWarnings) {
|
||||
if ("step" in w && w.step >= 1 && w.step <= byStep.length) {
|
||||
byStep[w.step - 1].push(chainWarningText(w));
|
||||
byStep[w.step - 1].push(chainWarningNotice(w));
|
||||
}
|
||||
}
|
||||
return byStep;
|
||||
});
|
||||
const downloadWarningTexts = $derived(
|
||||
runner.downloadWarnings.map(chainWarningText),
|
||||
const downloadNotices = $derived(
|
||||
runner.downloadWarnings.map(chainWarningNotice),
|
||||
);
|
||||
const stepError = $derived(runner.stepError);
|
||||
const sourceError = $derived(runner.sourceError);
|
||||
@@ -305,7 +304,7 @@
|
||||
textSource={upload.textSource}
|
||||
running={busy}
|
||||
fileCount={upload.sourceFiles.length}
|
||||
warnings={sourceWarningTexts}
|
||||
warnings={sourceNotices}
|
||||
errors={errorBadge(sourceError)}
|
||||
ontextinput={(v) => (upload.textSource = v)}
|
||||
onrendertext={run}
|
||||
@@ -407,7 +406,7 @@
|
||||
quality={output.quality}
|
||||
limitKb={output.limitKb}
|
||||
alphaLoss={output.alphaLoss}
|
||||
warnings={downloadWarningTexts}
|
||||
warnings={downloadNotices}
|
||||
error={actionError
|
||||
? {
|
||||
label: t("actions.actionFailed"),
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
<script lang="ts">
|
||||
import { t } from "$lib/i18n/t";
|
||||
import Badge from "./Badge.svelte";
|
||||
|
||||
interface Props {
|
||||
tone: "warning" | "danger" | "info";
|
||||
label: string;
|
||||
detail?: string;
|
||||
role?: "alert" | "status";
|
||||
}
|
||||
let { tone, label, detail = undefined, role = "status" }: Props = $props();
|
||||
|
||||
const id = $props.id();
|
||||
// Anchor name is per-instance: with a shared name several popovers would
|
||||
// fight over the same anchor element.
|
||||
let anchorName = `--badge-${id}`;
|
||||
let open = $state(false);
|
||||
</script>
|
||||
|
||||
<span class="detail-badge">
|
||||
<Badge {tone} {label} {role} />
|
||||
{#if detail}
|
||||
<button
|
||||
type="button"
|
||||
class="more"
|
||||
style:anchor-name={anchorName}
|
||||
aria-label={t("ui.detailsAria")}
|
||||
aria-expanded={open}
|
||||
aria-controls={id}
|
||||
popovertarget={id}
|
||||
>
|
||||
?
|
||||
</button>
|
||||
<span
|
||||
{id}
|
||||
class="popover"
|
||||
role="tooltip"
|
||||
popover="auto"
|
||||
style:position-anchor={anchorName}
|
||||
onbeforetoggle={(e) => (open = e.newState === "open")}
|
||||
>
|
||||
{detail}
|
||||
</span>
|
||||
{/if}
|
||||
</span>
|
||||
|
||||
<style>
|
||||
.detail-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-s);
|
||||
}
|
||||
.more {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
/* Мини-размер цели в 24px: попадание пальцем без промаха. */
|
||||
width: calc(var(--space-xxxl) * 0.66);
|
||||
height: calc(var(--space-xxxl) * 0.66);
|
||||
padding: 0;
|
||||
border: var(--size-border) solid var(--color-border);
|
||||
border-radius: var(--radius-s);
|
||||
background: var(--color-panel);
|
||||
color: var(--color-text-muted);
|
||||
font: var(--font-size-s) var(--font-mono);
|
||||
font-weight: bold;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
.more:hover,
|
||||
.more:focus-visible {
|
||||
color: var(--color-text);
|
||||
border-color: var(--color-main);
|
||||
}
|
||||
/* Нативный popover живёт в top layer: light dismiss, Escape и раскладка
|
||||
поверх всего — из коробки, без z-index и ручных подписок. */
|
||||
.popover {
|
||||
position: fixed;
|
||||
margin: 0;
|
||||
top: anchor(bottom);
|
||||
left: anchor(left);
|
||||
translate: 0 var(--space-s);
|
||||
/* Не уезжает за правый край: доступная ширина считается от якоря. */
|
||||
width: max-content;
|
||||
max-width: calc(100vw - anchor(left) - var(--space-xl));
|
||||
padding: var(--space-m) var(--space-l);
|
||||
border: var(--size-border-thick) solid var(--color-text-muted);
|
||||
border-radius: var(--radius-m);
|
||||
background: var(--color-background);
|
||||
color: var(--color-text);
|
||||
font: var(--font-size-s) var(--font-mono);
|
||||
font-weight: normal;
|
||||
text-align: left;
|
||||
text-transform: none;
|
||||
white-space: normal;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
</style>
|
||||
@@ -70,6 +70,10 @@ export const en: Dict = {
|
||||
warnSizeLimitMiss:
|
||||
"Could not fit into {limit} KB — downloaded file is {actual} KB",
|
||||
stepFailed: "Step {n} failed",
|
||||
warnPartialShort: "Some files failed",
|
||||
warnFirstOnlyShort: "First file only",
|
||||
warnSourceSkipShort: "Files skipped",
|
||||
warnSizeLimitMissShort: "Over size limit",
|
||||
},
|
||||
savedChains: {
|
||||
pageTitle: "Saved pipelines — easy-png-tools",
|
||||
@@ -172,6 +176,7 @@ export const en: Dict = {
|
||||
nothingFound: "Nothing found — try another word.",
|
||||
},
|
||||
ui: {
|
||||
detailsAria: "Show details",
|
||||
showMask: "Show mask",
|
||||
decrease: "Decrease",
|
||||
increase: "Increase",
|
||||
|
||||
@@ -72,6 +72,10 @@ export const ru: Dict = {
|
||||
warnSizeLimitMiss:
|
||||
"Не удалось ужаться до {limit} КБ — скачан файл на {actual} КБ",
|
||||
stepFailed: "Шаг {n}: ошибка",
|
||||
warnPartialShort: "Часть файлов с ошибкой",
|
||||
warnFirstOnlyShort: "Только первый файл",
|
||||
warnSourceSkipShort: "Файлы пропущены",
|
||||
warnSizeLimitMissShort: "Больше лимита",
|
||||
},
|
||||
savedChains: {
|
||||
pageTitle: "Сохранённые пайплайны — easy-png-tools",
|
||||
@@ -173,6 +177,7 @@ export const ru: Dict = {
|
||||
nothingFound: "Ничего не найдено — попробуйте другое слово.",
|
||||
},
|
||||
ui: {
|
||||
detailsAria: "Показать подробности",
|
||||
showMask: "Показать маску",
|
||||
decrease: "Уменьшить",
|
||||
increase: "Увеличить",
|
||||
|
||||
@@ -68,23 +68,41 @@ export function verdictTone(key: string): "success" | "danger" | "info" {
|
||||
return "info";
|
||||
}
|
||||
|
||||
/** Human-readable text for a chain run/source warning. */
|
||||
export function chainWarningText(w: ChainWarning): string {
|
||||
/** Short label for a warning badge; detail holds the full message (popover). */
|
||||
export interface WarningNotice {
|
||||
label: string;
|
||||
detail: string;
|
||||
}
|
||||
|
||||
/** Human-readable badge + popover text for a chain run/source warning. */
|
||||
export function chainWarningNotice(w: ChainWarning): WarningNotice {
|
||||
switch (w.kind) {
|
||||
case "partial":
|
||||
return t("chain.warnPartial", { n: w.step, ok: w.ok, total: w.total });
|
||||
return {
|
||||
label: t("chain.warnPartialShort"),
|
||||
detail: t("chain.warnPartial", { n: w.step, ok: w.ok, total: w.total }),
|
||||
};
|
||||
case "firstOnly":
|
||||
return t("chain.warnFirstOnly", { n: w.step, total: w.total });
|
||||
return {
|
||||
label: t("chain.warnFirstOnlyShort"),
|
||||
detail: t("chain.warnFirstOnly", { n: w.step, total: w.total }),
|
||||
};
|
||||
case "sourceSkip":
|
||||
return t("chain.warnSourceSkip", {
|
||||
skipped: w.skipped,
|
||||
total: w.total,
|
||||
});
|
||||
return {
|
||||
label: t("chain.warnSourceSkipShort"),
|
||||
detail: t("chain.warnSourceSkip", {
|
||||
skipped: w.skipped,
|
||||
total: w.total,
|
||||
}),
|
||||
};
|
||||
case "sizeLimitMiss":
|
||||
return t("chain.warnSizeLimitMiss", {
|
||||
limit: w.limitKb,
|
||||
actual: w.actualKb,
|
||||
});
|
||||
return {
|
||||
label: t("chain.warnSizeLimitMissShort"),
|
||||
detail: t("chain.warnSizeLimitMiss", {
|
||||
limit: w.limitKb,
|
||||
actual: w.actualKb,
|
||||
}),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user