feat: split logo scale by viewport
This commit is contained in:
@@ -1517,6 +1517,8 @@ _INITIAL_THEME_TOKEN_CSS_MAP = {
|
|||||||
"font_logo": "--font-logo",
|
"font_logo": "--font-logo",
|
||||||
"font_mono": "--font-mono",
|
"font_mono": "--font-mono",
|
||||||
"home_logo_scale": "--home-logo-scale",
|
"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_bg": "--admin-bg",
|
||||||
"admin_surface": "--admin-surface",
|
"admin_surface": "--admin-surface",
|
||||||
"admin_surface_2": "--admin-surface-2",
|
"admin_surface_2": "--admin-surface-2",
|
||||||
@@ -1528,6 +1530,12 @@ _INITIAL_THEME_TOKEN_CSS_MAP = {
|
|||||||
"admin_dim": "--admin-dim",
|
"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:
|
def _theme_css_href_for_html(theme: Any) -> str:
|
||||||
css_file = str(getattr(theme, "css_file", "") or "").strip()
|
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 {}
|
tokens = tokens if isinstance(tokens, dict) else {}
|
||||||
declarations = []
|
declarations = []
|
||||||
for token_key, css_name in _INITIAL_THEME_TOKEN_CSS_MAP.items():
|
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:
|
try:
|
||||||
scale = float(tokens.get(token_key) or 0)
|
scale = float(tokens.get(token_key) or 0)
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
|
|||||||
@@ -54,6 +54,8 @@ class ThemeTokens(BaseModel):
|
|||||||
font_logo: Optional[str] = None
|
font_logo: Optional[str] = None
|
||||||
font_mono: Optional[str] = None
|
font_mono: Optional[str] = None
|
||||||
home_logo_scale: Optional[int] = 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_bg: Optional[str] = None
|
||||||
admin_surface: Optional[str] = None
|
admin_surface: Optional[str] = None
|
||||||
admin_surface_2: 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)
|
hex_value = "".join(char * 2 for char in hex_value)
|
||||||
return f"#{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
|
@classmethod
|
||||||
def _normalize_home_logo_scale(cls, value: Optional[int]) -> Optional[int]:
|
def _normalize_home_logo_scale(cls, value: Optional[int]) -> Optional[int]:
|
||||||
if value is None:
|
if value is None:
|
||||||
return None
|
return None
|
||||||
scale = int(value)
|
scale = int(value)
|
||||||
if scale < 50 or scale > 300:
|
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
|
return scale
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -115,7 +115,8 @@ Email-шаблоны берут тот же бренд из настроек в
|
|||||||
"font_sans": "Inter, system-ui, sans-serif",
|
"font_sans": "Inter, system-ui, sans-serif",
|
||||||
"font_logo": "Inter, system-ui, sans-serif",
|
"font_logo": "Inter, system-ui, sans-serif",
|
||||||
"font_mono": "\"JetBrains Mono\", \"Fira Code\", monospace",
|
"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_bg": "#05040a",
|
||||||
"admin_surface": "#11101c",
|
"admin_surface": "#11101c",
|
||||||
"admin_surface_2": "#090815",
|
"admin_surface_2": "#090815",
|
||||||
@@ -132,7 +133,7 @@ Email-шаблоны берут тот же бренд из настроек в
|
|||||||
Поля верхнего уровня:
|
Поля верхнего уровня:
|
||||||
|
|
||||||
| Поле | Назначение |
|
| Поле | Назначение |
|
||||||
| --- | --- |
|
| -------------------- | ------------------------------------------------------------------------------------------------------- |
|
||||||
| `key` | Уникальный ключ темы, 1-64 символа: латиница, цифры, `_` и `-`. Если ключ не указан, берется имя папки. |
|
| `key` | Уникальный ключ темы, 1-64 символа: латиница, цифры, `_` и `-`. Если ключ не указан, берется имя папки. |
|
||||||
| `names` | Локализованные названия, например `ru` и `en`. |
|
| `names` | Локализованные названия, например `ru` и `en`. |
|
||||||
| `enabled` | Показывать тему пользователям. Отключенная тема не попадает в публичный каталог. |
|
| `enabled` | Показывать тему пользователям. Отключенная тема не попадает в публичный каталог. |
|
||||||
@@ -148,7 +149,7 @@ Email-шаблоны берут тот же бренд из настроек в
|
|||||||
Поддерживаемые токены:
|
Поддерживаемые токены:
|
||||||
|
|
||||||
| Токен | CSS-переменная | Что меняет |
|
| Токен | CSS-переменная | Что меняет |
|
||||||
| --- | --- | --- |
|
| ------------------------- | --------------------------- | ---------------------------------------------------------------------------------------------------------- |
|
||||||
| `color_scheme` | `color-scheme` | Нативная светлая/темная схема браузера: `dark` или `light`. |
|
| `color_scheme` | `color-scheme` | Нативная светлая/темная схема браузера: `dark` или `light`. |
|
||||||
| `style_preset` | CSS-класс пресета | Сейчас `win95`/`windows95` добавляет `theme-preset-win95`; остальные значения не дают специального класса. |
|
| `style_preset` | CSS-класс пресета | Сейчас `win95`/`windows95` добавляет `theme-preset-win95`; остальные значения не дают специального класса. |
|
||||||
| `accent` | `--accent` | Главный акцент: активные элементы, кнопки, прогресс, фокус. Только hex `#RGB` или `#RRGGBB`. |
|
| `accent` | `--accent` | Главный акцент: активные элементы, кнопки, прогресс, фокус. Только hex `#RGB` или `#RRGGBB`. |
|
||||||
@@ -167,7 +168,9 @@ Email-шаблоны берут тот же бренд из настроек в
|
|||||||
| `font_sans` | `--font-sans` | Основной шрифт интерфейса. |
|
| `font_sans` | `--font-sans` | Основной шрифт интерфейса. |
|
||||||
| `font_logo` | `--font-logo` | Шрифт бренда и заголовка. |
|
| `font_logo` | `--font-logo` | Шрифт бренда и заголовка. |
|
||||||
| `font_mono` | `--font-mono` | Моноширинный шрифт. |
|
| `font_mono` | `--font-mono` | Моноширинный шрифт. |
|
||||||
| `home_logo_scale` | `--home-logo-scale` | Масштаб логотипа на главной и входе, от `50` до `300` процентов. |
|
| `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_bg` | `--admin-bg` | Фон админ-панели. |
|
||||||
| `admin_surface` | `--admin-surface` | Основные карточки админки. |
|
| `admin_surface` | `--admin-surface` | Основные карточки админки. |
|
||||||
| `admin_surface_2` | `--admin-surface-2` | Вторичные поверхности админки. |
|
| `admin_surface_2` | `--admin-surface-2` | Вторичные поверхности админки. |
|
||||||
@@ -255,7 +258,8 @@ CSS можно писать для пользовательской части
|
|||||||
content: "";
|
content: "";
|
||||||
width: 16px;
|
width: 16px;
|
||||||
height: 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 и масштаб логотипа.
|
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`, если токенов мало.
|
8. Добавьте `style.css`, если токенов мало.
|
||||||
|
|
||||||
@@ -330,7 +334,6 @@ CSS можно писать для пользовательской части
|
|||||||
11. Сделайте тему дефолтной.
|
11. Сделайте тему дефолтной.
|
||||||
|
|
||||||
Есть два способа:
|
Есть два способа:
|
||||||
|
|
||||||
- в админке выбрать тему и сохранить;
|
- в админке выбрать тему и сохранить;
|
||||||
- указать `WEBAPP_DEFAULT_THEME=neon` в `.env`, если нужен жесткий override на уровне окружения.
|
- указать `WEBAPP_DEFAULT_THEME=neon` в `.env`, если нужен жесткий override на уровне окружения.
|
||||||
|
|
||||||
|
|||||||
@@ -178,10 +178,8 @@
|
|||||||
return `#${hex}`;
|
return `#${hex}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function homeLogoScale(theme) {
|
function homeLogoScale(theme, mode) {
|
||||||
const scale = Number(theme.tokens?.home_logo_scale || 100);
|
return themesStore.resolveThemeHomeLogoScale(theme, mode);
|
||||||
if (!Number.isFinite(scale)) return 100;
|
|
||||||
return Math.min(300, Math.max(50, Math.round(scale)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function openThemeAccentPicker(theme) {
|
function openThemeAccentPicker(theme) {
|
||||||
@@ -296,8 +294,8 @@
|
|||||||
themesStore.setThemeAccent(theme.key, value);
|
themesStore.setThemeAccent(theme.key, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
function setThemeHomeLogoScale(theme, value) {
|
function setThemeHomeLogoScale(theme, mode, value) {
|
||||||
themesStore.setThemeHomeLogoScale(theme.key, value);
|
themesStore.setThemeHomeLogoScale(theme.key, mode, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isThemeControlTarget(target) {
|
function isThemeControlTarget(target) {
|
||||||
@@ -586,11 +584,11 @@
|
|||||||
<span>{at("themes_use_in_admin", {}, "Использовать в админке")}</span>
|
<span>{at("themes_use_in_admin", {}, "Использовать в админке")}</span>
|
||||||
</label>
|
</label>
|
||||||
<div class="admin-theme-card-option appearance-logo-scale-row">
|
<div class="admin-theme-card-option appearance-logo-scale-row">
|
||||||
<span
|
<span class="appearance-logo-scale-label"
|
||||||
>{at(
|
>{at(
|
||||||
"appearance_theme_home_logo_scale",
|
"appearance_theme_home_logo_scale_desktop",
|
||||||
{},
|
{},
|
||||||
"Логотип на главной и входе"
|
"Логотип на десктопе"
|
||||||
)}</span
|
)}</span
|
||||||
>
|
>
|
||||||
<RangeInput
|
<RangeInput
|
||||||
@@ -598,9 +596,13 @@
|
|||||||
min="50"
|
min="50"
|
||||||
max="300"
|
max="300"
|
||||||
step="5"
|
step="5"
|
||||||
ariaLabel={at("appearance_theme_home_logo_scale", {}, "Home logo scale")}
|
ariaLabel={at(
|
||||||
value={homeLogoScale(theme)}
|
"appearance_theme_home_logo_scale_desktop",
|
||||||
onValueChange={(value) => setThemeHomeLogoScale(theme, value)}
|
{},
|
||||||
|
"Desktop logo scale"
|
||||||
|
)}
|
||||||
|
value={homeLogoScale(theme, "desktop")}
|
||||||
|
onValueChange={(value) => setThemeHomeLogoScale(theme, "desktop", value)}
|
||||||
/>
|
/>
|
||||||
<span class="appearance-logo-scale-value">
|
<span class="appearance-logo-scale-value">
|
||||||
<Input
|
<Input
|
||||||
@@ -609,8 +611,44 @@
|
|||||||
min="50"
|
min="50"
|
||||||
max="300"
|
max="300"
|
||||||
step="5"
|
step="5"
|
||||||
value={homeLogoScale(theme)}
|
value={homeLogoScale(theme, "desktop")}
|
||||||
oninput={(event) => setThemeHomeLogoScale(theme, event.currentTarget.value)}
|
oninput={(event) =>
|
||||||
|
setThemeHomeLogoScale(theme, "desktop", event.currentTarget.value)}
|
||||||
|
/>
|
||||||
|
%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="admin-theme-card-option appearance-logo-scale-row">
|
||||||
|
<span class="appearance-logo-scale-label"
|
||||||
|
>{at(
|
||||||
|
"appearance_theme_home_logo_scale_mobile",
|
||||||
|
{},
|
||||||
|
"Логотип на мобильных"
|
||||||
|
)}</span
|
||||||
|
>
|
||||||
|
<RangeInput
|
||||||
|
class="appearance-logo-scale-range"
|
||||||
|
min="50"
|
||||||
|
max="300"
|
||||||
|
step="5"
|
||||||
|
ariaLabel={at(
|
||||||
|
"appearance_theme_home_logo_scale_mobile",
|
||||||
|
{},
|
||||||
|
"Mobile logo scale"
|
||||||
|
)}
|
||||||
|
value={homeLogoScale(theme, "mobile")}
|
||||||
|
onValueChange={(value) => setThemeHomeLogoScale(theme, "mobile", value)}
|
||||||
|
/>
|
||||||
|
<span class="appearance-logo-scale-value">
|
||||||
|
<Input
|
||||||
|
class="input"
|
||||||
|
type="number"
|
||||||
|
min="50"
|
||||||
|
max="300"
|
||||||
|
step="5"
|
||||||
|
value={homeLogoScale(theme, "mobile")}
|
||||||
|
oninput={(event) =>
|
||||||
|
setThemeHomeLogoScale(theme, "mobile", event.currentTarget.value)}
|
||||||
/>
|
/>
|
||||||
%
|
%
|
||||||
</span>
|
</span>
|
||||||
@@ -847,10 +885,15 @@
|
|||||||
|
|
||||||
.appearance-logo-scale-row {
|
.appearance-logo-scale-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: auto minmax(96px, 1fr) auto;
|
grid-template-columns: minmax(112px, 0.8fr) minmax(96px, 1fr) auto;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.appearance-logo-scale-label {
|
||||||
|
min-width: 0;
|
||||||
|
line-height: 1.25;
|
||||||
|
}
|
||||||
|
|
||||||
:global(.appearance-logo-scale-range) {
|
:global(.appearance-logo-scale-range) {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
accent-color: var(--accent);
|
accent-color: var(--accent);
|
||||||
@@ -910,5 +953,13 @@
|
|||||||
.appearance-url-row {
|
.appearance-url-row {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.appearance-logo-scale-row {
|
||||||
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.appearance-logo-scale-range) {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -29,9 +29,13 @@ function applyDemoThemeTokens(catalog) {
|
|||||||
const themes = catalog?.themes || [];
|
const themes = catalog?.themes || [];
|
||||||
for (const theme of themes) {
|
for (const theme of themes) {
|
||||||
theme.tokens = theme.tokens || {};
|
theme.tokens = theme.tokens || {};
|
||||||
const currentScale = Number(theme.tokens.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) {
|
if (!Number.isFinite(currentScale) || currentScale > DEMO_HOME_LOGO_SCALE) {
|
||||||
theme.tokens.home_logo_scale = DEMO_HOME_LOGO_SCALE;
|
theme.tokens[key] = DEMO_HOME_LOGO_SCALE;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,23 @@ function cloneCatalog(catalog) {
|
|||||||
|
|
||||||
import { writable } from "svelte/store";
|
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 }) {
|
export function createThemesStore({ api, onThemesSaved, flash, at }) {
|
||||||
const state = writable({
|
const state = writable({
|
||||||
themesCatalog: { default_theme: "dark", themes: [] },
|
themesCatalog: { default_theme: "dark", themes: [] },
|
||||||
@@ -214,27 +231,29 @@ export function createThemesStore({ api, onThemesSaved, flash, at }) {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
function setThemeHomeLogoScale(key, scale) {
|
function setThemeHomeLogoScale(key, mode, scale) {
|
||||||
if (String(scale ?? "").trim() === "") scale = 100;
|
const normalizedMode = mode === "mobile" ? "mobile" : "desktop";
|
||||||
const numeric = Number(scale);
|
const nextScale = normalizeHomeLogoScale(scale);
|
||||||
const nextScale = Number.isFinite(numeric)
|
|
||||||
? Math.min(300, Math.max(50, Math.round(numeric)))
|
|
||||||
: 100;
|
|
||||||
state.update((s) => ({
|
state.update((s) => ({
|
||||||
...s,
|
...s,
|
||||||
themesCatalog: {
|
themesCatalog: {
|
||||||
...s.themesCatalog,
|
...s.themesCatalog,
|
||||||
themes: (s.themesCatalog.themes || []).map((theme) =>
|
themes: (s.themesCatalog.themes || []).map((theme) => {
|
||||||
theme.key === key
|
if (theme.key !== key) return theme;
|
||||||
? {
|
const desktopScale =
|
||||||
|
normalizedMode === "desktop" ? nextScale : resolveThemeHomeLogoScale(theme, "desktop");
|
||||||
|
const mobileScale =
|
||||||
|
normalizedMode === "mobile" ? nextScale : resolveThemeHomeLogoScale(theme, "mobile");
|
||||||
|
return {
|
||||||
...theme,
|
...theme,
|
||||||
tokens: {
|
tokens: {
|
||||||
...(theme.tokens || {}),
|
...(theme.tokens || {}),
|
||||||
home_logo_scale: nextScale === 100 ? null : nextScale,
|
home_logo_scale: null,
|
||||||
|
home_logo_scale_desktop: desktopScale === 100 ? null : desktopScale,
|
||||||
|
home_logo_scale_mobile: mobileScale === 100 ? null : mobileScale,
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
: theme
|
}),
|
||||||
),
|
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@@ -246,6 +265,7 @@ export function createThemesStore({ api, onThemesSaved, flash, at }) {
|
|||||||
setCurrentTheme,
|
setCurrentTheme,
|
||||||
setThemeAccent,
|
setThemeAccent,
|
||||||
setThemeHomeLogoScale,
|
setThemeHomeLogoScale,
|
||||||
|
resolveThemeHomeLogoScale,
|
||||||
togglePrimaryAccent,
|
togglePrimaryAccent,
|
||||||
toggleAdminUse,
|
toggleAdminUse,
|
||||||
uploadLogoFile,
|
uploadLogoFile,
|
||||||
|
|||||||
@@ -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",
|
key: "admin_appearance_themes_sub",
|
||||||
audience: "internal",
|
audience: "internal",
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ const TOKEN_TO_CSS_VAR = {
|
|||||||
font_logo: "--font-logo",
|
font_logo: "--font-logo",
|
||||||
font_mono: "--font-mono",
|
font_mono: "--font-mono",
|
||||||
home_logo_scale: "--home-logo-scale",
|
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_bg: "--admin-bg",
|
||||||
admin_surface: "--admin-surface",
|
admin_surface: "--admin-surface",
|
||||||
admin_surface_2: "--admin-surface-2",
|
admin_surface_2: "--admin-surface-2",
|
||||||
@@ -44,6 +46,12 @@ const TOKEN_TO_CSS_VAR = {
|
|||||||
admin_dim: "--admin-dim",
|
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 = {}) {
|
export function themeTokensToInlineStyle(tokens, primaryFallback = "#00fe7a", options = {}) {
|
||||||
const t = tokens && typeof tokens === "object" ? tokens : {};
|
const t = tokens && typeof tokens === "object" ? tokens : {};
|
||||||
const parts = [];
|
const parts = [];
|
||||||
@@ -54,7 +62,7 @@ export function themeTokensToInlineStyle(tokens, primaryFallback = "#00fe7a", op
|
|||||||
if (key === "accent") continue;
|
if (key === "accent") continue;
|
||||||
const value = t[key];
|
const value = t[key];
|
||||||
if (value === undefined || value === null || value === "") continue;
|
if (value === undefined || value === null || value === "") continue;
|
||||||
if (key === "home_logo_scale") {
|
if (LOGO_SCALE_TOKEN_KEYS.has(key)) {
|
||||||
const scale = Number(value);
|
const scale = Number(value);
|
||||||
if (!Number.isFinite(scale) || scale <= 0) continue;
|
if (!Number.isFinite(scale) || scale <= 0) continue;
|
||||||
parts.push(`${cssVar}:${scale / 100}`);
|
parts.push(`${cssVar}:${scale / 100}`);
|
||||||
|
|||||||
@@ -111,6 +111,7 @@ input {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.app-shell {
|
.app-shell {
|
||||||
|
--home-logo-scale-effective: var(--home-logo-scale-mobile, var(--home-logo-scale, 1));
|
||||||
min-height: 100dvh;
|
min-height: 100dvh;
|
||||||
background: var(--bg, #02070b) !important;
|
background: var(--bg, #02070b) !important;
|
||||||
}
|
}
|
||||||
@@ -119,3 +120,9 @@ input {
|
|||||||
--desktop-rail-width: 252px;
|
--desktop-rail-width: 252px;
|
||||||
--desktop-page-gutter: clamp(28px, 4vw, 72px);
|
--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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -64,9 +64,9 @@ a {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.app-shell .loader .brand-mark.brand-mark-lg {
|
.app-shell .loader .brand-mark.brand-mark-lg {
|
||||||
width: calc(4.125rem * var(--home-logo-scale, 1));
|
width: calc(4.125rem * var(--home-logo-scale-effective, 1));
|
||||||
height: calc(4.125rem * var(--home-logo-scale, 1));
|
height: calc(4.125rem * var(--home-logo-scale-effective, 1));
|
||||||
font-size: calc(2.875rem * var(--home-logo-scale, 1));
|
font-size: calc(2.875rem * var(--home-logo-scale-effective, 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-header,
|
.app-header,
|
||||||
@@ -2207,15 +2207,15 @@ a {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.app-shell .home-brand .brand-mark.brand-mark-xl {
|
.app-shell .home-brand .brand-mark.brand-mark-xl {
|
||||||
width: calc(6rem * var(--home-logo-scale, 1));
|
width: calc(6rem * var(--home-logo-scale-effective, 1));
|
||||||
height: calc(6rem * var(--home-logo-scale, 1));
|
height: calc(6rem * var(--home-logo-scale-effective, 1));
|
||||||
font-size: calc(4.375rem * var(--home-logo-scale, 1));
|
font-size: calc(4.375rem * var(--home-logo-scale-effective, 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-shell .login-brand-auth .brand-mark.brand-mark-xl {
|
.app-shell .login-brand-auth .brand-mark.brand-mark-xl {
|
||||||
width: calc(116px * var(--home-logo-scale, 1));
|
width: calc(116px * var(--home-logo-scale-effective, 1));
|
||||||
height: calc(116px * var(--home-logo-scale, 1));
|
height: calc(116px * var(--home-logo-scale-effective, 1));
|
||||||
font-size: calc(84px * var(--home-logo-scale, 1));
|
font-size: calc(84px * var(--home-logo-scale-effective, 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
.home-bottom {
|
.home-bottom {
|
||||||
@@ -3027,9 +3027,9 @@ a {
|
|||||||
.app-shell .home-brand .brand-mark,
|
.app-shell .home-brand .brand-mark,
|
||||||
.app-shell .home-brand .brand-mark.brand-mark-lg,
|
.app-shell .home-brand .brand-mark.brand-mark-lg,
|
||||||
.app-shell .home-brand .brand-mark.brand-mark-xl {
|
.app-shell .home-brand .brand-mark.brand-mark-xl {
|
||||||
width: calc(clamp(180px, 18vw, 260px) * 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, 1));
|
height: calc(clamp(180px, 18vw, 260px) * var(--home-logo-scale-effective, 1));
|
||||||
font-size: calc(clamp(124px, 13vw, 184px) * var(--home-logo-scale, 1));
|
font-size: calc(clamp(124px, 13vw, 184px) * var(--home-logo-scale-effective, 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
.home-bottom {
|
.home-bottom {
|
||||||
|
|||||||
@@ -1963,6 +1963,8 @@
|
|||||||
"admin_appearance_theme_accent_empty": "Not set",
|
"admin_appearance_theme_accent_empty": "Not set",
|
||||||
"admin_appearance_theme_accent_placeholder": "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": "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_sub": "Global theme, accent color, and preview",
|
||||||
"admin_appearance_themes_title": "Themes",
|
"admin_appearance_themes_title": "Themes",
|
||||||
"admin_appearance_use_custom_favicon": "Use a separate favicon",
|
"admin_appearance_use_custom_favicon": "Use a separate favicon",
|
||||||
|
|||||||
@@ -1963,6 +1963,8 @@
|
|||||||
"admin_appearance_theme_accent_empty": "Не задан",
|
"admin_appearance_theme_accent_empty": "Не задан",
|
||||||
"admin_appearance_theme_accent_placeholder": "Не задан",
|
"admin_appearance_theme_accent_placeholder": "Не задан",
|
||||||
"admin_appearance_theme_home_logo_scale": "Логотип на главной и входе",
|
"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_sub": "Глобальная тема, accent color и предпросмотр",
|
||||||
"admin_appearance_themes_title": "Темы",
|
"admin_appearance_themes_title": "Темы",
|
||||||
"admin_appearance_use_custom_favicon": "Использовать отдельную favicon",
|
"admin_appearance_use_custom_favicon": "Использовать отдельную favicon",
|
||||||
|
|||||||
@@ -559,10 +559,14 @@ class WebAppAssetTests(unittest.IsolatedAsyncioTestCase):
|
|||||||
|
|
||||||
def test_home_logo_scale_rules_beat_late_loaded_admin_brand_styles(self):
|
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")
|
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 .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 .login-brand-auth .brand-mark.brand-mark-xl", css)
|
||||||
self.assertIn(".app-shell .loader .brand-mark.brand-mark-lg", 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):
|
def test_prune_unused_appearance_assets_keeps_only_referenced_logo_and_favicons(self):
|
||||||
with tempfile.TemporaryDirectory() as tmpdir:
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
@@ -647,6 +651,8 @@ class WebAppAssetTests(unittest.IsolatedAsyncioTestCase):
|
|||||||
cfg = builtin_webapp_themes_config("#123456")
|
cfg = builtin_webapp_themes_config("#123456")
|
||||||
theme = cfg.theme_by_key("light")
|
theme = cfg.theme_by_key("light")
|
||||||
theme.tokens.home_logo_scale = 135
|
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")
|
request = SimpleNamespace(get=lambda key, default="": "nonce-value")
|
||||||
|
|
||||||
markup = subscription_webapp._initial_theme_head_markup(request, theme, "#123456")
|
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('nonce="nonce-value"', markup)
|
||||||
self.assertIn("--accent:#123456", markup)
|
self.assertIn("--accent:#123456", markup)
|
||||||
self.assertIn("--home-logo-scale:1.35", 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)
|
self.assertIn("color-scheme:light", markup)
|
||||||
|
|
||||||
def test_theme_asset_version_bumps_for_saved_default_css_theme(self):
|
def test_theme_asset_version_bumps_for_saved_default_css_theme(self):
|
||||||
|
|||||||
@@ -296,7 +296,7 @@ class WebappThemesConfigTests(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertEqual(cfg.theme_by_key("custom").tokens.accent, "#00ff88")
|
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(
|
cfg = WebappThemesConfig(
|
||||||
default_theme="custom",
|
default_theme="custom",
|
||||||
themes=[
|
themes=[
|
||||||
@@ -304,7 +304,12 @@ class WebappThemesConfigTests(unittest.TestCase):
|
|||||||
"key": "custom",
|
"key": "custom",
|
||||||
"enabled": True,
|
"enabled": True,
|
||||||
"default": 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]
|
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, 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"], 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):
|
def test_theme_accent_rejects_non_hex_values(self):
|
||||||
with self.assertRaises(ValueError):
|
with self.assertRaises(ValueError):
|
||||||
|
|||||||
Reference in New Issue
Block a user