feat: custom themes initial
This commit is contained in:
@@ -40,6 +40,13 @@
|
||||
import { normalizedEmail, telegramName } from "./lib/webapp/formatters.js";
|
||||
import { activeTariffName, buildTariffCatalog } from "./lib/webapp/tariffs.js";
|
||||
import { premiumTrafficPercent, trafficPercent } from "./lib/webapp/traffic.js";
|
||||
import {
|
||||
findThemeEntry,
|
||||
resolveEffectiveThemeKey,
|
||||
themeCssHref,
|
||||
themeEntryToInlineStyle,
|
||||
themeRootClass,
|
||||
} from "./lib/webapp/themeStyle.js";
|
||||
|
||||
/** Used-traffic percent from which top-up modals and CTAs unlock in the web app home screen */
|
||||
const TRAFFIC_TOPUP_UNLOCK_PERCENT = 80;
|
||||
@@ -243,7 +250,6 @@
|
||||
emoji: brandEmoji,
|
||||
emojiFont: brandEmojiFont,
|
||||
});
|
||||
$: accent = CFG.primaryColor || "#00fe7a";
|
||||
$: plans = data?.plans?.length ? data.plans : DEV_MOCK.data.plans;
|
||||
$: methods = data?.payment_methods?.length ? data.payment_methods : [];
|
||||
$: appSettings = data?.settings || DEV_MOCK.data.settings;
|
||||
@@ -300,6 +306,26 @@
|
||||
premiumTrafficPercent(subscription) >= TRAFFIC_TOPUP_UNLOCK_PERCENT)
|
||||
);
|
||||
$: user = data?.user || {};
|
||||
$: themesCatalog = data?.themes_catalog ||
|
||||
CFG.themesCatalog || { default_theme: "dark", themes: [] };
|
||||
$: resolvedThemeKey = resolveEffectiveThemeKey(themesCatalog);
|
||||
$: activeThemeEntry = findThemeEntry(themesCatalog, resolvedThemeKey);
|
||||
$: darkThemeEntry = findThemeEntry(themesCatalog, "dark");
|
||||
$: effectiveThemeEntry =
|
||||
screen === "admin" && activeThemeEntry?.use_in_admin === false
|
||||
? darkThemeEntry || activeThemeEntry
|
||||
: activeThemeEntry;
|
||||
$: shellStyle = themeEntryToInlineStyle(effectiveThemeEntry, CFG.primaryColor);
|
||||
$: shellToneClass =
|
||||
effectiveThemeEntry?.tokens?.color_scheme === "light" ? "theme-light" : "theme-dark";
|
||||
$: shellThemeClass = themeRootClass(effectiveThemeEntry);
|
||||
$: shellThemeCssHref = themeCssHref(effectiveThemeEntry);
|
||||
$: if (typeof document !== "undefined" && effectiveThemeEntry?.tokens) {
|
||||
const scheme = effectiveThemeEntry.tokens.color_scheme || "dark";
|
||||
document.documentElement.style.colorScheme = scheme;
|
||||
const bg = effectiveThemeEntry.tokens.bg;
|
||||
if (bg) document.body.style.backgroundColor = bg;
|
||||
}
|
||||
$: isAdmin = Boolean(user?.is_admin);
|
||||
$: if (screen === "admin" && !isAdmin) {
|
||||
screen = "settings";
|
||||
@@ -873,6 +899,9 @@
|
||||
|
||||
<svelte:head>
|
||||
<title>{brandTitle}</title>
|
||||
{#if shellThemeCssHref}
|
||||
<link rel="stylesheet" href={shellThemeCssHref} data-theme-css={resolvedThemeKey} />
|
||||
{/if}
|
||||
</svelte:head>
|
||||
|
||||
<Tooltip.Provider>
|
||||
@@ -880,7 +909,7 @@
|
||||
{#if isPreviewBoard}
|
||||
<PreviewBoard config={CFG} mockData={DEV_MOCK.data} />
|
||||
{:else}
|
||||
<div class="app-shell" style={`--accent: ${accent};`}>
|
||||
<div class="app-shell {shellToneClass} {shellThemeClass}" style={shellStyle}>
|
||||
{#if mode === "loading"}
|
||||
<div class="loader">
|
||||
<BrandMark {brand} size="md" />
|
||||
@@ -931,6 +960,7 @@
|
||||
onSectionChange={handleAdminSectionChange}
|
||||
onSettingsSaved={handleAdminPersistedSaved}
|
||||
onTariffsSaved={handleAdminPersistedSaved}
|
||||
onThemesSaved={handleAdminPersistedSaved}
|
||||
{brandTitle}
|
||||
{brand}
|
||||
appVersion={CFG.appVersion}
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
LayoutDashboard,
|
||||
Megaphone,
|
||||
Menu,
|
||||
Paintbrush,
|
||||
Plus,
|
||||
RefreshCw,
|
||||
Save,
|
||||
@@ -34,6 +35,7 @@
|
||||
import StatsSection from "./sections/StatsSection.svelte";
|
||||
import TariffEditorModal from "./sections/TariffEditorModal.svelte";
|
||||
import TariffsSection from "./sections/TariffsSection.svelte";
|
||||
import ThemesSection from "./sections/ThemesSection.svelte";
|
||||
import UserDetailModal from "./sections/UserDetailModal.svelte";
|
||||
import UsersSection from "./sections/UsersSection.svelte";
|
||||
import { createAdsStore } from "../lib/admin/stores/adsStore.js";
|
||||
@@ -44,6 +46,7 @@
|
||||
import { createSettingsStore } from "../lib/admin/stores/settingsStore.js";
|
||||
import { createStatsStore } from "../lib/admin/stores/statsStore.js";
|
||||
import { createTariffsStore } from "../lib/admin/stores/tariffsStore.js";
|
||||
import { createThemesStore } from "../lib/admin/stores/themesStore.js";
|
||||
import { createUsersStore } from "../lib/admin/stores/usersStore.js";
|
||||
import {
|
||||
fmtDate,
|
||||
@@ -70,6 +73,7 @@
|
||||
export let onSectionChange = () => {};
|
||||
export let onSettingsSaved = () => {};
|
||||
export let onTariffsSaved = () => {};
|
||||
export let onThemesSaved = () => {};
|
||||
export let brand = {};
|
||||
export let brandTitle = "/minishop";
|
||||
export let appVersion = "dev+local";
|
||||
@@ -111,6 +115,7 @@
|
||||
label: at("nav_system", {}, "Система"),
|
||||
items: [
|
||||
{ id: "tariffs", label: at("nav_tariffs", {}, "Тарифы"), icon: Coins },
|
||||
{ id: "themes", label: at("nav_themes", {}, "Темы"), icon: Paintbrush },
|
||||
{ id: "settings", label: at("nav_settings", {}, "Настройки"), icon: Sliders },
|
||||
],
|
||||
},
|
||||
@@ -153,6 +158,10 @@
|
||||
title: at("section_tariffs_title", {}, "Тарифы"),
|
||||
subtitle: at("section_tariffs_subtitle", {}, "Каталог продаж, периоды, пакеты и лимиты"),
|
||||
},
|
||||
themes: {
|
||||
title: at("section_themes_title", {}, "Темы Web App"),
|
||||
subtitle: at("section_themes_subtitle", {}, "Цвета, шрифты и темы оформления Mini App"),
|
||||
},
|
||||
settings: {
|
||||
title: at("section_settings_title", {}, "Настройки приложения"),
|
||||
subtitle: at("section_settings_subtitle", {}, "Оверрайды над .env, применяются мгновенно"),
|
||||
@@ -196,6 +205,7 @@
|
||||
const settingsStore = createSettingsStore({ api, onToast: flash, at });
|
||||
const statsStore = createStatsStore({ api, onToast: flash, at });
|
||||
const tariffsStore = createTariffsStore({ api, onToast: flash, onTariffsSaved, flash, at });
|
||||
const themesStore = createThemesStore({ api, onThemesSaved, flash, at });
|
||||
const usersStore = createUsersStore({ api, onToast: flash, at });
|
||||
|
||||
setContext("promosStore", promosStore);
|
||||
@@ -207,6 +217,7 @@
|
||||
setContext("settingsStore", settingsStore);
|
||||
setContext("usersStore", usersStore);
|
||||
setContext("tariffsStore", tariffsStore);
|
||||
setContext("themesStore", themesStore);
|
||||
|
||||
$: usersStore.setActive(active);
|
||||
$: dirtyCount = Object.keys($settingsStore.settingsDirty || {}).length;
|
||||
@@ -635,6 +646,10 @@
|
||||
<TariffsSection {at} {fmtMoney} />
|
||||
{/if}
|
||||
|
||||
{#if active === "themes"}
|
||||
<ThemesSection {at} {currentLang} />
|
||||
{/if}
|
||||
|
||||
{#if active === "settings"}
|
||||
<SettingsSection {at} {isCompact} {onSettingsSaved} {currentLang} />
|
||||
{/if}
|
||||
|
||||
@@ -0,0 +1,267 @@
|
||||
<script>
|
||||
import { Check, FileText, RefreshCw, Save } from "$components/ui/icons.js";
|
||||
import { getContext, onMount } from "svelte";
|
||||
import { AdminBadge, AdminButton, AdminEmptyState } from "$components/patterns/admin/index.js";
|
||||
import { localizedThemeName } from "$lib/webapp/themeStyle.js";
|
||||
|
||||
export let at;
|
||||
export let currentLang = "ru";
|
||||
|
||||
const themesStore = getContext("themesStore");
|
||||
|
||||
$: ({ themesCatalog, themesLoading, themesDir, themesSaving } = $themesStore);
|
||||
$: activeKey = themesCatalog.default_theme;
|
||||
|
||||
function themeTitle(theme) {
|
||||
return localizedThemeName(theme, currentLang) || "—";
|
||||
}
|
||||
|
||||
function themeDescription(theme) {
|
||||
const folder = `${themesDir || "data/themes"}/${theme.key}`;
|
||||
return theme.css_file ? `${folder}/${theme.css_file}` : `${folder}/theme.json`;
|
||||
}
|
||||
|
||||
function toggleAccent(event, theme) {
|
||||
event.stopPropagation();
|
||||
themesStore.togglePrimaryAccent(theme.key, event.currentTarget.checked);
|
||||
}
|
||||
|
||||
function toggleAdminTheme(event, theme) {
|
||||
event.stopPropagation();
|
||||
themesStore.toggleAdminUse(theme.key, event.currentTarget.checked);
|
||||
}
|
||||
|
||||
function isThemeOptionEvent(event) {
|
||||
return Boolean(event?.target?.closest?.(".admin-theme-card-option"));
|
||||
}
|
||||
|
||||
function selectTheme(theme, event = null) {
|
||||
if (isThemeOptionEvent(event)) return;
|
||||
if (!themesSaving) themesStore.setCurrentTheme(theme.key);
|
||||
}
|
||||
|
||||
function handleCardKeydown(event, theme) {
|
||||
if (isThemeOptionEvent(event)) return;
|
||||
if (event.key !== "Enter" && event.key !== " ") return;
|
||||
event.preventDefault();
|
||||
selectTheme(theme);
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
themesStore.loadThemes();
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if themesLoading}
|
||||
<AdminEmptyState>{at("loading", {}, "Загрузка…")}</AdminEmptyState>
|
||||
{:else}
|
||||
<article class="admin-card">
|
||||
<header class="admin-card-head">
|
||||
<div>
|
||||
<h3>{at("themes_catalog_title", {}, "Темы Web App")}</h3>
|
||||
<small
|
||||
>{at(
|
||||
"themes_catalog_sub",
|
||||
{},
|
||||
"Текущая тема выбирается карточкой; внешний вид редактируется файлами в папке темы"
|
||||
)}</small
|
||||
>
|
||||
</div>
|
||||
<div class="admin-editor-section-actions">
|
||||
<AdminButton
|
||||
size="sm"
|
||||
onclick={themesStore.loadThemes}
|
||||
disabled={themesLoading || themesSaving}
|
||||
>
|
||||
<RefreshCw size={13} />
|
||||
{at("btn_refresh", {}, "Обновить")}
|
||||
</AdminButton>
|
||||
<AdminButton
|
||||
size="sm"
|
||||
variant="primary"
|
||||
onclick={themesStore.saveThemes}
|
||||
disabled={themesLoading || themesSaving}
|
||||
>
|
||||
<Save size={13} />
|
||||
{at("btn_save", {}, "Сохранить")}
|
||||
</AdminButton>
|
||||
</div>
|
||||
</header>
|
||||
<div class="admin-card-body">
|
||||
{#if !themesCatalog.themes.length}
|
||||
<AdminEmptyState>
|
||||
{at(
|
||||
"themes_catalog_empty",
|
||||
{},
|
||||
"Каталог пуст. Добавьте папку темы в data/themes и обновите список."
|
||||
)}
|
||||
</AdminEmptyState>
|
||||
{:else}
|
||||
<div class="admin-theme-grid">
|
||||
{#each themesCatalog.themes as theme (theme.key)}
|
||||
{@const isCurrent = theme.key === activeKey}
|
||||
<div
|
||||
role="button"
|
||||
tabindex={themesSaving ? -1 : 0}
|
||||
class="admin-theme-card"
|
||||
class:is-current={isCurrent}
|
||||
class:is-disabled={theme.enabled === false}
|
||||
aria-pressed={isCurrent}
|
||||
aria-disabled={themesSaving}
|
||||
onclick={(event) => selectTheme(theme, event)}
|
||||
onkeydown={(event) => handleCardKeydown(event, theme)}
|
||||
>
|
||||
<span class="admin-theme-card-main">
|
||||
<span class="admin-theme-card-title">
|
||||
<strong>{themeTitle(theme)}</strong>
|
||||
{#if isCurrent}
|
||||
<AdminBadge variant="success">{at("status_current", {}, "Текущая")}</AdminBadge>
|
||||
{/if}
|
||||
</span>
|
||||
<small>{theme.key}</small>
|
||||
</span>
|
||||
<span class="admin-theme-card-meta">
|
||||
<FileText size={15} />
|
||||
<span>{themeDescription(theme)}</span>
|
||||
</span>
|
||||
<label class="admin-theme-card-option">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={theme.use_primary_accent !== false}
|
||||
disabled={themesSaving}
|
||||
onchange={(event) => toggleAccent(event, theme)}
|
||||
/>
|
||||
<span>{at("themes_use_primary_accent", {}, "Протягивать акцент")}</span>
|
||||
</label>
|
||||
<label class="admin-theme-card-option">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={theme.use_in_admin !== false}
|
||||
disabled={themesSaving}
|
||||
onchange={(event) => toggleAdminTheme(event, theme)}
|
||||
/>
|
||||
<span>{at("themes_use_in_admin", {}, "Использовать в админке")}</span>
|
||||
</label>
|
||||
<span class="admin-theme-card-check" aria-hidden="true">
|
||||
{#if isCurrent}<Check size={18} />{/if}
|
||||
</span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</article>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.admin-theme-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.admin-theme-card {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: 12px;
|
||||
min-height: 118px;
|
||||
padding: 14px;
|
||||
border: 1px solid var(--admin-border);
|
||||
border-radius: 8px;
|
||||
background: var(--admin-surface);
|
||||
color: var(--admin-text);
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.admin-theme-card:hover {
|
||||
border-color: var(--admin-border-strong);
|
||||
background: color-mix(in srgb, var(--admin-surface-2) 72%, var(--admin-surface));
|
||||
}
|
||||
|
||||
.admin-theme-card.is-current {
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent) 44%, transparent);
|
||||
}
|
||||
|
||||
.admin-theme-card.is-disabled {
|
||||
opacity: 0.58;
|
||||
}
|
||||
|
||||
.admin-theme-card-main {
|
||||
display: grid;
|
||||
align-content: start;
|
||||
gap: 5px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.admin-theme-card-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.admin-theme-card-title strong {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.admin-theme-card-main small,
|
||||
.admin-theme-card-meta {
|
||||
color: var(--admin-muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.admin-theme-card-meta {
|
||||
grid-column: 1 / -1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.admin-theme-card-meta span {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.admin-theme-card-option {
|
||||
grid-column: 1 / -1;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
width: fit-content;
|
||||
max-width: 100%;
|
||||
color: var(--admin-muted);
|
||||
font-size: 12px;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.admin-theme-card-option input {
|
||||
flex: 0 0 auto;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
margin: 0;
|
||||
accent-color: var(--accent);
|
||||
}
|
||||
|
||||
.admin-theme-card-option span {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.admin-theme-card-check {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 999px;
|
||||
color: var(--accent);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,113 @@
|
||||
function cloneCatalog(catalog) {
|
||||
return JSON.parse(JSON.stringify(catalog || { default_theme: "dark", themes: [] }));
|
||||
}
|
||||
|
||||
import { writable } from "svelte/store";
|
||||
|
||||
export function createThemesStore({ api, onThemesSaved, flash, at }) {
|
||||
const state = writable({
|
||||
themesCatalog: { default_theme: "dark", themes: [] },
|
||||
themesDir: "",
|
||||
themesLoading: false,
|
||||
themesSaving: false,
|
||||
});
|
||||
|
||||
async function loadThemes() {
|
||||
state.update((s) => ({ ...s, themesLoading: true }));
|
||||
try {
|
||||
const data = await api("/admin/themes");
|
||||
if (data?.ok) {
|
||||
state.update((s) => ({
|
||||
...s,
|
||||
themesCatalog: cloneCatalog(data.catalog),
|
||||
themesDir: data.themes_dir || "",
|
||||
}));
|
||||
} else {
|
||||
flash(data?.message || data?.error || at("load_failed", {}, "Не удалось загрузить темы"));
|
||||
}
|
||||
} finally {
|
||||
state.update((s) => ({ ...s, themesLoading: false }));
|
||||
}
|
||||
}
|
||||
|
||||
async function saveThemes(options = {}) {
|
||||
const silent = Boolean(options.silent);
|
||||
let catalog = null;
|
||||
state.update((s) => {
|
||||
catalog = cloneCatalog(s.themesCatalog);
|
||||
return { ...s, themesSaving: true };
|
||||
});
|
||||
try {
|
||||
const data = await api("/admin/themes", {
|
||||
method: "PUT",
|
||||
body: JSON.stringify({ catalog }),
|
||||
});
|
||||
if (data?.ok) {
|
||||
state.update((s) => ({
|
||||
...s,
|
||||
themesCatalog: cloneCatalog(data.catalog),
|
||||
themesDir: data.themes_dir || s.themesDir,
|
||||
}));
|
||||
if (!silent) flash(at("themes_saved", {}, "Темы сохранены"));
|
||||
if (typeof onThemesSaved === "function") onThemesSaved();
|
||||
} else {
|
||||
flash(data?.message || data?.error || at("themes_save_failed", {}, "Не удалось сохранить"));
|
||||
}
|
||||
} finally {
|
||||
state.update((s) => ({ ...s, themesSaving: false }));
|
||||
}
|
||||
}
|
||||
|
||||
async function setCurrentTheme(key) {
|
||||
let changed = false;
|
||||
state.update((s) => ({
|
||||
...s,
|
||||
themesCatalog: {
|
||||
...s.themesCatalog,
|
||||
default_theme: key,
|
||||
themes: (s.themesCatalog.themes || []).map((theme) => ({
|
||||
...theme,
|
||||
default: theme.key === key,
|
||||
})),
|
||||
},
|
||||
}));
|
||||
state.update((s) => {
|
||||
changed = s.themesCatalog.default_theme === key;
|
||||
return s;
|
||||
});
|
||||
if (changed) await saveThemes({ silent: true });
|
||||
}
|
||||
|
||||
function togglePrimaryAccent(key, enabled) {
|
||||
state.update((s) => ({
|
||||
...s,
|
||||
themesCatalog: {
|
||||
...s.themesCatalog,
|
||||
themes: (s.themesCatalog.themes || []).map((theme) =>
|
||||
theme.key === key ? { ...theme, use_primary_accent: Boolean(enabled) } : theme
|
||||
),
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
function toggleAdminUse(key, enabled) {
|
||||
state.update((s) => ({
|
||||
...s,
|
||||
themesCatalog: {
|
||||
...s.themesCatalog,
|
||||
themes: (s.themesCatalog.themes || []).map((theme) =>
|
||||
theme.key === key ? { ...theme, use_in_admin: Boolean(enabled) } : theme
|
||||
),
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
return {
|
||||
subscribe: state.subscribe,
|
||||
loadThemes,
|
||||
saveThemes,
|
||||
setCurrentTheme,
|
||||
togglePrimaryAccent,
|
||||
toggleAdminUse,
|
||||
};
|
||||
}
|
||||
@@ -35,6 +35,7 @@ export {
|
||||
Menu,
|
||||
MessageSquare,
|
||||
MousePointerClick,
|
||||
Paintbrush,
|
||||
Plus,
|
||||
QrCode,
|
||||
Radio,
|
||||
|
||||
@@ -34,6 +34,7 @@ export const ADMIN_SECTIONS = new Set([
|
||||
"broadcast",
|
||||
"logs",
|
||||
"tariffs",
|
||||
"themes",
|
||||
"settings",
|
||||
]);
|
||||
export const TELEGRAM_WEBAPP_SCRIPT_URL = "https://telegram.org/js/telegram-web-app.js";
|
||||
|
||||
@@ -195,6 +195,30 @@ export async function mockApi(path, options = {}, context = {}) {
|
||||
},
|
||||
};
|
||||
}
|
||||
if (path === "/admin/themes") {
|
||||
if (String(options.method || "GET").toUpperCase() === "PUT") {
|
||||
try {
|
||||
const body = options?.body ? JSON.parse(String(options.body)) : {};
|
||||
const catalog = body.catalog || body;
|
||||
if (catalog?.themes) {
|
||||
DEV_MOCK.config.themesCatalog = clone(catalog);
|
||||
DEV_MOCK.data.themes_catalog = clone(catalog);
|
||||
}
|
||||
} catch (_e) {
|
||||
void _e;
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
themes_dir: "data/themes",
|
||||
catalog: clone(DEV_MOCK.config.themesCatalog),
|
||||
};
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
themes_dir: "data/themes",
|
||||
catalog: clone(DEV_MOCK.config.themesCatalog),
|
||||
};
|
||||
}
|
||||
if (path === "/admin/settings") return { ok: true, sections: [] };
|
||||
if (cleanPath.startsWith("/admin/"))
|
||||
return { ok: true, payments: [], promos: [], logs: [], campaigns: [], total: 0 };
|
||||
|
||||
@@ -1,3 +1,27 @@
|
||||
const WINDOWS_95_THEME = {
|
||||
key: "windows95",
|
||||
names: { ru: "Windows 95", en: "Windows 95" },
|
||||
enabled: true,
|
||||
default: false,
|
||||
css_file: "style.css",
|
||||
tokens: {
|
||||
color_scheme: "light",
|
||||
style_preset: "win95",
|
||||
},
|
||||
};
|
||||
|
||||
const ASCII_THEME = {
|
||||
key: "ascii",
|
||||
names: { ru: "ASCII", en: "ASCII" },
|
||||
enabled: true,
|
||||
default: false,
|
||||
css_file: "style.css",
|
||||
tokens: {
|
||||
color_scheme: "dark",
|
||||
style_preset: "ascii",
|
||||
},
|
||||
};
|
||||
|
||||
export const DEV_MOCK = {
|
||||
config: {
|
||||
title: "/minishop",
|
||||
@@ -18,6 +42,37 @@ export const DEV_MOCK = {
|
||||
telegramOAuthRequestAccess: ["write"],
|
||||
appVersion: "dev+local",
|
||||
appRepositoryUrl: "https://github.com/3252a8/remnawave-minishop",
|
||||
themesCatalog: {
|
||||
default_theme: "dark",
|
||||
themes: [
|
||||
{
|
||||
key: "dark",
|
||||
names: { ru: "Тёмная", en: "Dark" },
|
||||
enabled: true,
|
||||
default: true,
|
||||
tokens: {
|
||||
color_scheme: "dark",
|
||||
accent: "#00fe7a",
|
||||
bg: "#03070b",
|
||||
panel: "#111820",
|
||||
text: "#f2f7f4",
|
||||
muted: "#a9b4b0",
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "light",
|
||||
names: { ru: "Светлая", en: "Light" },
|
||||
enabled: true,
|
||||
default: false,
|
||||
css_file: "style.css",
|
||||
tokens: {
|
||||
color_scheme: "light",
|
||||
},
|
||||
},
|
||||
WINDOWS_95_THEME,
|
||||
ASCII_THEME,
|
||||
],
|
||||
},
|
||||
},
|
||||
data: {
|
||||
ok: true,
|
||||
@@ -124,6 +179,35 @@ export const DEV_MOCK = {
|
||||
{ months: 12, title: "12 месяцев", inviter_days: 62, friend_days: 31 },
|
||||
],
|
||||
},
|
||||
themes_catalog: {
|
||||
default_theme: "dark",
|
||||
themes: [
|
||||
{
|
||||
key: "dark",
|
||||
names: { ru: "Тёмная", en: "Dark" },
|
||||
enabled: true,
|
||||
tokens: {
|
||||
color_scheme: "dark",
|
||||
accent: "#00fe7a",
|
||||
bg: "#03070b",
|
||||
panel: "#111820",
|
||||
text: "#f2f7f4",
|
||||
muted: "#a9b4b0",
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "light",
|
||||
names: { ru: "Светлая", en: "Light" },
|
||||
enabled: true,
|
||||
css_file: "style.css",
|
||||
tokens: {
|
||||
color_scheme: "light",
|
||||
},
|
||||
},
|
||||
WINDOWS_95_THEME,
|
||||
ASCII_THEME,
|
||||
],
|
||||
},
|
||||
settings: {
|
||||
support_url: "https://t.me/support",
|
||||
traffic_mode: false,
|
||||
@@ -143,6 +227,20 @@ export function applyPreviewMock(kind) {
|
||||
const mode = String(kind || "")
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
|
||||
const themeKeys = new Set((DEV_MOCK.config.themesCatalog.themes || []).map((theme) => theme.key));
|
||||
if (themeKeys.has(mode)) {
|
||||
DEV_MOCK.config.themesCatalog.default_theme = mode;
|
||||
DEV_MOCK.data.themes_catalog.default_theme = mode;
|
||||
for (const theme of DEV_MOCK.config.themesCatalog.themes || []) {
|
||||
theme.default = theme.key === mode;
|
||||
}
|
||||
for (const theme of DEV_MOCK.data.themes_catalog.themes || []) {
|
||||
theme.default = theme.key === mode;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (mode === "traffic") {
|
||||
DEV_MOCK.data.settings.traffic_mode = true;
|
||||
DEV_MOCK.data.settings.trial_available = false;
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
/** Maps JSON theme token keys to CSS custom properties used by the Mini App shell. */
|
||||
|
||||
const TOKEN_TO_CSS_VAR = {
|
||||
accent: "--accent",
|
||||
bg: "--bg",
|
||||
panel: "--panel",
|
||||
panel_2: "--panel-2",
|
||||
panel_3: "--panel-3",
|
||||
border: "--border",
|
||||
border_strong: "--border-strong",
|
||||
text: "--text",
|
||||
muted: "--muted",
|
||||
dim: "--dim",
|
||||
danger: "--danger",
|
||||
blue: "--blue",
|
||||
radius: "--radius",
|
||||
font_sans: "--font-sans",
|
||||
font_logo: "--font-logo",
|
||||
font_mono: "--font-mono",
|
||||
admin_bg: "--admin-bg",
|
||||
admin_surface: "--admin-surface",
|
||||
admin_surface_2: "--admin-surface-2",
|
||||
admin_elev: "--admin-elev",
|
||||
admin_border: "--admin-border",
|
||||
admin_border_strong: "--admin-border-strong",
|
||||
admin_text: "--admin-text",
|
||||
admin_muted: "--admin-muted",
|
||||
admin_dim: "--admin-dim",
|
||||
};
|
||||
|
||||
export function themeTokensToInlineStyle(tokens, primaryFallback = "#00fe7a", options = {}) {
|
||||
const t = tokens && typeof tokens === "object" ? tokens : {};
|
||||
const parts = [];
|
||||
const useFallbackAccent = options.fallbackAccent !== false;
|
||||
const accent = t.accent || (useFallbackAccent ? primaryFallback || "#00fe7a" : "");
|
||||
if (accent) parts.push(`--accent:${accent}`);
|
||||
for (const [key, cssVar] of Object.entries(TOKEN_TO_CSS_VAR)) {
|
||||
if (key === "accent") continue;
|
||||
const value = t[key];
|
||||
if (value === undefined || value === null || value === "") continue;
|
||||
parts.push(`${cssVar}:${String(value)}`);
|
||||
}
|
||||
return parts.join(";");
|
||||
}
|
||||
|
||||
export function findThemeEntry(themesCatalog, key) {
|
||||
const themes = themesCatalog?.themes || [];
|
||||
return themes.find((entry) => entry && entry.key === key) || null;
|
||||
}
|
||||
|
||||
export function resolveEffectiveThemeKey(themesCatalog) {
|
||||
const themes = themesCatalog?.themes || [];
|
||||
const byKey = (k) => themes.find((entry) => entry.key === k);
|
||||
const def = themesCatalog?.default_theme || themes[0]?.key || "dark";
|
||||
return byKey(def) ? def : themes[0]?.key || "dark";
|
||||
}
|
||||
|
||||
export function themePresetClass(tokens) {
|
||||
const preset = String(tokens?.style_preset || "")
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
if (!preset || preset === "none") return "";
|
||||
if (preset === "win95" || preset === "windows95") return "theme-preset-win95";
|
||||
return "";
|
||||
}
|
||||
|
||||
export function themeKeyClass(key) {
|
||||
const safe = String(key || "")
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/[^A-Za-z0-9_-]+/g, "-")
|
||||
.replace(/^-+|-+$/g, "");
|
||||
return safe ? `theme-key-${safe}` : "";
|
||||
}
|
||||
|
||||
export function themeCssClass(cssFile) {
|
||||
const filename = String(cssFile || "")
|
||||
.replace(/\\/g, "/")
|
||||
.split("/")
|
||||
.filter(Boolean)
|
||||
.pop();
|
||||
const slug = String(filename || "")
|
||||
.replace(/\.css$/i, "")
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9_-]+/g, "-")
|
||||
.replace(/^-+|-+$/g, "");
|
||||
return slug ? `theme-css-${slug}` : "";
|
||||
}
|
||||
|
||||
export function themeRootClass(theme) {
|
||||
return [
|
||||
themeKeyClass(theme?.key),
|
||||
themeCssClass(theme?.css_file),
|
||||
themePresetClass(theme?.tokens),
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
export function themeEntryToInlineStyle(theme, primaryFallback = "#00fe7a") {
|
||||
return themeTokensToInlineStyle(theme?.tokens, primaryFallback, {
|
||||
fallbackAccent: !theme?.css_file,
|
||||
});
|
||||
}
|
||||
|
||||
function encodeThemeCssPath(path) {
|
||||
return String(path || "")
|
||||
.replace(/\\/g, "/")
|
||||
.split("/")
|
||||
.filter(Boolean)
|
||||
.map(encodeURIComponent)
|
||||
.join("/");
|
||||
}
|
||||
|
||||
export function themeCssHref(theme) {
|
||||
const cssFile = String(theme?.css_file || "").trim();
|
||||
if (!cssFile) return "";
|
||||
if (/^(?:https?:)?\/\//i.test(cssFile) || cssFile.startsWith("data:")) return "";
|
||||
if (cssFile.startsWith("/")) return cssFile;
|
||||
const normalizedCssFile = cssFile.replace(/\\/g, "/").split("/").filter(Boolean).join("/");
|
||||
const key = String(theme?.key || "")
|
||||
.trim()
|
||||
.replace(/[^A-Za-z0-9_-]+/g, "-")
|
||||
.replace(/^-+|-+$/g, "");
|
||||
const themedPath =
|
||||
key && normalizedCssFile.split("/")[0] !== key
|
||||
? `${key}/${normalizedCssFile}`
|
||||
: normalizedCssFile;
|
||||
const encoded = encodeThemeCssPath(themedPath);
|
||||
return encoded ? `/webapp-theme-css/${encoded}` : "";
|
||||
}
|
||||
|
||||
export function localizedThemeName(theme, lang = "en") {
|
||||
const names = theme?.names || {};
|
||||
const key = String(lang || "")
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
const base = key.split("-")[0];
|
||||
return names[key] || names[base] || names.en || theme?.key || "";
|
||||
}
|
||||
@@ -7,17 +7,14 @@
|
||||
--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);
|
||||
linear-gradient(135deg, var(--surface-sheen), var(--surface-sheen-soft)), var(--admin-surface);
|
||||
--admin-card-shadow: var(--shadow-soft), inset 0 1px 0 var(--inset-highlight);
|
||||
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
width: 100vw;
|
||||
height: 100dvh;
|
||||
background: #02070b;
|
||||
background: var(--admin-bg);
|
||||
color: var(--admin-text);
|
||||
overflow: hidden;
|
||||
overscroll-behavior: contain;
|
||||
@@ -38,9 +35,9 @@
|
||||
gap: 2px;
|
||||
padding: 28px 14px;
|
||||
border-right: 1px solid var(--admin-border);
|
||||
background: rgba(7, 12, 17, 0.55);
|
||||
background: var(--rail-bg);
|
||||
backdrop-filter: blur(14px);
|
||||
box-shadow: inset -1px 0 0 rgba(255, 255, 255, 0.02);
|
||||
box-shadow: inset -1px 0 0 var(--admin-border);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
@@ -74,7 +71,8 @@
|
||||
color: var(--accent);
|
||||
font-size: 14px;
|
||||
font-weight: 800;
|
||||
letter-spacing: -0.01em;
|
||||
font-family: var(--font-logo);
|
||||
letter-spacing: 0;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
@@ -115,7 +113,10 @@
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
cursor: pointer;
|
||||
transition: background 0.12s ease, color 0.12s ease, border-color 0.12s ease;
|
||||
transition:
|
||||
background 0.12s ease,
|
||||
color 0.12s ease,
|
||||
border-color 0.12s ease;
|
||||
}
|
||||
|
||||
.admin-nav-item > svg {
|
||||
@@ -124,7 +125,7 @@
|
||||
}
|
||||
|
||||
.admin-nav-item:hover {
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
background: var(--surface-hover);
|
||||
color: var(--admin-text);
|
||||
}
|
||||
|
||||
@@ -169,7 +170,7 @@
|
||||
gap: 8px;
|
||||
border: 1px solid var(--admin-border);
|
||||
border-radius: 10px;
|
||||
background: rgba(255, 255, 255, 0.035);
|
||||
background: var(--surface-muted);
|
||||
color: var(--admin-text);
|
||||
padding: 8px 10px;
|
||||
text-align: left;
|
||||
@@ -251,7 +252,7 @@
|
||||
gap: 16px;
|
||||
padding: 0 28px;
|
||||
border-bottom: 1px solid var(--admin-border);
|
||||
background: color-mix(in srgb, #02070b 82%, transparent);
|
||||
background: color-mix(in srgb, var(--admin-bg) 82%, transparent);
|
||||
backdrop-filter: blur(14px);
|
||||
}
|
||||
|
||||
@@ -265,7 +266,7 @@
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.01em;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.admin-header-title small {
|
||||
@@ -427,7 +428,7 @@
|
||||
.admin-stat-card .admin-stat-value {
|
||||
font-size: 26px;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.02em;
|
||||
letter-spacing: 0;
|
||||
color: var(--admin-text);
|
||||
}
|
||||
|
||||
@@ -457,7 +458,7 @@
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--admin-text);
|
||||
letter-spacing: -0.01em;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.admin-dashboard-section-head small {
|
||||
@@ -528,7 +529,7 @@
|
||||
.admin-revenue-kpi-value {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.02em;
|
||||
letter-spacing: 0;
|
||||
color: var(--admin-text);
|
||||
line-height: 1.15;
|
||||
}
|
||||
@@ -606,7 +607,7 @@
|
||||
border: 1px solid var(--admin-border);
|
||||
background: var(--admin-surface);
|
||||
color: var(--admin-text);
|
||||
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.45);
|
||||
box-shadow: var(--shadow-popover);
|
||||
}
|
||||
|
||||
.admin-revenue-range-popover__title {
|
||||
@@ -946,7 +947,7 @@
|
||||
.admin-panel-dash-tile-value {
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.03em;
|
||||
letter-spacing: 0;
|
||||
line-height: 1.1;
|
||||
color: color-mix(in srgb, var(--accent) 22%, var(--admin-text));
|
||||
font-variant-numeric: tabular-nums;
|
||||
@@ -1248,7 +1249,7 @@
|
||||
}
|
||||
|
||||
.admin-table tbody tr:hover {
|
||||
background: rgba(255, 255, 255, 0.035);
|
||||
background: var(--surface-hover);
|
||||
}
|
||||
|
||||
.admin-table td.admin-cell-mono {
|
||||
@@ -1531,7 +1532,9 @@
|
||||
font-size: 13px;
|
||||
resize: vertical;
|
||||
outline: none;
|
||||
transition: border-color 0.12s ease, box-shadow 0.12s ease;
|
||||
transition:
|
||||
border-color 0.12s ease,
|
||||
box-shadow 0.12s ease;
|
||||
}
|
||||
|
||||
.admin-textarea:focus,
|
||||
@@ -2357,7 +2360,7 @@
|
||||
z-index: 80;
|
||||
transform: translateX(-100%);
|
||||
transition: transform 0.22s ease;
|
||||
box-shadow: 0 32px 80px rgba(0, 0, 0, 0.6);
|
||||
box-shadow: var(--shadow-strong);
|
||||
}
|
||||
|
||||
.admin-screen-wrap.is-sidebar-open .admin-sidebar {
|
||||
@@ -2367,7 +2370,7 @@
|
||||
.admin-sidebar-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
background: var(--overlay-scrim);
|
||||
z-index: 70;
|
||||
border: 0;
|
||||
cursor: pointer;
|
||||
@@ -2501,7 +2504,9 @@
|
||||
padding: 10px 12px;
|
||||
width: 100%;
|
||||
cursor: pointer;
|
||||
transition: background 0.12s ease, border-color 0.12s ease;
|
||||
transition:
|
||||
background 0.12s ease,
|
||||
border-color 0.12s ease;
|
||||
}
|
||||
|
||||
.settings-row.settings-row-admin:hover {
|
||||
@@ -2560,7 +2565,10 @@
|
||||
border-radius: 7px;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
transition: background 0.12s ease, color 0.12s ease, box-shadow 0.12s ease;
|
||||
transition:
|
||||
background 0.12s ease,
|
||||
color 0.12s ease,
|
||||
box-shadow 0.12s ease;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
@@ -2571,7 +2579,7 @@
|
||||
.admin-tabs-trigger[data-state="active"] {
|
||||
background: var(--admin-surface);
|
||||
color: var(--admin-text);
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.4);
|
||||
box-shadow: var(--shadow-soft);
|
||||
}
|
||||
|
||||
.admin-tabs-trigger:focus-visible {
|
||||
@@ -2641,7 +2649,9 @@
|
||||
border: 1px solid var(--admin-border);
|
||||
background: var(--admin-elev);
|
||||
cursor: pointer;
|
||||
transition: background 0.18s ease, border-color 0.18s ease;
|
||||
transition:
|
||||
background 0.18s ease,
|
||||
border-color 0.18s ease;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
@@ -2693,7 +2703,9 @@
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
outline: none;
|
||||
transition: border-color 0.12s ease, box-shadow 0.12s ease;
|
||||
transition:
|
||||
border-color 0.12s ease,
|
||||
box-shadow 0.12s ease;
|
||||
}
|
||||
|
||||
.admin-select-trigger:hover {
|
||||
@@ -2724,10 +2736,9 @@
|
||||
border: 1px solid var(--admin-border);
|
||||
border-radius: 10px;
|
||||
background:
|
||||
linear-gradient(135deg, rgba(255, 255, 255, 0.055), rgba(255, 255, 255, 0.018)),
|
||||
var(--admin-surface);
|
||||
linear-gradient(135deg, var(--surface-sheen), var(--surface-sheen-soft)), var(--admin-surface);
|
||||
padding: 4px;
|
||||
box-shadow: 0 16px 36px rgba(0, 0, 0, 0.45);
|
||||
box-shadow: var(--shadow-popover);
|
||||
outline: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -3202,4 +3213,4 @@
|
||||
grid-area: meta;
|
||||
white-space: normal;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,21 +4,14 @@
|
||||
format("woff2");
|
||||
font-display: swap;
|
||||
unicode-range:
|
||||
U+1F1E6-1F1FF,
|
||||
U+1F3F4,
|
||||
U+E0062-E0063,
|
||||
U+E0065,
|
||||
U+E0067,
|
||||
U+E006C,
|
||||
U+E006E,
|
||||
U+E0073-E0074,
|
||||
U+E0077,
|
||||
U+E007F;
|
||||
U+1F1E6-1F1FF, U+1F3F4, U+E0062-E0063, U+E0065, U+E0067, U+E006C, U+E006E, U+E0073-E0074,
|
||||
U+E0077, U+E007F;
|
||||
}
|
||||
|
||||
:root {
|
||||
--font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif;
|
||||
--font-mono: "JetBrains Mono", "Fira Code", monospace;
|
||||
--font-logo: var(--font-mono);
|
||||
color-scheme: dark;
|
||||
--accent: #00fe7a;
|
||||
--bg: #03070b;
|
||||
@@ -33,6 +26,19 @@
|
||||
--danger: #ff6b6b;
|
||||
--blue: #2d9cff;
|
||||
--radius: 8px;
|
||||
--accent-contrast: #03100a;
|
||||
--surface-sheen: rgba(255, 255, 255, 0.055);
|
||||
--surface-sheen-soft: rgba(255, 255, 255, 0.018);
|
||||
--surface-hover: rgba(255, 255, 255, 0.04);
|
||||
--surface-muted: rgba(255, 255, 255, 0.035);
|
||||
--surface-subtle-border: rgba(255, 255, 255, 0.1);
|
||||
--overlay-scrim: rgba(0, 0, 0, 0.56);
|
||||
--nav-bg: rgba(7, 12, 17, 0.88);
|
||||
--rail-bg: rgba(7, 12, 17, 0.55);
|
||||
--shadow-soft: 0 18px 48px rgba(0, 0, 0, 0.22);
|
||||
--shadow-strong: 0 26px 70px rgba(0, 0, 0, 0.46);
|
||||
--shadow-popover: 0 20px 34px rgba(0, 0, 0, 0.42);
|
||||
--inset-highlight: rgba(255, 255, 255, 0.05);
|
||||
|
||||
/* Admin design tokens — kept on :root so portal-rendered admin
|
||||
surfaces (dialogs, bits-ui Select.Portal content) inherit them. */
|
||||
@@ -59,6 +65,10 @@
|
||||
font-family: var(--font-sans);
|
||||
}
|
||||
|
||||
.theme-light {
|
||||
color-scheme: light;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
@@ -72,7 +82,7 @@ body,
|
||||
}
|
||||
|
||||
body {
|
||||
background: #02070b;
|
||||
background: var(--bg, #02070b);
|
||||
color: var(--text);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
letter-spacing: 0;
|
||||
@@ -86,10 +96,10 @@ input {
|
||||
|
||||
.app-shell {
|
||||
min-height: 100dvh;
|
||||
background: #02070b !important;
|
||||
background: var(--bg, #02070b) !important;
|
||||
}
|
||||
|
||||
:root {
|
||||
--desktop-rail-width: 252px;
|
||||
--desktop-page-gutter: clamp(28px, 4vw, 72px);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,11 +26,9 @@
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--admin-border);
|
||||
background:
|
||||
linear-gradient(135deg, rgba(255, 255, 255, 0.055), rgba(255, 255, 255, 0.018)),
|
||||
linear-gradient(135deg, var(--surface-sheen), var(--surface-sheen-soft)),
|
||||
var(--admin-surface);
|
||||
box-shadow:
|
||||
0 18px 48px rgba(0, 0, 0, 0.22),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.05);
|
||||
box-shadow: var(--shadow-soft), inset 0 1px 0 var(--inset-highlight);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
@@ -80,7 +78,7 @@
|
||||
margin: 0;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.02em;
|
||||
letter-spacing: 0;
|
||||
color: var(--admin-text);
|
||||
font-variant-numeric: tabular-nums;
|
||||
line-height: 1.15;
|
||||
@@ -89,7 +87,7 @@
|
||||
.admin-cn-card-title--section {
|
||||
font-size: 1.05rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.01em;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.admin-cn-card-skeleton {
|
||||
@@ -227,9 +225,9 @@
|
||||
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)
|
||||
color-mix(in srgb, var(--muted) 10%, transparent),
|
||||
color-mix(in srgb, var(--muted) 18%, transparent),
|
||||
color-mix(in srgb, var(--muted) 10%, transparent)
|
||||
);
|
||||
background-size: 220% 100%;
|
||||
animation: ui-skeleton-pulse 1.2s ease-in-out infinite;
|
||||
@@ -293,3 +291,4 @@
|
||||
background-position: -120% 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ button:disabled {
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 10px 26px rgba(0, 0, 0, 0.18);
|
||||
box-shadow: var(--shadow-soft);
|
||||
transition:
|
||||
transform 0.16s ease,
|
||||
border-color 0.16s ease,
|
||||
@@ -34,7 +34,7 @@ button:disabled {
|
||||
|
||||
.btn:hover:not(:disabled) {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 14px 32px rgba(0, 0, 0, 0.22);
|
||||
box-shadow: var(--shadow-soft);
|
||||
}
|
||||
|
||||
.btn:active:not(:disabled) {
|
||||
@@ -45,7 +45,7 @@ button:disabled {
|
||||
outline: none;
|
||||
box-shadow:
|
||||
0 0 0 3px color-mix(in srgb, var(--accent) 28%, transparent),
|
||||
0 14px 32px rgba(0, 0, 0, 0.22);
|
||||
var(--shadow-soft);
|
||||
}
|
||||
|
||||
.btn.wide,
|
||||
@@ -56,7 +56,7 @@ button:disabled {
|
||||
.btn-primary {
|
||||
border-color: color-mix(in srgb, var(--accent) 72%, white);
|
||||
background: linear-gradient(135deg, var(--accent), color-mix(in srgb, var(--accent) 76%, white));
|
||||
color: #03100a;
|
||||
color: var(--accent-contrast);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
@@ -111,7 +111,7 @@ button:disabled {
|
||||
margin-top: 12px;
|
||||
overflow: hidden;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
background: color-mix(in srgb, var(--muted) 18%, transparent);
|
||||
}
|
||||
|
||||
.progress span {
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
border: 0;
|
||||
background: rgba(0, 0, 0, 0.56);
|
||||
background: var(--overlay-scrim);
|
||||
backdrop-filter: blur(10px);
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -43,9 +43,9 @@
|
||||
padding: 18px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 24px;
|
||||
background: color-mix(in srgb, var(--panel) 94%, #07111a);
|
||||
background: color-mix(in srgb, var(--panel) 96%, var(--bg));
|
||||
color: var(--text);
|
||||
box-shadow: 0 26px 70px rgba(0, 0, 0, 0.46);
|
||||
box-shadow: var(--shadow-strong);
|
||||
}
|
||||
|
||||
.dialog-head {
|
||||
@@ -92,7 +92,7 @@
|
||||
.payment-submit-button {
|
||||
border-color: color-mix(in srgb, var(--accent) 72%, transparent);
|
||||
background: linear-gradient(180deg, color-mix(in srgb, var(--accent) 94%, white), var(--accent));
|
||||
color: #031009;
|
||||
color: var(--accent-contrast);
|
||||
box-shadow:
|
||||
0 16px 38px color-mix(in srgb, var(--accent) 28%, transparent),
|
||||
0 0 0 1px color-mix(in srgb, var(--accent) 45%, transparent),
|
||||
|
||||
@@ -12,8 +12,8 @@ a {
|
||||
min-height: 100dvh;
|
||||
margin: 0 auto;
|
||||
overflow-x: hidden;
|
||||
padding:
|
||||
max(16px, env(safe-area-inset-top)) max(var(--screen-gutter), var(--safe-inline)) max(18px, env(safe-area-inset-bottom)) max(var(--screen-gutter), var(--safe-inline));
|
||||
padding: max(16px, env(safe-area-inset-top)) max(var(--screen-gutter), var(--safe-inline))
|
||||
max(18px, env(safe-area-inset-bottom)) max(var(--screen-gutter), var(--safe-inline));
|
||||
}
|
||||
|
||||
.content {
|
||||
@@ -94,7 +94,7 @@ a {
|
||||
color: var(--text);
|
||||
font-size: 15px;
|
||||
font-weight: 850;
|
||||
font-family: var(--font-mono);
|
||||
font-family: var(--font-logo);
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
@@ -162,20 +162,19 @@ a {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
background:
|
||||
linear-gradient(135deg, rgba(255, 255, 255, 0.055), rgba(255, 255, 255, 0.018)),
|
||||
var(--panel);
|
||||
linear-gradient(135deg, var(--surface-sheen), var(--surface-sheen-soft)), var(--panel);
|
||||
box-shadow:
|
||||
0 18px 48px rgba(0, 0, 0, 0.22),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.05);
|
||||
var(--shadow-soft),
|
||||
inset 0 1px 0 var(--inset-highlight);
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.card-active {
|
||||
border-color: color-mix(in srgb, var(--accent) 76%, var(--border));
|
||||
box-shadow:
|
||||
0 18px 48px rgba(0, 0, 0, 0.22),
|
||||
var(--shadow-soft),
|
||||
0 0 0 1px color-mix(in srgb, var(--accent) 38%, transparent),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.05);
|
||||
inset 0 1px 0 var(--inset-highlight);
|
||||
}
|
||||
|
||||
.card-compact {
|
||||
@@ -189,7 +188,11 @@ a {
|
||||
.status-card-inactive {
|
||||
border-color: color-mix(in srgb, var(--danger) 58%, var(--border));
|
||||
background:
|
||||
linear-gradient(135deg, color-mix(in srgb, var(--danger) 14%, rgba(255, 255, 255, 0.03)), rgba(255, 255, 255, 0.012)),
|
||||
linear-gradient(
|
||||
135deg,
|
||||
color-mix(in srgb, var(--danger) 14%, var(--surface-sheen-soft)),
|
||||
var(--surface-sheen-soft)
|
||||
),
|
||||
var(--panel);
|
||||
}
|
||||
|
||||
@@ -392,7 +395,7 @@ a {
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.premium-server-list>div {
|
||||
.premium-server-list > div {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
@@ -401,7 +404,7 @@ a {
|
||||
.premium-server-list span {
|
||||
max-width: 100%;
|
||||
padding: 4px 8px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid var(--surface-subtle-border);
|
||||
border-radius: 999px;
|
||||
color: var(--text);
|
||||
font-size: 11px;
|
||||
@@ -416,7 +419,7 @@ a {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.topup-summary-card>div:not(.premium-server-list) {
|
||||
.topup-summary-card > div:not(.premium-server-list) {
|
||||
display: grid;
|
||||
gap: 3px;
|
||||
}
|
||||
@@ -440,7 +443,7 @@ a {
|
||||
gap: 11px;
|
||||
}
|
||||
|
||||
.trial-card-head>svg {
|
||||
.trial-card-head > svg {
|
||||
flex: 0 0 auto;
|
||||
color: var(--accent);
|
||||
}
|
||||
@@ -475,7 +478,7 @@ a {
|
||||
gap: 11px;
|
||||
}
|
||||
|
||||
.devices-summary-head>svg {
|
||||
.devices-summary-head > svg {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
@@ -529,7 +532,7 @@ a {
|
||||
place-items: center;
|
||||
border: 1px solid color-mix(in srgb, var(--accent) 38%, var(--border));
|
||||
border-radius: var(--radius);
|
||||
background: color-mix(in srgb, var(--accent) 9%, rgba(255, 255, 255, 0.03));
|
||||
background: color-mix(in srgb, var(--accent) 9%, var(--surface-muted));
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
@@ -604,9 +607,9 @@ a {
|
||||
.settings-row {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
background: linear-gradient(135deg, rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0.018));
|
||||
background: linear-gradient(135deg, var(--surface-sheen), var(--surface-sheen-soft));
|
||||
color: var(--text);
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.045);
|
||||
box-shadow: inset 0 1px 0 var(--inset-highlight);
|
||||
}
|
||||
|
||||
.period-card {
|
||||
@@ -652,10 +655,10 @@ a {
|
||||
.period-card.active,
|
||||
.method-card.active {
|
||||
border-color: var(--accent);
|
||||
background: color-mix(in srgb, var(--accent) 9%, rgba(255, 255, 255, 0.035));
|
||||
background: color-mix(in srgb, var(--accent) 9%, var(--surface-muted));
|
||||
box-shadow:
|
||||
0 0 0 1px color-mix(in srgb, var(--accent) 50%, transparent),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.06);
|
||||
inset 0 1px 0 var(--inset-highlight);
|
||||
}
|
||||
|
||||
.total-card {
|
||||
@@ -688,11 +691,11 @@ a {
|
||||
gap: 12px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
background: linear-gradient(135deg, rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0.018));
|
||||
background: linear-gradient(135deg, var(--surface-sheen), var(--surface-sheen-soft));
|
||||
color: var(--text);
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.045);
|
||||
box-shadow: inset 0 1px 0 var(--inset-highlight);
|
||||
}
|
||||
|
||||
.option-row-main,
|
||||
@@ -745,10 +748,10 @@ a {
|
||||
|
||||
.option-row.active {
|
||||
border-color: var(--accent);
|
||||
background: color-mix(in srgb, var(--accent) 9%, rgba(255, 255, 255, 0.035));
|
||||
background: color-mix(in srgb, var(--accent) 9%, var(--surface-muted));
|
||||
box-shadow:
|
||||
0 0 0 1px color-mix(in srgb, var(--accent) 50%, transparent),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.06);
|
||||
inset 0 1px 0 var(--inset-highlight);
|
||||
}
|
||||
|
||||
.tariff-row {
|
||||
@@ -764,7 +767,7 @@ a {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.change-action-row>svg {
|
||||
.change-action-row > svg {
|
||||
flex: 0 0 auto;
|
||||
margin-top: 1px;
|
||||
color: var(--accent);
|
||||
@@ -774,9 +777,9 @@ a {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
background: rgba(255, 255, 255, 0.035);
|
||||
background: var(--surface-muted);
|
||||
}
|
||||
|
||||
.topup-carryover-note p {
|
||||
@@ -798,11 +801,12 @@ a {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
border-radius: 999px;
|
||||
background:
|
||||
linear-gradient(90deg,
|
||||
rgba(255, 255, 255, 0.07) 0%,
|
||||
rgba(255, 255, 255, 0.14) 42%,
|
||||
rgba(255, 255, 255, 0.07) 84%);
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
color-mix(in srgb, var(--muted) 10%, transparent) 0%,
|
||||
color-mix(in srgb, var(--muted) 18%, transparent) 42%,
|
||||
color-mix(in srgb, var(--muted) 10%, transparent) 84%
|
||||
);
|
||||
background-size: 220% 100%;
|
||||
animation: skeleton-shimmer 1.15s ease-in-out infinite;
|
||||
}
|
||||
@@ -893,7 +897,7 @@ a {
|
||||
|
||||
.tariff-selected-card {
|
||||
min-height: 58px;
|
||||
background: color-mix(in srgb, var(--accent) 7%, rgba(255, 255, 255, 0.035));
|
||||
background: color-mix(in srgb, var(--accent) 7%, var(--surface-muted));
|
||||
}
|
||||
|
||||
.tariff-action-list {
|
||||
@@ -909,7 +913,7 @@ a {
|
||||
gap: 10px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
background: linear-gradient(135deg, rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0.018));
|
||||
background: linear-gradient(135deg, var(--surface-sheen), var(--surface-sheen-soft));
|
||||
color: var(--text);
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
@@ -942,7 +946,7 @@ a {
|
||||
|
||||
.tariff-action-card.active {
|
||||
border-color: var(--accent);
|
||||
background: color-mix(in srgb, var(--accent) 9%, rgba(255, 255, 255, 0.035));
|
||||
background: color-mix(in srgb, var(--accent) 9%, var(--surface-muted));
|
||||
}
|
||||
|
||||
.tariff-warning-card {
|
||||
@@ -969,7 +973,7 @@ a {
|
||||
min-height: 56px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(255, 255, 255, 0.035);
|
||||
background: var(--surface-muted);
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
@@ -1105,7 +1109,7 @@ a {
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
background: color-mix(in srgb, var(--panel-2) 86%, transparent);
|
||||
color: var(--text);
|
||||
padding: 12px;
|
||||
font-size: 12px;
|
||||
@@ -1132,7 +1136,7 @@ a {
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.bonus-card-head>svg {
|
||||
.bonus-card-head > svg {
|
||||
flex: 0 0 auto;
|
||||
color: var(--accent);
|
||||
}
|
||||
@@ -1165,7 +1169,7 @@ a {
|
||||
gap: 3px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
background: var(--surface-sheen-soft);
|
||||
padding: 10px 11px;
|
||||
}
|
||||
|
||||
@@ -1193,7 +1197,7 @@ a {
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
background: var(--surface-muted);
|
||||
}
|
||||
|
||||
.settings-avatar img {
|
||||
@@ -1245,15 +1249,15 @@ a {
|
||||
min-height: 50px;
|
||||
padding: 9px 10px;
|
||||
text-align: left;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
background: var(--surface-muted);
|
||||
}
|
||||
|
||||
.settings-row>svg:first-child {
|
||||
.settings-row > svg:first-child {
|
||||
color: var(--text);
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.settings-row>svg:last-child {
|
||||
.settings-row > svg:last-child {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
@@ -1277,10 +1281,10 @@ a {
|
||||
.settings-row-linked {
|
||||
grid-template-columns: 28px minmax(0, 1fr);
|
||||
border-color: color-mix(in srgb, var(--accent) 38%, var(--border));
|
||||
background: color-mix(in srgb, var(--accent) 11%, rgba(255, 255, 255, 0.03));
|
||||
background: color-mix(in srgb, var(--accent) 11%, var(--surface-muted));
|
||||
}
|
||||
|
||||
.settings-row-linked>svg:first-child {
|
||||
.settings-row-linked > svg:first-child {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
@@ -1290,12 +1294,8 @@ a {
|
||||
|
||||
.emoji-flag {
|
||||
font-family:
|
||||
"Twemoji Country Flags",
|
||||
"Apple Color Emoji",
|
||||
"Segoe UI Emoji",
|
||||
"Segoe UI Symbol",
|
||||
"Noto Color Emoji",
|
||||
sans-serif;
|
||||
"Twemoji Country Flags", "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol",
|
||||
"Noto Color Emoji", sans-serif;
|
||||
font-variant-emoji: emoji;
|
||||
line-height: 1;
|
||||
}
|
||||
@@ -1325,7 +1325,7 @@ a {
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.language-select-trigger>svg {
|
||||
.language-select-trigger > svg {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
@@ -1361,7 +1361,7 @@ a {
|
||||
border: 1px solid var(--border-strong);
|
||||
border-radius: var(--radius);
|
||||
background: var(--panel-3);
|
||||
box-shadow: 0 20px 34px rgba(0, 0, 0, 0.42);
|
||||
box-shadow: var(--shadow-popover);
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
transform-origin: top right;
|
||||
@@ -1387,7 +1387,7 @@ a {
|
||||
}
|
||||
|
||||
.language-select-item[data-highlighted] {
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
background: var(--surface-hover);
|
||||
}
|
||||
|
||||
.language-select-item[data-selected] {
|
||||
@@ -1409,11 +1409,11 @@ a {
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.language-select-item-main>span {
|
||||
.language-select-item-main > span {
|
||||
display: inline-block !important;
|
||||
}
|
||||
|
||||
.language-select-item-main>span:last-child {
|
||||
.language-select-item-main > span:last-child {
|
||||
margin-left: 6px;
|
||||
white-space: nowrap !important;
|
||||
overflow: hidden;
|
||||
@@ -1494,8 +1494,8 @@ a {
|
||||
min-height: 64px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
background: rgba(7, 12, 17, 0.88);
|
||||
box-shadow: 0 16px 38px rgba(0, 0, 0, 0.36);
|
||||
background: var(--nav-bg);
|
||||
box-shadow: var(--shadow-soft);
|
||||
backdrop-filter: blur(16px);
|
||||
}
|
||||
|
||||
@@ -1580,7 +1580,7 @@ a {
|
||||
color: var(--accent);
|
||||
font-size: 32px;
|
||||
font-weight: 900;
|
||||
font-family: var(--font-mono);
|
||||
font-family: var(--font-logo);
|
||||
line-height: 1.04;
|
||||
}
|
||||
|
||||
@@ -1739,13 +1739,13 @@ a {
|
||||
place-items: center;
|
||||
border: 1px solid color-mix(in srgb, var(--accent) 74%, var(--border));
|
||||
border-radius: var(--radius);
|
||||
background: rgba(255, 255, 255, 0.035);
|
||||
background: var(--surface-muted);
|
||||
font-size: 25px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.otp-slots span.filled {
|
||||
background: color-mix(in srgb, var(--accent) 9%, rgba(255, 255, 255, 0.04));
|
||||
background: color-mix(in srgb, var(--accent) 9%, var(--surface-muted));
|
||||
}
|
||||
|
||||
.link-button {
|
||||
@@ -1790,8 +1790,8 @@ a {
|
||||
max-width: min(420px, calc(100vw - 32px));
|
||||
border: 1px solid color-mix(in srgb, var(--accent) 46%, var(--border));
|
||||
border-radius: var(--radius);
|
||||
background: rgba(15, 21, 27, 0.92);
|
||||
box-shadow: 0 16px 42px rgba(0, 0, 0, 0.42);
|
||||
background: color-mix(in srgb, var(--panel) 92%, transparent);
|
||||
box-shadow: var(--shadow-popover);
|
||||
color: var(--text);
|
||||
padding: 12px 14px;
|
||||
font-size: 13px;
|
||||
@@ -1873,8 +1873,7 @@ a {
|
||||
border: 1px solid rgba(255, 255, 255, 0.22);
|
||||
border-radius: 18px;
|
||||
background:
|
||||
linear-gradient(135deg, rgba(255, 255, 255, 0.075), rgba(255, 255, 255, 0.015)),
|
||||
#071017;
|
||||
linear-gradient(135deg, rgba(255, 255, 255, 0.075), rgba(255, 255, 255, 0.015)), #071017;
|
||||
box-shadow:
|
||||
0 28px 80px rgba(0, 0, 0, 0.54),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.08);
|
||||
@@ -2011,7 +2010,6 @@ a {
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
|
||||
.content,
|
||||
.home-layout,
|
||||
.language-select-content,
|
||||
@@ -2039,7 +2037,7 @@ a {
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
body {
|
||||
background: #02070b;
|
||||
background: var(--bg);
|
||||
}
|
||||
|
||||
.app-shell {
|
||||
@@ -2060,16 +2058,16 @@ a {
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
padding:
|
||||
max(28px, env(safe-area-inset-top)) var(--desktop-page-gutter) 40px calc(var(--desktop-rail-width) + var(--desktop-page-gutter));
|
||||
padding: max(28px, env(safe-area-inset-top)) var(--desktop-page-gutter) 40px
|
||||
calc(var(--desktop-rail-width) + var(--desktop-page-gutter));
|
||||
overflow-x: visible;
|
||||
}
|
||||
|
||||
/* Centre and cap the width of the actual content blocks. */
|
||||
.phone-screen>.app-header,
|
||||
.phone-screen>main,
|
||||
.phone-screen>.home-layout,
|
||||
.phone-screen>nav.bottom-nav~* {
|
||||
.phone-screen > .app-header,
|
||||
.phone-screen > main,
|
||||
.phone-screen > .home-layout,
|
||||
.phone-screen > nav.bottom-nav ~ * {
|
||||
max-width: 1080px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
@@ -2079,12 +2077,11 @@ a {
|
||||
.phone-screen.auth-screen {
|
||||
width: min(100%, 460px);
|
||||
min-height: 100dvh;
|
||||
padding:
|
||||
max(16px, env(safe-area-inset-top)) clamp(24px, 4vw, 16px) 16px;
|
||||
padding: max(16px, env(safe-area-inset-top)) clamp(24px, 4vw, 16px) 16px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.phone-screen.auth-screen~.bottom-nav,
|
||||
.phone-screen.auth-screen ~ .bottom-nav,
|
||||
.auth-screen .bottom-nav {
|
||||
display: none !important;
|
||||
}
|
||||
@@ -2094,8 +2091,8 @@ a {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
align-items: stretch;
|
||||
min-height: 100dvh;
|
||||
padding:
|
||||
max(32px, env(safe-area-inset-top)) var(--desktop-page-gutter) 42px calc(var(--desktop-rail-width) + var(--desktop-page-gutter));
|
||||
padding: max(32px, env(safe-area-inset-top)) var(--desktop-page-gutter) 42px
|
||||
calc(var(--desktop-rail-width) + var(--desktop-page-gutter));
|
||||
}
|
||||
|
||||
/* Home: keep the phone-style vertical flow, but center it in the
|
||||
@@ -2113,7 +2110,7 @@ a {
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.home-layout>.home-brand {
|
||||
.home-layout > .home-brand {
|
||||
grid-column: auto;
|
||||
align-self: center;
|
||||
justify-items: center;
|
||||
@@ -2174,9 +2171,9 @@ a {
|
||||
border: 0 !important;
|
||||
border-right: 1px solid var(--border) !important;
|
||||
border-radius: 0 !important;
|
||||
background: rgba(7, 12, 17, 0.55) !important;
|
||||
background: var(--rail-bg) !important;
|
||||
backdrop-filter: blur(14px) !important;
|
||||
box-shadow: inset -1px 0 0 rgba(255, 255, 255, 0.02) !important;
|
||||
box-shadow: inset -1px 0 0 var(--border) !important;
|
||||
z-index: 40;
|
||||
}
|
||||
|
||||
@@ -2197,22 +2194,25 @@ a {
|
||||
font-size: 13px !important;
|
||||
color: var(--muted);
|
||||
border: 1px solid transparent;
|
||||
transition: background 0.12s ease, color 0.12s ease, border-color 0.12s ease;
|
||||
transition:
|
||||
background 0.12s ease,
|
||||
color 0.12s ease,
|
||||
border-color 0.12s ease;
|
||||
}
|
||||
|
||||
.bottom-nav button>svg {
|
||||
.bottom-nav button > svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.bottom-nav button>span {
|
||||
.bottom-nav button > span {
|
||||
text-align: left !important;
|
||||
font-size: 13px !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.bottom-nav button:hover {
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
background: var(--surface-hover);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
@@ -2235,7 +2235,7 @@ a {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.phone-screen>main.content.with-nav {
|
||||
.phone-screen > main.content.with-nav {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
@@ -2310,7 +2310,8 @@ a {
|
||||
color: var(--accent);
|
||||
font-size: 14px;
|
||||
font-weight: 800;
|
||||
letter-spacing: -0.01em;
|
||||
font-family: var(--font-logo);
|
||||
letter-spacing: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
Reference in New Issue
Block a user