mirror of
https://github.com/Ku6epXBOCTuK/easy-png-tools.git
synced 2026-09-14 13:36:36 +00:00
feat: add ui components
This commit is contained in:
@@ -306,10 +306,9 @@ src/routes/preview/tools/[id]/+page.svelte
|
||||
`[data-theme='dark']` (таблица §2). Без утилитарных классов.
|
||||
- [x] **[C5]** Примитивы batch 1: `AppShell`, `BlueprintGrid`, `CheckerCanvas`,
|
||||
`MonoLabel`, `StatusDot`/`StatusLine`.
|
||||
- [ ] **[C6]** Примитивы batch 2: `Panel`, `PanelHeading`, `SettingGroup`,
|
||||
- [x] **[C6]** Примитивы batch 2: `Panel`, `PanelHeading`, `SettingGroup`,
|
||||
`SettingsFooter`, `MetaRow`/`MetaList`, `Segmented`, `Badge`, `StepCard`,
|
||||
`ToolCard`, `ImageCard`/`PreviewTile`. _(при превышении 500 строк —
|
||||
разбить на C6a/C6b)_
|
||||
`ToolCard`, `ImageCard`/`PreviewTile`.
|
||||
- [ ] **[C7]** Контролы: `Icon`, `IconButton`, `Button`, `Field`, `SliderField`,
|
||||
`ColorField`, `TextField`, `SelectField`, `CheckboxField`, `Toggle`,
|
||||
`SegmentedControl`, `EmptyState`, `CodeBlock`, `DownloadButton`.
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from "svelte";
|
||||
|
||||
type Tone = "default" | "accent" | "success" | "amber";
|
||||
|
||||
interface Props {
|
||||
tone?: Tone;
|
||||
children: Snippet;
|
||||
}
|
||||
let { tone = "default", children }: Props = $props();
|
||||
</script>
|
||||
|
||||
<span class="badge badge--{tone}">
|
||||
{#if children}{@render children()}{/if}
|
||||
</span>
|
||||
|
||||
<style>
|
||||
.badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.3rem;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
padding: 0.15rem 0.45rem;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
color: var(--muted);
|
||||
}
|
||||
.badge--accent {
|
||||
color: var(--blue);
|
||||
border-color: color-mix(in srgb, var(--blue) 40%, var(--line));
|
||||
}
|
||||
.badge--success {
|
||||
color: var(--success);
|
||||
border-color: color-mix(in srgb, var(--success) 40%, var(--line));
|
||||
}
|
||||
.badge--amber {
|
||||
color: var(--amber);
|
||||
border-color: color-mix(in srgb, var(--amber) 40%, var(--line));
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,32 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from "svelte";
|
||||
import MonoLabel from "./MonoLabel.svelte";
|
||||
|
||||
interface Props {
|
||||
label: string;
|
||||
children: Snippet;
|
||||
}
|
||||
let { label, children }: Props = $props();
|
||||
</script>
|
||||
|
||||
<figure class="image-card">
|
||||
<MonoLabel>{label}</MonoLabel>
|
||||
<div class="image-canvas">{@render children()}</div>
|
||||
</figure>
|
||||
|
||||
<style>
|
||||
.image-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
margin: 0;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
padding: 0.6rem;
|
||||
background: var(--panel);
|
||||
}
|
||||
.image-canvas {
|
||||
border-radius: var(--radius);
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,22 @@
|
||||
<script lang="ts">
|
||||
import MetaRow from "./MetaRow.svelte";
|
||||
|
||||
interface Props {
|
||||
items: { caption: string; value: string }[];
|
||||
}
|
||||
let { items }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class="meta-list">
|
||||
{#each items as item (item.caption)}
|
||||
<MetaRow caption={item.caption} value={item.value} />
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.meta-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.15rem;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,34 @@
|
||||
<script lang="ts">
|
||||
interface Props {
|
||||
caption: string;
|
||||
value: string;
|
||||
}
|
||||
let { caption, value }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class="meta-row">
|
||||
<span class="meta-caption">{caption}</span>
|
||||
<span class="meta-value">{value}</span>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.meta-row {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
padding: 0.25rem 0;
|
||||
}
|
||||
.meta-caption {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
color: var(--muted);
|
||||
}
|
||||
.meta-value {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
color: var(--foreground);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from "svelte";
|
||||
|
||||
interface Props {
|
||||
children: Snippet;
|
||||
}
|
||||
let { children }: Props = $props();
|
||||
</script>
|
||||
|
||||
<section class="panel">
|
||||
{@render children()}
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.panel {
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,40 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from "svelte";
|
||||
import MonoLabel from "./MonoLabel.svelte";
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
eyebrow?: string;
|
||||
actions?: Snippet;
|
||||
}
|
||||
let { title, eyebrow, actions }: Props = $props();
|
||||
</script>
|
||||
|
||||
<header class="panel-heading">
|
||||
<div class="heading-text">
|
||||
{#if eyebrow}<MonoLabel accent>{eyebrow}</MonoLabel>{/if}
|
||||
<strong class="heading-title">{title}</strong>
|
||||
</div>
|
||||
{#if actions}<div class="heading-actions">{@render actions()}</div>{/if}
|
||||
</header>
|
||||
|
||||
<style>
|
||||
.panel-heading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
padding: 0.75rem 1rem;
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
.heading-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
.heading-title {
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
color: var(--foreground);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,32 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from "svelte";
|
||||
import MonoLabel from "./MonoLabel.svelte";
|
||||
|
||||
interface Props {
|
||||
label: string;
|
||||
children: Snippet;
|
||||
}
|
||||
let { label, children }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class="preview-tile">
|
||||
<MonoLabel>{label}</MonoLabel>
|
||||
<div class="tile-canvas">{@render children()}</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.preview-tile {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.3rem;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
padding: 0.4rem;
|
||||
background: var(--panel);
|
||||
}
|
||||
.tile-canvas {
|
||||
border-radius: var(--radius);
|
||||
overflow: hidden;
|
||||
min-height: 120px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,60 @@
|
||||
<script lang="ts">
|
||||
interface Option {
|
||||
value: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
options: Option[];
|
||||
value?: string;
|
||||
onchange?: (value: string) => void;
|
||||
}
|
||||
let { options, value = $bindable(), onchange }: Props = $props();
|
||||
|
||||
function select(v: string) {
|
||||
value = v;
|
||||
onchange?.(v);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="segmented" role="group">
|
||||
{#each options as opt (opt.value)}
|
||||
<button
|
||||
type="button"
|
||||
class="segment"
|
||||
class:active={value === opt.value}
|
||||
aria-pressed={value === opt.value}
|
||||
onclick={() => select(opt.value)}
|
||||
>
|
||||
{opt.label}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.segmented {
|
||||
display: inline-flex;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
overflow: hidden;
|
||||
background: var(--background);
|
||||
}
|
||||
.segment {
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--muted);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
padding: 0.3rem 0.7rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
.segment + .segment {
|
||||
border-left: 1px solid var(--line);
|
||||
}
|
||||
.segment.active {
|
||||
background: var(--blue);
|
||||
color: #fff;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,31 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from "svelte";
|
||||
import MonoLabel from "./MonoLabel.svelte";
|
||||
|
||||
interface Props {
|
||||
label: string;
|
||||
children: Snippet;
|
||||
}
|
||||
let { label, children }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class="setting-group">
|
||||
<MonoLabel>{label}</MonoLabel>
|
||||
<div class="setting-control">{@render children()}</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.setting-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
padding: 0.6rem 1rem;
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
.setting-control {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,21 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from "svelte";
|
||||
|
||||
interface Props {
|
||||
children: Snippet;
|
||||
}
|
||||
let { children }: Props = $props();
|
||||
</script>
|
||||
|
||||
<footer class="settings-footer">{@render children()}</footer>
|
||||
|
||||
<style>
|
||||
.settings-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem 1rem;
|
||||
border-top: 1px solid var(--line);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,82 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from "svelte";
|
||||
import { GripVertical, X } from "@lucide/svelte";
|
||||
import Badge from "./Badge.svelte";
|
||||
|
||||
interface Props {
|
||||
index: number;
|
||||
title?: string;
|
||||
type?: string;
|
||||
children?: Snippet;
|
||||
onremove?: () => void;
|
||||
}
|
||||
let { index, title, type, children, onremove }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class="step-card">
|
||||
<div class="step-heading">
|
||||
<span class="drag" aria-hidden="true"><GripVertical size={16} /></span>
|
||||
<span class="step-index">{index.toString().padStart(2, "0")}</span>
|
||||
{#if type}<Badge tone="accent">{type}</Badge>{/if}
|
||||
{#if title}<span class="step-title">{title}</span>{/if}
|
||||
<button
|
||||
type="button"
|
||||
class="step-remove"
|
||||
aria-label="Remove step"
|
||||
onclick={() => onremove?.()}
|
||||
>
|
||||
<X size={16} />
|
||||
</button>
|
||||
</div>
|
||||
{#if children}
|
||||
<div class="step-body">{@render children()}</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.step-card {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
background: var(--panel);
|
||||
}
|
||||
.step-heading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
.drag {
|
||||
color: var(--muted);
|
||||
cursor: grab;
|
||||
display: inline-flex;
|
||||
}
|
||||
.step-index {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
color: var(--muted);
|
||||
}
|
||||
.step-title {
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
color: var(--foreground);
|
||||
flex: 1;
|
||||
}
|
||||
.step-remove {
|
||||
margin-left: auto;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--muted);
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
}
|
||||
.step-remove:hover {
|
||||
color: var(--danger);
|
||||
}
|
||||
.step-body {
|
||||
padding: 0.75rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,72 @@
|
||||
<script lang="ts">
|
||||
import { ArrowUpRight } from "@lucide/svelte";
|
||||
import type { Snippet } from "svelte";
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
href: string;
|
||||
description?: string;
|
||||
index?: number;
|
||||
icon?: Snippet;
|
||||
}
|
||||
let { title, href, description, index, icon }: Props = $props();
|
||||
</script>
|
||||
|
||||
<!-- eslint-disable-next-line svelte/no-navigation-without-resolve -->
|
||||
<a class="tool-card" {href}>
|
||||
{#if icon}<span class="tool-icon">{@render icon()}</span>{/if}
|
||||
<span class="tool-index">
|
||||
{index !== undefined ? index.toString().padStart(2, "0") : ""}
|
||||
</span>
|
||||
<span class="tool-copy">
|
||||
<span class="tool-title">{title}</span>
|
||||
{#if description}<span class="tool-desc">{description}</span>{/if}
|
||||
</span>
|
||||
<span class="tool-arrow"><ArrowUpRight size={16} /></span>
|
||||
</a>
|
||||
|
||||
<style>
|
||||
.tool-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem 1rem;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
background: var(--panel);
|
||||
color: var(--foreground);
|
||||
text-decoration: none;
|
||||
}
|
||||
.tool-card:hover {
|
||||
border-color: var(--blue);
|
||||
}
|
||||
.tool-icon {
|
||||
color: var(--blue);
|
||||
display: inline-flex;
|
||||
}
|
||||
.tool-index {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
color: var(--muted);
|
||||
}
|
||||
.tool-copy {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
flex: 1;
|
||||
}
|
||||
.tool-title {
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.tool-desc {
|
||||
font-size: 0.8rem;
|
||||
color: var(--muted);
|
||||
}
|
||||
.tool-arrow {
|
||||
color: var(--muted);
|
||||
}
|
||||
.tool-card:hover .tool-arrow {
|
||||
color: var(--blue);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user