fix(ui): выровнять confirms и токены под ReUI PRO
Docker images / prepare-release (push) Successful in 8s
Docker images / backend-image (push) Successful in 1m31s
Docker images / frontend-image (push) Successful in 2m11s
Docker images / notify-webhook (push) Skipped
Docker images / updater-image (push) Successful in 40s
Docker images / publish-release (push) Successful in 10s

Добавить ui-design-contract, AlertDialog вместо hand-roll, CodeExportSheet для preview фильтров, ReUI Badge и gap вместо space-y.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Denozordec
2026-09-06 00:12:57 +07:00
co-authored by Cursor
parent 66509b26bd
commit 5e0c16e808
7 changed files with 212 additions and 180 deletions
+80 -58
View File
@@ -16,6 +16,17 @@ import { Separator } from "@/components/ui/separator"
import {
Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription, SheetFooter,
} from "@/components/ui/sheet"
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogMedia,
AlertDialogTitle,
} from "@/components/ui/alert-dialog"
import { Flag } from "@/components/flag"
import { StatusDot } from "@/components/status-dot"
import { servers } from "@/lib/data"
@@ -152,7 +163,7 @@ const PERM_OPTS: { v: PermLevel; label: string }[] = [
const PERM_COLOR: Record<PermLevel, string> = {
none: "bg-muted text-muted-foreground",
read: "bg-sky-500/10 text-sky-600 dark:text-sky-400",
write: "bg-emerald-500/10 text-emerald-600 dark:text-emerald-400",
write: "bg-success/10 text-success",
}
const SECTIONS_NAV = ["Общие", "EvoBGP", "Уведомления", "Пользователи", "API-ключи", "Безопасность"] as const
@@ -188,8 +199,8 @@ function PermPills({ value, onChange, disabled }: { value: PermLevel; onChange:
"px-2 text-[11px] font-medium transition-colors",
i < PERM_OPTS.length - 1 && "border-r border-input",
value === o.v
? o.v === "write" ? "bg-emerald-600 text-white dark:bg-emerald-500"
: o.v === "read" ? "bg-sky-600 text-white dark:bg-sky-500"
? o.v === "write" ? "bg-success text-white"
: o.v === "read" ? "bg-info text-white"
: "bg-muted-foreground/70 text-white"
: "text-muted-foreground hover:bg-muted",
)}
@@ -685,68 +696,79 @@ function UserSheet({ open, user, onSave, onClose }: {
// ─── delete confirm ───────────────────────────────────────────────────────────
function DatabaseRestoreConfirm({
open,
filename,
busy,
onConfirm,
onCancel,
}: {
open: boolean
filename: string
busy?: boolean
onConfirm: () => void
onCancel: () => void
}) {
return (
<div className="fixed inset-0 z-50 flex items-center justify-center">
<div className="absolute inset-0 bg-black/40 backdrop-blur-sm" onClick={onCancel} />
<div className="relative z-10 w-full max-w-sm mx-4 bg-card rounded-xl border shadow-2xl p-5 flex flex-col gap-4">
<div className="flex items-center gap-3">
<div className="size-9 rounded-full bg-destructive/10 flex items-center justify-center shrink-0">
<AlertCircleIcon className="size-4 text-destructive" />
</div>
<div>
<p className="text-sm font-semibold">Восстановить базу приложения?</p>
<p className="text-xs text-muted-foreground mt-0.5 break-all">{filename}</p>
</div>
</div>
<p className="text-xs text-muted-foreground">
Текущие данные SQLite на бекенде будут полностью заменены содержимым файла. Рекомендуется сначала скачать
актуальный бэкап.
</p>
<div className="flex gap-2">
<Button variant="outline" className="flex-1" onClick={onCancel} disabled={busy}>Отмена</Button>
<Button variant="destructive" className="flex-1" onClick={onConfirm} disabled={busy}>
<AlertDialog open={open} onOpenChange={(v) => { if (!v && !busy) onCancel() }}>
<AlertDialogContent size="default">
<AlertDialogHeader>
<AlertDialogMedia className="bg-destructive/10 text-destructive">
<AlertCircleIcon />
</AlertDialogMedia>
<AlertDialogTitle>Восстановить базу приложения?</AlertDialogTitle>
<AlertDialogDescription>
Файл: {filename}. Текущие данные SQLite на бекенде будут полностью заменены.
Рекомендуется сначала скачать актуальный бэкап.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={busy} onClick={onCancel}>Отмена</AlertDialogCancel>
<AlertDialogAction
variant="destructive"
disabled={busy}
onClick={onConfirm}
>
{busy ? <LoaderCircleIcon className="size-4 animate-spin" /> : null}
{busy ? "Восстановление…" : "Восстановить"}
</Button>
</div>
</div>
</div>
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)
}
function DeleteConfirm({ user, onConfirm, onCancel }: { user: User; onConfirm: () => void; onCancel: () => void }) {
function DeleteConfirm({
open,
user,
onConfirm,
onCancel,
}: {
open: boolean
user: User
onConfirm: () => void
onCancel: () => void
}) {
return (
<div className="fixed inset-0 z-50 flex items-center justify-center">
<div className="absolute inset-0 bg-black/40 backdrop-blur-sm" onClick={onCancel} />
<div className="relative z-10 w-full max-w-sm mx-4 bg-card rounded-xl border shadow-2xl p-5 flex flex-col gap-4">
<div className="flex items-center gap-3">
<div className="size-9 rounded-full bg-destructive/10 flex items-center justify-center shrink-0">
<TrashIcon className="size-4 text-destructive" />
</div>
<div>
<p className="text-sm font-semibold">Удалить пользователя?</p>
<p className="text-xs text-muted-foreground mt-0.5">{user.name} · @{user.login}</p>
</div>
</div>
<p className="text-xs text-muted-foreground">
Это действие нельзя отменить. Пользователь потеряет доступ немедленно.
</p>
<div className="flex gap-2">
<Button variant="outline" className="flex-1" onClick={onCancel}>Отмена</Button>
<Button variant="destructive" className="flex-1" onClick={onConfirm}>Удалить</Button>
</div>
</div>
</div>
<AlertDialog open={open} onOpenChange={(v) => { if (!v) onCancel() }}>
<AlertDialogContent size="default">
<AlertDialogHeader>
<AlertDialogMedia className="bg-destructive/10 text-destructive">
<TrashIcon />
</AlertDialogMedia>
<AlertDialogTitle>Удалить пользователя?</AlertDialogTitle>
<AlertDialogDescription>
{user.name} · @{user.login}. Это действие нельзя отменить.
Пользователь потеряет доступ немедленно.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel onClick={onCancel}>Отмена</AlertDialogCancel>
<AlertDialogAction variant="destructive" onClick={onConfirm}>
Удалить
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)
}
@@ -937,7 +959,7 @@ export default function SettingsPage() {
// ── Общие ──
if (section === "Общие") return (
<div className="space-y-4">
<div className="flex flex-col gap-4">
{/* ── Источник данных ── */}
<OpsPanel
@@ -960,9 +982,7 @@ export default function SettingsPage() {
"px-3 text-xs transition-colors border-input",
i === 0 && "border-r",
mode === v
? v === "live"
? "bg-emerald-600 text-white dark:bg-emerald-500"
: "bg-primary text-primary-foreground"
? "bg-primary text-primary-foreground"
: "text-muted-foreground hover:bg-muted",
)}>
{l}
@@ -972,7 +992,7 @@ export default function SettingsPage() {
</SettingRow>
) : (
<SettingRow label="Режим данных" description="В Docker-образе приложение работает только с живыми данными бекенда">
<span className="inline-flex h-8 items-center rounded-md border border-input bg-emerald-600 px-3 text-xs text-white dark:bg-emerald-500">
<span className="inline-flex h-8 items-center rounded-md border border-input bg-primary px-3 text-xs text-primary-foreground">
Живые
</span>
</SettingRow>
@@ -1173,7 +1193,7 @@ export default function SettingsPage() {
// ── EvoBGP ──
if (section === "EvoBGP") return (
<div className="space-y-4">
<div className="flex flex-col gap-4">
{(mode !== "live" || backendStatus !== true) && (
<Frame dense className="w-full">
<FramePanel className="px-4 py-4">
@@ -1357,7 +1377,7 @@ export default function SettingsPage() {
// ── Уведомления ──
if (section === "Уведомления") return (
<div className="space-y-4">
<div className="flex flex-col gap-4">
<OpsPanel title="Каналы уведомлений" contentClassName="divide-y px-5">
<SettingRow label="Email" description="Отправка уведомлений на [email protected]">
<FormToggle checked={notifEmail} onChange={setNotifEmail} />
@@ -1479,11 +1499,11 @@ export default function SettingsPage() {
// ── API-ключи ──
if (section === "API-ключи") return (
<div className="space-y-4">
<div className="flex flex-col gap-4">
<div className="flex justify-end">
<Button size="sm"><PlusIcon className="size-4" />Создать ключ</Button>
</div>
<div className="space-y-3">
<div className="flex flex-col gap-3">
{apiKeys.map(k => (
<Frame key={k.id} dense className="w-full">
<FramePanel className="pt-4 pb-3 px-4">
@@ -1546,7 +1566,7 @@ export default function SettingsPage() {
// ── Безопасность ──
if (section === "Безопасность") return (
<div className="space-y-4">
<div className="flex flex-col gap-4">
<OpsPanel title="Аутентификация" contentClassName="divide-y px-5">
<SettingRow label="Двухфакторная аутентификация (MFA)"
description="TOTP / Authenticator app для всех администраторов">
@@ -1647,6 +1667,7 @@ export default function SettingsPage() {
{/* delete confirm */}
{dbRestoreFile && (
<DatabaseRestoreConfirm
open={!!dbRestoreFile}
filename={dbRestoreFile.name}
busy={dbRestoreBusy}
onConfirm={() => { void handleSystemDatabaseRestoreConfirm() }}
@@ -1658,6 +1679,7 @@ export default function SettingsPage() {
)}
{deleteTarget && (
<DeleteConfirm
open={!!deleteTarget}
user={deleteTarget}
onConfirm={() => { setUsers(p => p.filter(u => u.id !== deleteTarget.id)); setDeleteTarget(null) }}
onCancel={() => setDeleteTarget(null)}