feat: add ui kit page
This commit is contained in:
+22
-3
@@ -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}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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. */
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
Reference in New Issue
Block a user