From ad275a7c834c2fc8571d2d36e46003c094eae406 Mon Sep 17 00:00:00 2001
From: 3252a8 <3252a8@proton.me>
Date: Sun, 24 May 2026 09:11:43 +0300
Subject: [PATCH] feat: polish admin settings layout
---
.../src/admin/sections/SettingsSection.svelte | 152 +++++++++++++++++-
frontend/src/styles/admin.css | 93 ++++++++++-
locales/en.json | 19 ++-
locales/ru.json | 19 ++-
4 files changed, 267 insertions(+), 16 deletions(-)
diff --git a/frontend/src/admin/sections/SettingsSection.svelte b/frontend/src/admin/sections/SettingsSection.svelte
index 050e061..361c4a9 100644
--- a/frontend/src/admin/sections/SettingsSection.svelte
+++ b/frontend/src/admin/sections/SettingsSection.svelte
@@ -38,6 +38,19 @@
let copiedWebhookKey = "";
let copiedWebhookTimer = null;
+ const PLATEGA_SBP_KEYS = new Set(["PLATEGA_SBP_ENABLED", "PLATEGA_SBP_METHOD"]);
+ const PLATEGA_CRYPTO_KEYS = new Set(["PLATEGA_CRYPTO_ENABLED", "PLATEGA_CRYPTO_METHOD"]);
+ const PLATEGA_LEGACY_KEYS = new Set(["PLATEGA_PAYMENT_METHOD"]);
+ const LEGACY_TARIFF_TRAFFIC_KEYS = new Set(["TRAFFIC_PACKAGES", "STARS_TRAFFIC_PACKAGES"]);
+ const SEMANTIC_FIELD_GROUP_ORDER = {
+ platega_common: 1,
+ platega_sbp: 2,
+ platega_crypto: 3,
+ platega_legacy: 4,
+ legacy_tariff_periods: 1,
+ legacy_tariff_traffic: 2,
+ };
+
$: settingsAllOpen =
visibleSettingsSections.length > 0 &&
settingsOpenSections.length === visibleSettingsSections.length;
@@ -220,6 +233,108 @@
}));
}
+ function fieldGroupMeta(
+ id,
+ titleKey,
+ titleFallback,
+ descriptionKey = "",
+ descriptionFallback = ""
+ ) {
+ return { id, titleKey, titleFallback, descriptionKey, descriptionFallback };
+ }
+
+ function plategaSemanticGroup(field) {
+ const key = String(field?.key || "");
+ if (PLATEGA_SBP_KEYS.has(key) || key.startsWith("PAYMENT_PLATEGA_SBP_")) {
+ return fieldGroupMeta(
+ "platega_sbp",
+ "settings_group_platega_sbp",
+ "SBP/card button",
+ "settings_group_platega_sbp_hint",
+ "Visibility, method ID, and labels for the SBP/card payment button."
+ );
+ }
+ if (PLATEGA_CRYPTO_KEYS.has(key) || key.startsWith("PAYMENT_PLATEGA_CRYPTO_")) {
+ return fieldGroupMeta(
+ "platega_crypto",
+ "settings_group_platega_crypto",
+ "Crypto button",
+ "settings_group_platega_crypto_hint",
+ "Visibility, method ID, and labels for the crypto payment button."
+ );
+ }
+ if (PLATEGA_LEGACY_KEYS.has(key)) {
+ return fieldGroupMeta(
+ "platega_legacy",
+ "settings_group_platega_legacy",
+ "Legacy compatibility",
+ "settings_group_platega_legacy_hint",
+ "Fallback method for old Platega callbacks and deployments."
+ );
+ }
+ return fieldGroupMeta(
+ "platega_common",
+ "settings_group_platega_common",
+ "Common settings",
+ "settings_group_platega_common_hint",
+ "Shared merchant credentials, redirects, and API endpoint."
+ );
+ }
+
+ function legacyTariffSemanticGroup(field) {
+ const key = String(field?.key || "");
+ if (LEGACY_TARIFF_TRAFFIC_KEYS.has(key)) {
+ return fieldGroupMeta(
+ "legacy_tariff_traffic",
+ "settings_group_legacy_tariff_traffic",
+ "Legacy traffic packages",
+ "settings_group_legacy_tariff_traffic_hint",
+ "Packages used only when the JSON tariff catalog is not configured."
+ );
+ }
+ return fieldGroupMeta(
+ "legacy_tariff_periods",
+ "settings_group_legacy_tariff_periods",
+ "Legacy subscription periods",
+ "settings_group_legacy_tariff_periods_hint",
+ "Month toggles and prices used only by the old remnawave-tg-shop mode."
+ );
+ }
+
+ function semanticFieldGroup(section, group, field) {
+ if (section?.id === "payments" && group?.id === "Platega") {
+ return plategaSemanticGroup(field);
+ }
+ if (section?.id === "pricing") {
+ return legacyTariffSemanticGroup(field);
+ }
+ return null;
+ }
+
+ function semanticFieldGroups(section, group) {
+ const fields = group?.fields || [];
+ const result = new Map();
+ for (const field of fields) {
+ const meta = semanticFieldGroup(section, group, field) || fieldGroupMeta("_default", "", "");
+ if (!result.has(meta.id)) {
+ result.set(meta.id, { ...meta, fields: [] });
+ }
+ result.get(meta.id).fields.push(field);
+ }
+ return Array.from(result.values()).sort(
+ (a, b) =>
+ (SEMANTIC_FIELD_GROUP_ORDER[a.id] || 999) - (SEMANTIC_FIELD_GROUP_ORDER[b.id] || 999)
+ );
+ }
+
+ function fieldGroupTitle(group) {
+ return group.titleKey ? at(group.titleKey, {}, group.titleFallback) : "";
+ }
+
+ function fieldGroupDescription(group) {
+ return group.descriptionKey ? at(group.descriptionKey, {}, group.descriptionFallback) : "";
+ }
+
function adminLocaleKey(key) {
const raw = String(key || "");
return raw.startsWith("admin_") ? raw.slice("admin_".length) : raw;
@@ -358,6 +473,35 @@
{/snippet}
+{#snippet renderGroupedFields(section, group)}
+ {@const fieldGroups = semanticFieldGroups(section, group)}
+ {#if fieldGroups.length === 1 && !fieldGroups[0].titleKey}
+ {#each fieldGroups[0].fields as field}
+ {@render renderField(field)}
+ {/each}
+ {:else}
+
+ {#each fieldGroups as fieldGroup}
+
+ {#if fieldGroup.titleKey}
+
+ {fieldGroupTitle(fieldGroup)}
+ {#if fieldGroupDescription(fieldGroup)}
+ {fieldGroupDescription(fieldGroup)}
+ {/if}
+
+ {/if}
+
+ {#each fieldGroup.fields as field}
+ {@render renderField(field)}
+ {/each}
+
+
+ {/each}
+
+ {/if}
+{/snippet}
+
{#snippet renderField(field)}
{@const revealed = isSecretRevealed(field.key)}
@@ -600,9 +744,7 @@
{#if rootGroup.webhook}
{@render renderWebhookHint(rootGroup.webhook)}
{/if}
- {#each rootGroup.fields as field}
- {@render renderField(field)}
- {/each}
+ {@render renderGroupedFields(section, rootGroup)}
{/if}
{#if labelGroups.length}
diff --git a/frontend/src/styles/admin.css b/frontend/src/styles/admin.css
index e0c08f1..ca84000 100644
--- a/frontend/src/styles/admin.css
+++ b/frontend/src/styles/admin.css
@@ -1823,7 +1823,6 @@
font-size: 13px;
font-weight: 600;
color: var(--admin-text);
- text-transform: capitalize;
word-break: break-word;
min-width: 0;
}
@@ -1851,8 +1850,43 @@
overflow: hidden;
}
+.admin-accordion-content[data-state="open"] {
+ animation: admin-accordion-open 140ms cubic-bezier(0.16, 1, 0.3, 1);
+}
+
.admin-accordion-content[data-state="closed"] {
- display: none;
+ animation: admin-accordion-close 110ms cubic-bezier(0.4, 0, 0.2, 1);
+}
+
+@keyframes admin-accordion-open {
+ from {
+ height: 0;
+ opacity: 0.72;
+ }
+
+ to {
+ height: var(--bits-accordion-content-height);
+ opacity: 1;
+ }
+}
+
+@keyframes admin-accordion-close {
+ from {
+ height: var(--bits-accordion-content-height);
+ opacity: 1;
+ }
+
+ to {
+ height: 0;
+ opacity: 0.72;
+ }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .admin-accordion-content[data-state="open"],
+ .admin-accordion-content[data-state="closed"] {
+ animation: none;
+ }
}
@media (max-width: 720px) {
@@ -1996,6 +2030,50 @@
text-decoration: underline;
}
+.admin-settings-field-groups {
+ display: grid;
+ gap: 10px;
+ padding: 12px 18px 18px;
+}
+
+.admin-settings-field-group {
+ display: grid;
+ min-width: 0;
+ overflow: hidden;
+ border: 1px solid color-mix(in srgb, var(--admin-border) 88%, transparent);
+ border-radius: 10px;
+ background: color-mix(in srgb, var(--admin-bg) 52%, transparent);
+}
+
+.admin-settings-field-group-head {
+ display: grid;
+ gap: 3px;
+ padding: 10px 14px;
+ border-bottom: 1px solid var(--admin-border);
+ background: color-mix(in srgb, var(--admin-surface-2) 56%, transparent);
+}
+
+.admin-settings-field-group-head strong {
+ color: var(--admin-text);
+ font-size: 12px;
+ font-weight: 750;
+ line-height: 1.25;
+}
+
+.admin-settings-field-group-head small {
+ color: var(--admin-muted);
+ font-size: 11px;
+ line-height: 1.35;
+}
+
+.admin-settings-field-group-body .admin-setting {
+ padding-inline: 14px;
+}
+
+.admin-settings-field-group-body .admin-setting:last-child {
+ border-bottom: 0;
+}
+
.admin-setting:last-child {
border-bottom: 0;
}
@@ -2762,6 +2840,14 @@
justify-self: start;
}
+ .admin-settings-field-groups {
+ padding: 10px 14px 14px;
+ }
+
+ .admin-settings-field-group-body .admin-setting {
+ padding-inline: 12px;
+ }
+
.admin-card-head {
padding: 12px 14px;
}
@@ -3489,8 +3575,7 @@
font-size: 12px;
font-weight: 700;
color: var(--admin-text);
- text-transform: uppercase;
- letter-spacing: 0.06em;
+ letter-spacing: 0;
min-width: 0;
}
diff --git a/locales/en.json b/locales/en.json
index bba929e..db21e6c 100644
--- a/locales/en.json
+++ b/locales/en.json
@@ -943,7 +943,7 @@
"admin_logs_empty": "No entries",
"admin_settings_section_general": "General",
"admin_settings_section_appearance": "Appearance",
- "admin_settings_section_pricing": "Tariffs and prices",
+ "admin_settings_section_pricing": "Legacy tariff compatibility",
"admin_settings_section_payments": "Payment systems",
"admin_settings_section_trial": "Trial",
"admin_settings_section_referral": "Referral program",
@@ -951,6 +951,7 @@
"admin_settings_section_devices": "Devices",
"admin_settings_section_support": "Support",
"admin_settings_subsection_common": "Common",
+ "admin_settings_subsection_checkout": "Checkout",
"admin_settings_subsection_remnawave": "Remnawave",
"admin_settings_subsection_telegram_stars": "Telegram Stars",
"admin_settings_subsection_yookassa": "YooKassa",
@@ -1241,9 +1242,21 @@
"admin_ads_col_status": "Status",
"admin_no_data": "No data",
"admin_settings_hint": "Changes in the admin panel take precedence over .env. The 'Reset' button returns the value from environment variables.",
- "admin_settings_legacy_tariffs_warning_title": "Legacy tariffs",
- "admin_settings_legacy_tariffs_warning_body": "These settings do not apply when tariffs are configured in the dedicated Tariffs section.",
+ "admin_settings_legacy_tariffs_warning_title": "remnawave-tg-shop legacy compatibility",
+ "admin_settings_legacy_tariffs_warning_body": "These fields are only for the old mode without a JSON catalog. They do not apply when tariffs are configured in the dedicated Tariffs section.",
"admin_settings_legacy_tariffs_warning_link": "Open Tariffs",
+ "admin_settings_group_platega_common": "Common settings",
+ "admin_settings_group_platega_common_hint": "Merchant details, API URL, and return pages shared by all Platega buttons.",
+ "admin_settings_group_platega_sbp": "SBP/card button",
+ "admin_settings_group_platega_sbp_hint": "Visibility, method ID, and labels for the card or SBP payment button.",
+ "admin_settings_group_platega_crypto": "Crypto button",
+ "admin_settings_group_platega_crypto_hint": "Visibility, method ID, and labels for the crypto payment button.",
+ "admin_settings_group_platega_legacy": "Legacy compatibility",
+ "admin_settings_group_platega_legacy_hint": "Fallback method for old Platega callbacks and deployments.",
+ "admin_settings_group_legacy_tariff_periods": "Legacy subscription periods",
+ "admin_settings_group_legacy_tariff_periods_hint": "Monthly periods and prices for compatibility with old remnawave-tg-shop mode.",
+ "admin_settings_group_legacy_tariff_traffic": "Legacy traffic packages",
+ "admin_settings_group_legacy_tariff_traffic_hint": "Traffic packages for the old mode without a JSON tariff catalog.",
"admin_collapse_all": "Collapse all",
"admin_expand_all": "Expand all",
"admin_settings_params_count": "{count} parameters",
diff --git a/locales/ru.json b/locales/ru.json
index c7a4985..b4790d6 100644
--- a/locales/ru.json
+++ b/locales/ru.json
@@ -943,7 +943,7 @@
"admin_logs_empty": "Записей нет",
"admin_settings_section_general": "Общие",
"admin_settings_section_appearance": "Внешний вид",
- "admin_settings_section_pricing": "Тарифы и цены",
+ "admin_settings_section_pricing": "Совместимость с legacy-тарифами",
"admin_settings_section_payments": "Платёжные системы",
"admin_settings_section_trial": "Триал",
"admin_settings_section_referral": "Реферальная программа",
@@ -951,6 +951,7 @@
"admin_settings_section_devices": "Устройства",
"admin_settings_section_support": "Поддержка",
"admin_settings_subsection_common": "Общие",
+ "admin_settings_subsection_checkout": "Оформление оплаты",
"admin_settings_subsection_remnawave": "Remnawave",
"admin_settings_subsection_telegram_stars": "Telegram Stars",
"admin_settings_subsection_yookassa": "YooKassa",
@@ -1241,9 +1242,21 @@
"admin_ads_col_status": "Статус",
"admin_no_data": "Нет данных",
"admin_settings_hint": "Изменения в админке имеют приоритет над .env. Кнопка «Сбросить» возвращает значение из переменных окружения.",
- "admin_settings_legacy_tariffs_warning_title": "Legacy-тарифы",
- "admin_settings_legacy_tariffs_warning_body": "Эти настройки не применяются, если тарифы настроены через отдельный раздел «Тарифы».",
+ "admin_settings_legacy_tariffs_warning_title": "Совместимость с remnawave-tg-shop legacy",
+ "admin_settings_legacy_tariffs_warning_body": "Эти поля нужны только для старого режима без JSON-каталога. Если тарифы настроены через отдельный раздел «Тарифы», они не применяются.",
"admin_settings_legacy_tariffs_warning_link": "Открыть тарифы",
+ "admin_settings_group_platega_common": "Общие настройки",
+ "admin_settings_group_platega_common_hint": "Данные мерчанта, URL API и страницы возврата для всех кнопок Platega.",
+ "admin_settings_group_platega_sbp": "Кнопка SBP/карта",
+ "admin_settings_group_platega_sbp_hint": "Включение, method ID и тексты кнопки для оплаты картой или через SBP.",
+ "admin_settings_group_platega_crypto": "Кнопка Crypto",
+ "admin_settings_group_platega_crypto_hint": "Включение, method ID и тексты кнопки для криптооплаты.",
+ "admin_settings_group_platega_legacy": "Legacy-совместимость",
+ "admin_settings_group_platega_legacy_hint": "Fallback-метод для старых callback и старых установок Platega.",
+ "admin_settings_group_legacy_tariff_periods": "Legacy-подписки на срок",
+ "admin_settings_group_legacy_tariff_periods_hint": "Месячные периоды и цены для режима совместимости со старым remnawave-tg-shop.",
+ "admin_settings_group_legacy_tariff_traffic": "Legacy-пакеты трафика",
+ "admin_settings_group_legacy_tariff_traffic_hint": "Пакеты трафика для старого режима без JSON-каталога тарифов.",
"admin_collapse_all": "Свернуть всё",
"admin_expand_all": "Развернуть всё",
"admin_settings_params_count": "{count} параметров",