refactor: migrate ui elements to lib

This commit is contained in:
3252a8
2026-05-12 09:05:22 +03:00
parent 6a367c1dec
commit fe976022ec
50 changed files with 1275 additions and 878 deletions
@@ -0,0 +1,21 @@
<script>
import { cn } from "$lib/utils.js";
export let variant = "muted";
let className = "";
export { className as class };
</script>
<span
class={cn(
"admin-badge",
variant === "success" && "admin-badge-success",
variant === "danger" && "admin-badge-danger",
variant === "warning" && "admin-badge-warning",
variant === "muted" && "admin-badge-muted",
className,
)}
{...$$restProps}
>
<slot />
</span>
@@ -0,0 +1,45 @@
<script>
import { cva } from "class-variance-authority";
import { cn } from "$lib/utils.js";
export let type = "button";
export let variant = "default";
export let size = "default";
export let disabled = false;
export let onclick = undefined;
let className = "";
export { className as class };
const buttonVariants = cva("admin-btn", {
variants: {
variant: {
default: "",
primary: "admin-btn-primary",
ghost: "admin-btn-ghost",
danger: "admin-btn-danger",
dangerSoft: "admin-btn-danger-soft",
icon: "admin-btn-icon",
},
size: {
default: "",
sm: "admin-btn-sm",
icon: "admin-btn-icon",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
});
</script>
<button
class={cn(buttonVariants({ variant, size }), className)}
{type}
{disabled}
onclick={onclick}
{...$$restProps}
>
<slot />
</button>
@@ -0,0 +1,16 @@
<script>
import { Label } from "$components/ui/primitives.js";
export let label = "";
export let hint = "";
</script>
<Label.Root class="admin-field-label">
{#if label}
<span>{label}</span>
{/if}
{#if hint}
<small>{hint}</small>
{/if}
<slot />
</Label.Root>
@@ -0,0 +1,24 @@
<script>
import { ChevronLeft, ChevronRight } from "$components/ui/icons.js";
import AdminButton from "./AdminButton.svelte";
export let meta = "";
export let prevLabel = "Back";
export let nextLabel = "Next";
export let prevDisabled = false;
export let nextDisabled = false;
export let onPrev = () => {};
export let onNext = () => {};
</script>
<div class="admin-pagination">
<span class="admin-pagination-meta">{meta}</span>
<div class="admin-pagination-buttons">
<AdminButton size="sm" disabled={prevDisabled} onclick={onPrev}>
<ChevronLeft size={14} /> {prevLabel}
</AdminButton>
<AdminButton size="sm" disabled={nextDisabled} onclick={onNext}>
{nextLabel} <ChevronRight size={14} />
</AdminButton>
</div>
</div>
@@ -0,0 +1,38 @@
<script>
import { Check, ChevronDown } from "$components/ui/icons.js";
import { Select } from "$components/ui/primitives.js";
export let value = "";
export let items = [];
export let ariaLabel = "";
export let placeholder = "";
export let disabled = false;
export let sideOffset = 6;
export let onValueChange = () => {};
let className = "";
export { className as class };
$: selected = items.find((item) => item.value === value);
function handleValueChange(next) {
value = next;
onValueChange(next);
}
</script>
<Select.Root type="single" {value} {items} {disabled} onValueChange={handleValueChange}>
<Select.Trigger class={`admin-select-trigger ${className}`.trim()} aria-label={ariaLabel || placeholder}>
<span>{selected?.label || placeholder}</span>
<ChevronDown size={14} class="admin-select-icon" />
</Select.Trigger>
<Select.Portal>
<Select.Content class="admin-select-content" {sideOffset}>
{#each items as item (item.value)}
<Select.Item value={item.value} label={item.label} class="admin-select-item">
<span>{item.label}</span>
<Check size={14} class="admin-select-item-check" />
</Select.Item>
{/each}
</Select.Content>
</Select.Portal>
</Select.Root>
@@ -0,0 +1,13 @@
<script>
import { cn } from "$lib/utils.js";
export let skeleton = false;
let className = "";
export { className as class };
</script>
<div class="admin-table-wrap">
<table class={cn("admin-table", skeleton && "admin-table-skeleton", className)} aria-hidden={skeleton ? "true" : undefined} {...$$restProps}>
<slot />
</table>
</div>
@@ -0,0 +1,38 @@
<script>
import Skeleton from "$components/ui/skeleton.svelte";
import AdminTable from "./AdminTable.svelte";
export let headers = [];
export let rows = 6;
export let actionColumn = false;
export let widths = [];
function widthFor(index) {
if (widths[index]) return widths[index];
if (actionColumn && index === headers.length - 1) return "92px";
if (index === 0) return "48px";
if (index === headers.length - 1) return "76px";
return index % 3 === 0 ? "56%" : "72%";
}
</script>
<AdminTable skeleton>
<thead>
<tr>
{#each headers as header}
<th class:admin-cell-actions={actionColumn && header === headers[headers.length - 1]}>{header}</th>
{/each}
</tr>
</thead>
<tbody>
{#each Array(rows) as _, rowIndex (rowIndex)}
<tr>
{#each headers as header, colIndex (`${rowIndex}-${colIndex}`)}
<td>
<Skeleton variant="line" width={widthFor(colIndex)} />
</td>
{/each}
</tr>
{/each}
</tbody>
</AdminTable>
@@ -0,0 +1,34 @@
<script>
import { cn } from "$lib/utils.js";
export let title = "";
export let value = "";
export let left = "";
export let percent = 0;
export let warning = false;
export let premium = false;
export let label = "";
$: clamped = Math.max(0, Math.min(100, Number(percent) || 0));
</script>
<div class={cn("admin-traffic-card", warning && "admin-traffic-card-warning", premium && "admin-traffic-card-premium")}>
<div class="admin-traffic-head">
<span>{title}</span>
<strong>{value}</strong>
</div>
<div
class={cn("admin-traffic-bar", premium && "admin-traffic-bar-premium")}
aria-label={label || title}
role="progressbar"
aria-valuemin="0"
aria-valuemax="100"
aria-valuenow={Math.round(clamped)}
>
<span style={`width: ${clamped}%`}></span>
</div>
<div class="admin-traffic-meta">
<span>{left}</span>
<span>{clamped}%</span>
</div>
</div>
@@ -1,4 +1,12 @@
export { default as AdminBadge } from "./AdminBadge.svelte";
export { default as AdminButton } from "./AdminButton.svelte";
export { default as AdminDashboardGrid } from "./AdminDashboardGrid.svelte";
export { default as AdminDashboardStack } from "./AdminDashboardStack.svelte";
export { default as AdminEmptyState } from "./AdminEmptyState.svelte";
export { default as AdminField } from "./AdminField.svelte";
export { default as AdminPagination } from "./AdminPagination.svelte";
export { default as AdminSelect } from "./AdminSelect.svelte";
export { default as AdminSectionHeader } from "./AdminSectionHeader.svelte";
export { default as AdminTable } from "./AdminTable.svelte";
export { default as AdminTableSkeleton } from "./AdminTableSkeleton.svelte";
export { default as AdminTrafficCard } from "./AdminTrafficCard.svelte";
@@ -0,0 +1,67 @@
<script>
import Skeleton from "$components/ui/skeleton.svelte";
export let label = "";
export let rows = 3;
export let actions = 0;
export let methods = 2;
export let showNote = false;
export let showPayButton = true;
export let showMeta = true;
</script>
<div class="dialog-skeleton" aria-label={label}>
{#if actions}
<div class="tariff-action-list">
{#each Array(actions) as _, index (index)}
<div class="tariff-action-card skeleton-row">
<span>
<Skeleton variant="title" />
<Skeleton variant="short" />
</span>
<Skeleton class="skeleton-line-price" />
</div>
{/each}
</div>
<div class="payment-divider" aria-hidden="true"></div>
{/if}
<div class="option-list">
{#each Array(rows) as _, index (index)}
<div class={`option-row ${showMeta ? "plan-row" : "change-action-row"} skeleton-row`}>
<span class="option-row-main">
<Skeleton variant="title" />
<Skeleton variant="short" />
</span>
{#if showMeta}
<span class="option-row-meta">
<Skeleton class="skeleton-line-price" />
<Skeleton variant="tiny" />
</span>
{/if}
</div>
{/each}
</div>
{#if showNote}
<div class="topup-carryover-note skeleton-carryover-note">
<Skeleton variant="line" />
<Skeleton variant="short" />
</div>
{/if}
{#if methods}
<div class="method-grid">
{#each Array(methods) as _, index (index)}
<div class="method-card skeleton-method">
<Skeleton variant="dot" />
<Skeleton class="skeleton-line-method" />
</div>
{/each}
</div>
{/if}
{#if showPayButton}
<Skeleton class="skeleton-pay-button" />
{/if}
</div>
@@ -0,0 +1,11 @@
<script>
import Card from "$components/ui/card.svelte";
import { cn } from "$lib/utils.js";
let className = "";
export { className as class };
</script>
<Card class={cn("empty-card", className)} {...$$restProps}>
<slot />
</Card>
@@ -0,0 +1,71 @@
<script>
import { Check, ChevronsUpDown, Globe2 } from "$components/ui/icons.js";
import { Select } from "$components/ui/primitives.js";
export let open = false;
export let value = "ru";
export let currentOption = null;
export let userLanguage = "";
export let options = [];
export let disabled = false;
export let clickGuard = false;
export let clickGuardArmed = false;
export let closeLabel = "Close";
export let label = "Language";
export let onOpenChange = () => {};
export let onValueChange = () => {};
function closeFromGuard(event) {
event.preventDefault();
event.stopPropagation();
if (clickGuardArmed) onOpenChange(false);
}
</script>
{#if open || clickGuard}
<button
class="language-select-guard"
class:language-select-guard--armed={clickGuardArmed}
type="button"
aria-label={closeLabel}
onpointerdown={closeFromGuard}
onclick={closeFromGuard}
></button>
{/if}
<div class="settings-row settings-row-language">
<Globe2 size={21} />
<Select.Root
type="single"
bind:open
{value}
items={options}
{disabled}
onOpenChange={onOpenChange}
onValueChange={onValueChange}
>
<Select.Trigger class="language-select-trigger" aria-label={label}>
<span class="language-select-copy">
<strong>{label}</strong>
<small class="language-select-current">
<span class="emoji-flag" aria-hidden="true">{currentOption?.flag || "🏳️"}</span>
{currentOption?.label || userLanguage}
</small>
</span>
<ChevronsUpDown size={16} />
</Select.Trigger>
<Select.Content class="language-select-content" side="bottom" align="end" sideOffset={6}>
<Select.Viewport class="language-select-viewport">
{#each options as option (option.value)}
<Select.Item value={option.value} label={option.label} class="language-select-item">
<span class="language-select-item-main">
<span class="emoji-flag" aria-hidden="true">{option.flag}</span>
<span>{option.label}</span>
</span>
<Check size={15} class="language-select-item-check" />
</Select.Item>
{/each}
</Select.Viewport>
</Select.Content>
</Select.Root>
</div>
@@ -0,0 +1,22 @@
<script>
import { cn } from "$lib/utils.js";
export let value = 0;
export let label = "";
let className = "";
export { className as class };
$: clamped = Math.max(0, Math.min(100, Number(value) || 0));
</script>
<div
class={cn("progress", className)}
role={label ? "progressbar" : undefined}
aria-label={label || undefined}
aria-valuemin={label ? "0" : undefined}
aria-valuemax={label ? "100" : undefined}
aria-valuenow={label ? Math.round(clamped) : undefined}
{...$$restProps}
>
<span style={`width: ${clamped}%`}></span>
</div>
@@ -0,0 +1,40 @@
<script>
import { Bitcoin, CreditCard } from "$components/ui/icons.js";
export let methods = [];
export let selectedMethod = "";
export let t = (key) => key;
export let onSelect = () => {};
function methodMeta(method) {
const id = String(method?.id || "").toLowerCase();
if (id.includes("platega_sbp")) return { title: t("wa_method_platega_sbp_card"), icon: CreditCard };
if (id.includes("platega_crypto")) return { title: t("wa_method_platega_crypto"), icon: Bitcoin };
if (id.includes("yookassa") || id.includes("card")) return { title: t("pay_with_yookassa_button"), icon: null };
if (id.includes("severpay")) return { title: t("pay_with_severpay_button"), icon: null };
if (id.includes("freekassa")) return { title: t("pay_with_sbp_button"), icon: null };
if (id.includes("cryptopay") || id.includes("crypto")) return { title: t("pay_with_cryptopay_button"), icon: null };
if (id.includes("stars")) return { title: t("pay_with_stars_button"), icon: null };
if (id.includes("sbp")) return { title: t("pay_with_sbp_button"), icon: null };
return { title: t("wa_method_other_title"), icon: null };
}
</script>
<div class="method-grid">
{#each methods as method}
{@const meta = methodMeta(method)}
<button
class:active={selectedMethod === method.id}
class="method-card"
type="button"
onclick={() => onSelect(method.id)}
>
<span class="method-card-main">
{#if meta.icon}
<svelte:component this={meta.icon} size={19} />
{/if}
<strong>{meta.title}</strong>
</span>
</button>
{/each}
</div>
@@ -0,0 +1,11 @@
<script>
import { cn } from "$lib/utils.js";
export let error = false;
let className = "";
export { className as class };
</script>
<p class={cn("status-line", error && "error", className)} {...$$restProps}>
<slot />
</p>
@@ -0,0 +1,6 @@
export { default as DialogOptionsSkeleton } from "./DialogOptionsSkeleton.svelte";
export { default as EmptyCard } from "./EmptyCard.svelte";
export { default as LinearProgress } from "./LinearProgress.svelte";
export { default as LanguageSelect } from "./LanguageSelect.svelte";
export { default as PaymentMethodGrid } from "./PaymentMethodGrid.svelte";
export { default as StatusMessage } from "./StatusMessage.svelte";
@@ -1,5 +1,5 @@
<script>
import { X } from "lucide-svelte";
import { X } from "$components/ui/icons.js";
import { cn } from "$lib/utils.js";
import Button from "./button.svelte";
@@ -0,0 +1,66 @@
export {
Activity,
ArrowLeft,
ArrowRight,
Bitcoin,
CalendarDays,
Check,
CheckCircle2,
ChevronDown,
ChevronLeft,
ChevronRight,
ChevronsUpDown,
Circle,
CircleX,
Coins,
Copy,
CreditCard,
Crown,
Database,
Download,
ExternalLink,
Eye,
EyeOff,
FileText,
Gift,
Globe2,
Home,
Info,
Key,
LayoutDashboard,
LockKeyhole,
Mail,
Map,
Megaphone,
Menu,
MessageSquare,
MousePointerClick,
Plus,
QrCode,
Radio,
RefreshCw,
Repeat2,
Save,
Send,
Server,
Settings,
Shield,
Sliders,
Smartphone,
Sparkles,
Tag,
Ticket,
Trash2,
TrendingDown,
TrendingUp,
TriangleAlert,
User,
UserMinus,
UserPlus,
UserRound,
Users,
UsersRound,
WalletCards,
X,
Zap,
} from "lucide-svelte";
@@ -3,5 +3,8 @@ export { default as Button } from "./button.svelte";
export { default as Dialog } from "./dialog.svelte";
export { default as Input } from "./input.svelte";
export { default as LegacyCard } from "./card.svelte";
export { default as Skeleton } from "./skeleton.svelte";
export { default as Spinner } from "./spinner.svelte";
export * as Icons from "./icons.js";
export * as Card from "./card/index.js";
export { Accordion, Label, Select, Separator, Switch, Tabs, Tooltip } from "./primitives.js";
@@ -0,0 +1,25 @@
<script>
import { cn } from "$lib/utils.js";
export let variant = "block";
export let width = "";
export let height = "";
let className = "";
export { className as class };
</script>
<span
class={cn(
"ui-skeleton",
variant === "line" && "ui-skeleton-line",
variant === "title" && "ui-skeleton-line ui-skeleton-title",
variant === "short" && "ui-skeleton-line ui-skeleton-short",
variant === "tiny" && "ui-skeleton-line ui-skeleton-tiny",
variant === "badge" && "ui-skeleton-badge",
variant === "dot" && "ui-skeleton-dot",
className,
)}
style={`${width ? `width:${width};` : ""}${height ? `height:${height};` : ""}`}
aria-hidden="true"
{...$$restProps}
></span>
@@ -0,0 +1,16 @@
<script>
import { cn } from "$lib/utils.js";
export let size = "default";
export let label = "";
let className = "";
export { className as class };
</script>
<span
class={cn("ui-spinner", size === "sm" && "ui-spinner-sm", size === "lg" && "ui-spinner-lg", className)}
role={label ? "status" : undefined}
aria-label={label || undefined}
aria-hidden={label ? undefined : "true"}
{...$$restProps}
></span>