fix(ux): resolve light theme issues and enhance theme management

- Fixed light theme to correctly apply the blue topbar and ensure text colors are consistent with the selected theme.
- Updated theme management to persist the selected theme and its corresponding palette across sessions.
- Enhanced documentation to clarify theme color mappings and behavior.
- Introduced built-in theme palettes for light and dark modes to streamline theme application.
- Improved CSS variable handling for better theme integration in the UI.

This update addresses user feedback regarding theme inconsistencies and improves overall theme functionality.
This commit is contained in:
UNITRONIX
2026-07-26 10:09:54 +02:00
parent e7acaa5c4f
commit 261a7fcd78
9 changed files with 327 additions and 57 deletions
+3
View File
@@ -3,6 +3,9 @@
### Added
- **UX 3.5 default console shell:** native full-list sidebar + topbar (promoted from retired Beta 3.1), glass branding, Dark/Light/Custom themes in Settings → Branding, resizable sidebar, tablet/phone drawer. Login / RdClient / remote viewer unchanged. Docs: `docs/wiki/UX-3.5.md`. Ships via panel update.
### Fixed
- **UX 3.5 light theme only recolored the blue topbar:** `branding.css` kept emitting saved dark palette colors, and `ui-polish.css` was not loaded on the console layout — so body/sidebar text stayed light-on-dark. Light/Dark now resolve built-in palettes in `generateThemeCss()`, glass defaults follow the mode, and the topbar theme toggle persists the matching palette.
### Changed
- **Console chrome:** TeamViewer-style rail/flyout and Desktop Mode are no longer loaded as the management UI; UX 3.5 is the single console interface on `dev`.
+5 -5
View File
@@ -35,13 +35,13 @@ Branding `themeMode` (Settings → Branding → Colors):
| Mode | Meaning |
|------|---------|
| `dark` | Built-in dark palette |
| `light` | Built-in light palette |
| `custom` | Operator-edited colors (auto-selected when a color picker changes) |
| `dark` | Built-in dark palette (always used for CSS, even if DB still has old custom hex values) |
| `light` | Built-in light palette + blue topbar; body/sidebar text use dark ink on light surfaces |
| `custom` | Operator-edited colors from branding form |
CSS: `data-theme` / `data-theme-mode` on `<html>`, colors and glass from `/css/branding.css` (`brandingService.generateThemeCss()`).
`/css/branding.css` is generated by `resolveThemeColors()` + `generateThemeCss()`. The topbar theme button persists `themeMode` **and** the matching built-in palette so a reload does not snap back to dark text on a “light” shell.
Glass/blur tokens (`--surface-glass-*`) map onto topbar, sidebar, and cards.
Glass: when `glassColor` is empty, light mode defaults to `#ffffff` and dark to `#161b22`.
## Breakpoints
+19 -12
View File
@@ -1,6 +1,7 @@
/**
* BetterDesk Console — UX 3.5 Design Tokens
* Spacing scale, chrome dimensions, glass surfaces for the default shell.
* Theme colors come from branding.css (resolveThemeColors); tokens here are fallbacks.
*/
:root {
@@ -12,13 +13,11 @@
--ux35-space-5: 24px;
--ux35-space-6: 32px;
/* Content padding — pages must use these, not ad-hoc values */
--ux35-content-padding: var(--ux35-space-4);
--ux35-content-padding-lg: var(--ux35-space-5);
--ux35-card-gap: var(--ux35-space-4);
--ux35-section-gap: var(--ux35-space-5);
/* Chrome */
--ux35-topbar-height: 48px;
--ux35-sidebar-width: 220px;
--ux35-sidebar-width-min: 200px;
@@ -28,7 +27,6 @@
--ux35-motion: 150ms;
--ux35-motion-fast: 120ms;
/* Semantic aliases → existing branding / glass tokens */
--ux35-bg: var(--bg-primary);
--ux35-topbar-bg: var(--surface-glass-bg-elevated, var(--bg-secondary));
--ux35-sidebar-bg: var(--surface-glass-bg-secondary, var(--bg-secondary));
@@ -40,26 +38,35 @@
--ux35-hover: var(--bg-hover);
--ux35-active-bg: var(--accent-blue-muted);
--ux35-primary: var(--accent-blue);
--ux35-topbar-fg: #fff;
--ux35-topbar-fg-muted: rgba(255, 255, 255, 0.75);
--ux35-topbar-fg: var(--text-primary);
--ux35-topbar-fg-muted: var(--text-secondary);
--ux35-glass-blur: var(--surface-glass-blur, 16px);
--ux35-glass-saturate: var(--surface-glass-saturate, 1.2);
}
/* Light: ISP-style blue topbar + light shell; body/sidebar text from branding palette */
[data-theme="light"] {
--ux35-topbar-bg: var(--accent-blue, #2b579a);
--ux35-sidebar-bg: var(--surface-glass-bg-secondary, rgba(255, 255, 255, 0.82));
--ux35-card-bg: var(--card-bg, rgba(255, 255, 255, 0.9));
--ux35-topbar-bg: var(--accent-blue, #0969da);
--ux35-topbar-fg: #ffffff;
--ux35-topbar-fg-muted: rgba(255, 255, 255, 0.78);
--ux35-sidebar-bg: var(--surface-glass-bg-secondary, var(--bg-secondary, #ffffff));
--ux35-card-bg: var(--card-bg, var(--bg-secondary, #ffffff));
--ux35-bg: var(--bg-primary, #f0f2f5);
--ux35-border: var(--border-primary, #dde1e6);
--ux35-border-light: var(--border-secondary, #eef0f3);
--ux35-border: var(--border-primary, #d0d7de);
--ux35-border-light: var(--border-secondary, #eaeef2);
--ux35-text: var(--text-primary, #1f2328);
--ux35-muted: var(--text-secondary, #656d76);
--ux35-hover: rgba(0, 0, 0, 0.04);
--ux35-active-bg: var(--accent-blue-muted, #e8eef7);
--ux35-hover: var(--bg-hover, rgba(0, 0, 0, 0.04));
--ux35-active-bg: var(--accent-blue-muted, rgba(9, 105, 218, 0.1));
--ux35-primary: var(--accent-blue, #0969da);
}
[data-theme="dark"] {
--ux35-topbar-bg: var(--surface-glass-bg-elevated, var(--bg-secondary, #161b22));
--ux35-topbar-fg: var(--text-primary, #e6edf3);
--ux35-topbar-fg-muted: var(--text-secondary, #8b949e);
}
@media (prefers-reduced-motion: reduce) {
:root {
--ux35-motion: 0ms;
+46 -11
View File
@@ -6,8 +6,8 @@
/* ── App shell ── */
body.app-page.ux35-page {
margin: 0;
background: var(--ux35-bg);
color: var(--ux35-text);
background: var(--ux35-bg, var(--bg-primary));
color: var(--ux35-text, var(--text-primary));
font-family: var(--font-family);
}
@@ -17,6 +17,8 @@ body.app-page.ux35-page {
height: 100vh;
height: 100dvh;
overflow: hidden;
background: var(--ux35-bg, var(--bg-primary));
color: var(--ux35-text, var(--text-primary));
}
.ux35-shell.embed-mode {
@@ -66,7 +68,7 @@ body.app-page.ux35-page {
border-radius: var(--ux35-radius-sm);
cursor: pointer;
}
.ux35-topbar-menu-btn:hover { background: rgba(255, 255, 255, 0.12); color: #fff; }
.ux35-topbar-menu-btn:hover { background: rgba(255, 255, 255, 0.12); color: var(--ux35-topbar-fg); }
.ux35-topbar-menu-btn .material-icons { font-size: 22px; }
.ux35-topbar-breadcrumb {
@@ -82,8 +84,8 @@ body.app-page.ux35-page {
color: var(--ux35-topbar-fg-muted);
text-decoration: none;
}
.ux35-topbar-breadcrumb a:hover { color: #fff; }
.ux35-topbar-breadcrumb .current { color: #fff; font-weight: 600; }
.ux35-topbar-breadcrumb a:hover { color: var(--ux35-topbar-fg); }
.ux35-topbar-breadcrumb .current { color: var(--ux35-topbar-fg); font-weight: 600; }
.ux35-topbar-breadcrumb .separator { opacity: 0.5; }
.ux35-topbar-spacer { flex: 1; }
@@ -106,9 +108,15 @@ body.app-page.ux35-page {
position: relative;
transition: background var(--ux35-motion-fast), color var(--ux35-motion-fast);
}
.ux35-topbar-btn:hover { background: rgba(255, 255, 255, 0.12); color: #fff; }
.ux35-topbar-btn:hover { background: rgba(127, 127, 127, 0.18); color: var(--ux35-topbar-fg); }
.ux35-topbar-btn .material-icons { font-size: 20px; }
[data-theme="light"] .ux35-topbar-btn:hover,
[data-theme="light"] .ux35-topbar-menu-btn:hover {
background: rgba(255, 255, 255, 0.16);
color: #fff;
}
.ux35-topbar .notif-badge {
position: absolute;
top: 2px;
@@ -320,7 +328,8 @@ body.app-page.ux35-page {
display: flex;
flex-direction: column;
overflow: hidden;
background: var(--ux35-bg);
background: var(--ux35-bg, var(--bg-primary));
color: var(--ux35-text, var(--text-primary));
}
.ux35-content.main-content {
@@ -440,8 +449,34 @@ body.embed-mode .ux35-content { padding: var(--ux35-content-padding); }
}
}
/* Hide legacy rail layout leftovers if any */
.app-page.ux35-page .app-layout > .sidebar,
.app-page.ux35-page .main-wrapper > .navbar {
display: none;
/* Topbar dropdowns inherit shell text; force readable panels in both themes */
.ux35-topbar .lang-dropdown,
.ux35-topbar .notif-dropdown {
background: var(--bg-elevated, var(--bg-secondary));
color: var(--text-primary);
border: 1px solid var(--border-primary);
}
.ux35-topbar .lang-option {
color: var(--text-primary);
}
.ux35-topbar .lang-option:hover {
background: var(--bg-hover);
}
/* Page body text follows theme tokens */
.ux35-content,
.ux35-content h1,
.ux35-content h2,
.ux35-content h3,
.ux35-content p,
.ux35-content .page-title,
.ux35-content .dashboard-header,
.ux35-content .devices-title h1 {
color: var(--text-primary);
}
.ux35-content .text-muted,
.ux35-content .page-subtitle,
.ux35-content .dashboard-header p,
.ux35-sidebar-heading {
color: var(--text-secondary);
}
+9 -1
View File
@@ -2169,12 +2169,16 @@
if (!radio.checked) return;
if (radio.value === 'light') {
applyBuiltInPalette({
bgPrimary: '#ffffff', bgSecondary: '#f6f8fa', bgTertiary: '#eaeef2', bgElevated: '#ffffff',
bgPrimary: '#f0f2f5', bgSecondary: '#ffffff', bgTertiary: '#eaeef2', bgElevated: '#ffffff',
textPrimary: '#1f2328', textSecondary: '#656d76',
accentBlue: '#0969da', accentBlueHover: '#0550ae',
accentGreen: '#1a7f37', accentRed: '#cf222e', accentYellow: '#9a6700', accentPurple: '#8250df',
borderPrimary: '#d0d7de', borderSecondary: '#eaeef2'
});
const glassColor = document.getElementById('glass-color');
const glassPicker = document.getElementById('glass-color-picker');
if (glassColor) glassColor.value = '#ffffff';
if (glassPicker) glassPicker.value = '#ffffff';
} else if (radio.value === 'dark') {
applyBuiltInPalette({
bgPrimary: '#0d1117', bgSecondary: '#161b22', bgTertiary: '#21262d', bgElevated: '#30363d',
@@ -2183,6 +2187,10 @@
accentGreen: '#2ea44f', accentRed: '#f85149', accentYellow: '#d29922', accentPurple: '#a371f7',
borderPrimary: '#30363d', borderSecondary: '#21262d'
});
const glassColor = document.getElementById('glass-color');
const glassPicker = document.getElementById('glass-color-picker');
if (glassColor) glassColor.value = '#161b22';
if (glassPicker) glassPicker.value = '#161b22';
}
onBrandingFieldChange();
});
+78 -9
View File
@@ -64,11 +64,75 @@
}
}
var THEME_PALETTES = {
dark: {
bgPrimary: '#0d1117', bgSecondary: '#161b22', bgTertiary: '#21262d', bgElevated: '#30363d',
textPrimary: '#e6edf3', textSecondary: '#8b949e',
accentBlue: '#58a6ff', accentBlueHover: '#79c0ff', accentBlueMuted: '#58a6ff',
accentGreen: '#2ea44f', accentGreenHover: '#3fb950', accentGreenMuted: '#2ea44f',
accentRed: '#f85149', accentRedHover: '#ff6b6b', accentRedMuted: '#f85149',
accentYellow: '#d29922', accentYellowHover: '#e3b341', accentYellowMuted: '#d29922',
accentPurple: '#a371f7', accentPurpleHover: '#bc8cff', accentPurpleMuted: '#a371f7',
borderPrimary: '#30363d', borderSecondary: '#21262d'
},
light: {
bgPrimary: '#f0f2f5', bgSecondary: '#ffffff', bgTertiary: '#eaeef2', bgElevated: '#ffffff',
textPrimary: '#1f2328', textSecondary: '#656d76',
accentBlue: '#0969da', accentBlueHover: '#0550ae', accentBlueMuted: '#0969da',
accentGreen: '#1a7f37', accentGreenHover: '#116329', accentGreenMuted: '#1a7f37',
accentRed: '#cf222e', accentRedHover: '#a40e26', accentRedMuted: '#cf222e',
accentYellow: '#9a6700', accentYellowHover: '#7d4e00', accentYellowMuted: '#9a6700',
accentPurple: '#8250df', accentPurpleHover: '#6639ba', accentPurpleMuted: '#8250df',
borderPrimary: '#d0d7de', borderSecondary: '#eaeef2'
}
};
function applyThemeLocally(next) {
document.documentElement.setAttribute('data-theme', next);
document.documentElement.setAttribute('data-theme-mode', next);
var palette = THEME_PALETTES[next] || THEME_PALETTES.dark;
var root = document.documentElement.style;
var map = {
bgPrimary: '--bg-primary', bgSecondary: '--bg-secondary', bgTertiary: '--bg-tertiary',
bgElevated: '--bg-elevated', textPrimary: '--text-primary', textSecondary: '--text-secondary',
accentBlue: '--accent-blue', accentBlueHover: '--accent-blue-hover', accentBlueMuted: '--accent-blue-muted',
accentGreen: '--accent-green', accentGreenHover: '--accent-green-hover', accentGreenMuted: '--accent-green-muted',
accentRed: '--accent-red', accentRedHover: '--accent-red-hover', accentRedMuted: '--accent-red-muted',
accentYellow: '--accent-yellow', accentYellowHover: '--accent-yellow-hover', accentYellowMuted: '--accent-yellow-muted',
accentPurple: '--accent-purple', accentPurpleHover: '--accent-purple-hover', accentPurpleMuted: '--accent-purple-muted',
borderPrimary: '--border-primary', borderSecondary: '--border-secondary'
};
Object.keys(palette).forEach(function (key) {
if (map[key]) root.setProperty(map[key], palette[key]);
});
// Soft glass for light/dark until branding.css reloads
if (next === 'light') {
root.setProperty('--surface-glass-bg-secondary', 'rgba(255,255,255,0.82)');
root.setProperty('--surface-glass-bg-elevated', 'rgba(255,255,255,0.9)');
root.setProperty('--surface-glass-border', 'rgba(208,215,222,0.9)');
root.setProperty('--card-bg', 'rgba(255,255,255,0.92)');
root.setProperty('--ux35-topbar-bg', palette.accentBlue);
root.setProperty('--ux35-topbar-fg', '#ffffff');
root.setProperty('--ux35-topbar-fg-muted', 'rgba(255,255,255,0.78)');
} else {
root.setProperty('--surface-glass-bg-secondary', 'rgba(22,27,34,0.55)');
root.setProperty('--surface-glass-bg-elevated', 'rgba(48,54,61,0.7)');
root.setProperty('--surface-glass-border', 'rgba(48,54,61,0.5)');
root.setProperty('--card-bg', 'rgba(22,27,34,0.55)');
root.removeProperty('--ux35-topbar-bg');
root.removeProperty('--ux35-topbar-fg');
root.removeProperty('--ux35-topbar-fg-muted');
}
root.setProperty('--ux35-bg', palette.bgPrimary);
root.setProperty('--ux35-text', palette.textPrimary);
root.setProperty('--ux35-muted', palette.textSecondary);
root.setProperty('--ux35-primary', palette.accentBlue);
syncThemeIcon();
}
/**
* Cycle visual preview: dark ↔ light.
* Server-side themeMode (Settings → Branding) is the source of truth;
* this only toggles local data-theme for immediate feedback when mode is dark/light.
* Custom mode opens Settings branding.
* Cycle visual theme: dark ↔ light.
* Persists themeMode + built-in palette so branding.css matches data-theme.
*/
function cycleThemePreview() {
var mode = document.documentElement.getAttribute('data-theme-mode') || 'dark';
@@ -77,9 +141,8 @@
return;
}
var next = document.documentElement.getAttribute('data-theme') === 'light' ? 'dark' : 'light';
document.documentElement.setAttribute('data-theme', next);
syncThemeIcon();
// Persist via branding API when operator has permission (best-effort)
applyThemeLocally(next);
if (window.BetterDesk && window.BetterDesk.csrfToken) {
fetch('/api/settings/branding', {
method: 'POST',
@@ -88,16 +151,22 @@
'X-CSRF-Token': window.BetterDesk.csrfToken
},
credentials: 'same-origin',
body: JSON.stringify({ themeMode: next })
body: JSON.stringify({
themeMode: next,
colors: THEME_PALETTES[next],
glassColor: next === 'light' ? '#ffffff' : '#161b22'
})
}).then(function (res) {
if (res.ok) {
document.documentElement.setAttribute('data-theme-mode', next);
var link = document.querySelector('link[href*="branding.css"]');
if (link) {
var url = new URL(link.href, window.location.origin);
url.searchParams.set('v', String(Date.now()));
link.href = url.pathname + url.search;
}
if (window.BetterDesk.branding) {
window.BetterDesk.branding.themeMode = next;
}
}
}).catch(function () { /* ignore */ });
}
+120 -19
View File
@@ -409,6 +409,84 @@ const DEFAULT_BRANDING = {
rdclientBgSize: 'cover'
};
/** Built-in palettes for themeMode light/dark (custom uses branding.colors). */
const BUILTIN_THEME_PALETTES = {
dark: {
bgPrimary: '#0d1117',
bgSecondary: '#161b22',
bgTertiary: '#21262d',
bgElevated: '#30363d',
textPrimary: '#e6edf3',
textSecondary: '#8b949e',
accentBlue: '#58a6ff',
accentBlueHover: '#79c0ff',
accentBlueMuted: '#58a6ff',
accentGreen: '#2ea44f',
accentGreenHover: '#3fb950',
accentGreenMuted: '#2ea44f',
accentRed: '#f85149',
accentRedHover: '#ff6b6b',
accentRedMuted: '#f85149',
accentYellow: '#d29922',
accentYellowHover: '#e3b341',
accentYellowMuted: '#d29922',
accentPurple: '#a371f7',
accentPurpleHover: '#bc8cff',
accentPurpleMuted: '#a371f7',
borderPrimary: '#30363d',
borderSecondary: '#21262d'
},
light: {
bgPrimary: '#f0f2f5',
bgSecondary: '#ffffff',
bgTertiary: '#eaeef2',
bgElevated: '#ffffff',
textPrimary: '#1f2328',
textSecondary: '#656d76',
accentBlue: '#0969da',
accentBlueHover: '#0550ae',
accentBlueMuted: '#0969da',
accentGreen: '#1a7f37',
accentGreenHover: '#116329',
accentGreenMuted: '#1a7f37',
accentRed: '#cf222e',
accentRedHover: '#a40e26',
accentRedMuted: '#cf222e',
accentYellow: '#9a6700',
accentYellowHover: '#7d4e00',
accentYellowMuted: '#9a6700',
accentPurple: '#8250df',
accentPurpleHover: '#6639ba',
accentPurpleMuted: '#8250df',
borderPrimary: '#d0d7de',
borderSecondary: '#eaeef2'
}
};
function normalizeThemeMode(mode) {
let m = String(mode || 'dark');
if (m === 'auto') m = 'dark';
if (!['dark', 'light', 'custom'].includes(m)) m = 'dark';
return m;
}
/**
* Resolve effective color map for CSS generation.
* light/dark ignore stale DB custom colors so themeMode always matches the UI.
*/
function resolveThemeColors(branding) {
const mode = normalizeThemeMode(branding && branding.themeMode);
if (mode === 'light' || mode === 'dark') {
return { ...BUILTIN_THEME_PALETTES[mode] };
}
const stored = (branding && branding.colors) || {};
const merged = { ...BUILTIN_THEME_PALETTES.dark };
for (const [key, value] of Object.entries(stored)) {
if (value && String(value).trim()) merged[key] = String(value).trim();
}
return merged;
}
// CSS variable name mapping
const COLOR_TO_CSS_VAR = {
bgPrimary: '--bg-primary',
@@ -580,10 +658,7 @@ async function saveBranding(updates) {
} else if (key === 'bgSize' || key === 'rdclientBgSize') {
entries.push({ key, value: normalizeBackgroundSize(value) });
} else if (key === 'themeMode') {
let mode = String(value);
if (mode === 'auto') mode = 'dark';
if (!['dark', 'light', 'custom'].includes(mode)) mode = 'dark';
entries.push({ key, value: mode });
entries.push({ key, value: normalizeThemeMode(value) });
} else if (key === 'customCss') {
// Security: Neutralize CSS-based XSS / external resource loading.
entries.push({ key, value: sanitizeCustomCss(value) });
@@ -735,17 +810,30 @@ function generateSemanticAliasCss(branding) {
' --color-text-muted: var(--text-secondary);',
' --color-border: var(--border-primary);',
' --focus-ring-color: var(--accent-blue-muted);',
` color-scheme: ${(branding.themeMode || 'dark') === 'light' ? 'light' : 'dark'};`,
` color-scheme: ${normalizeThemeMode(branding.themeMode) === 'light' ? 'light' : 'dark'};`,
' /* UX 3.5 chrome aliases (branding-aware) */',
' --ux35-bg: var(--bg-primary);',
' --ux35-sidebar-bg: var(--surface-glass-bg-secondary, var(--bg-secondary));',
' --ux35-card-bg: var(--card-bg, var(--surface-glass-bg-secondary, var(--bg-secondary)));',
' --ux35-border: var(--surface-glass-border, var(--border-primary));',
' --ux35-border-light: var(--border-secondary);',
' --ux35-text: var(--text-primary);',
' --ux35-muted: var(--text-secondary);',
' --ux35-hover: var(--bg-hover);',
' --ux35-primary: var(--accent-blue);',
' --ux35-active-bg: var(--accent-blue-muted);',
' --ux35-glass-blur: var(--surface-glass-blur, 16px);'
];
const mode = normalizeThemeMode(branding.themeMode);
if (mode === 'light') {
lines.push(' --ux35-topbar-bg: var(--accent-blue);');
lines.push(' --ux35-topbar-fg: #ffffff;');
lines.push(' --ux35-topbar-fg-muted: rgba(255, 255, 255, 0.78);');
} else {
lines.push(' --ux35-topbar-bg: var(--surface-glass-bg-elevated, var(--bg-secondary));');
lines.push(' --ux35-topbar-fg: var(--text-primary);');
lines.push(' --ux35-topbar-fg-muted: var(--text-secondary);');
}
return `:root {\n${lines.join('\n')}\n}\n`;
}
@@ -755,14 +843,16 @@ function generateSemanticAliasCss(branding) {
*/
function generateThemeCss() {
const branding = getBranding();
const themeMode = normalizeThemeMode(branding.themeMode);
const colors = resolveThemeColors(branding);
const overrides = [];
for (const [key, cssVar] of Object.entries(COLOR_TO_CSS_VAR)) {
const value = branding.colors[key];
if (value && value.trim()) {
const value = colors[key];
if (value && String(value).trim()) {
// For muted colors, auto-generate rgba if a hex color is provided
if (key.endsWith('Muted') && value.startsWith('#')) {
const hex = value.replace('#', '');
if (key.endsWith('Muted') && String(value).startsWith('#')) {
const hex = String(value).replace('#', '');
const r = parseInt(hex.substring(0, 2), 16);
const g = parseInt(hex.substring(2, 4), 16);
const b = parseInt(hex.substring(4, 6), 16);
@@ -772,26 +862,29 @@ function generateThemeCss() {
}
}
}
let css = '';
// Font CSS (imports + heading/body font variables)
const fontCss = fontService.generateFontCss(branding.fontHeading, branding.fontBody);
if (fontCss) {
css += fontCss + '\n';
}
// Color overrides
// Color overrides — always emit effective palette for the active themeMode
if (overrides.length > 0) {
css += `:root {\n${overrides.join('\n')}\n}\n`;
}
// Keep data-theme attribute in sync for component selectors (ui-polish / theme.css)
css += `html { color-scheme: ${themeMode === 'light' ? 'light' : 'dark'}; }\n`;
// Semantic aliases consumed by newer UI and legacy components that use
// --color-* names instead of the original BetterDesk token names.
css += generateSemanticAliasCss(branding);
css += generateSemanticAliasCss({ ...branding, themeMode, colors });
// Glass surface tokens
css += generateGlassCss(branding);
// Glass surface tokens (light mode uses light glass base when unset)
css += generateGlassCss({ ...branding, themeMode, colors });
// Background wallpaper (console + login) and custom CSS
css += generateBackgroundCss(branding);
@@ -846,9 +939,14 @@ function generateGlassCss(branding) {
const blur = clampNumber(branding.glassBlur, 0, 40) ?? 16;
const opacity = (clampNumber(branding.glassOpacity, 0, 100) ?? 55) / 100;
let color = (branding.glassColor || '').trim();
const mode = normalizeThemeMode(branding.themeMode);
if (!color || !/^#[0-9a-fA-F]{6}$/.test(color)) {
const fallback = (branding.colors && branding.colors.bgSecondary) || '';
color = /^#[0-9a-fA-F]{6}$/.test(fallback) ? fallback : '#161b22';
const fromColors = branding.colors && branding.colors.bgSecondary;
if (/^#[0-9a-fA-F]{6}$/.test(fromColors || '')) {
color = fromColors;
} else {
color = mode === 'light' ? '#ffffff' : '#161b22';
}
}
const rgb = hexToRgb(color);
if (!rgb) return '';
@@ -1188,6 +1286,9 @@ function invalidateCache() {
module.exports = {
DEFAULT_BRANDING,
COLOR_TO_CSS_VAR,
BUILTIN_THEME_PALETTES,
normalizeThemeMode,
resolveThemeColors,
loadBranding,
getBranding,
saveBranding,
+46
View File
@@ -0,0 +1,46 @@
/**
* Theme palette resolution for UX 3.5 light/dark/custom
*/
const {
resolveThemeColors,
normalizeThemeMode,
BUILTIN_THEME_PALETTES
} = require('../services/brandingService');
describe('UX 3.5 theme palettes', () => {
test('normalizeThemeMode maps auto → dark and rejects junk', () => {
expect(normalizeThemeMode('auto')).toBe('dark');
expect(normalizeThemeMode('light')).toBe('light');
expect(normalizeThemeMode('custom')).toBe('custom');
expect(normalizeThemeMode('nope')).toBe('dark');
});
test('light mode ignores stale dark DB colors', () => {
const colors = resolveThemeColors({
themeMode: 'light',
colors: {
bgPrimary: '#0d1117',
textPrimary: '#e6edf3'
}
});
expect(colors.bgPrimary).toBe(BUILTIN_THEME_PALETTES.light.bgPrimary);
expect(colors.textPrimary).toBe(BUILTIN_THEME_PALETTES.light.textPrimary);
expect(colors.accentBlue).toBe('#0969da');
});
test('dark mode uses built-in dark palette', () => {
const colors = resolveThemeColors({ themeMode: 'dark', colors: {} });
expect(colors.bgPrimary).toBe('#0d1117');
expect(colors.textPrimary).toBe('#e6edf3');
});
test('custom mode keeps operator colors over dark defaults', () => {
const colors = resolveThemeColors({
themeMode: 'custom',
colors: { bgPrimary: '#112233', textPrimary: '#abcdef' }
});
expect(colors.bgPrimary).toBe('#112233');
expect(colors.textPrimary).toBe('#abcdef');
expect(colors.bgSecondary).toBe(BUILTIN_THEME_PALETTES.dark.bgSecondary);
});
});
+1
View File
@@ -11,6 +11,7 @@
<link rel="stylesheet" href="/css/main.css?v=<%= cacheVersion %>">
<link rel="stylesheet" href="/css/theme.css?v=<%= cacheVersion %>">
<link rel="stylesheet" href="/css/ui-polish.css?v=<%= cacheVersion %>">
<link rel="stylesheet" href="/css/branding.css?v=<%= typeof brandingRevision !== 'undefined' ? brandingRevision : cacheVersion %>">
<link rel="stylesheet" href="/css/ux35-tokens.css?v=<%= cacheVersion %>">
<link rel="stylesheet" href="/css/ux35.css?v=<%= cacheVersion %>">