From d36948b853f029d670d35d639614e50ca37806d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Jasi=C5=84ski?= Date: Fri, 5 Jun 2026 19:48:02 +0200 Subject: [PATCH] feat(mobile-ux): implement theme toggle, client-side persistence, and light mode style overrides --- src/NexusReader.Maui/wwwroot/index.html | 13 ++ .../Organisms/GlobalIntelligence.razor.css | 205 ++++++++++++++++++ .../Components/Organisms/ReaderCanvas.razor | 23 ++ .../Organisms/ReaderCanvas.razor.css | 88 ++++++++ .../Layout/ReaderLayout.razor | 2 + .../Services/IThemeService.cs | 1 + .../Services/ThemeService.cs | 29 +++ src/NexusReader.UI.Shared/wwwroot/app.css | 4 + src/NexusReader.UI.Shared/wwwroot/js/theme.js | 14 ++ src/NexusReader.Web/Components/App.razor | 14 ++ 10 files changed, 393 insertions(+) create mode 100644 src/NexusReader.UI.Shared/wwwroot/js/theme.js diff --git a/src/NexusReader.Maui/wwwroot/index.html b/src/NexusReader.Maui/wwwroot/index.html index 8570c5a..26a7ed5 100644 --- a/src/NexusReader.Maui/wwwroot/index.html +++ b/src/NexusReader.Maui/wwwroot/index.html @@ -7,6 +7,18 @@ + @@ -73,6 +85,7 @@ } }; + diff --git a/src/NexusReader.UI.Shared/Components/Organisms/GlobalIntelligence.razor.css b/src/NexusReader.UI.Shared/Components/Organisms/GlobalIntelligence.razor.css index 2036e84..bc1502b 100644 --- a/src/NexusReader.UI.Shared/Components/Organisms/GlobalIntelligence.razor.css +++ b/src/NexusReader.UI.Shared/Components/Organisms/GlobalIntelligence.razor.css @@ -543,3 +543,208 @@ from { transform: translateY(20px); opacity: 0; } to { transform: translateY(0); opacity: 1; } } + +/* ========================================================================== + Light Mode Theme Overrides + ========================================================================== */ + +.theme-light .sheet-content { + background: rgba(244, 241, 234, 0.95); /* Matches Premium Paper theme */ + border-top-color: rgba(16, 185, 129, 0.3); + box-shadow: 0 -10px 40px rgba(0, 0, 0, 0.08); +} + +.theme-light .sheet-drag-handle { + background-color: rgba(0, 0, 0, 0.15); +} + +.theme-light .sheet-header { + border-bottom-color: rgba(0, 0, 0, 0.06); +} + +.theme-light .header-info h3 { + color: #2d2a26; +} + +.theme-light .header-info .subtitle { + color: #7c766b; +} + +.theme-light .close-btn { + color: #7c766b; +} + +.theme-light .close-btn:hover { + background-color: rgba(0, 0, 0, 0.05); + color: #2d2a26; +} + +.theme-light .welcome-container h4 { + color: #2d2a26; +} + +.theme-light .welcome-container p { + color: #7c766b; +} + +.theme-light .ai-avatar-badge { + background: linear-gradient(135deg, rgba(16, 185, 129, 0.08) 0%, rgba(6, 182, 212, 0.08) 100%); + border-color: rgba(16, 185, 129, 0.25); +} + +.theme-light .ai-avatar-badge ::deep i { + color: #10b981; +} + +.theme-light .welcome-glow-icon { + background: rgba(16, 185, 129, 0.05); + border-color: rgba(16, 185, 129, 0.25); + box-shadow: 0 0 20px rgba(16, 185, 129, 0.08); +} + +.theme-light .welcome-glow-icon ::deep i { + color: #10b981; +} + +/* Chat bubble styling overrides */ +.theme-light .ai-bubble { + background-color: rgba(255, 255, 255, 0.6); + border-color: rgba(0, 0, 0, 0.05); +} + +.theme-light .ai-bubble .sender-name { + color: #7c766b; +} + +.theme-light .user-bubble { + background-color: rgba(16, 185, 129, 0.06); + border-color: rgba(16, 185, 129, 0.18); +} + +.theme-light .user-bubble .sender-name { + color: #10b981; +} + +.theme-light .message-time { + color: #8c867b; +} + +.theme-light .message-text { + color: #2d2a26; +} + +.theme-light .message-text strong { + color: #1a1917; +} + +/* Code block / Citation styling overrides */ +.theme-light .nexus-mobile-code-block { + background-color: #faf8f5; + border-left-color: #10b981; + color: #2d2a26; +} + +.theme-light .nexus-mobile-inline-code { + background-color: rgba(0, 0, 0, 0.04); + color: #b91c1c; +} + +.theme-light .nexus-mobile-citation { + background-color: rgba(16, 185, 129, 0.08); + border-color: rgba(16, 185, 129, 0.25); + color: #10b981; +} + +.theme-light .typing-indicator span { + background-color: rgba(0, 0, 0, 0.3); +} + +.theme-light .loading-label { + color: #7c766b; +} + +/* Footer / Input overrides */ +.theme-light .sheet-footer { + background-color: rgba(234, 230, 221, 0.6); + border-top-color: rgba(0, 0, 0, 0.06); +} + +.theme-light .scope-indicator { + color: #7c766b; +} + +.theme-light .scope-indicator ::deep i { + color: #8c867b; +} + +.theme-light .nexus-mobile-input { + background-color: rgba(255, 255, 255, 0.85); + border-color: rgba(0, 0, 0, 0.08); + color: #2d2a26; +} + +.theme-light .nexus-mobile-input:focus { + border-color: rgba(16, 185, 129, 0.4); + background-color: #ffffff; + box-shadow: 0 0 8px rgba(16, 185, 129, 0.15); +} + +.theme-light .send-btn.disabled { + background: rgba(0, 0, 0, 0.04); + color: rgba(0, 0, 0, 0.25); +} + +/* Citation modal overrides */ +.theme-light .citation-modal { + background: #ffffff; + border-color: rgba(0, 0, 0, 0.06); + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.08); +} + +.theme-light .citation-modal .modal-header { + border-bottom-color: rgba(0, 0, 0, 0.06); +} + +.theme-light .citation-modal .book-title { + color: #2d2a26; +} + +.theme-light .citation-modal .book-title ::deep i { + color: #10b981; +} + +.theme-light .citation-modal .modal-body { + color: #2d2a26; +} + +.theme-light .citation-modal .citation-author, +.theme-light .citation-modal .citation-page { + color: #7c766b; +} + +.theme-light .citation-modal .citation-author strong, +.theme-light .citation-modal .citation-page strong { + color: #2d2a26; +} + +.theme-light .citation-modal .citation-snippet { + background: rgba(16, 185, 129, 0.04); + border-left-color: #10b981; + color: #2d2a26; +} + +.theme-light .citation-modal .modal-footer { + border-top-color: rgba(0, 0, 0, 0.06); +} + +.theme-light .citation-modal .btn-nexus { + background: linear-gradient(135deg, rgba(16, 185, 129, 0.08) 0%, rgba(6, 182, 212, 0.08) 100%); + border-color: rgba(16, 185, 129, 0.3); + color: #10b981; +} + +.theme-light .citation-modal .btn-nexus:hover { + background: linear-gradient(135deg, rgba(16, 185, 129, 0.15) 0%, rgba(6, 182, 212, 0.15) 100%); + border-color: rgba(16, 185, 129, 0.5); + box-shadow: 0 0 10px rgba(16, 185, 129, 0.15); +} diff --git a/src/NexusReader.UI.Shared/Components/Organisms/ReaderCanvas.razor b/src/NexusReader.UI.Shared/Components/Organisms/ReaderCanvas.razor index 736442a..b13e133 100644 --- a/src/NexusReader.UI.Shared/Components/Organisms/ReaderCanvas.razor +++ b/src/NexusReader.UI.Shared/Components/Organisms/ReaderCanvas.razor @@ -39,6 +39,29 @@ + + } diff --git a/src/NexusReader.UI.Shared/Components/Organisms/ReaderCanvas.razor.css b/src/NexusReader.UI.Shared/Components/Organisms/ReaderCanvas.razor.css index 1916fb3..6876cdd 100644 --- a/src/NexusReader.UI.Shared/Components/Organisms/ReaderCanvas.razor.css +++ b/src/NexusReader.UI.Shared/Components/Organisms/ReaderCanvas.razor.css @@ -416,6 +416,7 @@ gap: 0.25rem; height: 100%; min-width: 0; + margin-right: 40px; /* Space to prevent overlap with absolute theme toggle button */ } .nexus-mobile-chapter-title { @@ -479,4 +480,91 @@ .theme-light .nexus-chapter-nav-btn:hover:not(:disabled) { background: rgba(0, 0, 0, 0.06); +} + +/* Minimalist Theme Toggle Button with Glassmorphism */ +.nexus-theme-toggle-btn { + position: absolute; + right: 1rem; + top: 50%; + transform: translateY(-50%); + width: 36px; + height: 36px; + border-radius: 50%; + border: 1px solid rgba(255, 255, 255, 0.08); + background: rgba(255, 255, 255, 0.03); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + z-index: 1001; + padding: 0; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); +} + +.nexus-theme-toggle-btn:hover { + background: rgba(255, 255, 255, 0.08); + border-color: rgba(255, 255, 255, 0.15); + transform: translateY(-50%) scale(1.05); +} + +.nexus-theme-toggle-btn:active { + transform: translateY(-50%) scale(0.95); +} + +/* Theme specifics for Light Mode */ +.theme-light .nexus-theme-toggle-btn { + border-color: rgba(0, 0, 0, 0.08); + background: rgba(0, 0, 0, 0.03); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05); +} + +.theme-light .nexus-theme-toggle-btn:hover { + background: rgba(0, 0, 0, 0.06); + border-color: rgba(0, 0, 0, 0.12); +} + +/* Icon Styling and Transition */ +.theme-toggle-icon { + width: 18px; + height: 18px; + stroke-width: 2px; +} + +/* In Dark Mode, the icon should be brand green (#10b981) */ +.theme-toggle-icon.moon { + color: #10b981; + filter: drop-shadow(0 0 4px rgba(16, 185, 129, 0.3)); + animation: morphMoon 0.4s cubic-bezier(0.4, 0, 0.2, 1) forwards; +} + +/* In Light Mode, the icon should be a dark earthy gray (#2d2a26) */ +.theme-toggle-icon.sun { + color: #2d2a26; + animation: morphSun 0.4s cubic-bezier(0.4, 0, 0.2, 1) forwards; +} + +@keyframes morphMoon { + 0% { + transform: rotate(-45deg) scale(0.7); + opacity: 0; + } + 100% { + transform: rotate(0deg) scale(1); + opacity: 1; + } +} + +@keyframes morphSun { + 0% { + transform: rotate(45deg) scale(0.7); + opacity: 0; + } + 100% { + transform: rotate(0deg) scale(1); + opacity: 1; + } } \ No newline at end of file diff --git a/src/NexusReader.UI.Shared/Layout/ReaderLayout.razor b/src/NexusReader.UI.Shared/Layout/ReaderLayout.razor index 97633c3..4b6744d 100644 --- a/src/NexusReader.UI.Shared/Layout/ReaderLayout.razor +++ b/src/NexusReader.UI.Shared/Layout/ReaderLayout.razor @@ -458,6 +458,8 @@ { if (firstRender) { + await ThemeService.InitializeAsync(); + try { var module = await JS.InvokeAsync("import", "./_content/NexusReader.UI.Shared/js/layoutResizer.js"); diff --git a/src/NexusReader.UI.Shared/Services/IThemeService.cs b/src/NexusReader.UI.Shared/Services/IThemeService.cs index 1b20eff..5b75973 100644 --- a/src/NexusReader.UI.Shared/Services/IThemeService.cs +++ b/src/NexusReader.UI.Shared/Services/IThemeService.cs @@ -4,5 +4,6 @@ public interface IThemeService { bool IsLightMode { get; } event Func? OnThemeChanged; + Task InitializeAsync(); Task ToggleTheme(); } diff --git a/src/NexusReader.UI.Shared/Services/ThemeService.cs b/src/NexusReader.UI.Shared/Services/ThemeService.cs index 2223346..5778ed3 100644 --- a/src/NexusReader.UI.Shared/Services/ThemeService.cs +++ b/src/NexusReader.UI.Shared/Services/ThemeService.cs @@ -1,13 +1,42 @@ +using Microsoft.JSInterop; + namespace NexusReader.UI.Shared.Services; public sealed class ThemeService : IThemeService { + private readonly IJSRuntime _jsRuntime; public bool IsLightMode { get; private set; } = false; public event Func? OnThemeChanged; + public ThemeService(IJSRuntime jsRuntime) + { + _jsRuntime = jsRuntime; + } + + public async Task InitializeAsync() + { + try + { + IsLightMode = await _jsRuntime.InvokeAsync("themeInterop.isLightMode"); + if (OnThemeChanged != null) await OnThemeChanged(); + } + catch + { + // Fail silently during prerendering or if JS is not available yet + } + } + public async Task ToggleTheme() { IsLightMode = !IsLightMode; + try + { + await _jsRuntime.InvokeVoidAsync("themeInterop.setLightMode", IsLightMode); + } + catch + { + // Fail silently + } if (OnThemeChanged != null) await OnThemeChanged(); } } diff --git a/src/NexusReader.UI.Shared/wwwroot/app.css b/src/NexusReader.UI.Shared/wwwroot/app.css index cbe822d..b93e6a0 100644 --- a/src/NexusReader.UI.Shared/wwwroot/app.css +++ b/src/NexusReader.UI.Shared/wwwroot/app.css @@ -283,6 +283,10 @@ body { overflow-x: hidden; } +body, .reader-canvas, .reader-flow-container, .nexus-mobile-reader-header, .nexus-mobile-chapter-title, .nexus-mobile-escape-btn { + transition: background-color 0.4s ease, color 0.4s ease, border-color 0.4s ease, box-shadow 0.4s ease; +} + /* Modern Scrollbars */ ::-webkit-scrollbar { width: 6px; diff --git a/src/NexusReader.UI.Shared/wwwroot/js/theme.js b/src/NexusReader.UI.Shared/wwwroot/js/theme.js new file mode 100644 index 0000000..0c1af2c --- /dev/null +++ b/src/NexusReader.UI.Shared/wwwroot/js/theme.js @@ -0,0 +1,14 @@ +window.themeInterop = { + isLightMode: function () { + return document.documentElement.classList.contains('theme-light'); + }, + setLightMode: function (isLight) { + if (isLight) { + document.documentElement.classList.add('theme-light'); + localStorage.setItem('theme', 'light'); + } else { + document.documentElement.classList.remove('theme-light'); + localStorage.setItem('theme', 'dark'); + } + } +}; diff --git a/src/NexusReader.Web/Components/App.razor b/src/NexusReader.Web/Components/App.razor index 4d84118..55e6932 100644 --- a/src/NexusReader.Web/Components/App.razor +++ b/src/NexusReader.Web/Components/App.razor @@ -9,6 +9,19 @@ + + @@ -42,6 +55,7 @@ setTimeout(hidePreloader, 3000); })(); +