feat: polish admin settings layout
This commit is contained in:
@@ -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 @@
|
||||
</div>
|
||||
{/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)}
|
||||
{@const revealed = isSecretRevealed(field.key)}
|
||||
<div class="admin-setting" class:is-overridden={isOverridden(field)}>
|
||||
@@ -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}
|
||||
<Accordion.Root
|
||||
@@ -646,9 +788,7 @@
|
||||
{#if group.webhook}
|
||||
{@render renderWebhookHint(group.webhook)}
|
||||
{/if}
|
||||
{#each group.fields as field}
|
||||
{@render renderField(field)}
|
||||
{/each}
|
||||
{@render renderGroupedFields(section, group)}
|
||||
</div>
|
||||
</Accordion.Content>
|
||||
</Accordion.Item>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user