feat: theme - 02

This commit is contained in:
kodi
2026-03-12 20:13:50 +01:00
parent 09c3e14dea
commit e25d43200f
14 changed files with 295 additions and 9 deletions
+15 -2
View File
@@ -6,7 +6,17 @@ from backend.app.db.settings_repository import SettingsRepository
from backend.app.security.path_guard import PathGuard
VALID_THEMES = {"default", "macos-soft", "midnight", "graphite", "windows11"}
VALID_THEMES = {
"default",
"macos-soft",
"midnight",
"graphite",
"windows11",
"commander-electric",
"nord-arctic",
"catppuccin-soft",
"fluent-neon",
}
VALID_COLOR_MODES = {"dark", "light"}
@@ -86,7 +96,10 @@ class SettingsService:
raise AppError(
status_code=400,
code="invalid_request",
message="Theme must be one of: default, macos-soft, midnight, graphite, windows11",
message=(
"Theme must be one of: default, macos-soft, midnight, graphite, windows11, "
"commander-electric, nord-arctic, catppuccin-soft, fluent-neon"
),
)
return normalized
Binary file not shown.
@@ -157,6 +157,13 @@ class SettingsApiGoldenTest(unittest.TestCase):
self.assertEqual(response.json()["selected_theme"], "midnight")
self.assertEqual(response.json()["selected_color_mode"], "dark")
def test_settings_selected_theme_accepts_new_built_in_family(self) -> None:
response = self._request("POST", "/api/settings", {"selected_theme": "commander-electric"})
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()["selected_theme"], "commander-electric")
self.assertEqual(response.json()["selected_color_mode"], "dark")
def test_settings_selected_color_mode_persistence(self) -> None:
response = self._request("POST", "/api/settings", {"selected_color_mode": "light"})
@@ -32,6 +32,10 @@ class UiSmokeGoldenTest(unittest.TestCase):
self.assertIn('/ui/theme-midnight.css', body)
self.assertIn('/ui/theme-graphite.css', body)
self.assertIn('/ui/theme-windows11.css', body)
self.assertIn('/ui/theme-commander-electric.css', body)
self.assertIn('/ui/theme-nord-arctic.css', body)
self.assertIn('/ui/theme-catppuccin-soft.css', body)
self.assertIn('/ui/theme-fluent-neon.css', body)
self.assertIn('id="workspace"', body)
self.assertIn('id="footer-bar"', body)
self.assertIn('id="title-zone-actions"', body)
@@ -82,6 +86,10 @@ class UiSmokeGoldenTest(unittest.TestCase):
self.assertIn('value="midnight"', body)
self.assertIn('value="graphite"', body)
self.assertIn('value="windows11"', body)
self.assertIn('value="commander-electric"', body)
self.assertIn('value="nord-arctic"', body)
self.assertIn('value="catppuccin-soft"', body)
self.assertIn('value="fluent-neon"', 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)
@@ -138,11 +146,19 @@ class UiSmokeGoldenTest(unittest.TestCase):
self.assertTrue((static_root / "theme-midnight.css").exists())
self.assertTrue((static_root / "theme-graphite.css").exists())
self.assertTrue((static_root / "theme-windows11.css").exists())
self.assertTrue((static_root / "theme-commander-electric.css").exists())
self.assertTrue((static_root / "theme-nord-arctic.css").exists())
self.assertTrue((static_root / "theme-catppuccin-soft.css").exists())
self.assertTrue((static_root / "theme-fluent-neon.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('const VALID_THEME_FAMILIES = [', app_js)
self.assertIn('"commander-electric"', app_js)
self.assertIn('"nord-arctic"', app_js)
self.assertIn('"catppuccin-soft"', app_js)
self.assertIn('"fluent-neon"', 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)
@@ -218,6 +234,10 @@ class UiSmokeGoldenTest(unittest.TestCase):
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")
commander_theme_css = (static_root / "theme-commander-electric.css").read_text(encoding="utf-8")
nord_theme_css = (static_root / "theme-nord-arctic.css").read_text(encoding="utf-8")
catppuccin_theme_css = (static_root / "theme-catppuccin-soft.css").read_text(encoding="utf-8")
fluent_theme_css = (static_root / "theme-fluent-neon.css").read_text(encoding="utf-8")
self.assertIn('#theme-toggle', base_css)
self.assertIn('.settings-card', base_css)
self.assertIn('.settings-tabs', base_css)
@@ -226,6 +246,8 @@ class UiSmokeGoldenTest(unittest.TestCase):
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-svg.is-filled', base_css)
self.assertIn('.entry-media-detail', base_css)
self.assertIn('.entry-media-icon.file', base_css)
self.assertIn('.editor-card', base_css)
self.assertIn('.editor-host', base_css)
@@ -236,6 +258,10 @@ class UiSmokeGoldenTest(unittest.TestCase):
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)
self.assertIn(':root[data-theme-family="commander-electric"][data-color-mode="dark"]', commander_theme_css)
self.assertIn(':root[data-theme-family="nord-arctic"][data-color-mode="dark"]', nord_theme_css)
self.assertIn(':root[data-theme-family="catppuccin-soft"][data-color-mode="dark"]', catppuccin_theme_css)
self.assertIn(':root[data-theme-family="fluent-neon"][data-color-mode="dark"]', fluent_theme_css)
app_js_url = app.url_path_for("ui", path="/app.js")
base_css_url = app.url_path_for("ui", path="/base.css")
+16 -6
View File
@@ -60,7 +60,17 @@ let settingsState = {
selectedTheme: "default",
selectedColorMode: "dark",
};
const VALID_THEME_FAMILIES = ["default", "macos-soft", "midnight", "graphite", "windows11"];
const VALID_THEME_FAMILIES = [
"default",
"macos-soft",
"midnight",
"graphite",
"windows11",
"commander-electric",
"nord-arctic",
"catppuccin-soft",
"fluent-neon",
];
const VALID_COLOR_MODES = ["dark", "light"];
let searchState = {
pane: "left",
@@ -473,11 +483,11 @@ function iconTypeForEntry(entry) {
function mediaIconSvg(type) {
const icons = {
folder: '<svg viewBox="0 0 24 24" class="entry-media-svg" aria-hidden="true"><path d="M3.5 7.5a2 2 0 0 1 2-2h4l2 2h7a2 2 0 0 1 2 2v7a2.5 2.5 0 0 1-2.5 2.5H6a2.5 2.5 0 0 1-2.5-2.5z"/></svg>',
file: '<svg viewBox="0 0 24 24" class="entry-media-svg" aria-hidden="true"><path d="M7 3.5h6l4 4v12a1.5 1.5 0 0 1-1.5 1.5h-8A1.5 1.5 0 0 1 6 19.5V5A1.5 1.5 0 0 1 7.5 3.5z"/><path d="M13 3.5V8h4.5"/></svg>',
image: '<svg viewBox="0 0 24 24" class="entry-media-svg" aria-hidden="true"><rect x="4" y="5" width="16" height="14" rx="2"/><circle cx="9" cy="10" r="1.6"/><path d="M6.5 17l4.2-4.3 2.8 2.8 1.9-2L18.5 17z"/></svg>',
video: '<svg viewBox="0 0 24 24" class="entry-media-svg" aria-hidden="true"><rect x="4" y="6" width="11" height="12" rx="2"/><path d="M16 10.2l4-2.2v8l-4-2.2z"/></svg>',
pdf: '<svg viewBox="0 0 24 24" class="entry-media-svg" aria-hidden="true"><path d="M7 3.5h6l4 4v12a1.5 1.5 0 0 1-1.5 1.5h-8A1.5 1.5 0 0 1 6 19.5V5A1.5 1.5 0 0 1 7.5 3.5z"/><path d="M8 16h8"/><path d="M8.2 12.4h2.1a1.4 1.4 0 1 0 0-2.8H8.2z"/><path d="M12.2 9.6v5.6h1.6a1.9 1.9 0 0 0 0-3.8h-1.6"/><path d="M17 9.6h-2.6v5.6"/><path d="M14.8 12.2h1.7"/></svg>',
folder: '<svg viewBox="0 0 24 24" class="entry-media-svg is-filled" aria-hidden="true"><path d="M3.5 7.5a2 2 0 0 1 2-2h4l2 2h7a2 2 0 0 1 2 2v7a2.5 2.5 0 0 1-2.5 2.5H6a2.5 2.5 0 0 1-2.5-2.5z"/><path d="M3.5 9h17" class="entry-media-detail"/></svg>',
file: '<svg viewBox="0 0 24 24" class="entry-media-svg is-filled" aria-hidden="true"><path d="M7 3.5h6l4 4v12a1.5 1.5 0 0 1-1.5 1.5h-8A1.5 1.5 0 0 1 6 19.5V5A1.5 1.5 0 0 1 7.5 3.5z"/><path d="M13 3.5V8h4.5" class="entry-media-detail"/></svg>',
image: '<svg viewBox="0 0 24 24" class="entry-media-svg is-filled" aria-hidden="true"><rect x="4" y="5" width="16" height="14" rx="2"/><circle cx="9" cy="10" r="1.6" class="entry-media-detail-solid"/><path d="M6.5 17l4.2-4.3 2.8 2.8 1.9-2L18.5 17z" class="entry-media-detail"/></svg>',
video: '<svg viewBox="0 0 24 24" class="entry-media-svg is-filled" aria-hidden="true"><rect x="4" y="6" width="11" height="12" rx="2"/><path d="M16 10.2l4-2.2v8l-4-2.2z"/><path d="M9.2 10l3.8 2.1-3.8 2.1z" class="entry-media-detail-solid"/></svg>',
pdf: '<svg viewBox="0 0 24 24" class="entry-media-svg is-filled" aria-hidden="true"><path d="M7 3.5h6l4 4v12a1.5 1.5 0 0 1-1.5 1.5h-8A1.5 1.5 0 0 1 6 19.5V5A1.5 1.5 0 0 1 7.5 3.5z"/><path d="M8 16h8M8.2 12.4h2.1a1.4 1.4 0 1 0 0-2.8H8.2zM12.2 9.6v5.6h1.6a1.9 1.9 0 0 0 0-3.8h-1.6M17 9.6h-2.6v5.6M14.8 12.2h1.7" class="entry-media-detail"/></svg>',
text: '<svg viewBox="0 0 24 24" class="entry-media-svg" aria-hidden="true"><path d="M7 3.5h6l4 4v12a1.5 1.5 0 0 1-1.5 1.5h-8A1.5 1.5 0 0 1 6 19.5V5A1.5 1.5 0 0 1 7.5 3.5z"/><path d="M9 10h6"/><path d="M9 13h6"/><path d="M9 16h4"/></svg>',
markdown: '<svg viewBox="0 0 24 24" class="entry-media-svg" aria-hidden="true"><path d="M7 3.5h6l4 4v12a1.5 1.5 0 0 1-1.5 1.5h-8A1.5 1.5 0 0 1 6 19.5V5A1.5 1.5 0 0 1 7.5 3.5z"/><path d="M8.5 15v-4l1.8 2 1.7-2v4"/><path d="M13.6 11.2h2.6"/><path d="M14.9 10.2v5.2"/><path d="M13.9 14.5l1 1 1-1"/></svg>',
json: '<svg viewBox="0 0 24 24" class="entry-media-svg" aria-hidden="true"><path d="M7 3.5h6l4 4v12a1.5 1.5 0 0 1-1.5 1.5h-8A1.5 1.5 0 0 1 6 19.5V5A1.5 1.5 0 0 1 7.5 3.5z"/><path d="M10 9c-.9.6-1.3 1.5-1.3 3s.4 2.4 1.3 3"/><path d="M14 9c.9.6 1.3 1.5 1.3 3s-.4 2.4-1.3 3"/><circle cx="12" cy="12" r=".8" fill="currentColor" stroke="none"/></svg>',
+18
View File
@@ -316,6 +316,24 @@ button:disabled {
stroke-linejoin: round;
}
.entry-media-svg.is-filled {
fill: currentColor;
stroke: currentColor;
}
.entry-media-svg .entry-media-detail {
fill: none;
stroke: var(--color-surface);
stroke-width: 1.45;
stroke-linecap: round;
stroke-linejoin: round;
}
.entry-media-svg .entry-media-detail-solid {
fill: var(--color-surface);
stroke: none;
}
.entry-media-icon.folder {
color: color-mix(in srgb, #d1a85e 72%, var(--color-text-muted));
}
+8
View File
@@ -10,6 +10,10 @@
<link rel="stylesheet" href="/ui/theme-midnight.css">
<link rel="stylesheet" href="/ui/theme-graphite.css">
<link rel="stylesheet" href="/ui/theme-windows11.css">
<link rel="stylesheet" href="/ui/theme-commander-electric.css">
<link rel="stylesheet" href="/ui/theme-nord-arctic.css">
<link rel="stylesheet" href="/ui/theme-catppuccin-soft.css">
<link rel="stylesheet" href="/ui/theme-fluent-neon.css">
</head>
<body>
<div id="app-shell">
@@ -124,6 +128,10 @@
<option value="midnight">Midnight</option>
<option value="graphite">Graphite</option>
<option value="windows11">Windows 11</option>
<option value="commander-electric">Commander Electric</option>
<option value="nord-arctic">Nord Arctic</option>
<option value="catppuccin-soft">Catppuccin Soft</option>
<option value="fluent-neon">Fluent Neon</option>
</select>
</label>
<div id="settings-interface-error" class="error"></div>
+49
View File
@@ -0,0 +1,49 @@
:root[data-theme-family="catppuccin-soft"][data-color-mode="dark"] {
--color-page-bg: #1e1f2c;
--color-surface: #2b2d42;
--color-surface-elevated: #353853;
--color-border: #585b7b;
--color-border-strong: #b4befe;
--color-text-primary: #f4f1fb;
--color-text-muted: #c4bedb;
--color-accent: #f5c2e7;
--color-accent-contrast: #44253c;
--color-selection-bg: #4a466a;
--color-selection-border: #f2a6d9;
--color-current-row-bg: #3d3a58;
--color-current-row-border: #89dceb;
--color-active-pane-border: #89dceb;
--color-button-bg: #44425f;
--color-button-hover: #514e71;
--color-button-secondary-bg: #373550;
--color-list-header-bg: rgba(245, 194, 231, 0.08);
--color-list-row-hover: rgba(245, 194, 231, 0.08);
--color-danger: #f2a6b3;
--color-danger-bg: #5b3040;
--color-overlay-bg: rgba(12, 10, 20, 0.62);
}
:root[data-theme-family="catppuccin-soft"][data-color-mode="light"] {
--color-page-bg: #f7f2fb;
--color-surface: #fff9fd;
--color-surface-elevated: #f4ebf7;
--color-border: #dccde6;
--color-border-strong: #c49ecf;
--color-text-primary: #3d314c;
--color-text-muted: #7d6d8f;
--color-accent: #b85ba0;
--color-accent-contrast: #ffffff;
--color-selection-bg: #f0dff1;
--color-selection-border: #d695c5;
--color-current-row-bg: #f7ebf4;
--color-current-row-border: #97ccd6;
--color-active-pane-border: #97ccd6;
--color-button-bg: #f2e5f0;
--color-button-hover: #ead8e6;
--color-button-secondary-bg: #f8f0f7;
--color-list-header-bg: rgba(184, 91, 160, 0.05);
--color-list-row-hover: rgba(184, 91, 160, 0.05);
--color-danger: #b34d69;
--color-danger-bg: #fbe7ec;
--color-overlay-bg: rgba(35, 22, 44, 0.22);
}
+53
View File
@@ -0,0 +1,53 @@
:root[data-theme-family="commander-electric"][data-color-mode="dark"] {
--color-page-bg: #08111f;
--color-surface: #0e2344;
--color-surface-elevated: #13305f;
--color-border: #29528f;
--color-border-strong: #67b8ff;
--color-text-primary: #edf6ff;
--color-text-muted: #9fc1ea;
--color-accent: #53d5ff;
--color-accent-contrast: #032239;
--color-selection-bg: #184784;
--color-selection-border: #66d6ff;
--color-current-row-bg: #123563;
--color-current-row-border: #ffe36a;
--color-active-pane-border: #ffe36a;
--color-button-bg: #174177;
--color-button-hover: #205394;
--color-button-secondary-bg: #102d58;
--color-list-header-bg: rgba(83, 213, 255, 0.08);
--color-list-row-hover: rgba(83, 213, 255, 0.1);
--color-danger: #ffb1a1;
--color-danger-bg: #5f261f;
--color-overlay-bg: rgba(2, 8, 18, 0.7);
--shadow-panel: 0 8px 20px rgba(0, 12, 30, 0.24);
--shadow-elevated: 0 18px 40px rgba(0, 12, 34, 0.36);
}
:root[data-theme-family="commander-electric"][data-color-mode="light"] {
--color-page-bg: #dceaff;
--color-surface: #f8fbff;
--color-surface-elevated: #dfeeff;
--color-border: #8bb0e3;
--color-border-strong: #3478d6;
--color-text-primary: #0e2344;
--color-text-muted: #4f6891;
--color-accent: #006fd6;
--color-accent-contrast: #ffffff;
--color-selection-bg: #cae2ff;
--color-selection-border: #4ba5ff;
--color-current-row-bg: #e7f2ff;
--color-current-row-border: #d0aa19;
--color-active-pane-border: #d0aa19;
--color-button-bg: #d5e8ff;
--color-button-hover: #c0dcff;
--color-button-secondary-bg: #e8f2ff;
--color-list-header-bg: rgba(0, 111, 214, 0.06);
--color-list-row-hover: rgba(0, 111, 214, 0.06);
--color-danger: #b23c2f;
--color-danger-bg: #fde9e4;
--color-overlay-bg: rgba(8, 23, 45, 0.24);
--shadow-panel: 0 8px 18px rgba(28, 66, 120, 0.12);
--shadow-elevated: 0 18px 36px rgba(28, 66, 120, 0.18);
}
+53
View File
@@ -0,0 +1,53 @@
:root[data-theme-family="fluent-neon"][data-color-mode="dark"] {
--color-page-bg: #11161d;
--color-surface: #1a2430;
--color-surface-elevated: #202d3c;
--color-border: #38506a;
--color-border-strong: #67c9ff;
--color-text-primary: #edf7ff;
--color-text-muted: #9fb4c7;
--color-accent: #3fb4ff;
--color-accent-contrast: #05233d;
--color-selection-bg: #1f4460;
--color-selection-border: #70d2ff;
--color-current-row-bg: #1a364d;
--color-current-row-border: #8fbeff;
--color-active-pane-border: #70d2ff;
--color-button-bg: #21405a;
--color-button-hover: #29506f;
--color-button-secondary-bg: #1a3146;
--color-list-header-bg: rgba(63, 180, 255, 0.08);
--color-list-row-hover: rgba(63, 180, 255, 0.08);
--color-danger: #f1a5a5;
--color-danger-bg: #53292f;
--color-overlay-bg: rgba(8, 12, 18, 0.64);
--shadow-panel: 0 10px 24px rgba(0, 20, 40, 0.24);
--shadow-elevated: 0 20px 44px rgba(0, 22, 44, 0.34);
}
:root[data-theme-family="fluent-neon"][data-color-mode="light"] {
--color-page-bg: #eff7ff;
--color-surface: #ffffff;
--color-surface-elevated: #e8f3ff;
--color-border: #c8ddf4;
--color-border-strong: #66b8ff;
--color-text-primary: #132536;
--color-text-muted: #667f95;
--color-accent: #0d89ec;
--color-accent-contrast: #ffffff;
--color-selection-bg: #dff1ff;
--color-selection-border: #6cc6ff;
--color-current-row-bg: #edf7ff;
--color-current-row-border: #8fbfff;
--color-active-pane-border: #4bb4ff;
--color-button-bg: #dfeeff;
--color-button-hover: #d0e6ff;
--color-button-secondary-bg: #edf5ff;
--color-list-header-bg: rgba(13, 137, 236, 0.05);
--color-list-row-hover: rgba(13, 137, 236, 0.05);
--color-danger: #bb3845;
--color-danger-bg: #fde8eb;
--color-overlay-bg: rgba(18, 34, 50, 0.22);
--shadow-panel: 0 10px 22px rgba(33, 102, 176, 0.12);
--shadow-elevated: 0 20px 42px rgba(33, 102, 176, 0.18);
}
+49
View File
@@ -0,0 +1,49 @@
:root[data-theme-family="nord-arctic"][data-color-mode="dark"] {
--color-page-bg: #1b222d;
--color-surface: #242d3a;
--color-surface-elevated: #2b3645;
--color-border: #435366;
--color-border-strong: #88a6c8;
--color-text-primary: #ecf2f9;
--color-text-muted: #adc0d4;
--color-accent: #88c0d0;
--color-accent-contrast: #16303d;
--color-selection-bg: #344359;
--color-selection-border: #8ab8ca;
--color-current-row-bg: #2d394b;
--color-current-row-border: #a6bdd7;
--color-active-pane-border: #9cc4de;
--color-button-bg: #314052;
--color-button-hover: #3a4b60;
--color-button-secondary-bg: #293545;
--color-list-header-bg: rgba(136, 192, 208, 0.06);
--color-list-row-hover: rgba(136, 192, 208, 0.07);
--color-danger: #e6a3a3;
--color-danger-bg: #54343a;
--color-overlay-bg: rgba(12, 18, 24, 0.58);
}
:root[data-theme-family="nord-arctic"][data-color-mode="light"] {
--color-page-bg: #edf3f8;
--color-surface: #fbfdff;
--color-surface-elevated: #e8eef5;
--color-border: #c7d4e2;
--color-border-strong: #88a6c8;
--color-text-primary: #243240;
--color-text-muted: #667789;
--color-accent: #4f7c98;
--color-accent-contrast: #ffffff;
--color-selection-bg: #dde8f2;
--color-selection-border: #94adc6;
--color-current-row-bg: #eef4f9;
--color-current-row-border: #b1c2d4;
--color-active-pane-border: #6f8fab;
--color-button-bg: #e7eef6;
--color-button-hover: #dae5f0;
--color-button-secondary-bg: #f2f6fa;
--color-list-header-bg: rgba(79, 124, 152, 0.05);
--color-list-row-hover: rgba(79, 124, 152, 0.05);
--color-danger: #b44b56;
--color-danger-bg: #f8e9eb;
--color-overlay-bg: rgba(18, 28, 39, 0.22);
}