diff --git a/webui/backend/app/services/__pycache__/settings_service.cpython-313.pyc b/webui/backend/app/services/__pycache__/settings_service.cpython-313.pyc index 40f6905..808d9e4 100644 Binary files a/webui/backend/app/services/__pycache__/settings_service.cpython-313.pyc and b/webui/backend/app/services/__pycache__/settings_service.cpython-313.pyc differ diff --git a/webui/backend/app/services/settings_service.py b/webui/backend/app/services/settings_service.py index b09c259..67bb70c 100644 --- a/webui/backend/app/services/settings_service.py +++ b/webui/backend/app/services/settings_service.py @@ -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 diff --git a/webui/backend/data/tasks.db b/webui/backend/data/tasks.db index c68b04c..85b6b85 100644 Binary files a/webui/backend/data/tasks.db and b/webui/backend/data/tasks.db differ diff --git a/webui/backend/tests/golden/__pycache__/test_api_settings_golden.cpython-313.pyc b/webui/backend/tests/golden/__pycache__/test_api_settings_golden.cpython-313.pyc index 3f4b719..b586f81 100644 Binary files a/webui/backend/tests/golden/__pycache__/test_api_settings_golden.cpython-313.pyc and b/webui/backend/tests/golden/__pycache__/test_api_settings_golden.cpython-313.pyc differ diff --git a/webui/backend/tests/golden/__pycache__/test_ui_smoke_golden.cpython-313.pyc b/webui/backend/tests/golden/__pycache__/test_ui_smoke_golden.cpython-313.pyc index 6e539dd..41012b3 100644 Binary files a/webui/backend/tests/golden/__pycache__/test_ui_smoke_golden.cpython-313.pyc and b/webui/backend/tests/golden/__pycache__/test_ui_smoke_golden.cpython-313.pyc differ diff --git a/webui/backend/tests/golden/test_api_settings_golden.py b/webui/backend/tests/golden/test_api_settings_golden.py index 1f00ac6..a45dfcb 100644 --- a/webui/backend/tests/golden/test_api_settings_golden.py +++ b/webui/backend/tests/golden/test_api_settings_golden.py @@ -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"}) diff --git a/webui/backend/tests/golden/test_ui_smoke_golden.py b/webui/backend/tests/golden/test_ui_smoke_golden.py index fafa613..8773702 100644 --- a/webui/backend/tests/golden/test_ui_smoke_golden.py +++ b/webui/backend/tests/golden/test_ui_smoke_golden.py @@ -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") diff --git a/webui/html/app.js b/webui/html/app.js index 7a32e61..80a6bad 100644 --- a/webui/html/app.js +++ b/webui/html/app.js @@ -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: '', - file: '', - image: '', - video: '', - pdf: '', + folder: '', + file: '', + image: '', + video: '', + pdf: '', text: '', markdown: '', json: '', diff --git a/webui/html/base.css b/webui/html/base.css index 6c2a127..8caa504 100644 --- a/webui/html/base.css +++ b/webui/html/base.css @@ -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)); } diff --git a/webui/html/index.html b/webui/html/index.html index e197f90..ccf2473 100644 --- a/webui/html/index.html +++ b/webui/html/index.html @@ -10,6 +10,10 @@ + + + +
@@ -124,6 +128,10 @@ + + + +
diff --git a/webui/html/theme-catppuccin-soft.css b/webui/html/theme-catppuccin-soft.css new file mode 100644 index 0000000..93fe679 --- /dev/null +++ b/webui/html/theme-catppuccin-soft.css @@ -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); +} diff --git a/webui/html/theme-commander-electric.css b/webui/html/theme-commander-electric.css new file mode 100644 index 0000000..b243724 --- /dev/null +++ b/webui/html/theme-commander-electric.css @@ -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); +} diff --git a/webui/html/theme-fluent-neon.css b/webui/html/theme-fluent-neon.css new file mode 100644 index 0000000..351b0d3 --- /dev/null +++ b/webui/html/theme-fluent-neon.css @@ -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); +} diff --git a/webui/html/theme-nord-arctic.css b/webui/html/theme-nord-arctic.css new file mode 100644 index 0000000..1e4715a --- /dev/null +++ b/webui/html/theme-nord-arctic.css @@ -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); +}