feat: add tool search component, add chainable flag

This commit is contained in:
2026-08-23 21:55:45 +05:00
parent ce7f5ff680
commit c93a8e493e
5 changed files with 348 additions and 83 deletions
+3 -6
View File
@@ -1,7 +1,6 @@
<script lang="ts">
import '../app.css';
import favicon from '$lib/assets/favicon.svg';
import { CATEGORIES } from '$lib/categories';
let { children } = $props();
</script>
@@ -13,11 +12,9 @@
<div class="app">
<header>
<a href="/" class="brand">easy-png-tools</a>
<nav aria-label="Категории инструментов">
<a class="nav-link workspace-link" href="/workspace">Рабочая область</a>
{#each CATEGORIES as category (category.id)}
<a class="nav-link" href="/#{category.id}">{category.label}</a>
{/each}
<nav aria-label="Разделы">
<a class="nav-link workspace-link" href="/">Рабочая область</a>
<a class="nav-link" href="/list-tools">Каталог</a>
</nav>
</header>
+40 -77
View File
@@ -1,97 +1,60 @@
<script lang="ts">
import { CATEGORIES } from '$lib/categories';
import { TOOLS } from '$lib/registry';
import { getTool } from '$lib/registry';
import ToolPage from '$lib/components/ToolPage.svelte';
import ToolSearch from '$lib/components/search/ToolSearch.svelte';
let selectedId = $state<string | null>(null);
let selected = $derived(selectedId !== null ? (getTool(selectedId) ?? null) : null);
</script>
<svelte:head>
<title>easy-png-tools — PNG-утилиты прямо в браузере</title>
<meta
name="description"
content="Набор PNG-утилит, которые работают полностью локально в вашем браузере: конвертация, прозрачность, цвет, геометрия и анализ изображений."
/>
<title>
{selected ? `${selected.title} easy-png-tools` : 'easy-png-tools — PNG-утилиты прямо в браузере'}
</title>
</svelte:head>
<h1>easy-png-tools</h1>
<p class="lead text-muted">
Набор утилит для работы с PNG. Все операции выполняются локально в браузере — файлы никуда не
отправляются.
</p>
{#each CATEGORIES as category (category.id)}
{@const categoryTools = TOOLS.filter((tool) => tool.category === category.id)}
{#if categoryTools.length > 0}
<section id={category.id} class="category" aria-labelledby="{category.id}-heading">
<h2 id="{category.id}-heading" class="heading-section">{category.label}</h2>
<div class="grid">
{#each categoryTools as tool (tool.id)}
<a class="card panel" href="/tools/{tool.id}">
<span class="card-title">{tool.title}</span>
<span class="card-desc text-caption text-muted">{tool.description}</span>
</a>
{/each}
</div>
</section>
{/if}
{/each}
{#if !selected}
<section class="hero">
<h1>Что делаем с изображением?</h1>
<p class="lead text-muted">Найдите инструмент — все операции выполняются локально в браузере.</p>
<ToolSearch size="hero" onSelect={(id) => (selectedId = id)} />
</section>
{:else}
<section class="workbench">
<button type="button" class="back text-caption text-muted" onclick={() => (selectedId = null)}>
← Сменить инструмент
</button>
{#key selectedId}
<ToolPage tool={selected} />
{/key}
</section>
{/if}
<style>
.hero {
text-align: center;
padding-top: var(--space-5);
}
h1 {
margin-bottom: var(--space-1);
margin-bottom: var(--space-2);
}
.lead {
max-width: 48rem;
margin-bottom: var(--space-5);
max-width: 40rem;
margin: 0 auto var(--space-4);
}
.category {
margin-bottom: var(--space-5);
scroll-margin-top: 5rem;
}
h2 {
.back {
display: inline-block;
margin-bottom: var(--space-3);
border: 0;
background: none;
cursor: pointer;
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr));
gap: var(--space-3);
}
.card {
display: flex;
flex-direction: column;
gap: var(--space-2);
padding: var(--space-3) var(--space-3) var(--space-4);
color: inherit;
transition:
border-color var(--transition-fast),
box-shadow var(--transition-fast),
transform var(--transition-fast);
}
.card:hover {
text-decoration: none;
border-color: var(--accent);
box-shadow: var(--shadow-card);
transform: translateY(-1px);
}
.card-title {
font-weight: 600;
font-size: var(--text-m);
}
.card:hover .card-title {
.back:hover {
color: var(--accent);
}
.card-desc {
display: -webkit-box;
-webkit-line-clamp: 3;
line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
</style>
+96
View File
@@ -0,0 +1,96 @@
<script lang="ts">
import { CATEGORIES } from '$lib/categories';
import { TOOLS } from '$lib/registry';
</script>
<svelte:head>
<title>Все инструменты — easy-png-tools</title>
<meta
name="description"
content="Полный каталог PNG-утилит: конвертация, прозрачность, цвет, геометрия, анализ и генерация изображений."
/>
</svelte:head>
<h1>Каталог инструментов</h1>
<p class="lead text-muted">
{TOOLS.length} утилит для работы с PNG. Все операции выполняются локально в браузере.
</p>
{#each CATEGORIES as category (category.id)}
{@const categoryTools = TOOLS.filter((tool) => tool.category === category.id)}
{#if categoryTools.length > 0}
<section id={category.id} class="category" aria-labelledby="{category.id}-heading">
<h2 id="{category.id}-heading" class="heading-section">{category.label}</h2>
<div class="grid">
{#each categoryTools as tool (tool.id)}
<a class="card panel" href="/tools/{tool.id}">
<span class="card-title">{tool.title}</span>
<span class="card-desc text-caption text-muted">{tool.description}</span>
</a>
{/each}
</div>
</section>
{/if}
{/each}
<style>
h1 {
margin-bottom: var(--space-1);
}
.lead {
max-width: 48rem;
margin-bottom: var(--space-5);
}
.category {
margin-bottom: var(--space-5);
scroll-margin-top: 5rem;
}
h2 {
margin-bottom: var(--space-3);
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr));
gap: var(--space-3);
}
.card {
display: flex;
flex-direction: column;
gap: var(--space-2);
padding: var(--space-3) var(--space-3) var(--space-4);
color: inherit;
transition:
border-color var(--transition-fast),
box-shadow var(--transition-fast),
transform var(--transition-fast);
}
.card:hover {
text-decoration: none;
border-color: var(--accent);
box-shadow: var(--shadow-card);
transform: translateY(-1px);
}
.card-title {
font-weight: 600;
font-size: var(--text-m);
}
.card:hover .card-title {
color: var(--accent);
}
.card-desc {
display: -webkit-box;
-webkit-line-clamp: 3;
line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
</style>