feat: update - use design system components

This commit is contained in:
2026-08-22 14:34:27 +05:00
parent 15e2e010ec
commit c7b56bbda7
8 changed files with 100 additions and 162 deletions
+15 -11
View File
@@ -1,21 +1,18 @@
<script lang="ts"> <script lang="ts">
import Button from './ui/Button.svelte';
import { downloadBlob, encode } from '$lib/core/io'; import { downloadBlob, encode } from '$lib/core/io';
import type { PixelImage } from '$lib/core/types'; import type { PixelImage } from '$lib/core/types';
import type { OutputFormat } from '$lib/registry'; import type { OutputFormat } from '$lib/registry';
let { interface Props {
image,
format,
baseName,
params,
onError
}: {
image: PixelImage | null; image: PixelImage | null;
format?: OutputFormat; format?: OutputFormat;
baseName: string; baseName: string;
params: Record<string, unknown>; params: Record<string, unknown>;
onError?: (e: unknown) => void; onError?: (e: unknown) => void;
} = $props(); }
let { image, format, baseName, params, onError }: Props = $props();
let busy = $state(false); let busy = $state(false);
@@ -36,6 +33,13 @@
} }
</script> </script>
<button class="primary" onclick={download} disabled={!image || !format || busy}> <Button
{busy ? 'Готовим файл…' : `Скачать .${format?.ext ?? 'png'}`} variant="primary"
</button> fullWidth
disabled={!image || !format}
{busy}
busyText="Готовим файл…"
onclick={download}
>
Скачать .{format?.ext ?? 'png'}
</Button>
+4 -6
View File
@@ -33,7 +33,7 @@
</script> </script>
<div <div
class="dropzone" class="dropzone panel"
class:dragging class:dragging
role="button" role="button"
tabindex="0" tabindex="0"
@@ -81,16 +81,14 @@
gap: var(--space-2); gap: var(--space-2);
min-height: 14rem; min-height: 14rem;
padding: var(--space-4); padding: var(--space-4);
border: 2px dashed var(--border); border-style: dashed;
border-radius: var(--radius-m);
background: var(--surface);
color: var(--text-muted); color: var(--text-muted);
text-align: center; text-align: center;
cursor: pointer; cursor: pointer;
user-select: none; user-select: none;
transition: transition:
border-color 120ms ease, border-color var(--transition-fast),
background 120ms ease; background var(--transition-fast);
} }
.dropzone:hover, .dropzone:hover,
+8 -7
View File
@@ -1,20 +1,24 @@
<script lang="ts"> <script lang="ts">
import type { ImageInfo } from '$lib/core/analyze'; import type { ImageInfo } from '$lib/core/analyze';
let { info }: { info: ImageInfo | null } = $props(); interface Props {
info: ImageInfo | null;
}
let { info }: Props = $props();
</script> </script>
{#if info} {#if info}
<dl> <dl>
<div> <div class="panel">
<dt>Размеры</dt> <dt>Размеры</dt>
<dd>{info.width} × {info.height} px</dd> <dd>{info.width} × {info.height} px</dd>
</div> </div>
<div> <div class="panel">
<dt>Альфа-канал</dt> <dt>Альфа-канал</dt>
<dd>{info.hasAlpha ? 'есть — есть полупрозрачные пиксели' : 'нет'}</dd> <dd>{info.hasAlpha ? 'есть — есть полупрозрачные пиксели' : 'нет'}</dd>
</div> </div>
<div> <div class="panel">
<dt>Уникальных цветов (RGBA)</dt> <dt>Уникальных цветов (RGBA)</dt>
<dd>{info.colorCount.toLocaleString('ru-RU')}</dd> <dd>{info.colorCount.toLocaleString('ru-RU')}</dd>
</div> </div>
@@ -33,9 +37,6 @@
justify-content: space-between; justify-content: space-between;
gap: var(--space-3); gap: var(--space-3);
padding: var(--space-2) var(--space-3); padding: var(--space-2) var(--space-3);
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius-s);
} }
dt { dt {
+30 -80
View File
@@ -1,89 +1,39 @@
<script lang="ts"> <script lang="ts">
import CheckboxField from './ui/CheckboxField.svelte';
import ColorField from './ui/ColorField.svelte';
import SelectField from './ui/SelectField.svelte';
import TextField from './ui/TextField.svelte';
import type { ParamDef } from '$lib/registry'; import type { ParamDef } from '$lib/registry';
let { interface Props {
params,
values = $bindable()
}: {
params: ParamDef[]; params: ParamDef[];
values: Record<string, any>; values: Record<string, any>;
} = $props(); }
let { params, values = $bindable() }: Props = $props();
</script> </script>
{#each params as param (param.id)} {#each params as param (param.id)}
<div class="field"> {#if param.type === 'checkbox'}
{#if param.type === 'checkbox'} <CheckboxField id={param.id} label={param.label} bind:checked={values[param.id]} />
<label class="checkbox"> {:else if param.type === 'number'}
<input type="checkbox" bind:checked={values[param.id]} /> <TextField
{param.label} id={param.id}
</label> label={param.label}
{:else} type="number"
<label for={param.id}>{param.label}</label> min={param.min}
{#if param.type === 'number'} max={param.max}
<input step={param.step}
id={param.id} bind:value={values[param.id]}
type="number" />
min={param.min} {:else if param.type === 'select'}
max={param.max} <SelectField
step={param.step} id={param.id}
bind:value={values[param.id]} label={param.label}
/> options={param.options}
{:else if param.type === 'select'} bind:value={values[param.id]}
<select id={param.id} bind:value={values[param.id]}> />
{#each param.options as option (option.value)} {:else if param.type === 'color'}
<option value={option.value}>{option.label}</option> <ColorField id={param.id} label={param.label} bind:value={values[param.id]} />
{/each} {/if}
</select>
{:else if param.type === 'color'}
<input id={param.id} type="color" bind:value={values[param.id]} />
{/if}
{/if}
</div>
{/each} {/each}
<style>
.field {
display: flex;
flex-direction: column;
gap: var(--space-1);
margin-bottom: var(--space-3);
}
label,
.checkbox {
font-size: 0.9rem;
color: var(--text-muted);
}
input,
select {
padding: var(--space-1) var(--space-2);
border: 1px solid var(--border);
border-radius: var(--radius-s);
background: var(--surface);
max-width: 16rem;
}
input[type='number'] {
font-family: var(--font-mono);
}
input[type='color'] {
height: 2.4rem;
padding: 2px;
cursor: pointer;
}
.checkbox {
display: flex;
align-items: center;
gap: var(--space-2);
color: var(--text);
cursor: pointer;
}
.checkbox input {
width: 1rem;
height: 1rem;
}
</style>
+23 -33
View File
@@ -3,8 +3,10 @@
import { decodeFile } from '$lib/core/io'; import { decodeFile } from '$lib/core/io';
import type { PixelImage } from '$lib/core/types'; import type { PixelImage } from '$lib/core/types';
import { defaultParams, outputOf, sanitizeParams, type ToolEntry } from '$lib/registry'; import { defaultParams, outputOf, sanitizeParams, type ToolEntry } from '$lib/registry';
import Button from './ui/Button.svelte';
import DownloadButton from './DownloadButton.svelte'; import DownloadButton from './DownloadButton.svelte';
import DropZone from './DropZone.svelte'; import DropZone from './DropZone.svelte';
import EmptyState from './ui/EmptyState.svelte';
import InfoPanel from './InfoPanel.svelte'; import InfoPanel from './InfoPanel.svelte';
import ParamForm from './ParamForm.svelte'; import ParamForm from './ParamForm.svelte';
import Preview from './Preview.svelte'; import Preview from './Preview.svelte';
@@ -75,10 +77,10 @@
<section> <section>
<h1>{tool.title}</h1> <h1>{tool.title}</h1>
<p class="description">{tool.description}</p> <p class="description text-muted">{tool.description}</p>
{#if errorText} {#if errorText}
<div class="error" role="alert">{errorText}</div> <div class="error-banner" role="alert">{errorText}</div>
{/if} {/if}
{#if !source} {#if !source}
@@ -86,28 +88,37 @@
{:else} {:else}
<div class="layout"> <div class="layout">
<div class="result-col"> <div class="result-col">
<h2>{isInfo ? 'Изображение' : 'Результат'}</h2> <h2 class="heading-section">{isInfo ? 'Изображение' : 'Результат'}</h2>
{#if status === 'processing' && !result} {#if status === 'processing' && !result}
<p class="hint">Обработка…</p> <EmptyState title="Обработка…" hint="Изображение обрабатывается, это займёт немного времени" />
{:else} {:else}
<Preview image={isInfo ? source : result} /> <Preview image={isInfo ? source : result} />
{/if} {/if}
{#if isInfo && info} {#if isInfo && info}
<InfoPanel {info} /> <InfoPanel {info} />
{/if} {/if}
<button class="secondary" onclick={reset}>Загрузить другое изображение</button> <div class="reset-row">
<Button variant="secondary" onclick={reset}>Загрузить другое изображение</Button>
</div>
</div> </div>
{#if !isInfo} {#if !isInfo}
<div class="params-col"> <div class="params-col">
<h2>Параметры</h2> <h2 class="heading-section">Параметры</h2>
{#if tool.params.length > 0} {#if tool.params.length > 0}
<ParamForm params={tool.params} bind:values /> <ParamForm params={tool.params} bind:values />
<button class="primary" onclick={apply} disabled={status === 'processing'}> <Button
{status === 'processing' ? 'Обработка…' : 'Применить'} onclick={apply}
</button> busy={status === 'processing'}
busyText="Обработка…"
fullWidth
>
Применить
</Button>
{:else} {:else}
<p class="hint">У этого инструмента нет параметров — результат уже готов.</p> <p class="hint text-caption text-muted">
У этого инструмента нет параметров — результат уже готов.
</p>
{/if} {/if}
<div class="download-row"> <div class="download-row">
<DownloadButton <DownloadButton
@@ -130,19 +141,12 @@
} }
.description { .description {
color: var(--text-muted);
max-width: 48rem; max-width: 48rem;
margin-bottom: var(--space-4); margin-bottom: var(--space-4);
} }
.error { .error-banner {
padding: var(--space-2) var(--space-3);
margin-bottom: var(--space-3); margin-bottom: var(--space-3);
border: 1px solid #e5484d;
border-radius: var(--radius-s);
background: color-mix(in srgb, #e5484d 8%, var(--surface));
color: #b3261e;
font-size: 0.9rem;
} }
.layout { .layout {
@@ -161,33 +165,19 @@
.result-col h2, .result-col h2,
.params-col h2 { .params-col h2 {
margin-bottom: var(--space-2); margin-bottom: var(--space-2);
font-size: 1rem;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.04em;
} }
.result-col button.secondary { .reset-row {
margin-top: var(--space-3); margin-top: var(--space-3);
} }
.params-col button.primary {
width: 100%;
}
.download-row { .download-row {
margin-top: var(--space-4); margin-top: var(--space-4);
padding-top: var(--space-3); padding-top: var(--space-3);
border-top: 1px solid var(--border); border-top: 1px solid var(--border);
} }
.download-row :global(button) {
width: 100%;
}
.hint { .hint {
color: var(--text-muted);
font-size: 0.9rem;
margin-bottom: var(--space-3); margin-bottom: var(--space-3);
} }
</style> </style>
+7 -1
View File
@@ -7,6 +7,7 @@
disabled?: boolean; disabled?: boolean;
busy?: boolean; busy?: boolean;
busyText?: string; busyText?: string;
fullWidth?: boolean;
onclick?: () => void; onclick?: () => void;
children: Snippet; children: Snippet;
} }
@@ -17,6 +18,7 @@
disabled = false, disabled = false,
busy = false, busy = false,
busyText = '', busyText = '',
fullWidth = false,
onclick, onclick,
children children
}: Props = $props(); }: Props = $props();
@@ -24,7 +26,7 @@
<button <button
{type} {type}
class={variant} class={fullWidth ? `${variant} fullwidth` : variant}
aria-busy={busy} aria-busy={busy}
disabled={disabled || busy} disabled={disabled || busy}
onclick={onclick} onclick={onclick}
@@ -74,4 +76,8 @@
opacity: 0.55; opacity: 0.55;
cursor: not-allowed; cursor: not-allowed;
} }
.fullwidth {
width: 100%;
}
</style> </style>
+4 -4
View File
@@ -25,7 +25,9 @@
</main> </main>
<footer> <footer>
<p>Все операции выполняются локально в вашем браузере — файлы никуда не отправляются.</p> <p class="text-caption text-muted">
Все операции выполняются локально в вашем браузере — файлы никуда не отправляются.
</p>
</footer> </footer>
</div> </div>
@@ -66,7 +68,7 @@
.nav-link { .nav-link {
color: var(--text-muted); color: var(--text-muted);
font-size: 0.9rem; font-size: var(--text-s);
padding: var(--space-1) var(--space-2); padding: var(--space-1) var(--space-2);
border-radius: var(--radius-s); border-radius: var(--radius-s);
} }
@@ -88,8 +90,6 @@
footer { footer {
padding: var(--space-3) var(--space-4); padding: var(--space-3) var(--space-4);
border-top: 1px solid var(--border); border-top: 1px solid var(--border);
color: var(--text-muted);
font-size: 0.85rem;
text-align: center; text-align: center;
} }
</style> </style>
+9 -20
View File
@@ -12,7 +12,7 @@
</svelte:head> </svelte:head>
<h1>easy-png-tools</h1> <h1>easy-png-tools</h1>
<p class="lead"> <p class="lead text-muted">
Набор утилит для работы с PNG. Все операции выполняются локально в браузере — файлы никуда не Набор утилит для работы с PNG. Все операции выполняются локально в браузере — файлы никуда не
отправляются. отправляются.
</p> </p>
@@ -21,12 +21,12 @@
{@const categoryTools = TOOLS.filter((tool) => tool.category === category.id)} {@const categoryTools = TOOLS.filter((tool) => tool.category === category.id)}
{#if categoryTools.length > 0} {#if categoryTools.length > 0}
<section id={category.id} class="category" aria-labelledby="{category.id}-heading"> <section id={category.id} class="category" aria-labelledby="{category.id}-heading">
<h2 id="{category.id}-heading">{category.label}</h2> <h2 id="{category.id}-heading" class="heading-section">{category.label}</h2>
<div class="grid"> <div class="grid">
{#each categoryTools as tool (tool.id)} {#each categoryTools as tool (tool.id)}
<a class="card" href="/tools/{tool.id}"> <a class="card panel" href="/tools/{tool.id}">
<span class="card-title">{tool.title}</span> <span class="card-title">{tool.title}</span>
<span class="card-desc">{tool.description}</span> <span class="card-desc text-caption text-muted">{tool.description}</span>
</a> </a>
{/each} {/each}
</div> </div>
@@ -40,7 +40,6 @@
} }
.lead { .lead {
color: var(--text-muted);
max-width: 48rem; max-width: 48rem;
margin-bottom: var(--space-5); margin-bottom: var(--space-5);
} }
@@ -51,10 +50,6 @@
} }
h2 { h2 {
font-size: 1.1rem;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.04em;
margin-bottom: var(--space-3); margin-bottom: var(--space-3);
} }
@@ -69,27 +64,23 @@
flex-direction: column; flex-direction: column;
gap: var(--space-2); gap: var(--space-2);
padding: var(--space-3) var(--space-3) var(--space-4); padding: var(--space-3) var(--space-3) var(--space-4);
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius-m);
text-decoration: none;
color: inherit; color: inherit;
transition: transition:
border-color 120ms ease, border-color var(--transition-fast),
box-shadow 120ms ease, box-shadow var(--transition-fast),
transform 120ms ease; transform var(--transition-fast);
} }
.card:hover { .card:hover {
text-decoration: none; text-decoration: none;
border-color: var(--accent); border-color: var(--accent);
box-shadow: 0 4px 14px rgb(37 99 235 / 12%); box-shadow: var(--shadow-card);
transform: translateY(-1px); transform: translateY(-1px);
} }
.card-title { .card-title {
font-weight: 600; font-weight: 600;
font-size: 0.95rem; font-size: var(--text-m);
} }
.card:hover .card-title { .card:hover .card-title {
@@ -97,8 +88,6 @@
} }
.card-desc { .card-desc {
font-size: 0.85rem;
color: var(--text-muted);
display: -webkit-box; display: -webkit-box;
-webkit-line-clamp: 3; -webkit-line-clamp: 3;
line-clamp: 3; line-clamp: 3;