feat: add ui kit page

This commit is contained in:
2026-08-27 21:35:01 +05:00
parent 8437c95a28
commit 06d185add6
6 changed files with 332 additions and 6 deletions
+22 -3
View File
@@ -316,10 +316,9 @@ src/routes/preview/tools/[id]/+page.svelte
### Шаг 2. Витрина kit (проверка компонентов) ### Шаг 2. Витрина kit (проверка компонентов)
- [ ] **[C8]** `preview/+layout.svelte` (импорт `design2.css` + `<AppShell>`) и - [x] **[C8]** `preview/+layout.svelte` (импорт `design2.css` + `<AppShell>` + переключатель темы) и
временная `preview/kit` страница со всеми состояниями (light/dark, временная `preview/kit` страница со всеми состояниями (light/dark,
hover/disabled, варианты Button/Segmented/Badge). Проверка: визуально hover/disabled, варианты Button/Segmented/Badge и пр.)
против `refs-html/`, старый сайт не затронут.
### Шаг 3. Новые страницы ### Шаг 3. Новые страницы
@@ -383,3 +382,23 @@ src/routes/preview/tools/[id]/+page.svelte
> только когда old-сайт уже выключен). > только когда old-сайт уже выключен).
> - Очерёдность зависимостей: `design2.css` (C3) и kit-компоненты (C5–C7) > - Очерёдность зависимостей: `design2.css` (C3) и kit-компоненты (C5–C7)
> должны существовать до страниц, которые их импортируют (C8–C14). > должны существовать до страниц, которые их импортируют (C8–C14).
## Бэклог полировки (после C9–C21, отдельным проходом)
Конкретные расхождения витрины `preview/kit` с `refs-html/`. Править
системно в конце, а не по ходу сборки базы.
- **Toggle** в дизайне прямоугольный, у нас — закруглённый. Сделать
прямоугольным (или уточнить радиус у свича отдельно от общих токенов).
- **Радиусы бордеров** не совпадают: в `refs` радиусы вообще 0, а мы взяли
ненулевой `--radius` для всех бордеров. Проверить токен `--radius` против
референсов; возможно он должен быть 0 (или близко), а скругления оставить
только там, где они реально есть в дизайне.
- **DownloadButton**: нет отступа между текстом и размером файла
(`size`-чип). Добавить `gap`/отступ внутри кнопки.
- **Поля ввода (controls)**: выглядят без паддингов (возможно артефакт самой
витрины, но визуально плохо). Проверить паддинги в `TextField`/`SelectField`/
`ColorField`/`SliderField` против референса и поправить.
- **Segmented**: на витрине последняя кнопка растянута и нечитаемо, что это
сегмент. Проверить, как `Segmented` выглядит в реальных раскладках
(C9–C14); возможно нужен фиксированный/авто размер сегментов или перенос.
@@ -4,6 +4,7 @@
type?: string; type?: string;
placeholder?: string; placeholder?: string;
readonly?: boolean; readonly?: boolean;
label?: string;
oninput?: (value: string) => void; oninput?: (value: string) => void;
} }
let { let {
@@ -11,6 +12,7 @@
type = "text", type = "text",
placeholder = "", placeholder = "",
readonly = false, readonly = false,
label,
oninput, oninput,
}: Props = $props(); }: Props = $props();
@@ -26,6 +28,7 @@
{placeholder} {placeholder}
{readonly} {readonly}
{value} {value}
aria-label={label}
oninput={handle} oninput={handle}
/> />
+4 -3
View File
@@ -1,20 +1,21 @@
<script lang="ts"> <script lang="ts">
import { ArrowUpRight } from "@lucide/svelte"; import { ArrowUpRight } from "@lucide/svelte";
import type { Snippet } from "svelte"; import type { Component } from "svelte";
import Icon from "./Icon.svelte";
interface Props { interface Props {
title: string; title: string;
href: string; href: string;
description?: string; description?: string;
index?: number; index?: number;
icon?: Snippet; icon?: Component<{ size?: number; class?: string }>;
} }
let { title, href, description, index, icon }: Props = $props(); let { title, href, description, index, icon }: Props = $props();
</script> </script>
<!-- eslint-disable-next-line svelte/no-navigation-without-resolve --> <!-- eslint-disable-next-line svelte/no-navigation-without-resolve -->
<a class="tool-card" {href}> <a class="tool-card" {href}>
{#if icon}<span class="tool-icon">{@render icon()}</span>{/if} {#if icon}<span class="tool-icon"><Icon {icon} size={18} /></span>{/if}
<span class="tool-index"> <span class="tool-index">
{index !== undefined ? index.toString().padStart(2, "0") : ""} {index !== undefined ? index.toString().padStart(2, "0") : ""}
</span> </span>
+12
View File
@@ -5,6 +5,18 @@
@import "@fontsource/ibm-plex-mono/400.css"; @import "@fontsource/ibm-plex-mono/400.css";
@import "@fontsource/ibm-plex-mono/500.css"; @import "@fontsource/ibm-plex-mono/500.css";
/* Минимальный reset (новый дизайн грузится только на preview-маршрутах). */
*,
*::before,
*::after {
box-sizing: border-box;
}
html,
body {
margin: 0;
padding: 0;
}
/* Новый дизайн: токены (§2 плана redesign). /* Новый дизайн: токены (§2 плана redesign).
Файл грузится только layout'ом новой ветки (preview/+layout), поэтому Файл грузится только layout'ом новой ветки (preview/+layout), поэтому
глобально не конфликтует со старым app.css. */ глобально не конфликтует со старым app.css. */
+40
View File
@@ -0,0 +1,40 @@
<script lang="ts">
import "$lib/styles/design2.css";
import type { Snippet } from "svelte";
import AppShell from "$lib/components/kit/AppShell.svelte";
import IconButton from "$lib/components/kit/IconButton.svelte";
import { Sun, Moon } from "@lucide/svelte";
interface Props {
children: Snippet;
}
let { children }: Props = $props();
let theme = $state<"light" | "dark">("light");
function toggle() {
theme = theme === "light" ? "dark" : "light";
}
</script>
<div class="preview-root" data-theme={theme}>
<AppShell>{@render children()}</AppShell>
<div class="theme-switch">
<IconButton
icon={theme === "light" ? Moon : Sun}
label="Toggle theme"
onclick={toggle}
/>
</div>
</div>
<style>
.preview-root {
display: block;
}
.theme-switch {
position: fixed;
top: 1rem;
right: 1rem;
z-index: 20;
}
</style>
+251
View File
@@ -0,0 +1,251 @@
<script lang="ts">
import Panel from "$lib/components/kit/Panel.svelte";
import PanelHeading from "$lib/components/kit/PanelHeading.svelte";
import SettingGroup from "$lib/components/kit/SettingGroup.svelte";
import SettingsFooter from "$lib/components/kit/SettingsFooter.svelte";
import MetaList from "$lib/components/kit/MetaList.svelte";
import Badge from "$lib/components/kit/Badge.svelte";
import Segmented from "$lib/components/kit/Segmented.svelte";
import StepCard from "$lib/components/kit/StepCard.svelte";
import ToolCard from "$lib/components/kit/ToolCard.svelte";
import ImageCard from "$lib/components/kit/ImageCard.svelte";
import PreviewTile from "$lib/components/kit/PreviewTile.svelte";
import CheckerCanvas from "$lib/components/kit/CheckerCanvas.svelte";
import StatusLine from "$lib/components/kit/StatusLine.svelte";
import Button from "$lib/components/kit/Button.svelte";
import IconButton from "$lib/components/kit/IconButton.svelte";
import SliderField from "$lib/components/kit/SliderField.svelte";
import ColorField from "$lib/components/kit/ColorField.svelte";
import TextField from "$lib/components/kit/TextField.svelte";
import SelectField from "$lib/components/kit/SelectField.svelte";
import CheckboxField from "$lib/components/kit/CheckboxField.svelte";
import Toggle from "$lib/components/kit/Toggle.svelte";
import EmptyState from "$lib/components/kit/EmptyState.svelte";
import CodeBlock from "$lib/components/kit/CodeBlock.svelte";
import DownloadButton from "$lib/components/kit/DownloadButton.svelte";
import { Plus, Trash2, Download, ImageOff, Palette } from "@lucide/svelte";
let mode = $state("preview");
let radius = $state(8);
let tint = $state("#3b82f6");
let fileName = $state("image.png");
let format = $state("png");
let lossless = $state(true);
let animated = $state(false);
const tools = [
{
title: "Gradient",
href: "#",
description: "Linear & radial gradients",
},
{
title: "Resize",
href: "#",
description: "Pixel-perfect scaling",
},
{
title: "Quantize",
href: "#",
description: "Reduce color depth",
},
];
</script>
<div class="showcase">
<header class="showcase-head">
<span class="showcase-eyebrow">KIT SHOWCASE</span>
<StatusLine label="rendering ok" />
</header>
<Panel>
<PanelHeading title="Buttons" eyebrow="controls" />
<div class="stack">
<SettingGroup label="variants">
<Button onclick={() => {}}>Primary</Button>
<Button variant="ghost" onclick={() => {}}>Ghost</Button>
<Button variant="accent" onclick={() => {}}>Accent</Button>
<Button variant="danger" onclick={() => {}}>Danger</Button>
</SettingGroup>
<SettingGroup label="states">
<Button disabled onclick={() => {}}>Disabled</Button>
<IconButton icon={Plus} label="Add" onclick={() => {}} />
<IconButton
icon={Trash2}
label="Delete"
variant="solid"
onclick={() => {}}
/>
<IconButton
icon={Download}
label="Download"
disabled
onclick={() => {}}
/>
</SettingGroup>
</div>
<SettingsFooter>
<DownloadButton label="Download PNG" size="1.2 MB" onclick={() => {}} />
</SettingsFooter>
</Panel>
<Panel>
<PanelHeading title="Inputs" eyebrow="controls" />
<div class="stack">
<SliderField
label="corner radius"
bind:value={radius}
min={0}
max={32}
suffix="px"
/>
<ColorField label="accent tint" bind:value={tint} />
<TextField
label="file name"
bind:value={fileName}
placeholder="image.png"
/>
<SelectField
label="format"
bind:value={format}
options={[
{ value: "png", label: "PNG" },
{ value: "webp", label: "WebP" },
{ value: "avif", label: "AVIF" },
]}
/>
<SettingGroup label="options">
<CheckboxField label="lossless" bind:checked={lossless} />
<Toggle bind:checked={animated} label="animate" />
</SettingGroup>
</div>
</Panel>
<Panel>
<PanelHeading title="Segmented & badges" eyebrow="controls" />
<div class="stack">
<Segmented
bind:value={mode}
options={[
{ value: "preview", label: "Preview" },
{ value: "compare", label: "Compare" },
{ value: "code", label: "Code" },
]}
/>
<SettingGroup label="tones">
<Badge>default</Badge>
<Badge tone="accent">accent</Badge>
<Badge tone="success">success</Badge>
<Badge tone="amber">amber</Badge>
</SettingGroup>
</div>
</Panel>
<Panel>
<PanelHeading title="Steps" eyebrow="pipeline" />
<div class="stack">
<StepCard index={1} type="BACKGROUND" title="Gradient fill">
<ColorField label="color" bind:value={tint} />
</StepCard>
<StepCard index={2} type="TRANSFORM" title="Resize" onremove={() => {}}>
<SliderField label="scale" bind:value={radius} suffix="%" />
</StepCard>
</div>
</Panel>
<Panel>
<PanelHeading title="Tools" eyebrow="catalog" />
<div class="tool-grid">
{#each tools as tool, i (tool.title)}
<ToolCard
title={tool.title}
href={tool.href}
description={tool.description}
index={i + 1}
icon={Palette}
/>
{/each}
</div>
</Panel>
<Panel>
<PanelHeading title="Preview" eyebrow="canvas" />
<div class="preview-grid">
<ImageCard label="result.png · 512×512">
<CheckerCanvas size="sm" />
</ImageCard>
<PreviewTile label="tile-01">
<CheckerCanvas size="sm" />
</PreviewTile>
<PreviewTile label="tile-02">
<CheckerCanvas size="sm" />
</PreviewTile>
</div>
<SettingsFooter>
<MetaList
items={[
{ caption: "dimensions", value: "512 × 512" },
{ caption: "format", value: "PNG" },
{ caption: "size", value: "48.2 KB" },
]}
/>
</SettingsFooter>
</Panel>
<Panel>
<PanelHeading title="States & code" eyebrow="misc" />
<div class="stack">
<EmptyState
icon={ImageOff}
title="No image loaded"
description="Drop a PNG to start processing"
>
<Button onclick={() => {}}>Open file</Button>
</EmptyState>
<CodeBlock
code={'export const config = {\n radius: 8,\n tint: "#3b82f6",\n};'}
lang="ts"
copyable
oncopy={() => {}}
/>
</div>
</Panel>
</div>
<style>
.showcase {
max-width: 880px;
margin: 0 auto;
padding: 2.5rem 1.25rem 4rem;
display: flex;
flex-direction: column;
gap: 1.25rem;
}
.showcase-head {
display: flex;
align-items: center;
justify-content: space-between;
}
.showcase-eyebrow {
font-family: var(--font-mono);
font-size: 12px;
letter-spacing: 0.18em;
color: var(--muted);
}
.stack {
display: flex;
flex-direction: column;
}
.tool-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
gap: 0.75rem;
padding: 0.75rem 1rem;
}
.preview-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 0.75rem;
padding: 0.75rem 1rem;
}
</style>