diff --git a/project_docs/BUILTIN_THEMES_V1_1_DESIGN.md b/project_docs/BUILTIN_THEMES_V1_1_DESIGN.md new file mode 100644 index 0000000..428e1bc --- /dev/null +++ b/project_docs/BUILTIN_THEMES_V1_1_DESIGN.md @@ -0,0 +1,247 @@ +# Built-in Themes v1.1 + +## 1. Theme families voor v1 +Aanbevolen built-in theme families voor v1.1: +- `default` +- `macos-soft` +- `midnight` +- `graphite` +- `windows11` + +Deze set is klein genoeg om beheersbaar te blijven en groot genoeg om visueel zinvolle keuze te bieden zonder een theme-explosie te veroorzaken. + +## 2. LCARS +`lcars` hoort expliciet niet in v1.1. + +Aanbevolen behandeling: +- niet opnemen in deze slice +- later als aparte v2 onderzoeken + +Reden: +- `lcars` is visueel extreem uitgesproken +- hoger risico op regressie in leesbaarheid, functiebalkgebruik, row states, modals en paneelcontrast +- grotere kans dat componentstructuur visueel gaat knellen tegen de huidige dual-pane workflow + +Conclusie: +- `lcars` beter behandelen als aparte latere theme-slice met eigen UX-validatie + +## 3. Theme-model +Het bestaande model blijft leidend: +- `selected_theme` = theme family +- `selected_color_mode` = `dark` of `light` + +Beide blijven persistent opgeslagen in SQLite settings. + +Voorbeelden van effectieve combinaties: +- `default-dark` +- `default-light` +- `macos-soft-dark` +- `macos-soft-light` +- `midnight-dark` +- `graphite-light` +- `windows11-dark` + +Belangrijk: +- theme family en color mode blijven twee gescheiden concepten +- de hoofdinterface-toggle blijft alleen `selected_color_mode` wisselen +- `Settings > Interface` blijft alleen `selected_theme` beheren + +## 4. CSS-architectuur +Voorkeursrichting voor v1.1: +- één gedeelde `base.css` voor: + - layout + - spacing + - componentstructuur + - modals + - panelen + - functiebalk + - lijst/tabelstructuur + - algemene componentbasis +- aparte CSS-bestanden per theme-family: + - `theme-default.css` + - `theme-macos-soft.css` + - `theme-midnight.css` + - `theme-graphite.css` + - `theme-windows11.css` +- light/dark binnen dezelfde family geregeld via selectors of tokens + +Dus expliciet niet: +- één enorm all-in-one CSS-bestand met alles door elkaar +- per `theme+mode` een volledig apart layoutbestand + +Aanbevolen structuur: +- `base.css` +- `theme-default.css` +- `theme-macos-soft.css` +- `theme-midnight.css` +- `theme-graphite.css` +- `theme-windows11.css` + +Binnen elk theme-family bestand: +- alleen tokens en beperkte theme-afwerking +- geen duplicatie van layoutregels +- geen alternatieve componentstructuur + +Voorbeeldselector-richting: +- `:root[data-theme-family="macos-soft"][data-color-mode="dark"] { ... }` +- `:root[data-theme-family="macos-soft"][data-color-mode="light"] { ... }` + +Of equivalent via custom properties. + +## 5. Waarom deze architectuur onderhoudbaarder is +Deze architectuur is onderhoudbaarder omdat hij drie dingen schoon van elkaar scheidt: +- wat de UI is: layout en componentstructuur in `base.css` +- welke stijl-family actief is: family-bestand +- welke kleurmodus actief is: dark/light tokens binnen die family + +Voordelen: +- layout-CSS staat op één plek +- theme-bestanden blijven klein en thematisch leesbaar +- regressies zijn makkelijker te isoleren per family +- nieuwe family toevoegen vereist minder risico op breken van bestaande layout +- dark/light binnen één family blijft samenhangend beheerd + +Hoe duplicatie van layout-CSS voorkomen wordt: +- `base.css` bevat alle structurele regels +- family-bestanden overschrijven alleen tokens en kleine visuele accenten +- geen herhaling van paneelgrid, modal layout, functiebalkstructuur of lijstlayout per family + +Hoe theme switching schoon blijft: +- frontend hoeft alleen `selected_theme` en `selected_color_mode` te lezen +- die waarden vertalen naar theme-attributen op `document.documentElement` +- CSS doet de rest via selectors/custom properties +- geen runtime CSS-generatie nodig +- geen vrije bestandsselectie +- geen ingewikkelde asset-resolutie + +## 6. Visuele richting per theme +Alle themes behouden exact dezelfde layout en informatiearchitectuur. Alleen visuele stijl verschilt. + +### `default` +Doel: +- neutrale baseline +- functioneel +- rustig +- compact + +Karakter: +- de huidige standaardstijl, iets verfijnd maar niet uitgesproken + +### `macos-soft` +Doel: +- zacht +- verfijnd +- vriendelijk + +Karakter: +- subtiele surfaces +- zachte grijstinten +- lichte premium desktop-app indruk +- iets vriendelijkere rounding/shadows, zonder layout te veranderen + +### `midnight` +Doel: +- donker +- gefocust +- rustig + +Karakter: +- diepere donkere panelen +- koele accenten +- sterke maar nette current/selected contrasten +- geschikt voor langdurig gebruik in dark mode + +### `graphite` +Doel: +- sober +- professioneel +- bijna monochroom + +Karakter: +- grijs-gedreven palette +- minimale accentkleur +- contrast via luminantie in plaats van felle tinten + +### `windows11` +Doel: +- helder +- modern +- clean desktop-app gevoel + +Karakter: +- lichtere surfaces +- subtiele border/surface scheiding +- iets luchtiger accentgebruik +- behoud van compacte file-manager ergonomie + +## 7. Settings UI +`Settings > Interface` toont een dropdown/select met de theme families: +- `default` +- `macos-soft` +- `midnight` +- `graphite` +- `windows11` + +Dark/light blijft via de bestaande toggle in de hoofdinterface. + +Dus: +- Interface tab = keuze van style family +- Main interface toggle = keuze van color mode + +Geen extra theme-complexiteit in v1.1: +- geen preview gallery +- geen import/export +- geen vrije CSS-keuze +- geen uploads + +## 8. Backend-impact +Backend-aanpassing blijft klein: +- whitelist van `selected_theme` uitbreiden met: + - `default` + - `macos-soft` + - `midnight` + - `graphite` + - `windows11` +- `selected_color_mode` blijft bestaan zoals nu +- geen vrije css-bestandskeuze +- geen uploadmechanisme +- geen nieuwe dependencies + +De bestaande settings-opslag en settings-API kunnen verder hetzelfde model blijven gebruiken. + +## 9. Regressierisico +Belangrijkste risico’s: +- leesbaarheid per theme/mode +- current row / selected row contrast te zwak of te hard +- modals die in bepaalde families te vlak worden +- functiebalk die visueel wegvalt +- editor/viewers die niet goed mee themen +- thumbnail/icon-slot met te weinig contrast +- CSS-fragmentatie als family-bestanden toch structurele regels gaan bevatten +- duplicatie als layout-regels alsnog in family-bestanden belanden + +Belangrijk mitigatieprincipe: +- family-bestanden beperken tot visuele tokens en kleine afwerkingsregels +- geen layout-overrides per family +- consistent regressietesten van dezelfde states in alle families + +## 10. Aanbeveling +Aanbevolen richting voor deze app: +- ja, `base.css` + aparte CSS per theme-family is de juiste richting + +Waarom: +- laag regressierisico +- duidelijke scheiding tussen structuur en uiterlijk +- onderhoudbaar bij toekomstige uitbreiding +- sluit aan op het bestaande model van `selected_theme` + `selected_color_mode` +- voorkomt zowel een gigantisch all-in-one stylesheet als onnodige duplicatie per `theme+mode` + +Expliciete aanbevelingen: +- gebruik gedeelde basis-CSS voor layout/componentstructuur +- gebruik aparte CSS per theme-family +- regel dark/light binnen dezelfde family via selectors/tokens +- behandel `lcars` expliciet als aparte latere slice + +Conclusie: +- deze architectuur is de juiste basis voor built-in themes in deze app +- `lcars` moet niet worden meegetrokken in v1.1, maar apart worden ontworpen en gevalideerd diff --git a/project_docs/BUILTIN_THEMES_V1_DESIGN.md b/project_docs/BUILTIN_THEMES_V1_DESIGN.md new file mode 100644 index 0000000..25d46d3 --- /dev/null +++ b/project_docs/BUILTIN_THEMES_V1_DESIGN.md @@ -0,0 +1,284 @@ +# Built-in Themes v1 + +## 1. Doel +Built-in themes voegen nu waarde toe omdat de webui functioneel volwassen genoeg is om visuele voorkeuren relevant te maken zonder dat de workflow eerst nog instabiel is. De huidige app heeft al een vaste dual-pane structuur, modals, functiebalk, viewers en editorflow. Dat maakt het logisch om stijlvarianten toe te voegen zolang de interactie en informatiearchitectuur gelijk blijven. + +Dit past goed binnen de bestaande `Settings > Interface` structuur, omdat theme-keuze daar een stabiele, globale UI-voorkeur is. De bestaande scheiding tussen theme-family en dark/light mode blijft daarbij bruikbaar: +- `selected_theme` = stijlset / family +- `selected_color_mode` = `dark` of `light` binnen die family + +## 2. Scope +Built-in theme families voor v1: +- `default` +- `macos-soft` +- `midnight` +- `graphite` +- `windows11` + +Expliciet niet in v1: +- `lcars` +- vrije theme-bestanden +- upload of filesystem picker +- layoutvarianten per theme +- component-specifieke thema-engine buiten CSS tokens/selectors + +`lcars` hoort beter in een latere v2-slice. Reden: het is visueel extreem uitgesproken, legt druk op contrast, spacing, functiebalkleesbaarheid en waarschijnlijk ook op de dual-pane ergonomie. Dat is een hoger UX-risico dan de rustige families hierboven. + +## 3. Theme-model +Het bestaande settingsmodel blijft leidend: +- `selected_theme`: theme-family key +- `selected_color_mode`: `dark` of `light` + +Elke built-in family ondersteunt beide modi: +- `default-light` +- `default-dark` +- `macos-soft-light` +- `macos-soft-dark` +- `midnight-light` +- `midnight-dark` +- `graphite-light` +- `graphite-dark` +- `windows11-light` +- `windows11-dark` + +De frontend combineert beide settings tot de effectieve UI-state, bijvoorbeeld via een attribuut zoals: +- `data-theme="macos-soft-light"` +- of combinatie van `data-theme-family` + `data-color-mode` + +Aanbevolen voor v1: beide attributen zetten. +Reden: duidelijkere CSS-structuur en minder fragiele string-parsing in selectors. + +Aanbevolen HTML-state: +- `data-theme-family="macos-soft"` +- `data-color-mode="dark"` + +## 4. CSS-architectuur +Aanbevolen richting: +- een gedeelde `base.css` of equivalent voor: + - layout + - spacing + - componentstructuur + - modals + - panelen + - functiebalk + - tabel/lijststructuur + - algemene componentbasis +- aparte CSS-bestanden per theme-family: + - `theme-default.css` + - `theme-macos-soft.css` + - `theme-midnight.css` + - `theme-graphite.css` + - `theme-windows11.css` +- light/dark binnen dezelfde family regelen met selectors en tokens in dat family-bestand + +Voorbeeldrichting: +- `base.css` +- `theme-default.css` +- `theme-macos-soft.css` +- `theme-midnight.css` +- `theme-graphite.css` +- `theme-windows11.css` + +In elk family-bestand staan alleen tokens en beperkte theme-specifieke afwerkingen, bijvoorbeeld: +- background colors +- surface colors +- border colors +- accent colors +- selection/current row tuning +- shadow/radius tuning waar nodig + +Niet in theme-bestanden: +- grid-structuur +- flex-layout van panelen +- componentmarkup-afhankelijke layoutlogica +- duplicatie van modals/paneel/functiebalk CSS + +Waarom dit onderhoudbaarder is dan één groot CSS-bestand: +- theme-logica blijft per family lokaal leesbaar +- layout en componentstructuur blijven centraal +- minder kans dat een nieuwe family per ongeluk core layout overschrijft +- eenvoudiger regressietesten per family +- duidelijkere grens tussen “wat is de UI” en “hoe ziet de UI eruit” + +Waarom ook niet per theme+mode volledig losse bestanden: +- te veel duplicatie +- onnodig onderhoud van dark/light varianten +- grotere kans op drift tussen light en dark binnen dezelfde family + +## 5. Visuele richting per theme +Alle themes behouden exact dezelfde layout en componentstructuur. Alleen styling verschilt. + +### `default` +Huidige neutrale baseline. +- rustig +- compact +- functioneel +- donkere modus als primaire baseline +- lichte modus als nette tegenhanger + +### `macos-soft` +Doel: zachter, verfijnder, subtiel premium. +- lichtere surfaces +- subtiele separators +- iets zachtere contrasten +- afgeronde panelen/modals iets vriendelijker, maar niet groter +- ingetogen blauw/grijs accent + +### `midnight` +Doel: donker, gefocust, licht dramatisch maar nog rustig. +- diepe donkere oppervlakken +- koele blauwe accenten +- duidelijke current row / selected row contrasten +- geschikt voor langdurig gebruik in donkere modus + +### `graphite` +Doel: sober, professioneel, bijna monochroom. +- neutraal grijs systeem +- minimale accentkleur +- contrast via value shifts in plaats van kleurigheid +- goed voor gebruikers die een stille UI willen + +### `windows11` +Doel: helder, modern, iets luchtiger. +- zachtere paneelsurfaces +- subtiele border+surface lagen +- lichtblauw accent +- iets meer "clean desktop app" gevoel zonder de layout te veranderen + +## 6. Settings UI +`Settings > Interface` toont een dropdown/select met alleen de built-in theme families: +- `default` +- `macos-soft` +- `midnight` +- `graphite` +- `windows11` + +Dark/light blijft in de hoofdinterface via de bestaande toggle. +Die toggle blijft dus een snelle dagelijkse keuze voor kleurmodus, niet voor theme-family. + +Geen extra complexiteit in v1: +- geen preview gallery +- geen screenshot previews +- geen themetekstblokken met uitgebreide beschrijvingen +- geen extra subinstellingen per theme + +## 7. Backend-impact +Backend-aanpassing is beperkt: +- whitelist voor `selected_theme` uitbreiden van alleen `default` naar: + - `default` + - `macos-soft` + - `midnight` + - `graphite` + - `windows11` +- `selected_color_mode` blijft: + - `dark` + - `light` +- settings-opslagmodel blijft verder gelijk +- geen nieuwe dependency +- geen vrije filesystemtoegang + +Dit is laag risico omdat alleen validatie van settings-uitbreiding nodig is; de settings-API en SQLite-opslag bestaan al. + +## 8. Frontend-impact +Aanbevolen organisatie: +- `base.css` blijft altijd geladen +- alle family-bestanden worden ook geladen, maar zijn strikt gescoped op theme selectors + - of dynamisch geladen/swapped, als dat later nodig blijkt + +Aanbevolen v1-richting: alle theme CSS-bestanden statisch laden, maar strikt scopen. +Reden: +- eenvoudiger startup +- minder runtime asset-wisselcomplexiteit +- minder kans op flash of incomplete styling +- aanvaardbaar zolang het aantal families klein blijft + +Selector-richting: +- `:root[data-theme-family="macos-soft"][data-color-mode="dark"] { ... }` +- `:root[data-theme-family="macos-soft"][data-color-mode="light"] { ... }` + +Of equivalent met custom properties: +- family-bestanden vullen tokens op basis van family+mode +- `base.css` gebruikt alleen tokens + +Aanbevolen toepassing: +- startup leest `selected_theme` en `selected_color_mode` +- zet beide attributen vroeg op `document.documentElement` +- bestaande toggle wijzigt alleen `data-color-mode` +- Interface settings wijzigen alleen `data-theme-family` + +Dit houdt startup en theme-switching schoon en voorspelbaar. + +## 9. Regressierisico +Belangrijkste risico’s: +- leesbaarheid en contrast per family/mode +- current row / selected row onvoldoende onderscheid +- actieve paneelrand te zwak of te dominant +- modals en functiebalk die in sommige themes te vlak worden +- thumbnail/icon-slot die wegvalt tegen achtergrond +- CSS-fragmentatie als family-bestanden toch layoutregels gaan bevatten +- editor/viewers die visueel uit de toon vallen als tokens niet breed genoeg zijn + +Belangrijk mitigatieprincipe: +- theme-bestanden mogen alleen token- en lichte afwerkingsverschillen bevatten +- geen layout overrides per family +- smoke-validatie en handmatige check op alle states: + - normal row + - current row + - selected row + - current+selected + - inactive selected + - modal + - functiebalk + - editor/viewers + +## 10. Teststrategie +### Backend golden tests +- whitelist accepteert alle built-in themes +- ongeldige theme key blijft geblokkeerd +- `selected_color_mode` gedrag blijft intact +- fallback naar `default` en `dark` blijft correct + +### UI smoke/regressietests +- `Settings > Interface` bevat alle built-in theme opties +- startup leest theme + color mode uit backend +- hoofdinterface dark/light toggle blijft bestaan +- data-attributen of equivalent theme-state worden correct toegepast +- modals, functiebalk en panelen blijven renderen onder theme-switches + +### Handmatige validatie +Per family in light en dark: +- panel readability +- current row zichtbaarheid +- selected row zichtbaarheid +- inactive pane selectie +- viewer/editor modal contrast +- thumbnail/icon-slot contrast +- functiebalk leesbaarheid +- settings modal tabs en form controls + +## 11. Aanbeveling +Aanbevolen v1-richting met laag regressierisico: +- built-in themes alleen als veilige whitelist keys +- families: + - `default` + - `macos-soft` + - `midnight` + - `graphite` + - `windows11` +- `lcars` expliciet uitstellen naar een aparte latere theme-slice +- architectuur: + - gedeelde `base.css` + - aparte CSS per theme-family + - dark/light binnen elke family via selectors/tokens + +Expliciete beoordeling van de voorgestelde architectuur: +- `gedeelde base.css` +- `aparte CSS per theme-family` + +Dit is de juiste richting voor deze app. +Reden: +- houdt layout en theming schoon gescheiden +- voorkomt één onleesbaar gigantisch CSS-bestand +- voorkomt ook duplicatie van complete layoutbestanden per theme+mode +- past goed bij de bestaande settings-architectuur met `selected_theme` + `selected_color_mode` +- blijft onderhoudbaar als later nog 1-3 families bijkomen 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 2fbc306..40f6905 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 950f602..b09c259 100644 --- a/webui/backend/app/services/settings_service.py +++ b/webui/backend/app/services/settings_service.py @@ -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 diff --git a/webui/backend/data/tasks.db b/webui/backend/data/tasks.db index 0828ee2..c68b04c 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 1b6dd31..3f4b719 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 8b64308..6e539dd 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 b385d05..1f00ac6 100644 --- a/webui/backend/tests/golden/test_api_settings_golden.py +++ b/webui/backend/tests/golden/test_api_settings_golden.py @@ -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: diff --git a/webui/backend/tests/golden/test_ui_smoke_golden.py b/webui/backend/tests/golden/test_ui_smoke_golden.py index b437d51..fafa613 100644 --- a/webui/backend/tests/golden/test_ui_smoke_golden.py +++ b/webui/backend/tests/golden/test_ui_smoke_golden.py @@ -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__": diff --git a/webui/html/app.js b/webui/html/app.js index 6d53bcd..7a32e61 100644 --- a/webui/html/app.js +++ b/webui/html/app.js @@ -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; diff --git a/webui/html/style.css b/webui/html/base.css similarity index 90% rename from webui/html/style.css rename to webui/html/base.css index 0675c58..6c2a127 100644 --- a/webui/html/style.css +++ b/webui/html/base.css @@ -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); diff --git a/webui/html/index.html b/webui/html/index.html index d0c3a5f..e197f90 100644 --- a/webui/html/index.html +++ b/webui/html/index.html @@ -4,7 +4,12 @@ WebManager v2 - + + + + + +
@@ -115,6 +120,10 @@ Theme
diff --git a/webui/html/theme-default.css b/webui/html/theme-default.css new file mode 100644 index 0000000..8de091d --- /dev/null +++ b/webui/html/theme-default.css @@ -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); +} diff --git a/webui/html/theme-graphite.css b/webui/html/theme-graphite.css new file mode 100644 index 0000000..c13d5af --- /dev/null +++ b/webui/html/theme-graphite.css @@ -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); +} diff --git a/webui/html/theme-macos-soft.css b/webui/html/theme-macos-soft.css new file mode 100644 index 0000000..b30bbf1 --- /dev/null +++ b/webui/html/theme-macos-soft.css @@ -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); +} diff --git a/webui/html/theme-midnight.css b/webui/html/theme-midnight.css new file mode 100644 index 0000000..a40f763 --- /dev/null +++ b/webui/html/theme-midnight.css @@ -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); +} diff --git a/webui/html/theme-windows11.css b/webui/html/theme-windows11.css new file mode 100644 index 0000000..0ab4df7 --- /dev/null +++ b/webui/html/theme-windows11.css @@ -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); +}