From d8fe88eb0111238985ed778c862c0f341034dcb4 Mon Sep 17 00:00:00 2001
From: 3252a8 <3252a8@proton.me>
Date: Wed, 3 Jun 2026 12:24:18 +0300
Subject: [PATCH] feat: split logo scale by viewport
---
backend/bot/app/web/webapp/assets.py | 10 +-
backend/config/webapp_themes_config.py | 6 +-
docs/features/webapp-themes.md | 91 ++++++++++---------
.../admin/sections/AppearanceSection.svelte | 81 ++++++++++++++---
frontend/src/docsDemoEntry.js | 10 +-
frontend/src/lib/admin/stores/themesStore.js | 54 +++++++----
frontend/src/lib/webapp/demoDataset.js | 48 ++++++++++
frontend/src/lib/webapp/themeStyle.js | 10 +-
frontend/src/styles/base.css | 7 ++
frontend/src/styles/webapp.css | 24 ++---
locales/en.json | 2 +
locales/ru.json | 2 +
tests/test_webapp_assets.py | 8 ++
tests/test_webapp_themes_config.py | 13 ++-
14 files changed, 269 insertions(+), 97 deletions(-)
diff --git a/backend/bot/app/web/webapp/assets.py b/backend/bot/app/web/webapp/assets.py
index 3206b03..f02a871 100644
--- a/backend/bot/app/web/webapp/assets.py
+++ b/backend/bot/app/web/webapp/assets.py
@@ -1517,6 +1517,8 @@ _INITIAL_THEME_TOKEN_CSS_MAP = {
"font_logo": "--font-logo",
"font_mono": "--font-mono",
"home_logo_scale": "--home-logo-scale",
+ "home_logo_scale_desktop": "--home-logo-scale-desktop",
+ "home_logo_scale_mobile": "--home-logo-scale-mobile",
"admin_bg": "--admin-bg",
"admin_surface": "--admin-surface",
"admin_surface_2": "--admin-surface-2",
@@ -1528,6 +1530,12 @@ _INITIAL_THEME_TOKEN_CSS_MAP = {
"admin_dim": "--admin-dim",
}
+_INITIAL_THEME_LOGO_SCALE_TOKENS = {
+ "home_logo_scale",
+ "home_logo_scale_desktop",
+ "home_logo_scale_mobile",
+}
+
def _theme_css_href_for_html(theme: Any) -> str:
css_file = str(getattr(theme, "css_file", "") or "").strip()
@@ -1571,7 +1579,7 @@ def _initial_theme_head_markup(request: web.Request, theme: Any, primary_color:
tokens = tokens if isinstance(tokens, dict) else {}
declarations = []
for token_key, css_name in _INITIAL_THEME_TOKEN_CSS_MAP.items():
- if token_key == "home_logo_scale":
+ if token_key in _INITIAL_THEME_LOGO_SCALE_TOKENS:
try:
scale = float(tokens.get(token_key) or 0)
except (TypeError, ValueError):
diff --git a/backend/config/webapp_themes_config.py b/backend/config/webapp_themes_config.py
index 243697b..d3e527c 100644
--- a/backend/config/webapp_themes_config.py
+++ b/backend/config/webapp_themes_config.py
@@ -54,6 +54,8 @@ class ThemeTokens(BaseModel):
font_logo: Optional[str] = None
font_mono: Optional[str] = None
home_logo_scale: Optional[int] = None
+ home_logo_scale_desktop: Optional[int] = None
+ home_logo_scale_mobile: Optional[int] = None
admin_bg: Optional[str] = None
admin_surface: Optional[str] = None
admin_surface_2: Optional[str] = None
@@ -80,14 +82,14 @@ class ThemeTokens(BaseModel):
hex_value = "".join(char * 2 for char in hex_value)
return f"#{hex_value}"
- @field_validator("home_logo_scale")
+ @field_validator("home_logo_scale", "home_logo_scale_desktop", "home_logo_scale_mobile")
@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")
+ raise ValueError("home logo scale must be between 50 and 300 percent")
return scale
diff --git a/docs/features/webapp-themes.md b/docs/features/webapp-themes.md
index bb6f102..7357d65 100644
--- a/docs/features/webapp-themes.md
+++ b/docs/features/webapp-themes.md
@@ -115,7 +115,8 @@ Email-шаблоны берут тот же бренд из настроек в
"font_sans": "Inter, system-ui, sans-serif",
"font_logo": "Inter, system-ui, sans-serif",
"font_mono": "\"JetBrains Mono\", \"Fira Code\", monospace",
- "home_logo_scale": 120,
+ "home_logo_scale_desktop": 120,
+ "home_logo_scale_mobile": 95,
"admin_bg": "#05040a",
"admin_surface": "#11101c",
"admin_surface_2": "#090815",
@@ -131,52 +132,54 @@ Email-шаблоны берут тот же бренд из настроек в
Поля верхнего уровня:
-| Поле | Назначение |
-| --- | --- |
-| `key` | Уникальный ключ темы, 1-64 символа: латиница, цифры, `_` и `-`. Если ключ не указан, берется имя папки. |
-| `names` | Локализованные названия, например `ru` и `en`. |
-| `enabled` | Показывать тему пользователям. Отключенная тема не попадает в публичный каталог. |
-| `default` | Делает тему выбранной по умолчанию, если `WEBAPP_DEFAULT_THEME` не задан. |
+| Поле | Назначение |
+| -------------------- | ------------------------------------------------------------------------------------------------------- |
+| `key` | Уникальный ключ темы, 1-64 символа: латиница, цифры, `_` и `-`. Если ключ не указан, берется имя папки. |
+| `names` | Локализованные названия, например `ru` и `en`. |
+| `enabled` | Показывать тему пользователям. Отключенная тема не попадает в публичный каталог. |
+| `default` | Делает тему выбранной по умолчанию, если `WEBAPP_DEFAULT_THEME` не задан. |
| `use_primary_accent` | Если `true`, тема может получить accent из настройки внешнего вида, когда в `tokens.accent` ничего нет. |
-| `use_in_admin` | Если `false`, пользовательская часть использует тему, но админка откатывается на `dark`. |
-| `css_file` | CSS-файл внутри папки темы. Может быть `style.css` или вложенный путь вроде `css/theme.css`. |
-| `assets_version` | Версия ассетов. Для встроенных тем используется для обновления старых файлов в `data/themes`. |
-| `tokens` | Дизайн-токены, которые превращаются в CSS-переменные на `.app-shell`. |
+| `use_in_admin` | Если `false`, пользовательская часть использует тему, но админка откатывается на `dark`. |
+| `css_file` | CSS-файл внутри папки темы. Может быть `style.css` или вложенный путь вроде `css/theme.css`. |
+| `assets_version` | Версия ассетов. Для встроенных тем используется для обновления старых файлов в `data/themes`. |
+| `tokens` | Дизайн-токены, которые превращаются в CSS-переменные на `.app-shell`. |
## Токены
Поддерживаемые токены:
-| Токен | CSS-переменная | Что меняет |
-| --- | --- | --- |
-| `color_scheme` | `color-scheme` | Нативная светлая/темная схема браузера: `dark` или `light`. |
-| `style_preset` | CSS-класс пресета | Сейчас `win95`/`windows95` добавляет `theme-preset-win95`; остальные значения не дают специального класса. |
-| `accent` | `--accent` | Главный акцент: активные элементы, кнопки, прогресс, фокус. Только hex `#RGB` или `#RRGGBB`. |
-| `bg` | `--bg` | Основной фон приложения. |
-| `panel` | `--panel` | Основные карточки и поверхности. |
-| `panel_2` | `--panel-2` | Вторичные поверхности. |
-| `panel_3` | `--panel-3` | Поверхности повышенной вложенности, dropdown/popover. |
-| `border` | `--border` | Обычные границы. |
-| `border_strong` | `--border-strong` | Усиленные границы и hover-состояния. |
-| `text` | `--text` | Основной текст. |
-| `muted` | `--muted` | Вторичный текст. |
-| `dim` | `--dim` | Еще более тихий текст и служебные подписи. |
-| `danger` | `--danger` | Ошибки и опасные действия. |
-| `blue` | `--blue` | Синий вспомогательный цвет. |
-| `radius` | `--radius` | Базовый радиус карточек, кнопок и контролов. |
-| `font_sans` | `--font-sans` | Основной шрифт интерфейса. |
-| `font_logo` | `--font-logo` | Шрифт бренда и заголовка. |
-| `font_mono` | `--font-mono` | Моноширинный шрифт. |
-| `home_logo_scale` | `--home-logo-scale` | Масштаб логотипа на главной и входе, от `50` до `300` процентов. |
-| `admin_bg` | `--admin-bg` | Фон админ-панели. |
-| `admin_surface` | `--admin-surface` | Основные карточки админки. |
-| `admin_surface_2` | `--admin-surface-2` | Вторичные поверхности админки. |
-| `admin_elev` | `--admin-elev` | Elevated-поверхности админки. |
-| `admin_border` | `--admin-border` | Границы админки. |
-| `admin_border_strong` | `--admin-border-strong` | Усиленные границы админки. |
-| `admin_text` | `--admin-text` | Основной текст админки. |
-| `admin_muted` | `--admin-muted` | Вторичный текст админки. |
-| `admin_dim` | `--admin-dim` | Тихие подписи админки. |
+| Токен | CSS-переменная | Что меняет |
+| ------------------------- | --------------------------- | ---------------------------------------------------------------------------------------------------------- |
+| `color_scheme` | `color-scheme` | Нативная светлая/темная схема браузера: `dark` или `light`. |
+| `style_preset` | CSS-класс пресета | Сейчас `win95`/`windows95` добавляет `theme-preset-win95`; остальные значения не дают специального класса. |
+| `accent` | `--accent` | Главный акцент: активные элементы, кнопки, прогресс, фокус. Только hex `#RGB` или `#RRGGBB`. |
+| `bg` | `--bg` | Основной фон приложения. |
+| `panel` | `--panel` | Основные карточки и поверхности. |
+| `panel_2` | `--panel-2` | Вторичные поверхности. |
+| `panel_3` | `--panel-3` | Поверхности повышенной вложенности, dropdown/popover. |
+| `border` | `--border` | Обычные границы. |
+| `border_strong` | `--border-strong` | Усиленные границы и hover-состояния. |
+| `text` | `--text` | Основной текст. |
+| `muted` | `--muted` | Вторичный текст. |
+| `dim` | `--dim` | Еще более тихий текст и служебные подписи. |
+| `danger` | `--danger` | Ошибки и опасные действия. |
+| `blue` | `--blue` | Синий вспомогательный цвет. |
+| `radius` | `--radius` | Базовый радиус карточек, кнопок и контролов. |
+| `font_sans` | `--font-sans` | Основной шрифт интерфейса. |
+| `font_logo` | `--font-logo` | Шрифт бренда и заголовка. |
+| `font_mono` | `--font-mono` | Моноширинный шрифт. |
+| `home_logo_scale_desktop` | `--home-logo-scale-desktop` | Масштаб логотипа на desktop layout, от `50` до `300` процентов. |
+| `home_logo_scale_mobile` | `--home-logo-scale-mobile` | Масштаб логотипа на mobile layout, от `50` до `300` процентов. |
+| `home_logo_scale` | `--home-logo-scale` | Legacy fallback для старых тем; используется, если desktop/mobile token не задан. |
+| `admin_bg` | `--admin-bg` | Фон админ-панели. |
+| `admin_surface` | `--admin-surface` | Основные карточки админки. |
+| `admin_surface_2` | `--admin-surface-2` | Вторичные поверхности админки. |
+| `admin_elev` | `--admin-elev` | Elevated-поверхности админки. |
+| `admin_border` | `--admin-border` | Границы админки. |
+| `admin_border_strong` | `--admin-border-strong` | Усиленные границы админки. |
+| `admin_text` | `--admin-text` | Основной текст админки. |
+| `admin_muted` | `--admin-muted` | Вторичный текст админки. |
+| `admin_dim` | `--admin-dim` | Тихие подписи админки. |
Если `css_file` не задан, интерфейс полностью строится на токенах и общих стилях. Если `css_file` задан, токены все равно применяются первыми, а CSS темы может уточнить или полностью переопределить внешний вид.
@@ -255,7 +258,8 @@ CSS можно писать для пользовательской части
content: "";
width: 16px;
height: 16px;
- background: url("/webapp-theme-assets/neon/icons/spark.png") center / contain no-repeat;
+ background: url("/webapp-theme-assets/neon/icons/spark.png") center / contain
+ no-repeat;
}
```
@@ -303,7 +307,7 @@ CSS можно писать для пользовательской части
7. Подберите accent и масштаб логотипа.
- В админке можно менять accent и `home_logo_scale` без ручного редактирования JSON. При сохранении backend перепишет `theme.json` в `WEBAPP_THEMES_DIR`, выставит ровно один `default` и сбросит кеш публичных настроек.
+ В админке можно менять accent, `home_logo_scale_desktop` и `home_logo_scale_mobile` без ручного редактирования JSON. При сохранении backend перепишет `theme.json` в `WEBAPP_THEMES_DIR`, выставит ровно один `default` и сбросит кеш публичных настроек. Старый `home_logo_scale` сохраняется как fallback для уже существующих тем.
8. Добавьте `style.css`, если токенов мало.
@@ -330,7 +334,6 @@ CSS можно писать для пользовательской части
11. Сделайте тему дефолтной.
Есть два способа:
-
- в админке выбрать тему и сохранить;
- указать `WEBAPP_DEFAULT_THEME=neon` в `.env`, если нужен жесткий override на уровне окружения.
diff --git a/frontend/src/admin/sections/AppearanceSection.svelte b/frontend/src/admin/sections/AppearanceSection.svelte
index 968c41c..26ad2a2 100644
--- a/frontend/src/admin/sections/AppearanceSection.svelte
+++ b/frontend/src/admin/sections/AppearanceSection.svelte
@@ -178,10 +178,8 @@
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 homeLogoScale(theme, mode) {
+ return themesStore.resolveThemeHomeLogoScale(theme, mode);
}
function openThemeAccentPicker(theme) {
@@ -296,8 +294,8 @@
themesStore.setThemeAccent(theme.key, value);
}
- function setThemeHomeLogoScale(theme, value) {
- themesStore.setThemeHomeLogoScale(theme.key, value);
+ function setThemeHomeLogoScale(theme, mode, value) {
+ themesStore.setThemeHomeLogoScale(theme.key, mode, value);
}
function isThemeControlTarget(target) {
@@ -586,11 +584,11 @@
{at("themes_use_in_admin", {}, "Использовать в админке")}
- {at(
- "appearance_theme_home_logo_scale",
+ "appearance_theme_home_logo_scale_desktop",
{},
- "Логотип на главной и входе"
+ "Логотип на десктопе"
)}
setThemeHomeLogoScale(theme, value)}
+ ariaLabel={at(
+ "appearance_theme_home_logo_scale_desktop",
+ {},
+ "Desktop logo scale"
+ )}
+ value={homeLogoScale(theme, "desktop")}
+ onValueChange={(value) => setThemeHomeLogoScale(theme, "desktop", value)}
/>
setThemeHomeLogoScale(theme, event.currentTarget.value)}
+ value={homeLogoScale(theme, "desktop")}
+ oninput={(event) =>
+ setThemeHomeLogoScale(theme, "desktop", event.currentTarget.value)}
+ />
+ %
+
+
+
+ {at(
+ "appearance_theme_home_logo_scale_mobile",
+ {},
+ "Логотип на мобильных"
+ )}
+ setThemeHomeLogoScale(theme, "mobile", value)}
+ />
+
+
+ setThemeHomeLogoScale(theme, "mobile", event.currentTarget.value)}
/>
%
@@ -847,10 +885,15 @@
.appearance-logo-scale-row {
display: grid;
- grid-template-columns: auto minmax(96px, 1fr) auto;
+ grid-template-columns: minmax(112px, 0.8fr) minmax(96px, 1fr) auto;
width: 100%;
}
+ .appearance-logo-scale-label {
+ min-width: 0;
+ line-height: 1.25;
+ }
+
:global(.appearance-logo-scale-range) {
width: 100%;
accent-color: var(--accent);
@@ -910,5 +953,13 @@
.appearance-url-row {
grid-template-columns: 1fr;
}
+
+ .appearance-logo-scale-row {
+ grid-template-columns: minmax(0, 1fr) auto;
+ }
+
+ :global(.appearance-logo-scale-range) {
+ grid-column: 1 / -1;
+ }
}
diff --git a/frontend/src/docsDemoEntry.js b/frontend/src/docsDemoEntry.js
index 42cf6af..882ddfa 100644
--- a/frontend/src/docsDemoEntry.js
+++ b/frontend/src/docsDemoEntry.js
@@ -29,9 +29,13 @@ function applyDemoThemeTokens(catalog) {
const themes = catalog?.themes || [];
for (const theme of themes) {
theme.tokens = theme.tokens || {};
- const currentScale = Number(theme.tokens.home_logo_scale);
- if (!Number.isFinite(currentScale) || currentScale > DEMO_HOME_LOGO_SCALE) {
- theme.tokens.home_logo_scale = DEMO_HOME_LOGO_SCALE;
+ const logoScaleKeys = ["home_logo_scale_desktop", "home_logo_scale_mobile"];
+ for (const key of ["home_logo_scale", ...logoScaleKeys]) {
+ if (key !== "home_logo_scale" && !(key in theme.tokens)) continue;
+ const currentScale = Number(theme.tokens[key]);
+ if (!Number.isFinite(currentScale) || currentScale > DEMO_HOME_LOGO_SCALE) {
+ theme.tokens[key] = DEMO_HOME_LOGO_SCALE;
+ }
}
}
}
diff --git a/frontend/src/lib/admin/stores/themesStore.js b/frontend/src/lib/admin/stores/themesStore.js
index 7d20d45..e4b64a4 100644
--- a/frontend/src/lib/admin/stores/themesStore.js
+++ b/frontend/src/lib/admin/stores/themesStore.js
@@ -4,6 +4,23 @@ function cloneCatalog(catalog) {
import { writable } from "svelte/store";
+const HOME_LOGO_SCALE_TOKEN = {
+ desktop: "home_logo_scale_desktop",
+ mobile: "home_logo_scale_mobile",
+};
+
+function normalizeHomeLogoScale(scale) {
+ if (String(scale ?? "").trim() === "") scale = 100;
+ const numeric = Number(scale);
+ return Number.isFinite(numeric) ? Math.min(300, Math.max(50, Math.round(numeric))) : 100;
+}
+
+function resolveThemeHomeLogoScale(theme, mode = "desktop") {
+ const tokens = theme?.tokens || {};
+ const modeKey = HOME_LOGO_SCALE_TOKEN[mode] || HOME_LOGO_SCALE_TOKEN.desktop;
+ return normalizeHomeLogoScale(tokens[modeKey] ?? tokens.home_logo_scale ?? 100);
+}
+
export function createThemesStore({ api, onThemesSaved, flash, at }) {
const state = writable({
themesCatalog: { default_theme: "dark", themes: [] },
@@ -214,27 +231,29 @@ export function createThemesStore({ api, onThemesSaved, flash, at }) {
}));
}
- function setThemeHomeLogoScale(key, scale) {
- if (String(scale ?? "").trim() === "") scale = 100;
- const numeric = Number(scale);
- const nextScale = Number.isFinite(numeric)
- ? Math.min(300, Math.max(50, Math.round(numeric)))
- : 100;
+ function setThemeHomeLogoScale(key, mode, scale) {
+ const normalizedMode = mode === "mobile" ? "mobile" : "desktop";
+ const nextScale = normalizeHomeLogoScale(scale);
state.update((s) => ({
...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
- ),
+ themes: (s.themesCatalog.themes || []).map((theme) => {
+ if (theme.key !== key) return theme;
+ const desktopScale =
+ normalizedMode === "desktop" ? nextScale : resolveThemeHomeLogoScale(theme, "desktop");
+ const mobileScale =
+ normalizedMode === "mobile" ? nextScale : resolveThemeHomeLogoScale(theme, "mobile");
+ return {
+ ...theme,
+ tokens: {
+ ...(theme.tokens || {}),
+ home_logo_scale: null,
+ home_logo_scale_desktop: desktopScale === 100 ? null : desktopScale,
+ home_logo_scale_mobile: mobileScale === 100 ? null : mobileScale,
+ },
+ };
+ }),
},
}));
}
@@ -246,6 +265,7 @@ export function createThemesStore({ api, onThemesSaved, flash, at }) {
setCurrentTheme,
setThemeAccent,
setThemeHomeLogoScale,
+ resolveThemeHomeLogoScale,
togglePrimaryAccent,
toggleAdminUse,
uploadLogoFile,
diff --git a/frontend/src/lib/webapp/demoDataset.js b/frontend/src/lib/webapp/demoDataset.js
index 86a7363..e35e514 100644
--- a/frontend/src/lib/webapp/demoDataset.js
+++ b/frontend/src/lib/webapp/demoDataset.js
@@ -113529,6 +113529,54 @@ export const DEMO_DATASET = {
},
},
},
+ {
+ key: "admin_appearance_theme_home_logo_scale_desktop",
+ audience: "internal",
+ values: {
+ ru: {
+ base: "Логотип на десктопе",
+ fallback: "Логотип на десктопе",
+ effective: "Логотип на десктопе",
+ override: "",
+ overridden: false,
+ updated_at: null,
+ updated_by: null,
+ },
+ en: {
+ base: "Desktop logo scale",
+ fallback: "Логотип на десктопе",
+ effective: "Desktop logo scale",
+ override: "",
+ overridden: false,
+ updated_at: null,
+ updated_by: null,
+ },
+ },
+ },
+ {
+ key: "admin_appearance_theme_home_logo_scale_mobile",
+ audience: "internal",
+ values: {
+ ru: {
+ base: "Логотип на мобильных",
+ fallback: "Логотип на мобильных",
+ effective: "Логотип на мобильных",
+ override: "",
+ overridden: false,
+ updated_at: null,
+ updated_by: null,
+ },
+ en: {
+ base: "Mobile logo scale",
+ fallback: "Логотип на мобильных",
+ effective: "Mobile logo scale",
+ override: "",
+ overridden: false,
+ updated_at: null,
+ updated_by: null,
+ },
+ },
+ },
{
key: "admin_appearance_themes_sub",
audience: "internal",
diff --git a/frontend/src/lib/webapp/themeStyle.js b/frontend/src/lib/webapp/themeStyle.js
index e10f0ec..343f42a 100644
--- a/frontend/src/lib/webapp/themeStyle.js
+++ b/frontend/src/lib/webapp/themeStyle.js
@@ -33,6 +33,8 @@ const TOKEN_TO_CSS_VAR = {
font_logo: "--font-logo",
font_mono: "--font-mono",
home_logo_scale: "--home-logo-scale",
+ home_logo_scale_desktop: "--home-logo-scale-desktop",
+ home_logo_scale_mobile: "--home-logo-scale-mobile",
admin_bg: "--admin-bg",
admin_surface: "--admin-surface",
admin_surface_2: "--admin-surface-2",
@@ -44,6 +46,12 @@ const TOKEN_TO_CSS_VAR = {
admin_dim: "--admin-dim",
};
+const LOGO_SCALE_TOKEN_KEYS = new Set([
+ "home_logo_scale",
+ "home_logo_scale_desktop",
+ "home_logo_scale_mobile",
+]);
+
export function themeTokensToInlineStyle(tokens, primaryFallback = "#00fe7a", options = {}) {
const t = tokens && typeof tokens === "object" ? tokens : {};
const parts = [];
@@ -54,7 +62,7 @@ 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") {
+ if (LOGO_SCALE_TOKEN_KEYS.has(key)) {
const scale = Number(value);
if (!Number.isFinite(scale) || scale <= 0) continue;
parts.push(`${cssVar}:${scale / 100}`);
diff --git a/frontend/src/styles/base.css b/frontend/src/styles/base.css
index 0ff948b..8134cd3 100644
--- a/frontend/src/styles/base.css
+++ b/frontend/src/styles/base.css
@@ -111,6 +111,7 @@ input {
}
.app-shell {
+ --home-logo-scale-effective: var(--home-logo-scale-mobile, var(--home-logo-scale, 1));
min-height: 100dvh;
background: var(--bg, #02070b) !important;
}
@@ -119,3 +120,9 @@ input {
--desktop-rail-width: 252px;
--desktop-page-gutter: clamp(28px, 4vw, 72px);
}
+
+@media (min-width: 1024px) {
+ .app-shell {
+ --home-logo-scale-effective: var(--home-logo-scale-desktop, var(--home-logo-scale, 1));
+ }
+}
diff --git a/frontend/src/styles/webapp.css b/frontend/src/styles/webapp.css
index 24d631b..92054ae 100644
--- a/frontend/src/styles/webapp.css
+++ b/frontend/src/styles/webapp.css
@@ -64,9 +64,9 @@ a {
}
.app-shell .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));
+ width: calc(4.125rem * var(--home-logo-scale-effective, 1));
+ height: calc(4.125rem * var(--home-logo-scale-effective, 1));
+ font-size: calc(2.875rem * var(--home-logo-scale-effective, 1));
}
.app-header,
@@ -2207,15 +2207,15 @@ a {
}
.app-shell .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));
+ width: calc(6rem * var(--home-logo-scale-effective, 1));
+ height: calc(6rem * var(--home-logo-scale-effective, 1));
+ font-size: calc(4.375rem * var(--home-logo-scale-effective, 1));
}
.app-shell .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));
+ width: calc(116px * var(--home-logo-scale-effective, 1));
+ height: calc(116px * var(--home-logo-scale-effective, 1));
+ font-size: calc(84px * var(--home-logo-scale-effective, 1));
}
.home-bottom {
@@ -3027,9 +3027,9 @@ a {
.app-shell .home-brand .brand-mark,
.app-shell .home-brand .brand-mark.brand-mark-lg,
.app-shell .home-brand .brand-mark.brand-mark-xl {
- 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));
+ width: calc(clamp(180px, 18vw, 260px) * var(--home-logo-scale-effective, 1));
+ height: calc(clamp(180px, 18vw, 260px) * var(--home-logo-scale-effective, 1));
+ font-size: calc(clamp(124px, 13vw, 184px) * var(--home-logo-scale-effective, 1));
}
.home-bottom {
diff --git a/locales/en.json b/locales/en.json
index 9bf6bd9..202a86a 100644
--- a/locales/en.json
+++ b/locales/en.json
@@ -1963,6 +1963,8 @@
"admin_appearance_theme_accent_empty": "Not set",
"admin_appearance_theme_accent_placeholder": "Not set",
"admin_appearance_theme_home_logo_scale": "Home and login logo",
+ "admin_appearance_theme_home_logo_scale_desktop": "Desktop logo scale",
+ "admin_appearance_theme_home_logo_scale_mobile": "Mobile logo scale",
"admin_appearance_themes_sub": "Global theme, accent color, and preview",
"admin_appearance_themes_title": "Themes",
"admin_appearance_use_custom_favicon": "Use a separate favicon",
diff --git a/locales/ru.json b/locales/ru.json
index d041744..f624495 100644
--- a/locales/ru.json
+++ b/locales/ru.json
@@ -1963,6 +1963,8 @@
"admin_appearance_theme_accent_empty": "Не задан",
"admin_appearance_theme_accent_placeholder": "Не задан",
"admin_appearance_theme_home_logo_scale": "Логотип на главной и входе",
+ "admin_appearance_theme_home_logo_scale_desktop": "Логотип на десктопе",
+ "admin_appearance_theme_home_logo_scale_mobile": "Логотип на мобильных",
"admin_appearance_themes_sub": "Глобальная тема, accent color и предпросмотр",
"admin_appearance_themes_title": "Темы",
"admin_appearance_use_custom_favicon": "Использовать отдельную favicon",
diff --git a/tests/test_webapp_assets.py b/tests/test_webapp_assets.py
index 795c6a6..50b09d9 100644
--- a/tests/test_webapp_assets.py
+++ b/tests/test_webapp_assets.py
@@ -559,10 +559,14 @@ class WebAppAssetTests(unittest.IsolatedAsyncioTestCase):
def test_home_logo_scale_rules_beat_late_loaded_admin_brand_styles(self):
css = Path("frontend/src/styles/webapp.css").read_text(encoding="utf-8")
+ base_css = Path("frontend/src/styles/base.css").read_text(encoding="utf-8")
self.assertIn(".app-shell .home-brand .brand-mark.brand-mark-xl", css)
self.assertIn(".app-shell .login-brand-auth .brand-mark.brand-mark-xl", css)
self.assertIn(".app-shell .loader .brand-mark.brand-mark-lg", css)
+ self.assertIn("--home-logo-scale-effective", css)
+ self.assertIn("--home-logo-scale-mobile", base_css)
+ self.assertIn("--home-logo-scale-desktop", base_css)
def test_prune_unused_appearance_assets_keeps_only_referenced_logo_and_favicons(self):
with tempfile.TemporaryDirectory() as tmpdir:
@@ -647,6 +651,8 @@ class WebAppAssetTests(unittest.IsolatedAsyncioTestCase):
cfg = builtin_webapp_themes_config("#123456")
theme = cfg.theme_by_key("light")
theme.tokens.home_logo_scale = 135
+ theme.tokens.home_logo_scale_desktop = 150
+ theme.tokens.home_logo_scale_mobile = 85
request = SimpleNamespace(get=lambda key, default="": "nonce-value")
markup = subscription_webapp._initial_theme_head_markup(request, theme, "#123456")
@@ -655,6 +661,8 @@ class WebAppAssetTests(unittest.IsolatedAsyncioTestCase):
self.assertIn('nonce="nonce-value"', markup)
self.assertIn("--accent:#123456", markup)
self.assertIn("--home-logo-scale:1.35", markup)
+ self.assertIn("--home-logo-scale-desktop:1.5", markup)
+ self.assertIn("--home-logo-scale-mobile:0.85", markup)
self.assertIn("color-scheme:light", markup)
def test_theme_asset_version_bumps_for_saved_default_css_theme(self):
diff --git a/tests/test_webapp_themes_config.py b/tests/test_webapp_themes_config.py
index ec0a2a2..2fb943d 100644
--- a/tests/test_webapp_themes_config.py
+++ b/tests/test_webapp_themes_config.py
@@ -296,7 +296,7 @@ class WebappThemesConfigTests(unittest.TestCase):
self.assertEqual(cfg.theme_by_key("custom").tokens.accent, "#00ff88")
- def test_theme_home_logo_scale_is_public_token(self):
+ def test_theme_home_logo_scales_are_public_tokens(self):
cfg = WebappThemesConfig(
default_theme="custom",
themes=[
@@ -304,7 +304,12 @@ class WebappThemesConfigTests(unittest.TestCase):
"key": "custom",
"enabled": True,
"default": True,
- "tokens": {"color_scheme": "dark", "home_logo_scale": 135},
+ "tokens": {
+ "color_scheme": "dark",
+ "home_logo_scale": 135,
+ "home_logo_scale_desktop": 150,
+ "home_logo_scale_mobile": 85,
+ },
}
],
)
@@ -313,7 +318,11 @@ class WebappThemesConfigTests(unittest.TestCase):
custom = payload["themes"][0]
self.assertEqual(cfg.theme_by_key("custom").tokens.home_logo_scale, 135)
+ self.assertEqual(cfg.theme_by_key("custom").tokens.home_logo_scale_desktop, 150)
+ self.assertEqual(cfg.theme_by_key("custom").tokens.home_logo_scale_mobile, 85)
self.assertEqual(custom["tokens"]["home_logo_scale"], 135)
+ self.assertEqual(custom["tokens"]["home_logo_scale_desktop"], 150)
+ self.assertEqual(custom["tokens"]["home_logo_scale_mobile"], 85)
def test_theme_accent_rejects_non_hex_values(self):
with self.assertRaises(ValueError):