feat: theme - 01

This commit is contained in:
kodi
2026-03-12 18:49:13 +01:00
parent ab83ee3f20
commit 09c3e14dea
17 changed files with 852 additions and 82 deletions
@@ -6,7 +6,7 @@ from backend.app.db.settings_repository import SettingsRepository
from backend.app.security.path_guard import PathGuard
VALID_THEMES = {"default"}
VALID_THEMES = {"default", "macos-soft", "midnight", "graphite", "windows11"}
VALID_COLOR_MODES = {"dark", "light"}
@@ -86,7 +86,7 @@ class SettingsService:
raise AppError(
status_code=400,
code="invalid_request",
message="Theme must be one of: default",
message="Theme must be one of: default, macos-soft, midnight, graphite, windows11",
)
return normalized
Binary file not shown.
@@ -151,10 +151,10 @@ class SettingsApiGoldenTest(unittest.TestCase):
self.assertEqual(response.json()["selected_color_mode"], "dark")
def test_settings_selected_theme_persistence(self) -> None:
response = self._request("POST", "/api/settings", {"selected_theme": "default"})
response = self._request("POST", "/api/settings", {"selected_theme": "midnight"})
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()["selected_theme"], "default")
self.assertEqual(response.json()["selected_theme"], "midnight")
self.assertEqual(response.json()["selected_color_mode"], "dark")
def test_settings_selected_color_mode_persistence(self) -> None:
@@ -26,6 +26,12 @@ class UiSmokeGoldenTest(unittest.TestCase):
self.assertTrue(index_path.exists())
body = index_path.read_text(encoding="utf-8")
self.assertIn('/ui/base.css', body)
self.assertIn('/ui/theme-default.css', body)
self.assertIn('/ui/theme-macos-soft.css', body)
self.assertIn('/ui/theme-midnight.css', body)
self.assertIn('/ui/theme-graphite.css', body)
self.assertIn('/ui/theme-windows11.css', body)
self.assertIn('id="workspace"', body)
self.assertIn('id="footer-bar"', body)
self.assertIn('id="title-zone-actions"', body)
@@ -71,6 +77,11 @@ class UiSmokeGoldenTest(unittest.TestCase):
self.assertIn("Show thumbnails", body)
self.assertIn('id="settings-selected-theme"', body)
self.assertIn("Theme", body)
self.assertIn('value="default"', body)
self.assertIn('value="macos-soft"', body)
self.assertIn('value="midnight"', body)
self.assertIn('value="graphite"', body)
self.assertIn('value="windows11"', body)
self.assertNotIn('id="settings-selected-color-mode"', body)
self.assertIn('id="settings-startup-path-left"', body)
self.assertIn('id="settings-startup-path-right"', body)
@@ -121,11 +132,19 @@ class UiSmokeGoldenTest(unittest.TestCase):
mount = self._ui_mount()
static_root = Path(mount.app.directory)
self.assertTrue((static_root / "app.js").exists())
self.assertTrue((static_root / "style.css").exists())
self.assertTrue((static_root / "base.css").exists())
self.assertTrue((static_root / "theme-default.css").exists())
self.assertTrue((static_root / "theme-macos-soft.css").exists())
self.assertTrue((static_root / "theme-midnight.css").exists())
self.assertTrue((static_root / "theme-graphite.css").exists())
self.assertTrue((static_root / "theme-windows11.css").exists())
app_js = (static_root / "app.js").read_text(encoding="utf-8")
self.assertIn('currentPath: "/Volumes"', app_js)
self.assertIn('selectedTheme: "default"', app_js)
self.assertIn('selectedColorMode: "dark"', app_js)
self.assertIn('const VALID_THEME_FAMILIES = ["default", "macos-soft", "midnight", "graphite", "windows11"];', app_js)
self.assertIn('document.documentElement.dataset.themeFamily', app_js)
self.assertIn('document.documentElement.dataset.colorMode', app_js)
self.assertIn('function effectiveThemeKey(theme, colorMode)', app_js)
self.assertIn("document.documentElement.dataset.theme", app_js)
self.assertIn('document.getElementById("theme-toggle").onclick = toggleTheme;', app_js)
@@ -193,26 +212,37 @@ class UiSmokeGoldenTest(unittest.TestCase):
self.assertIn('sources: selectedItems.map((item) => item.path)', app_js)
self.assertIn("function rootKeyFromPath(path)", app_js)
self.assertIn("function isNestedPath(sourcePath, destinationPath)", app_js)
style_css = (static_root / "style.css").read_text(encoding="utf-8")
self.assertIn(':root[data-theme="default-dark"]', style_css)
self.assertIn(':root[data-theme="default-light"]', style_css)
self.assertIn('#theme-toggle', style_css)
self.assertIn('.settings-card', style_css)
self.assertIn('.settings-tabs', style_css)
self.assertIn('.entry-media-slot', style_css)
self.assertIn('.entry-media-icon.folder', style_css)
self.assertIn('.entry-media-icon.video', style_css)
self.assertIn('.entry-media-icon.pdf', style_css)
self.assertIn('.entry-media-svg', style_css)
self.assertIn('.entry-media-icon.file', style_css)
self.assertIn('.editor-card', style_css)
self.assertIn('.editor-host', style_css)
self.assertNotIn('.select-marker', style_css)
base_css = (static_root / "base.css").read_text(encoding="utf-8")
default_theme_css = (static_root / "theme-default.css").read_text(encoding="utf-8")
macos_theme_css = (static_root / "theme-macos-soft.css").read_text(encoding="utf-8")
midnight_theme_css = (static_root / "theme-midnight.css").read_text(encoding="utf-8")
graphite_theme_css = (static_root / "theme-graphite.css").read_text(encoding="utf-8")
windows_theme_css = (static_root / "theme-windows11.css").read_text(encoding="utf-8")
self.assertIn('#theme-toggle', base_css)
self.assertIn('.settings-card', base_css)
self.assertIn('.settings-tabs', base_css)
self.assertIn('.entry-media-slot', base_css)
self.assertIn('.entry-media-icon.folder', base_css)
self.assertIn('.entry-media-icon.video', base_css)
self.assertIn('.entry-media-icon.pdf', base_css)
self.assertIn('.entry-media-svg', base_css)
self.assertIn('.entry-media-icon.file', base_css)
self.assertIn('.editor-card', base_css)
self.assertIn('.editor-host', base_css)
self.assertNotIn('.select-marker', base_css)
self.assertIn(':root[data-theme-family="default"][data-color-mode="dark"]', default_theme_css)
self.assertIn(':root[data-theme-family="default"][data-color-mode="light"]', default_theme_css)
self.assertIn(':root[data-theme-family="macos-soft"][data-color-mode="dark"]', macos_theme_css)
self.assertIn(':root[data-theme-family="midnight"][data-color-mode="dark"]', midnight_theme_css)
self.assertIn(':root[data-theme-family="graphite"][data-color-mode="dark"]', graphite_theme_css)
self.assertIn(':root[data-theme-family="windows11"][data-color-mode="dark"]', windows_theme_css)
app_js_url = app.url_path_for("ui", path="/app.js")
style_css_url = app.url_path_for("ui", path="/style.css")
base_css_url = app.url_path_for("ui", path="/base.css")
theme_default_url = app.url_path_for("ui", path="/theme-default.css")
self.assertEqual(app_js_url, "/ui/app.js")
self.assertEqual(style_css_url, "/ui/style.css")
self.assertEqual(base_css_url, "/ui/base.css")
self.assertEqual(theme_default_url, "/ui/theme-default.css")
if __name__ == "__main__":
+14 -9
View File
@@ -60,24 +60,29 @@ let settingsState = {
selectedTheme: "default",
selectedColorMode: "dark",
};
const VALID_THEME_FAMILIES = ["default", "macos-soft", "midnight", "graphite", "windows11"];
const VALID_COLOR_MODES = ["dark", "light"];
let searchState = {
pane: "left",
path: "/Volumes",
query: "",
};
function effectiveThemeKey(theme, colorMode) {
const family = theme === "default" ? "default" : "default";
const mode = colorMode === "light" ? "light" : "dark";
const family = VALID_THEME_FAMILIES.includes(theme) ? theme : "default";
const mode = VALID_COLOR_MODES.includes(colorMode) ? colorMode : "dark";
return `${family}-${mode}`;
}
function currentColorMode() {
return (document.documentElement.dataset.theme || "").endsWith("-light") ? "light" : "dark";
return document.documentElement.dataset.colorMode === "light" ? "light" : "dark";
}
function applyTheme(theme, colorMode) {
const nextTheme = effectiveThemeKey(theme, colorMode);
const mode = colorMode === "light" ? "light" : "dark";
const family = VALID_THEME_FAMILIES.includes(theme) ? theme : "default";
const mode = VALID_COLOR_MODES.includes(colorMode) ? colorMode : "dark";
const nextTheme = effectiveThemeKey(family, mode);
document.documentElement.dataset.themeFamily = family;
document.documentElement.dataset.colorMode = mode;
document.documentElement.dataset.theme = nextTheme;
const icon = document.getElementById("theme-toggle-icon");
const button = document.getElementById("theme-toggle");
@@ -516,8 +521,8 @@ async function loadSettings() {
settingsState.showThumbnails = !!data.show_thumbnails;
settingsState.preferredStartupPathLeft = data.preferred_startup_path_left || null;
settingsState.preferredStartupPathRight = data.preferred_startup_path_right || null;
settingsState.selectedTheme = data.selected_theme || "default";
settingsState.selectedColorMode = data.selected_color_mode || "dark";
settingsState.selectedTheme = VALID_THEME_FAMILIES.includes(data.selected_theme) ? data.selected_theme : "default";
settingsState.selectedColorMode = VALID_COLOR_MODES.includes(data.selected_color_mode) ? data.selected_color_mode : "dark";
const elements = settingsElements();
if (elements.showThumbnailsInput) {
elements.showThumbnailsInput.checked = settingsState.showThumbnails;
@@ -538,8 +543,8 @@ async function saveSettings(update) {
settingsState.showThumbnails = !!data.show_thumbnails;
settingsState.preferredStartupPathLeft = data.preferred_startup_path_left || null;
settingsState.preferredStartupPathRight = data.preferred_startup_path_right || null;
settingsState.selectedTheme = data.selected_theme || "default";
settingsState.selectedColorMode = data.selected_color_mode || "dark";
settingsState.selectedTheme = VALID_THEME_FAMILIES.includes(data.selected_theme) ? data.selected_theme : "default";
settingsState.selectedColorMode = VALID_COLOR_MODES.includes(data.selected_color_mode) ? data.selected_color_mode : "dark";
const elements = settingsElements();
if (elements.showThumbnailsInput) {
elements.showThumbnailsInput.checked = settingsState.showThumbnails;
@@ -6,56 +6,6 @@
--shadow-panel: 0 2px 8px rgba(8, 14, 22, 0.06);
}
:root[data-theme="default-dark"] {
--color-page-bg: #161c25;
--color-surface: #1d2531;
--color-surface-elevated: #222c39;
--color-border: #314052;
--color-border-strong: #55739f;
--color-text-primary: #e7edf6;
--color-text-muted: #9aa9bd;
--color-accent: #6aa5ff;
--color-accent-contrast: #07192f;
--color-selection-bg: #233754;
--color-selection-border: #5c8fda;
--color-current-row-bg: #1f2d42;
--color-current-row-border: #6a87b5;
--color-active-pane-border: #78adff;
--color-button-bg: #283444;
--color-button-hover: #314258;
--color-button-secondary-bg: #202935;
--color-list-header-bg: rgba(255, 255, 255, 0.02);
--color-list-row-hover: rgba(106, 165, 255, 0.08);
--color-danger: #ff8e8e;
--color-danger-bg: #462328;
--color-overlay-bg: rgba(8, 12, 18, 0.62);
}
:root[data-theme="default-light"] {
--color-page-bg: #eef3f9;
--color-surface: #ffffff;
--color-surface-elevated: #f8fbff;
--color-border: #d6e0ec;
--color-border-strong: #7ca0d1;
--color-text-primary: #172233;
--color-text-muted: #617086;
--color-accent: #235ec7;
--color-accent-contrast: #ffffff;
--color-selection-bg: #e5eefc;
--color-selection-border: #7b9fdb;
--color-current-row-bg: #f1f6ff;
--color-current-row-border: #a2bce8;
--color-active-pane-border: #235ec7;
--color-button-bg: #f6f9fd;
--color-button-hover: #edf3fb;
--color-button-secondary-bg: #f3f6fb;
--color-list-header-bg: #f8fbff;
--color-list-row-hover: #f5f9ff;
--color-danger: #b42323;
--color-danger-bg: #fdecec;
--color-overlay-bg: rgba(18, 28, 40, 0.30);
}
:root {
--bg: var(--color-page-bg);
--panel: var(--color-surface);
+10 -1
View File
@@ -4,7 +4,12 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>WebManager v2</title>
<link rel="stylesheet" href="/ui/style.css">
<link rel="stylesheet" href="/ui/base.css">
<link rel="stylesheet" href="/ui/theme-default.css">
<link rel="stylesheet" href="/ui/theme-macos-soft.css">
<link rel="stylesheet" href="/ui/theme-midnight.css">
<link rel="stylesheet" href="/ui/theme-graphite.css">
<link rel="stylesheet" href="/ui/theme-windows11.css">
</head>
<body>
<div id="app-shell">
@@ -115,6 +120,10 @@
<span>Theme</span>
<select id="settings-selected-theme">
<option value="default">Default</option>
<option value="macos-soft">macOS Soft</option>
<option value="midnight">Midnight</option>
<option value="graphite">Graphite</option>
<option value="windows11">Windows 11</option>
</select>
</label>
<div id="settings-interface-error" class="error"></div>
+49
View File
@@ -0,0 +1,49 @@
:root[data-theme-family="default"][data-color-mode="dark"] {
--color-page-bg: #161c25;
--color-surface: #1d2531;
--color-surface-elevated: #222c39;
--color-border: #314052;
--color-border-strong: #55739f;
--color-text-primary: #e7edf6;
--color-text-muted: #9aa9bd;
--color-accent: #6aa5ff;
--color-accent-contrast: #07192f;
--color-selection-bg: #233754;
--color-selection-border: #5c8fda;
--color-current-row-bg: #1f2d42;
--color-current-row-border: #6a87b5;
--color-active-pane-border: #78adff;
--color-button-bg: #283444;
--color-button-hover: #314258;
--color-button-secondary-bg: #202935;
--color-list-header-bg: rgba(255, 255, 255, 0.02);
--color-list-row-hover: rgba(106, 165, 255, 0.08);
--color-danger: #ff8e8e;
--color-danger-bg: #462328;
--color-overlay-bg: rgba(8, 12, 18, 0.62);
}
:root[data-theme-family="default"][data-color-mode="light"] {
--color-page-bg: #eef3f9;
--color-surface: #ffffff;
--color-surface-elevated: #f8fbff;
--color-border: #d6e0ec;
--color-border-strong: #7ca0d1;
--color-text-primary: #172233;
--color-text-muted: #617086;
--color-accent: #235ec7;
--color-accent-contrast: #ffffff;
--color-selection-bg: #e5eefc;
--color-selection-border: #7b9fdb;
--color-current-row-bg: #f1f6ff;
--color-current-row-border: #a2bce8;
--color-active-pane-border: #235ec7;
--color-button-bg: #f6f9fd;
--color-button-hover: #edf3fb;
--color-button-secondary-bg: #f3f6fb;
--color-list-header-bg: #f8fbff;
--color-list-row-hover: #f5f9ff;
--color-danger: #b42323;
--color-danger-bg: #fdecec;
--color-overlay-bg: rgba(18, 28, 40, 0.30);
}
+49
View File
@@ -0,0 +1,49 @@
:root[data-theme-family="graphite"][data-color-mode="dark"] {
--color-page-bg: #171717;
--color-surface: #202020;
--color-surface-elevated: #272727;
--color-border: #3c3c3c;
--color-border-strong: #7b7b7b;
--color-text-primary: #efefef;
--color-text-muted: #ababab;
--color-accent: #8ca0b5;
--color-accent-contrast: #111111;
--color-selection-bg: #313131;
--color-selection-border: #8f9bab;
--color-current-row-bg: #2a2a2a;
--color-current-row-border: #8c8c8c;
--color-active-pane-border: #a8b6c4;
--color-button-bg: #2d2d2d;
--color-button-hover: #353535;
--color-button-secondary-bg: #272727;
--color-list-header-bg: rgba(255, 255, 255, 0.02);
--color-list-row-hover: rgba(255, 255, 255, 0.05);
--color-danger: #e19a9a;
--color-danger-bg: #472c2c;
--color-overlay-bg: rgba(8, 8, 8, 0.64);
}
:root[data-theme-family="graphite"][data-color-mode="light"] {
--color-page-bg: #f0f0f0;
--color-surface: #ffffff;
--color-surface-elevated: #f7f7f7;
--color-border: #dbdbdb;
--color-border-strong: #a2a2a2;
--color-text-primary: #232323;
--color-text-muted: #757575;
--color-accent: #5f7180;
--color-accent-contrast: #ffffff;
--color-selection-bg: #ececec;
--color-selection-border: #b0bcc6;
--color-current-row-bg: #f5f5f5;
--color-current-row-border: #c1c1c1;
--color-active-pane-border: #7d8f9f;
--color-button-bg: #f8f8f8;
--color-button-hover: #f0f0f0;
--color-button-secondary-bg: #f3f3f3;
--color-list-header-bg: #f8f8f8;
--color-list-row-hover: #f4f4f4;
--color-danger: #ab3333;
--color-danger-bg: #fdeeee;
--color-overlay-bg: rgba(24, 24, 24, 0.26);
}
+49
View File
@@ -0,0 +1,49 @@
:root[data-theme-family="macos-soft"][data-color-mode="dark"] {
--color-page-bg: #171c21;
--color-surface: #20262d;
--color-surface-elevated: #252d35;
--color-border: #3b4753;
--color-border-strong: #7a98bc;
--color-text-primary: #e8edf3;
--color-text-muted: #a4b0bc;
--color-accent: #6e98d5;
--color-accent-contrast: #08131f;
--color-selection-bg: #2a3644;
--color-selection-border: #88a7cd;
--color-current-row-bg: #313d4b;
--color-current-row-border: #97aec8;
--color-active-pane-border: #96b2d6;
--color-button-bg: #2a3139;
--color-button-hover: #333c46;
--color-button-secondary-bg: #262d34;
--color-list-header-bg: rgba(255, 255, 255, 0.03);
--color-list-row-hover: rgba(150, 178, 214, 0.09);
--color-danger: #eaa0a0;
--color-danger-bg: #4b292c;
--color-overlay-bg: rgba(10, 14, 19, 0.56);
}
:root[data-theme-family="macos-soft"][data-color-mode="light"] {
--color-page-bg: #f3f5f8;
--color-surface: #ffffff;
--color-surface-elevated: #fafbfd;
--color-border: #d8dfe8;
--color-border-strong: #99afc8;
--color-text-primary: #1c2632;
--color-text-muted: #6e7a88;
--color-accent: #5d80b8;
--color-accent-contrast: #ffffff;
--color-selection-bg: #edf2f8;
--color-selection-border: #abc0d8;
--color-current-row-bg: #f4f7fb;
--color-current-row-border: #bfcee1;
--color-active-pane-border: #83a0c7;
--color-button-bg: #f7f8fb;
--color-button-hover: #eff3f7;
--color-button-secondary-bg: #f4f6f9;
--color-list-header-bg: #fbfcfd;
--color-list-row-hover: #f5f8fb;
--color-danger: #b84c4c;
--color-danger-bg: #fceeee;
--color-overlay-bg: rgba(18, 24, 30, 0.22);
}
+49
View File
@@ -0,0 +1,49 @@
:root[data-theme-family="midnight"][data-color-mode="dark"] {
--color-page-bg: #10131b;
--color-surface: #171c28;
--color-surface-elevated: #1c2230;
--color-border: #2d3750;
--color-border-strong: #5b79c4;
--color-text-primary: #edf2fb;
--color-text-muted: #90a0bf;
--color-accent: #7aa4ff;
--color-accent-contrast: #071427;
--color-selection-bg: #1f3054;
--color-selection-border: #6f96f1;
--color-current-row-bg: #18243d;
--color-current-row-border: #6b84bc;
--color-active-pane-border: #82afff;
--color-button-bg: #212a3e;
--color-button-hover: #293653;
--color-button-secondary-bg: #1a2130;
--color-list-header-bg: rgba(255, 255, 255, 0.02);
--color-list-row-hover: rgba(122, 164, 255, 0.09);
--color-danger: #ff9ea5;
--color-danger-bg: #48262c;
--color-overlay-bg: rgba(4, 8, 15, 0.7);
}
:root[data-theme-family="midnight"][data-color-mode="light"] {
--color-page-bg: #edf2fb;
--color-surface: #ffffff;
--color-surface-elevated: #f5f8ff;
--color-border: #d4dcef;
--color-border-strong: #7694d9;
--color-text-primary: #182236;
--color-text-muted: #617393;
--color-accent: #315fc8;
--color-accent-contrast: #ffffff;
--color-selection-bg: #e1ebff;
--color-selection-border: #85a3e6;
--color-current-row-bg: #edf3ff;
--color-current-row-border: #a7bbe8;
--color-active-pane-border: #3c6bd3;
--color-button-bg: #f5f8ff;
--color-button-hover: #ebf1ff;
--color-button-secondary-bg: #f1f5fd;
--color-list-header-bg: #f6f9ff;
--color-list-row-hover: #f2f6ff;
--color-danger: #b2293c;
--color-danger-bg: #fdecef;
--color-overlay-bg: rgba(15, 23, 40, 0.28);
}
+49
View File
@@ -0,0 +1,49 @@
:root[data-theme-family="windows11"][data-color-mode="dark"] {
--color-page-bg: #14181f;
--color-surface: #1b212b;
--color-surface-elevated: #222a35;
--color-border: #344050;
--color-border-strong: #6d8fb7;
--color-text-primary: #ebf1f8;
--color-text-muted: #9facc0;
--color-accent: #7bb0ff;
--color-accent-contrast: #081b33;
--color-selection-bg: #23344c;
--color-selection-border: #78a9f4;
--color-current-row-bg: #1e2b3f;
--color-current-row-border: #7892b7;
--color-active-pane-border: #83b6ff;
--color-button-bg: #253142;
--color-button-hover: #2e3d53;
--color-button-secondary-bg: #1e2834;
--color-list-header-bg: rgba(255, 255, 255, 0.025);
--color-list-row-hover: rgba(123, 176, 255, 0.085);
--color-danger: #f0a0a0;
--color-danger-bg: #4b2a2d;
--color-overlay-bg: rgba(8, 12, 18, 0.58);
}
:root[data-theme-family="windows11"][data-color-mode="light"] {
--color-page-bg: #edf3fb;
--color-surface: #ffffff;
--color-surface-elevated: #f7fbff;
--color-border: #d3deec;
--color-border-strong: #88a7d1;
--color-text-primary: #162231;
--color-text-muted: #64758d;
--color-accent: #2b66d2;
--color-accent-contrast: #ffffff;
--color-selection-bg: #e5efff;
--color-selection-border: #89aae1;
--color-current-row-bg: #f1f6ff;
--color-current-row-border: #adc3e8;
--color-active-pane-border: #3c78df;
--color-button-bg: #f4f8ff;
--color-button-hover: #ebf2ff;
--color-button-secondary-bg: #f1f5fb;
--color-list-header-bg: #f8fbff;
--color-list-row-hover: #f3f7ff;
--color-danger: #b22f2f;
--color-danger-bg: #fdecec;
--color-overlay-bg: rgba(17, 26, 38, 0.24);
}