From a1725a68729c5cdc73f282b56a8be2106372b1e3 Mon Sep 17 00:00:00 2001 From: 3252a8 <3252a8@proton.me> Date: Fri, 15 May 2026 13:54:47 +0300 Subject: [PATCH] feat: custom themes logo size tuning --- README.md | 2 + .../admin/sections/AppearanceSection.svelte | 66 +++++++++++++++++++ .../src/lib/admin/stores/themesStore.js | 26 ++++++++ .../web/frontend/src/lib/webapp/themeStyle.js | 7 ++ bot/app/web/frontend/src/styles/webapp.css | 38 +++++++---- .../web/templates/subscription_webapp.html | 2 +- bot/app/web/themes/windows95/style.css | 11 +++- bot/app/web/webapp/assets.py | 6 +- config/webapp_themes_config.py | 11 ++++ tests/test_webapp_themes_config.py | 26 +++++++- 10 files changed, 175 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index d619f58..a23d9c4 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # Remnawave Minishop +![Remnawave Minishop](docs/remnawave-minishop.webp) + Remnawave Minishop - Telegram-бот и Web App (Mini App) для продажи и управления подписками Remnawave. Бот обрабатывает регистрацию, оплату, продление, пробный период, промокоды, рефералов и поддержку в чате. Web App показывает ссылку подключения, срок действия, трафик, оплату, устройства и вход по Telegram Mini Apps `initData`, Telegram OAuth / OpenID Connect и одноразовому email-коду. Проект является переработанным форком [kavore/remnawave-tg-shop](https://github.com/kavore/remnawave-tg-shop). Для переноса данных из прежнего стека используйте [инструкцию по миграции](docs/migration-to-minishop.md). diff --git a/bot/app/web/frontend/src/admin/sections/AppearanceSection.svelte b/bot/app/web/frontend/src/admin/sections/AppearanceSection.svelte index 758592e..b5b7dab 100644 --- a/bot/app/web/frontend/src/admin/sections/AppearanceSection.svelte +++ b/bot/app/web/frontend/src/admin/sections/AppearanceSection.svelte @@ -134,6 +134,12 @@ return `#${hex}`; } + function homeLogoScale(theme) { + const scale = Number(theme.tokens?.home_logo_scale || 100); + if (!Number.isFinite(scale)) return 100; + return Math.min(300, Math.max(50, Math.round(scale))); + } + function openThemeAccentPicker(theme) { themesStore.setThemeAccent(theme.key, pickerHex(theme.tokens?.accent || "#00fe7a")); } @@ -216,6 +222,10 @@ themesStore.setThemeAccent(theme.key, value); } + function setThemeHomeLogoScale(theme, value) { + themesStore.setThemeHomeLogoScale(theme.key, value); + } + function selectTheme(theme, event = null) { if (event?.target?.closest?.("button,input,label")) return; if (!themesSaving) themesStore.setCurrentTheme(theme.key); @@ -472,6 +482,37 @@ /> {at("themes_use_in_admin", {}, "Использовать в админке")} +
({ + ...s, + themesCatalog: { + ...s.themesCatalog, + themes: (s.themesCatalog.themes || []).map((theme) => + theme.key === key + ? { + ...theme, + tokens: { + ...(theme.tokens || {}), + home_logo_scale: nextScale === 100 ? null : nextScale, + }, + } + : theme + ), + }, + })); + } + return { subscribe: state.subscribe, loadThemes, saveThemes, setCurrentTheme, setThemeAccent, + setThemeHomeLogoScale, togglePrimaryAccent, toggleAdminUse, uploadLogoFile, diff --git a/bot/app/web/frontend/src/lib/webapp/themeStyle.js b/bot/app/web/frontend/src/lib/webapp/themeStyle.js index e312c37..c415cd0 100644 --- a/bot/app/web/frontend/src/lib/webapp/themeStyle.js +++ b/bot/app/web/frontend/src/lib/webapp/themeStyle.js @@ -17,6 +17,7 @@ const TOKEN_TO_CSS_VAR = { font_sans: "--font-sans", font_logo: "--font-logo", font_mono: "--font-mono", + home_logo_scale: "--home-logo-scale", admin_bg: "--admin-bg", admin_surface: "--admin-surface", admin_surface_2: "--admin-surface-2", @@ -38,6 +39,12 @@ export function themeTokensToInlineStyle(tokens, primaryFallback = "#00fe7a", op if (key === "accent") continue; const value = t[key]; if (value === undefined || value === null || value === "") continue; + if (key === "home_logo_scale") { + const scale = Number(value); + if (!Number.isFinite(scale) || scale <= 0) continue; + parts.push(`${cssVar}:${scale / 100}`); + continue; + } parts.push(`${cssVar}:${String(value)}`); } return parts.join(";"); diff --git a/bot/app/web/frontend/src/styles/webapp.css b/bot/app/web/frontend/src/styles/webapp.css index 18c1416..9b7d6dd 100644 --- a/bot/app/web/frontend/src/styles/webapp.css +++ b/bot/app/web/frontend/src/styles/webapp.css @@ -30,12 +30,18 @@ a { display: grid; min-height: 100dvh; place-items: center; - align-content: center; + align-content: safe center; gap: 14px; color: var(--muted); font-weight: 800; } +.loader .brand-mark.brand-mark-lg { + width: calc(4.125rem * var(--home-logo-scale, 1)); + height: calc(4.125rem * var(--home-logo-scale, 1)); + font-size: calc(2.875rem * var(--home-logo-scale, 1)); +} + .app-header, .preview-header, .screen-head { @@ -1530,17 +1536,29 @@ a { .home-layout { display: grid; min-height: calc(100dvh - 34px); - grid-template-rows: minmax(0, 1fr) auto; + grid-template-rows: minmax(min-content, 1fr) auto; gap: 18px; padding-bottom: 86px; animation: section-enter 0.24s ease-out both; } .home-brand { - align-self: center; + align-self: safe center; padding-top: 0; } +.home-brand .brand-mark.brand-mark-xl { + width: calc(6rem * var(--home-logo-scale, 1)); + height: calc(6rem * var(--home-logo-scale, 1)); + font-size: calc(4.375rem * var(--home-logo-scale, 1)); +} + +.login-brand-auth .brand-mark.brand-mark-xl { + width: calc(116px * var(--home-logo-scale, 1)); + height: calc(116px * var(--home-logo-scale, 1)); + font-size: calc(84px * var(--home-logo-scale, 1)); +} + .home-bottom { display: grid; gap: 10px; @@ -1549,7 +1567,7 @@ a { .auth-screen { display: grid; - align-content: center; + align-content: safe center; } .auth-card-wrap { @@ -1568,12 +1586,6 @@ a { gap: 5px; } -.login-brand-auth .brand-mark.brand-mark-xl { - width: 116px; - height: 116px; - font-size: 84px; -} - .login-brand h1 { margin: 0; overflow-wrap: anywhere; @@ -2129,9 +2141,9 @@ a { .home-brand .brand-mark, .home-brand .brand-mark-lg, .home-brand .brand-mark-xl { - width: clamp(180px, 18vw, 260px); - height: clamp(180px, 18vw, 260px); - font-size: clamp(124px, 13vw, 184px); + width: calc(clamp(180px, 18vw, 260px) * var(--home-logo-scale, 1)); + height: calc(clamp(180px, 18vw, 260px) * var(--home-logo-scale, 1)); + font-size: calc(clamp(124px, 13vw, 184px) * var(--home-logo-scale, 1)); } .home-bottom { diff --git a/bot/app/web/templates/subscription_webapp.html b/bot/app/web/templates/subscription_webapp.html index cf213f6..492c5c8 100644 --- a/bot/app/web/templates/subscription_webapp.html +++ b/bot/app/web/templates/subscription_webapp.html @@ -9,7 +9,7 @@ /minishop - + diff --git a/bot/app/web/themes/windows95/style.css b/bot/app/web/themes/windows95/style.css index 68a5709..bdbdccc 100644 --- a/bot/app/web/themes/windows95/style.css +++ b/bot/app/web/themes/windows95/style.css @@ -1,3 +1,5 @@ +@import url("https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap&subset=cyrillic"); + .theme-key-windows95 { color-scheme: light; image-rendering: pixelated; @@ -15,7 +17,7 @@ --blue: #000080; --radius: 0px; --font-sans: "MS Sans Serif", Tahoma, "Segoe UI", sans-serif; - --font-logo: "MS Sans Serif", Tahoma, "Segoe UI", sans-serif; + --font-logo: "Press Start 2P", "Pixel Cyr", "MS Sans Serif", Tahoma, "Segoe UI", sans-serif; --font-mono: Consolas, "Courier New", monospace; --accent-contrast: #ffffff; --surface-sheen: transparent; @@ -89,6 +91,8 @@ .theme-key-windows95 svg.lucide-bitcoin, .theme-key-windows95 svg.lucide-check, .theme-key-windows95 svg.lucide-check-circle-2, +.theme-key-windows95 svg.lucide-circle-check, +.theme-key-windows95 svg.lucide-circle-check-big, .theme-key-windows95 svg.lucide-chevrons-up-down, .theme-key-windows95 svg.lucide-circle-x, .theme-key-windows95 svg.lucide-coins, @@ -142,6 +146,7 @@ background-repeat: no-repeat; background-size: contain; image-rendering: pixelated; + filter: none !important; } .theme-key-windows95 svg.lucide-arrow-left { @@ -547,6 +552,10 @@ color: var(--accent); } +.theme-key-windows95 .traffic-top strong { + color: var(--muted); +} + .theme-key-windows95 .btn-primary, .theme-key-windows95 .bottom-nav button.active { color: #ffffff; diff --git a/bot/app/web/webapp/assets.py b/bot/app/web/webapp/assets.py index 198ba08..c0916fd 100644 --- a/bot/app/web/webapp/assets.py +++ b/bot/app/web/webapp/assets.py @@ -857,12 +857,12 @@ async def index_route(request: web.Request) -> web.Response: brand_asset_url = _webapp_animated_emoji_asset_path(settings.WEBAPP_LOGO_EMOJI) if brand_asset_url: html = html.replace( - '', # noqa: E501 - f'', # noqa: E501 + '', + f'', ) else: html = html.replace( - '', # noqa: E501 + '', "", ) return web.Response(text=html, content_type="text/html", charset="utf-8") diff --git a/config/webapp_themes_config.py b/config/webapp_themes_config.py index 4f713bc..495cdeb 100644 --- a/config/webapp_themes_config.py +++ b/config/webapp_themes_config.py @@ -38,6 +38,7 @@ class ThemeTokens(BaseModel): font_sans: Optional[str] = None font_logo: Optional[str] = None font_mono: Optional[str] = None + home_logo_scale: Optional[int] = None admin_bg: Optional[str] = None admin_surface: Optional[str] = None admin_surface_2: Optional[str] = None @@ -64,6 +65,16 @@ class ThemeTokens(BaseModel): hex_value = "".join(char * 2 for char in hex_value) return f"#{hex_value}" + @field_validator("home_logo_scale") + @classmethod + def _normalize_home_logo_scale(cls, value: Optional[int]) -> Optional[int]: + if value is None: + return None + scale = int(value) + if scale < 50 or scale > 300: + raise ValueError("home_logo_scale must be between 50 and 300 percent") + return scale + class WebappTheme(BaseModel): """Single theme descriptor loaded from WEBAPP_THEMES_DIR//theme.json.""" diff --git a/tests/test_webapp_themes_config.py b/tests/test_webapp_themes_config.py index f8a9d43..1cb81fe 100644 --- a/tests/test_webapp_themes_config.py +++ b/tests/test_webapp_themes_config.py @@ -33,7 +33,7 @@ class WebappThemesConfigTests(unittest.TestCase): self.assertEqual(win95.tokens.style_preset, "win95") self.assertFalse(win95.use_primary_accent) self.assertTrue(win95.use_in_admin) - self.assertEqual(win95.assets_version, 1) + self.assertEqual(win95.assets_version, 3) ascii_theme = cfg.theme_by_key("ascii") self.assertIsNotNone(ascii_theme) self.assertEqual(ascii_theme.css_file, "style.css") @@ -294,6 +294,25 @@ class WebappThemesConfigTests(unittest.TestCase): self.assertEqual(cfg.theme_by_key("custom").tokens.accent, "#00ff88") + def test_theme_home_logo_scale_is_public_token(self): + cfg = WebappThemesConfig( + default_theme="custom", + themes=[ + { + "key": "custom", + "enabled": True, + "default": True, + "tokens": {"color_scheme": "dark", "home_logo_scale": 135}, + } + ], + ) + + payload = public_themes_catalog_payload(cfg, "#abc123") + custom = payload["themes"][0] + + self.assertEqual(cfg.theme_by_key("custom").tokens.home_logo_scale, 135) + self.assertEqual(custom["tokens"]["home_logo_scale"], 135) + def test_theme_accent_rejects_non_hex_values(self): with self.assertRaises(ValueError): WebappThemesConfig( @@ -361,11 +380,14 @@ class WebappThemesConfigTests(unittest.TestCase): descriptor["assets_version"], cfg.theme_by_key("windows95").assets_version, ) - self.assertEqual(descriptor["assets_version"], 1) + self.assertEqual(descriptor["assets_version"], 3) self.assertIn("lucide-house", css) self.assertIn("lucide-earth", css) self.assertIn("lucide-circle-check", css) self.assertIn("lucide-circle-check-big", css) + self.assertIn("filter: none !important", css) + self.assertIn("Press Start 2P", css) + self.assertIn(".theme-key-windows95 .traffic-top strong", css) self.assertTrue((stale_theme_dir / "icons" / "dashboard.png").exists()) def test_resolved_refreshes_stale_builtin_light_assets(self):