feat: polish admin settings layout

This commit is contained in:
3252a8
2026-05-24 09:11:43 +03:00
parent 02b57ead00
commit ad275a7c83
4 changed files with 267 additions and 16 deletions
@@ -38,6 +38,19 @@
let copiedWebhookKey = ""; let copiedWebhookKey = "";
let copiedWebhookTimer = null; 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 = $: settingsAllOpen =
visibleSettingsSections.length > 0 && visibleSettingsSections.length > 0 &&
settingsOpenSections.length === visibleSettingsSections.length; 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) { function adminLocaleKey(key) {
const raw = String(key || ""); const raw = String(key || "");
return raw.startsWith("admin_") ? raw.slice("admin_".length) : raw; return raw.startsWith("admin_") ? raw.slice("admin_".length) : raw;
@@ -358,6 +473,35 @@
</div> </div>
{/snippet} {/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}
<div class="admin-settings-field-groups">
{#each fieldGroups as fieldGroup}
<section class="admin-settings-field-group">
{#if fieldGroup.titleKey}
<header class="admin-settings-field-group-head">
<strong>{fieldGroupTitle(fieldGroup)}</strong>
{#if fieldGroupDescription(fieldGroup)}
<small>{fieldGroupDescription(fieldGroup)}</small>
{/if}
</header>
{/if}
<div class="admin-settings-field-group-body">
{#each fieldGroup.fields as field}
{@render renderField(field)}
{/each}
</div>
</section>
{/each}
</div>
{/if}
{/snippet}
{#snippet renderField(field)} {#snippet renderField(field)}
{@const revealed = isSecretRevealed(field.key)} {@const revealed = isSecretRevealed(field.key)}
<div class="admin-setting" class:is-overridden={isOverridden(field)}> <div class="admin-setting" class:is-overridden={isOverridden(field)}>
@@ -600,9 +744,7 @@
{#if rootGroup.webhook} {#if rootGroup.webhook}
{@render renderWebhookHint(rootGroup.webhook)} {@render renderWebhookHint(rootGroup.webhook)}
{/if} {/if}
{#each rootGroup.fields as field} {@render renderGroupedFields(section, rootGroup)}
{@render renderField(field)}
{/each}
{/if} {/if}
{#if labelGroups.length} {#if labelGroups.length}
<Accordion.Root <Accordion.Root
@@ -646,9 +788,7 @@
{#if group.webhook} {#if group.webhook}
{@render renderWebhookHint(group.webhook)} {@render renderWebhookHint(group.webhook)}
{/if} {/if}
{#each group.fields as field} {@render renderGroupedFields(section, group)}
{@render renderField(field)}
{/each}
</div> </div>
</Accordion.Content> </Accordion.Content>
</Accordion.Item> </Accordion.Item>
+89 -4
View File
@@ -1823,7 +1823,6 @@
font-size: 13px; font-size: 13px;
font-weight: 600; font-weight: 600;
color: var(--admin-text); color: var(--admin-text);
text-transform: capitalize;
word-break: break-word; word-break: break-word;
min-width: 0; min-width: 0;
} }
@@ -1851,8 +1850,43 @@
overflow: hidden; 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"] { .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) { @media (max-width: 720px) {
@@ -1996,6 +2030,50 @@
text-decoration: underline; 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 { .admin-setting:last-child {
border-bottom: 0; border-bottom: 0;
} }
@@ -2762,6 +2840,14 @@
justify-self: start; justify-self: start;
} }
.admin-settings-field-groups {
padding: 10px 14px 14px;
}
.admin-settings-field-group-body .admin-setting {
padding-inline: 12px;
}
.admin-card-head { .admin-card-head {
padding: 12px 14px; padding: 12px 14px;
} }
@@ -3489,8 +3575,7 @@
font-size: 12px; font-size: 12px;
font-weight: 700; font-weight: 700;
color: var(--admin-text); color: var(--admin-text);
text-transform: uppercase; letter-spacing: 0;
letter-spacing: 0.06em;
min-width: 0; min-width: 0;
} }
+16 -3
View File
@@ -943,7 +943,7 @@
"admin_logs_empty": "No entries", "admin_logs_empty": "No entries",
"admin_settings_section_general": "General", "admin_settings_section_general": "General",
"admin_settings_section_appearance": "Appearance", "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_payments": "Payment systems",
"admin_settings_section_trial": "Trial", "admin_settings_section_trial": "Trial",
"admin_settings_section_referral": "Referral program", "admin_settings_section_referral": "Referral program",
@@ -951,6 +951,7 @@
"admin_settings_section_devices": "Devices", "admin_settings_section_devices": "Devices",
"admin_settings_section_support": "Support", "admin_settings_section_support": "Support",
"admin_settings_subsection_common": "Common", "admin_settings_subsection_common": "Common",
"admin_settings_subsection_checkout": "Checkout",
"admin_settings_subsection_remnawave": "Remnawave", "admin_settings_subsection_remnawave": "Remnawave",
"admin_settings_subsection_telegram_stars": "Telegram Stars", "admin_settings_subsection_telegram_stars": "Telegram Stars",
"admin_settings_subsection_yookassa": "YooKassa", "admin_settings_subsection_yookassa": "YooKassa",
@@ -1241,9 +1242,21 @@
"admin_ads_col_status": "Status", "admin_ads_col_status": "Status",
"admin_no_data": "No data", "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_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_title": "remnawave-tg-shop legacy compatibility",
"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_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_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_collapse_all": "Collapse all",
"admin_expand_all": "Expand all", "admin_expand_all": "Expand all",
"admin_settings_params_count": "{count} parameters", "admin_settings_params_count": "{count} parameters",
+16 -3
View File
@@ -943,7 +943,7 @@
"admin_logs_empty": "Записей нет", "admin_logs_empty": "Записей нет",
"admin_settings_section_general": "Общие", "admin_settings_section_general": "Общие",
"admin_settings_section_appearance": "Внешний вид", "admin_settings_section_appearance": "Внешний вид",
"admin_settings_section_pricing": "Тарифы и цены", "admin_settings_section_pricing": "Совместимость с legacy-тарифами",
"admin_settings_section_payments": "Платёжные системы", "admin_settings_section_payments": "Платёжные системы",
"admin_settings_section_trial": "Триал", "admin_settings_section_trial": "Триал",
"admin_settings_section_referral": "Реферальная программа", "admin_settings_section_referral": "Реферальная программа",
@@ -951,6 +951,7 @@
"admin_settings_section_devices": "Устройства", "admin_settings_section_devices": "Устройства",
"admin_settings_section_support": "Поддержка", "admin_settings_section_support": "Поддержка",
"admin_settings_subsection_common": "Общие", "admin_settings_subsection_common": "Общие",
"admin_settings_subsection_checkout": "Оформление оплаты",
"admin_settings_subsection_remnawave": "Remnawave", "admin_settings_subsection_remnawave": "Remnawave",
"admin_settings_subsection_telegram_stars": "Telegram Stars", "admin_settings_subsection_telegram_stars": "Telegram Stars",
"admin_settings_subsection_yookassa": "YooKassa", "admin_settings_subsection_yookassa": "YooKassa",
@@ -1241,9 +1242,21 @@
"admin_ads_col_status": "Статус", "admin_ads_col_status": "Статус",
"admin_no_data": "Нет данных", "admin_no_data": "Нет данных",
"admin_settings_hint": "Изменения в админке имеют приоритет над .env. Кнопка «Сбросить» возвращает значение из переменных окружения.", "admin_settings_hint": "Изменения в админке имеют приоритет над .env. Кнопка «Сбросить» возвращает значение из переменных окружения.",
"admin_settings_legacy_tariffs_warning_title": "Legacy-тарифы", "admin_settings_legacy_tariffs_warning_title": "Совместимость с remnawave-tg-shop legacy",
"admin_settings_legacy_tariffs_warning_body": "Эти настройки не применяются, если тарифы настроены через отдельный раздел «Тарифы».", "admin_settings_legacy_tariffs_warning_body": "Эти поля нужны только для старого режима без JSON-каталога. Если тарифы настроены через отдельный раздел «Тарифы», они не применяются.",
"admin_settings_legacy_tariffs_warning_link": "Открыть тарифы", "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_collapse_all": "Свернуть всё",
"admin_expand_all": "Развернуть всё", "admin_expand_all": "Развернуть всё",
"admin_settings_params_count": "{count} параметров", "admin_settings_params_count": "{count} параметров",