From fe976022ec6fe260673420dff4f665b5ceb38c7a Mon Sep 17 00:00:00 2001 From: 3252a8 <3252a8@proton.me> Date: Tue, 12 May 2026 09:05:22 +0300 Subject: [PATCH] refactor: migrate ui elements to lib --- bot/app/web/frontend/src/App.svelte | 2 + bot/app/web/frontend/src/PreviewBoard.svelte | 5 +- .../web/frontend/src/admin/AdminPanel.svelte | 100 +++++++++-- .../src/admin/sections/AdsSection.svelte | 93 +++++----- .../admin/sections/BroadcastSection.svelte | 33 ++-- .../src/admin/sections/LogsSection.svelte | 58 +++---- .../src/admin/sections/PaymentsSection.svelte | 66 ++++--- .../src/admin/sections/PromosSection.svelte | 90 +++++----- .../src/admin/sections/SettingsSection.svelte | 31 ++-- .../src/admin/sections/StatsSection.svelte | 71 ++------ .../admin/sections/TariffEditorModal.svelte | 135 ++++++--------- .../src/admin/sections/TariffsSection.svelte | 50 +++--- .../src/admin/sections/UserDetailModal.svelte | 118 ++++++------- .../src/admin/sections/UsersSection.svelte | 100 ++++------- .../patterns/admin/AdminBadge.svelte | 21 +++ .../patterns/admin/AdminButton.svelte | 45 +++++ .../patterns/admin/AdminField.svelte | 16 ++ .../patterns/admin/AdminPagination.svelte | 24 +++ .../patterns/admin/AdminSelect.svelte | 38 ++++ .../patterns/admin/AdminTable.svelte | 13 ++ .../patterns/admin/AdminTableSkeleton.svelte | 38 ++++ .../patterns/admin/AdminTrafficCard.svelte | 34 ++++ .../lib/components/patterns/admin/index.js | 8 + .../webapp/DialogOptionsSkeleton.svelte | 67 +++++++ .../patterns/webapp/EmptyCard.svelte | 11 ++ .../patterns/webapp/LanguageSelect.svelte | 71 ++++++++ .../patterns/webapp/LinearProgress.svelte | 22 +++ .../patterns/webapp/PaymentMethodGrid.svelte | 40 +++++ .../patterns/webapp/StatusMessage.svelte | 11 ++ .../lib/components/patterns/webapp/index.js | 6 + .../src/lib/components/ui/dialog.svelte | 2 +- .../frontend/src/lib/components/ui/icons.js | 66 +++++++ .../frontend/src/lib/components/ui/index.js | 3 + .../src/lib/components/ui/skeleton.svelte | 25 +++ .../src/lib/components/ui/spinner.svelte | 16 ++ .../web/frontend/src/preview/BackTitle.svelte | 2 +- .../src/preview/PreviewMethods.svelte | 2 +- .../frontend/src/preview/PreviewNav.svelte | 2 +- .../frontend/src/styles/admin-controls.css | 13 +- bot/app/web/frontend/src/styles/admin.css | 127 ++++++++------ .../web/frontend/src/styles/components.css | 113 +++++++++++- bot/app/web/frontend/src/styles/webapp.css | 5 + .../web/frontend/src/webapp/BottomNav.svelte | 2 +- .../frontend/src/webapp/PaymentDialogs.svelte | 78 ++------- .../frontend/src/webapp/TariffDialogs.svelte | 163 ++---------------- .../src/webapp/auth/AuthScreen.svelte | 16 +- .../src/webapp/screens/DevicesScreen.svelte | 19 +- .../src/webapp/screens/HomeScreen.svelte | 11 +- .../src/webapp/screens/InviteScreen.svelte | 7 +- .../src/webapp/screens/SettingsScreen.svelte | 64 ++----- 50 files changed, 1275 insertions(+), 878 deletions(-) create mode 100644 bot/app/web/frontend/src/lib/components/patterns/admin/AdminBadge.svelte create mode 100644 bot/app/web/frontend/src/lib/components/patterns/admin/AdminButton.svelte create mode 100644 bot/app/web/frontend/src/lib/components/patterns/admin/AdminField.svelte create mode 100644 bot/app/web/frontend/src/lib/components/patterns/admin/AdminPagination.svelte create mode 100644 bot/app/web/frontend/src/lib/components/patterns/admin/AdminSelect.svelte create mode 100644 bot/app/web/frontend/src/lib/components/patterns/admin/AdminTable.svelte create mode 100644 bot/app/web/frontend/src/lib/components/patterns/admin/AdminTableSkeleton.svelte create mode 100644 bot/app/web/frontend/src/lib/components/patterns/admin/AdminTrafficCard.svelte create mode 100644 bot/app/web/frontend/src/lib/components/patterns/webapp/DialogOptionsSkeleton.svelte create mode 100644 bot/app/web/frontend/src/lib/components/patterns/webapp/EmptyCard.svelte create mode 100644 bot/app/web/frontend/src/lib/components/patterns/webapp/LanguageSelect.svelte create mode 100644 bot/app/web/frontend/src/lib/components/patterns/webapp/LinearProgress.svelte create mode 100644 bot/app/web/frontend/src/lib/components/patterns/webapp/PaymentMethodGrid.svelte create mode 100644 bot/app/web/frontend/src/lib/components/patterns/webapp/StatusMessage.svelte create mode 100644 bot/app/web/frontend/src/lib/components/patterns/webapp/index.js create mode 100644 bot/app/web/frontend/src/lib/components/ui/icons.js create mode 100644 bot/app/web/frontend/src/lib/components/ui/skeleton.svelte create mode 100644 bot/app/web/frontend/src/lib/components/ui/spinner.svelte diff --git a/bot/app/web/frontend/src/App.svelte b/bot/app/web/frontend/src/App.svelte index de4f3db..79857ff 100644 --- a/bot/app/web/frontend/src/App.svelte +++ b/bot/app/web/frontend/src/App.svelte @@ -376,6 +376,8 @@ }, 220); return; } + languageClickGuard = true; + languageClickGuardArmed = false; languageClickGuardTimer = window.setTimeout(() => { languageClickGuard = false; languageClickGuardTimer = null; diff --git a/bot/app/web/frontend/src/PreviewBoard.svelte b/bot/app/web/frontend/src/PreviewBoard.svelte index cac8087..0160881 100644 --- a/bot/app/web/frontend/src/PreviewBoard.svelte +++ b/bot/app/web/frontend/src/PreviewBoard.svelte @@ -17,10 +17,11 @@ Ticket, UserRound, Zap, - } from "lucide-svelte"; + } from "$components/ui/icons.js"; import Button from "$components/ui/button.svelte"; import Card from "$components/ui/card.svelte"; + import { LinearProgress } from "$components/patterns/webapp/index.js"; import BackTitle from "./preview/BackTitle.svelte"; import PhoneFrame from "./preview/PhoneFrame.svelte"; import PreviewMethods from "./preview/PreviewMethods.svelte"; @@ -78,7 +79,7 @@
Использовано трафика{sub.traffic_used} из {sub.traffic_limit}
-
+
18%
diff --git a/bot/app/web/frontend/src/admin/AdminPanel.svelte b/bot/app/web/frontend/src/admin/AdminPanel.svelte index 66c5fb8..e0bfb05 100644 --- a/bot/app/web/frontend/src/admin/AdminPanel.svelte +++ b/bot/app/web/frontend/src/admin/AdminPanel.svelte @@ -18,9 +18,10 @@ Sparkles, Tag, UsersRound, - } from "lucide-svelte"; + } from "$components/ui/icons.js"; import { onMount, setContext } from "svelte"; import { Select } from "$components/ui/primitives.js"; + import { AdminBadge, AdminButton } from "$components/patterns/admin/index.js"; import BrandMark from "../BrandMark.svelte"; import AdsSection from "./sections/AdsSection.svelte"; @@ -140,6 +141,11 @@ } let sidebarOpen = false; let isCompact = false; + let adminLanguageMenuOpen = false; + let adminLanguageClickGuard = false; + let adminLanguageClickGuardArmed = false; + let adminLanguageClickGuardTimer = null; + let adminLanguageClickGuardArmTimer = null; function flash(text) { onToast(text); @@ -246,6 +252,44 @@ isCompact = Boolean(event?.matches); } + function clearAdminLanguageClickGuard() { + if (adminLanguageClickGuardTimer) { + window.clearTimeout(adminLanguageClickGuardTimer); + adminLanguageClickGuardTimer = null; + } + if (adminLanguageClickGuardArmTimer) { + window.clearTimeout(adminLanguageClickGuardArmTimer); + adminLanguageClickGuardArmTimer = null; + } + adminLanguageClickGuard = false; + adminLanguageClickGuardArmed = false; + } + + function setAdminLanguageMenuOpen(open) { + adminLanguageMenuOpen = Boolean(open); + clearAdminLanguageClickGuard(); + if (adminLanguageMenuOpen) { + adminLanguageClickGuard = true; + adminLanguageClickGuardArmTimer = window.setTimeout(() => { + adminLanguageClickGuardArmed = true; + adminLanguageClickGuardArmTimer = null; + }, 220); + return; + } + adminLanguageClickGuard = true; + adminLanguageClickGuardArmed = false; + adminLanguageClickGuardTimer = window.setTimeout(() => { + adminLanguageClickGuard = false; + adminLanguageClickGuardTimer = null; + }, 260); + } + + function closeAdminLanguageFromGuard(event) { + event.preventDefault(); + event.stopPropagation(); + if (adminLanguageClickGuardArmed) setAdminLanguageMenuOpen(false); + } + onMount(() => { if (typeof window !== "undefined" && typeof window.matchMedia === "function") { compactMql = window.matchMedia("(max-width: 720px)"); @@ -262,6 +306,7 @@ else if (compactMql.removeListener) compactMql.removeListener(onCompactChange); } if (typeof window !== "undefined") window.removeEventListener("popstate", onPopState); + clearAdminLanguageClickGuard(); }; }); @@ -274,6 +319,16 @@ {#if sidebarOpen} {/if} + {#if adminLanguageMenuOpen || adminLanguageClickGuard} + + {/if}
{#each NAV_GROUPS as group} @@ -304,7 +359,15 @@ {#if languageOptions.length}
- + {t("wa_settings_language", {}, at("language", {}, "Язык"))} @@ -351,37 +414,37 @@
{#if active === "stats"} - + {/if} {#if active === "payments"} - + {/if} {#if active === "promos"} - + {/if} {#if active === "ads"} - + {/if} {#if active === "tariffs"} - + {/if} {#if active === "settings"} {#if dirtyCount} - {at("settings_dirty_count", { count: dirtyCount }, "Изменений: " + dirtyCount)} + {at("settings_dirty_count", { count: dirtyCount }, "Изменений: " + dirtyCount)} {/if} - + {/if}
@@ -395,7 +458,6 @@ + {/if} {#if active === "logs"} diff --git a/bot/app/web/frontend/src/admin/sections/AdsSection.svelte b/bot/app/web/frontend/src/admin/sections/AdsSection.svelte index 7dbe733..b9be62f 100644 --- a/bot/app/web/frontend/src/admin/sections/AdsSection.svelte +++ b/bot/app/web/frontend/src/admin/sections/AdsSection.svelte @@ -1,8 +1,15 @@ {at("tariff_label_model", {}, "Модель тарификации")} {at("tariff_model_period_label", {}, "Период")} — {at("tariff_model_period_desc", {}, "пользователь покупает фиксированный срок (1/3/12 мес. и т.д.)")}. {at("tariff_model_traffic_label", {}, "Трафик")} — {at("tariff_model_traffic_desc", {}, "пользователь покупает пакеты гигабайт по фиксированной цене за GB")} - - - {tariffDraft.billing_model === "traffic" ? at("tariff_model_traffic_label", {}, "Трафик") : at("tariff_model_period_label", {}, "Период")} - - - - - - {at("tariff_model_period_label", {}, "Период")} - - - - {at("tariff_model_traffic_label", {}, "Трафик")} - - - - - + @@ -112,29 +109,16 @@
{at("tariff_label_squads", {}, "Базовые Internal Squads")} {panelSquadsLoading ? at("loading_squads", {}, "Загружаю список из панели…") : at("tariff_hint_squads", {}, "Сквады Remnawave, к которым подключается пользователь по этому тарифу. Выберите один или несколько")} - { tariffsStore.addSquadToDraft("squadUuids", value); selectedBaseSquad = ""; }} - > - - {at("btn_add_squad", {}, "Добавить сквад")} - - - - - {#each panelSquads as squad} - - {squad.name} - - - {/each} - - - + />
{#each normalizeUuidList(tariffDraft.squadUuids) as uuid}
- - + tariffsStore.addDraftRow("premiumTopupRubRows", { gb: 10, price: "" })}> {at("tariff_btn_package_rub", {}, "Пакет ₽")} + tariffsStore.addDraftRow("premiumTopupStarsRows", { gb: 10, price: "" })}> {at("tariff_btn_package_stars", {}, "Пакет ⭐")}
@@ -254,7 +225,7 @@
- + tariffsStore.removeDraftRow("premiumTopupRubRows", index)} aria-label={at("btn_delete", {}, "Удалить")}>
{/each}
@@ -271,7 +242,7 @@
- + tariffsStore.removeDraftRow("premiumTopupStarsRows", index)} aria-label={at("btn_delete", {}, "Удалить")}>
{/each}
@@ -287,9 +258,9 @@ {at("tariff_pricing_period_title", {}, "Периоды подписки и цены")} {at("tariff_pricing_period_subtitle", {}, "Каждая строка — отдельный вариант на витрине: за сколько месяцев пользователь платит и сколько это стоит")} - + {#if !tariffDraft.periodRows.length}

{at("tariff_pricing_empty", {}, "Добавьте хотя бы один период — без него тариф не появится на витрине.")}

@@ -306,9 +277,9 @@ - + {/each} @@ -322,8 +293,8 @@ {at("tariff_pricing_traffic_subtitle", {}, "Базовая витрина для трафиковой модели. Каждая строка — пакет «N гигабайт за N единиц валюты»")}
- - + tariffsStore.addDraftRow("trafficRubRows", { gb: 10, price: "" })}> {at("tariff_btn_package_rub", {}, "Пакет ₽")} + tariffsStore.addDraftRow("trafficStarsRows", { gb: 10, price: "" })}> {at("tariff_btn_package_stars", {}, "Пакет ⭐")}
@@ -340,7 +311,7 @@
- + tariffsStore.removeDraftRow("trafficRubRows", index)} aria-label={at("btn_delete", {}, "Удалить")}>
{/each}
@@ -357,7 +328,7 @@
- + tariffsStore.removeDraftRow("trafficStarsRows", index)} aria-label={at("btn_delete", {}, "Удалить")}>
{/each} @@ -375,8 +346,8 @@ {at("tariff_topup_subtitle", {}, "Когда у пользователя кончился месячный лимит, ему предложат купить дополнительный пакет, не меняя срок подписки")}
- - + tariffsStore.addDraftRow("topupRubRows", { gb: 10, price: "" })}> {at("tariff_btn_package_rub", {}, "Пакет ₽")} + tariffsStore.addDraftRow("topupStarsRows", { gb: 10, price: "" })}> {at("tariff_btn_package_stars", {}, "Пакет ⭐")}
@@ -393,7 +364,7 @@
- + tariffsStore.removeDraftRow("topupRubRows", index)} aria-label={at("btn_delete", {}, "Удалить")}>
{/each}
@@ -410,7 +381,7 @@
- + tariffsStore.removeDraftRow("topupStarsRows", index)} aria-label={at("btn_delete", {}, "Удалить")}>
{/each} @@ -429,8 +400,8 @@ {at("tariff_hwid_packages_subtitle", {}, "Расширяет лимит, указанный во вкладке «Основное». Каждая строка — пакет «+N устройств за N единиц валюты»")}
- - + tariffsStore.addDraftRow("hwidRubRows", { count: 1, price: "" })}> {at("tariff_btn_package_rub", {}, "Пакет ₽")} + tariffsStore.addDraftRow("hwidStarsRows", { count: 1, price: "" })}> {at("tariff_btn_package_stars", {}, "Пакет ⭐")}
@@ -447,7 +418,7 @@
- + tariffsStore.removeDraftRow("hwidRubRows", index)} aria-label={at("btn_delete", {}, "Удалить")}>
{/each}
@@ -464,7 +435,7 @@
- + tariffsStore.removeDraftRow("hwidStarsRows", index)} aria-label={at("btn_delete", {}, "Удалить")}>
{/each} @@ -474,10 +445,10 @@
- - +
@@ -490,9 +461,9 @@ class="admin-dialog" >
- - +
diff --git a/bot/app/web/frontend/src/admin/sections/TariffsSection.svelte b/bot/app/web/frontend/src/admin/sections/TariffsSection.svelte index 797dde0..d796a80 100644 --- a/bot/app/web/frontend/src/admin/sections/TariffsSection.svelte +++ b/bot/app/web/frontend/src/admin/sections/TariffsSection.svelte @@ -1,6 +1,7 @@ {#if tariffsLoading} -
{at("loading", {}, "Загрузка…")}
+ {at("loading", {}, "Загрузка…")} {:else}
@@ -72,19 +73,19 @@ {tariffsPath || "data/tariffs.json"}
- - +
{#if !tariffsCatalog.tariffs.length} -
+ {at("tariffs_catalog_empty", {}, "Каталог пуст. Добавьте первый тариф, после сохранения будет создан JSON-файл каталога.")} -
+ {:else}
{#each tariffsCatalog.tariffs as tariff} @@ -94,15 +95,15 @@
{tariffName(tariff)} {#if tariff.key === tariffsCatalog.default_tariff} - {at("status_default", {}, "Default")} + {at("status_default", {}, "Default")} {/if}
{tariff.key}
{#if tariff.enabled === false} - {at("status_disabled", {}, "Выключен")} + {at("status_disabled", {}, "Выключен")} {:else} - {at("status_active", {}, "Активен")} + {at("status_active", {}, "Активен")} {/if}

{tariff.descriptions?.ru || tariff.descriptions?.en || at("no_description", {}, "Без описания")}

@@ -114,29 +115,28 @@ {at("tariff_devices", {}, "Устройства")}: {tariff.hwid_device_limit ?? "env"}
- - - - +
{/each} diff --git a/bot/app/web/frontend/src/admin/sections/UserDetailModal.svelte b/bot/app/web/frontend/src/admin/sections/UserDetailModal.svelte index abb7384..277bcea 100644 --- a/bot/app/web/frontend/src/admin/sections/UserDetailModal.svelte +++ b/bot/app/web/frontend/src/admin/sections/UserDetailModal.svelte @@ -1,11 +1,12 @@ + + + + diff --git a/bot/app/web/frontend/src/lib/components/patterns/admin/AdminButton.svelte b/bot/app/web/frontend/src/lib/components/patterns/admin/AdminButton.svelte new file mode 100644 index 0000000..d20615d --- /dev/null +++ b/bot/app/web/frontend/src/lib/components/patterns/admin/AdminButton.svelte @@ -0,0 +1,45 @@ + + + diff --git a/bot/app/web/frontend/src/lib/components/patterns/admin/AdminField.svelte b/bot/app/web/frontend/src/lib/components/patterns/admin/AdminField.svelte new file mode 100644 index 0000000..ee1ad9e --- /dev/null +++ b/bot/app/web/frontend/src/lib/components/patterns/admin/AdminField.svelte @@ -0,0 +1,16 @@ + + + + {#if label} + {label} + {/if} + {#if hint} + {hint} + {/if} + + diff --git a/bot/app/web/frontend/src/lib/components/patterns/admin/AdminPagination.svelte b/bot/app/web/frontend/src/lib/components/patterns/admin/AdminPagination.svelte new file mode 100644 index 0000000..ffcff6a --- /dev/null +++ b/bot/app/web/frontend/src/lib/components/patterns/admin/AdminPagination.svelte @@ -0,0 +1,24 @@ + + +
+ {meta} +
+ + {prevLabel} + + + {nextLabel} + +
+
diff --git a/bot/app/web/frontend/src/lib/components/patterns/admin/AdminSelect.svelte b/bot/app/web/frontend/src/lib/components/patterns/admin/AdminSelect.svelte new file mode 100644 index 0000000..042ee83 --- /dev/null +++ b/bot/app/web/frontend/src/lib/components/patterns/admin/AdminSelect.svelte @@ -0,0 +1,38 @@ + + + + + {selected?.label || placeholder} + + + + + {#each items as item (item.value)} + + {item.label} + + + {/each} + + + diff --git a/bot/app/web/frontend/src/lib/components/patterns/admin/AdminTable.svelte b/bot/app/web/frontend/src/lib/components/patterns/admin/AdminTable.svelte new file mode 100644 index 0000000..21ff138 --- /dev/null +++ b/bot/app/web/frontend/src/lib/components/patterns/admin/AdminTable.svelte @@ -0,0 +1,13 @@ + + +
+ + +
+
diff --git a/bot/app/web/frontend/src/lib/components/patterns/admin/AdminTableSkeleton.svelte b/bot/app/web/frontend/src/lib/components/patterns/admin/AdminTableSkeleton.svelte new file mode 100644 index 0000000..a766e99 --- /dev/null +++ b/bot/app/web/frontend/src/lib/components/patterns/admin/AdminTableSkeleton.svelte @@ -0,0 +1,38 @@ + + + + + + {#each headers as header} + {header} + {/each} + + + + {#each Array(rows) as _, rowIndex (rowIndex)} + + {#each headers as header, colIndex (`${rowIndex}-${colIndex}`)} + + + + {/each} + + {/each} + + diff --git a/bot/app/web/frontend/src/lib/components/patterns/admin/AdminTrafficCard.svelte b/bot/app/web/frontend/src/lib/components/patterns/admin/AdminTrafficCard.svelte new file mode 100644 index 0000000..8c28945 --- /dev/null +++ b/bot/app/web/frontend/src/lib/components/patterns/admin/AdminTrafficCard.svelte @@ -0,0 +1,34 @@ + + +
+
+ {title} + {value} +
+
+ +
+
+ {left} + {clamped}% +
+
diff --git a/bot/app/web/frontend/src/lib/components/patterns/admin/index.js b/bot/app/web/frontend/src/lib/components/patterns/admin/index.js index a29622f..f44099e 100644 --- a/bot/app/web/frontend/src/lib/components/patterns/admin/index.js +++ b/bot/app/web/frontend/src/lib/components/patterns/admin/index.js @@ -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"; diff --git a/bot/app/web/frontend/src/lib/components/patterns/webapp/DialogOptionsSkeleton.svelte b/bot/app/web/frontend/src/lib/components/patterns/webapp/DialogOptionsSkeleton.svelte new file mode 100644 index 0000000..a1edbf4 --- /dev/null +++ b/bot/app/web/frontend/src/lib/components/patterns/webapp/DialogOptionsSkeleton.svelte @@ -0,0 +1,67 @@ + + +
+ {#if actions} +
+ {#each Array(actions) as _, index (index)} +
+ + + + + +
+ {/each} +
+ + {/if} + +
+ {#each Array(rows) as _, index (index)} +
+ + + + + {#if showMeta} + + + + + {/if} +
+ {/each} +
+ + {#if showNote} +
+ + +
+ {/if} + + {#if methods} +
+ {#each Array(methods) as _, index (index)} +
+ + +
+ {/each} +
+ {/if} + + {#if showPayButton} + + {/if} +
diff --git a/bot/app/web/frontend/src/lib/components/patterns/webapp/EmptyCard.svelte b/bot/app/web/frontend/src/lib/components/patterns/webapp/EmptyCard.svelte new file mode 100644 index 0000000..6872dde --- /dev/null +++ b/bot/app/web/frontend/src/lib/components/patterns/webapp/EmptyCard.svelte @@ -0,0 +1,11 @@ + + + + + diff --git a/bot/app/web/frontend/src/lib/components/patterns/webapp/LanguageSelect.svelte b/bot/app/web/frontend/src/lib/components/patterns/webapp/LanguageSelect.svelte new file mode 100644 index 0000000..379c39b --- /dev/null +++ b/bot/app/web/frontend/src/lib/components/patterns/webapp/LanguageSelect.svelte @@ -0,0 +1,71 @@ + + +{#if open || clickGuard} + +{/if} + +
+ + + + + {label} + + + {currentOption?.label || userLanguage} + + + + + + + {#each options as option (option.value)} + + + + {option.label} + + + + {/each} + + + +
diff --git a/bot/app/web/frontend/src/lib/components/patterns/webapp/LinearProgress.svelte b/bot/app/web/frontend/src/lib/components/patterns/webapp/LinearProgress.svelte new file mode 100644 index 0000000..73e4769 --- /dev/null +++ b/bot/app/web/frontend/src/lib/components/patterns/webapp/LinearProgress.svelte @@ -0,0 +1,22 @@ + + +
+ +
diff --git a/bot/app/web/frontend/src/lib/components/patterns/webapp/PaymentMethodGrid.svelte b/bot/app/web/frontend/src/lib/components/patterns/webapp/PaymentMethodGrid.svelte new file mode 100644 index 0000000..18a9e0e --- /dev/null +++ b/bot/app/web/frontend/src/lib/components/patterns/webapp/PaymentMethodGrid.svelte @@ -0,0 +1,40 @@ + + +
+ {#each methods as method} + {@const meta = methodMeta(method)} + + {/each} +
diff --git a/bot/app/web/frontend/src/lib/components/patterns/webapp/StatusMessage.svelte b/bot/app/web/frontend/src/lib/components/patterns/webapp/StatusMessage.svelte new file mode 100644 index 0000000..b58f741 --- /dev/null +++ b/bot/app/web/frontend/src/lib/components/patterns/webapp/StatusMessage.svelte @@ -0,0 +1,11 @@ + + +

+ +

diff --git a/bot/app/web/frontend/src/lib/components/patterns/webapp/index.js b/bot/app/web/frontend/src/lib/components/patterns/webapp/index.js new file mode 100644 index 0000000..564a40e --- /dev/null +++ b/bot/app/web/frontend/src/lib/components/patterns/webapp/index.js @@ -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"; diff --git a/bot/app/web/frontend/src/lib/components/ui/dialog.svelte b/bot/app/web/frontend/src/lib/components/ui/dialog.svelte index 77ec261..2726f0f 100644 --- a/bot/app/web/frontend/src/lib/components/ui/dialog.svelte +++ b/bot/app/web/frontend/src/lib/components/ui/dialog.svelte @@ -1,5 +1,5 @@ + + diff --git a/bot/app/web/frontend/src/lib/components/ui/spinner.svelte b/bot/app/web/frontend/src/lib/components/ui/spinner.svelte new file mode 100644 index 0000000..5a26124 --- /dev/null +++ b/bot/app/web/frontend/src/lib/components/ui/spinner.svelte @@ -0,0 +1,16 @@ + + + diff --git a/bot/app/web/frontend/src/preview/BackTitle.svelte b/bot/app/web/frontend/src/preview/BackTitle.svelte index b7ade24..cd7fd39 100644 --- a/bot/app/web/frontend/src/preview/BackTitle.svelte +++ b/bot/app/web/frontend/src/preview/BackTitle.svelte @@ -1,5 +1,5 @@ diff --git a/bot/app/web/frontend/src/styles/admin-controls.css b/bot/app/web/frontend/src/styles/admin-controls.css index acfca78..926a138 100644 --- a/bot/app/web/frontend/src/styles/admin-controls.css +++ b/bot/app/web/frontend/src/styles/admin-controls.css @@ -52,7 +52,7 @@ height: 36px; border-radius: 8px; border: 1px solid var(--admin-border-strong); - background: var(--admin-bg); + background: color-mix(in srgb, var(--admin-bg) 82%, var(--admin-surface-2)); color: var(--admin-text); padding: 0 12px; font-size: 13px; @@ -88,7 +88,9 @@ padding: 0 14px; border-radius: 8px; border: 1px solid var(--admin-border); - background: var(--admin-surface-2); + background: + linear-gradient(135deg, rgba(255, 255, 255, 0.052), rgba(255, 255, 255, 0.014)), + var(--admin-surface-2); color: var(--admin-text); font-size: 13px; font-weight: 500; @@ -98,7 +100,10 @@ } .admin-btn:hover:not(:disabled) { - background: var(--admin-elev); + background: + linear-gradient(135deg, rgba(255, 255, 255, 0.068), rgba(255, 255, 255, 0.018)), + var(--admin-elev); + border-color: var(--admin-border-strong); } .admin-btn:disabled { @@ -124,7 +129,7 @@ } .admin-btn.admin-btn-ghost:hover:not(:disabled) { - background: var(--admin-surface-2); + background: rgba(255, 255, 255, 0.04); color: var(--admin-text); } diff --git a/bot/app/web/frontend/src/styles/admin.css b/bot/app/web/frontend/src/styles/admin.css index 3ed1735..55f1097 100644 --- a/bot/app/web/frontend/src/styles/admin.css +++ b/bot/app/web/frontend/src/styles/admin.css @@ -6,12 +6,20 @@ .admin-screen-wrap { --admin-sidebar-w: var(--desktop-rail-width); --admin-header-h: 60px; + --admin-card-bg: + linear-gradient(135deg, rgba(255, 255, 255, 0.055), rgba(255, 255, 255, 0.018)), + var(--admin-surface); + --admin-card-shadow: + 0 18px 48px rgba(0, 0, 0, 0.22), + inset 0 1px 0 rgba(255, 255, 255, 0.05); position: fixed; inset: 0; width: 100vw; height: 100dvh; - background: var(--admin-bg); + background: + radial-gradient(circle at 18% 0%, color-mix(in srgb, var(--accent) 10%, transparent), transparent 32%), + #02070b; color: var(--admin-text); overflow: hidden; overscroll-behavior: contain; @@ -29,10 +37,12 @@ height: 100%; display: flex; flex-direction: column; - gap: 4px; - padding: 14px 12px 18px; + gap: 2px; + padding: 28px 14px; border-right: 1px solid var(--admin-border); - background: var(--admin-surface); + background: rgba(7, 12, 17, 0.55); + backdrop-filter: blur(14px); + box-shadow: inset -1px 0 0 rgba(255, 255, 255, 0.02); overflow-y: auto; } @@ -42,17 +52,18 @@ .admin-sidebar-brand { display: grid; - grid-template-columns: 36px minmax(0, 1fr) 32px; + grid-template-columns: 38px minmax(0, 1fr) 34px; align-items: center; - gap: 10px; - padding: 6px 6px 14px; - margin-bottom: 6px; + gap: 12px; + padding: 4px 12px 18px; + margin-bottom: 8px; border-bottom: 1px solid var(--admin-border); + color: var(--admin-text); } .admin-sidebar-brand .admin-brand-mark { - width: 36px; - height: 36px; + width: 38px; + height: 38px; display: grid; place-items: center; color: var(--accent); @@ -60,9 +71,14 @@ .admin-sidebar-brand strong { display: block; - font-size: 13px; - font-weight: 700; + min-width: 0; + overflow: hidden; + color: var(--accent); + font-size: 14px; + font-weight: 800; letter-spacing: -0.01em; + text-overflow: ellipsis; + white-space: nowrap; } .admin-sidebar-brand small { @@ -82,54 +98,52 @@ .admin-nav { display: grid; - gap: 1px; + gap: 2px; } .admin-nav-item { position: relative; display: grid; - grid-template-columns: 18px minmax(0, 1fr) auto; + grid-template-columns: 22px minmax(0, 1fr) auto; align-items: center; - gap: 10px; - padding: 9px 12px; - border-radius: 8px; - border: 0; + gap: 12px; + padding: 12px 14px; + border-radius: 10px; + border: 1px solid transparent; background: transparent; color: var(--admin-muted); font-size: 13px; - font-weight: 500; + font-weight: 600; text-align: left; width: 100%; cursor: pointer; - transition: background 0.12s ease, color 0.12s ease; + transition: background 0.12s ease, color 0.12s ease, border-color 0.12s ease; +} + +.admin-nav-item > svg { + width: 20px; + height: 20px; } .admin-nav-item:hover { - background: var(--admin-surface-2); + background: rgba(255, 255, 255, 0.04); color: var(--admin-text); } .admin-nav-item.active { - background: var(--admin-elev); + background: color-mix(in srgb, var(--accent) 16%, transparent); + border-color: color-mix(in srgb, var(--accent) 30%, transparent); color: var(--admin-text); font-weight: 600; } .admin-nav-item.active::before { - content: ""; - position: absolute; - width: 3px; - height: 18px; - left: -4px; - top: 50%; - transform: translateY(-50%); - border-radius: 2px; - background: var(--accent); + display: none; } .admin-sidebar-footer { margin-top: auto; - padding: 10px 6px 0; + padding: 14px 0 0; border-top: 1px solid var(--admin-border); font-size: 11px; color: var(--admin-dim); @@ -156,8 +170,8 @@ justify-content: space-between; gap: 8px; border: 1px solid var(--admin-border); - border-radius: 8px; - background: var(--admin-surface-2); + border-radius: 10px; + background: rgba(255, 255, 255, 0.035); color: var(--admin-text); padding: 8px 10px; text-align: left; @@ -237,9 +251,10 @@ grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: 16px; - padding: 0 24px; + padding: 0 28px; border-bottom: 1px solid var(--admin-border); - background: var(--admin-bg); + background: color-mix(in srgb, #02070b 82%, transparent); + backdrop-filter: blur(14px); } .admin-header-title { @@ -285,7 +300,7 @@ overscroll-behavior: contain; overflow-y: auto; overflow-x: hidden; - padding: 22px 24px 32px; + padding: 24px 28px 36px; display: flex; flex-direction: column; gap: 16px; @@ -300,8 +315,9 @@ .admin-card { border: 1px solid var(--admin-border); - background: var(--admin-surface); + background: var(--admin-card-bg); border-radius: 12px; + box-shadow: var(--admin-card-shadow); display: grid; grid-template-columns: minmax(0, 1fr); min-width: 0; @@ -347,7 +363,7 @@ padding: 48px 16px; border: 1px dashed var(--admin-border); border-radius: 12px; - background: var(--admin-surface); + background: var(--admin-card-bg); color: var(--admin-muted); font-size: 13px; } @@ -383,8 +399,9 @@ .admin-stat-card { border: 1px solid var(--admin-border); - background: var(--admin-surface); + background: var(--admin-card-bg); border-radius: 12px; + box-shadow: var(--admin-card-shadow); padding: 16px 18px; display: grid; gap: 6px; @@ -443,8 +460,9 @@ .admin-revenue-panel { border: 1px solid var(--admin-border); - background: var(--admin-surface); + background: var(--admin-card-bg); border-radius: 12px; + box-shadow: var(--admin-card-shadow); padding: 16px 18px; display: grid; gap: 14px; @@ -572,8 +590,9 @@ .admin-dashboard-panel-card { border: 1px solid var(--admin-border); - background: var(--admin-surface); + background: var(--admin-card-bg); border-radius: 12px; + box-shadow: var(--admin-card-shadow); padding: 16px 18px; display: grid; gap: 14px; @@ -646,8 +665,9 @@ .admin-stat-skeleton-card { border: 1px solid var(--admin-border); - background: var(--admin-surface); + background: var(--admin-card-bg); border-radius: 12px; + box-shadow: var(--admin-card-shadow); padding: 16px 18px; display: grid; gap: 10px; @@ -678,7 +698,8 @@ padding: 12px; border: 1px solid var(--admin-border); border-radius: 12px; - background: var(--admin-surface); + background: var(--admin-card-bg); + box-shadow: var(--admin-card-shadow); } .admin-toolbar-card { @@ -689,7 +710,8 @@ padding: 12px; border: 1px solid var(--admin-border); border-radius: 12px; - background: var(--admin-surface); + background: var(--admin-card-bg); + box-shadow: var(--admin-card-shadow); } .admin-toolbar-search { @@ -776,7 +798,8 @@ .admin-table-wrap { border: 1px solid var(--admin-border); border-radius: 12px; - background: var(--admin-surface); + background: var(--admin-card-bg); + box-shadow: var(--admin-card-shadow); overflow: hidden; } @@ -799,7 +822,7 @@ text-transform: uppercase; letter-spacing: 0.05em; padding: 12px 16px; - background: var(--admin-surface-2); + background: color-mix(in srgb, var(--admin-surface-2) 72%, var(--admin-bg)); border-bottom: 1px solid var(--admin-border); white-space: nowrap; } @@ -820,7 +843,7 @@ } .admin-table tbody tr:hover { - background: color-mix(in srgb, var(--admin-elev) 50%, transparent); + background: rgba(255, 255, 255, 0.035); } .admin-table td.admin-cell-mono { @@ -2235,8 +2258,8 @@ height: 36px; padding: 0 12px; border: 1px solid var(--admin-border-strong); - border-radius: 8px; - background: var(--admin-bg); + border-radius: 10px; + background: color-mix(in srgb, var(--admin-bg) 82%, var(--admin-surface-2)); color: var(--admin-text); font: inherit; font-size: 13px; @@ -2273,7 +2296,9 @@ overflow-y: auto; border: 1px solid var(--admin-border); border-radius: 10px; - background: var(--admin-surface); + background: + linear-gradient(135deg, rgba(255, 255, 255, 0.055), rgba(255, 255, 255, 0.018)), + var(--admin-surface); padding: 4px; box-shadow: 0 16px 36px rgba(0, 0, 0, 0.45); outline: none; diff --git a/bot/app/web/frontend/src/styles/components.css b/bot/app/web/frontend/src/styles/components.css index a611d43..6c394e6 100644 --- a/bot/app/web/frontend/src/styles/components.css +++ b/bot/app/web/frontend/src/styles/components.css @@ -25,8 +25,12 @@ min-width: 0; border-radius: 12px; border: 1px solid var(--admin-border); - background: var(--admin-surface); - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); + background: + linear-gradient(135deg, rgba(255, 255, 255, 0.055), rgba(255, 255, 255, 0.018)), + var(--admin-surface); + box-shadow: + 0 18px 48px rgba(0, 0, 0, 0.22), + inset 0 1px 0 rgba(255, 255, 255, 0.05); position: relative; overflow: hidden; } @@ -180,3 +184,108 @@ background: color-mix(in srgb, var(--admin-surface-2) 70%, var(--admin-bg)); color: var(--admin-muted); } + +.ui-spinner, +.telegram-button-spinner { + width: 16px; + height: 16px; + border-radius: 999px; + border: 2px solid currentColor; + border-right-color: transparent; + display: inline-block; + flex: 0 0 auto; + animation: ui-spin 0.72s linear infinite; +} + +.ui-spinner-sm { + width: 13px; + height: 13px; + border-width: 2px; +} + +.ui-spinner-lg { + width: 24px; + height: 24px; + border-width: 3px; +} + +@keyframes ui-spin { + to { + transform: rotate(360deg); + } +} + +.ui-skeleton, +.admin-skeleton, +.skeleton-line, +.skeleton-dot, +.skeleton-pay-button { + display: inline-block; + background: linear-gradient( + 90deg, + rgba(255, 255, 255, 0.08), + rgba(255, 255, 255, 0.14), + rgba(255, 255, 255, 0.08) + ); + background-size: 220% 100%; + animation: ui-skeleton-pulse 1.2s ease-in-out infinite; +} + +.ui-skeleton { + width: 100%; + min-height: 40px; + border-radius: 10px; +} + +.ui-skeleton-line, +.admin-skeleton-line, +.skeleton-line { + width: 100%; + height: 10px; + min-height: 0; + border-radius: 999px; +} + +.ui-skeleton-title, +.skeleton-line-title { + height: 14px; + min-height: 0; + width: 72%; +} + +.ui-skeleton-short, +.admin-skeleton-line-short, +.skeleton-line-short { + width: 56%; +} + +.ui-skeleton-tiny, +.admin-skeleton-line-tiny, +.skeleton-line-tiny { + width: 34%; +} + +.ui-skeleton-badge, +.admin-skeleton-badge { + width: 64px; + height: 18px; + min-height: 0; + border-radius: 999px; +} + +.ui-skeleton-dot, +.skeleton-dot { + width: 18px; + height: 18px; + min-height: 0; + border-radius: 999px; +} + +@keyframes ui-skeleton-pulse { + 0% { + background-position: 120% 0; + } + 100% { + background-position: -120% 0; + } +} diff --git a/bot/app/web/frontend/src/styles/webapp.css b/bot/app/web/frontend/src/styles/webapp.css index 0cc93f0..c88a916 100644 --- a/bot/app/web/frontend/src/styles/webapp.css +++ b/bot/app/web/frontend/src/styles/webapp.css @@ -821,22 +821,26 @@ a { .skeleton-line-price { width: 58px; height: 12px; + min-height: 0; } .skeleton-line-tiny { width: 34px; height: 9px; + min-height: 0; opacity: 0.7; } .skeleton-line-method { width: 72px; height: 12px; + min-height: 0; } .skeleton-dot { width: 18px; height: 18px; + min-height: 0; flex: 0 0 auto; } @@ -2303,6 +2307,7 @@ a { } .bottom-nav .rail-brand strong { + color: var(--accent); font-size: 14px; font-weight: 800; letter-spacing: -0.01em; diff --git a/bot/app/web/frontend/src/webapp/BottomNav.svelte b/bot/app/web/frontend/src/webapp/BottomNav.svelte index c49e8ce..b45070f 100644 --- a/bot/app/web/frontend/src/webapp/BottomNav.svelte +++ b/bot/app/web/frontend/src/webapp/BottomNav.svelte @@ -1,5 +1,5 @@