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):