From f6819d50b7fa1c812ae3f6ab95c04137c3a537ea Mon Sep 17 00:00:00 2001 From: Antigravity Date: Fri, 5 Jun 2026 18:02:33 +0000 Subject: [PATCH] feat(mobile-ux): implement theme toggle, client-side persistence, and light mode style overrides (#71) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolves #72 ## Description This PR implements the theme toggle mechanism, client-side persistence, and comprehensive light mode style overrides for the mobile reader layout in **NexusReader**. ### Key Changes 1. **ThemeService & State Management**: - Enhanced `ThemeService` with `InitializeAsync` and `ToggleTheme` using JS Interop. - Synchronized theme state changes via `OnThemeChanged` event. 2. **Local Storage Persistence & FOUC Prevention**: - Added client-side script in `App.razor` and MAUI `index.html` to instantly apply `.theme-light` from `localStorage` before prerendering/rendering to prevent a Flash of Unthemed Content (FOUC). - Created `theme.js` containing JS helper methods (`themeInterop.isLightMode`, `themeInterop.setLightMode`) to interface with `localStorage` and `document.documentElement` class list. 3. **UI Theme Toggle**: - Added a minimalist glassmorphic theme toggle button in the header of the `ReaderCanvas.razor` with dynamic transitions and icon morphing between sun and moon SVG icons. 4. **Light Mode Stylesheets Overrides**: - Added detailed light mode scoped styles (`.theme-light`) for the `ReaderCanvas` and the bottom-sheet `GlobalIntelligence` AI chat panel, utilizing earthy gray text (`#2d2a26`) on warm paper-like backgrounds (`rgba(244, 241, 234, 0.95)` / `#faf8f5`) to maintain high-quality aesthetic consistency. ### Verification - Solution built successfully without errors (`dotnet build NexusReader.slnx --no-restore`). - Scoped CSS isolation checked and validated for both Web and MAUI contexts. --------- Co-authored-by: Marek Jasiński Reviewed-on: https://git.archimap.cloud/mjasin/Nexus.Reader/pulls/71 Co-authored-by: Antigravity Co-committed-by: Antigravity --- src/NexusReader.Maui/wwwroot/index.html | 13 + .../Organisms/GlobalIntelligence.razor.css | 205 ++++++++++++++++ .../Organisms/MobileReaderToolbar.razor | 89 ++++--- .../Organisms/MobileReaderToolbar.razor.css | 225 ++++++++++-------- .../Components/Organisms/ReaderCanvas.razor | 33 ++- .../Organisms/ReaderCanvas.razor.css | 106 ++++++++- .../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 ++ 12 files changed, 602 insertions(+), 133 deletions(-) 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/MobileReaderToolbar.razor b/src/NexusReader.UI.Shared/Components/Organisms/MobileReaderToolbar.razor index 168cd08..8d1bc0e 100644 --- a/src/NexusReader.UI.Shared/Components/Organisms/MobileReaderToolbar.razor +++ b/src/NexusReader.UI.Shared/Components/Organisms/MobileReaderToolbar.razor @@ -3,56 +3,58 @@ @using NexusReader.UI.Shared.Models @using NexusReader.Application.Utilities @namespace NexusReader.UI.Shared.Components.Organisms +@implements IDisposable @inject IReaderInteractionService InteractionService @inject IReaderStateService StateService +@inject IThemeService ThemeService -
- -
+
+ +
+ Postęp + - -
+ + + + + - -
- - - -
+ + + + +
@@ -107,11 +109,17 @@ private bool IsCheckpointsOpen { get; set; } + protected override void OnInitialized() + { + ThemeService.OnThemeChanged += HandleThemeChanged; + } + + private Task HandleThemeChanged() => InvokeAsync(StateHasChanged); private double GetDashOffset() { - // Circumference of r=16 is 2 * pi * 16 = 100.53 - double circumference = 100.53; + // Circumference of r=12 is 2 * pi * 12 = 75.40 + double circumference = 75.40; double progress = Math.Clamp(ScrollPercentage, 0, 100); return circumference - (progress / 100.0) * circumference; } @@ -148,4 +156,9 @@ await OnAssistantClick.InvokeAsync(); } } + + public void Dispose() + { + ThemeService.OnThemeChanged -= HandleThemeChanged; + } } diff --git a/src/NexusReader.UI.Shared/Components/Organisms/MobileReaderToolbar.razor.css b/src/NexusReader.UI.Shared/Components/Organisms/MobileReaderToolbar.razor.css index e8569c3..092943c 100644 --- a/src/NexusReader.UI.Shared/Components/Organisms/MobileReaderToolbar.razor.css +++ b/src/NexusReader.UI.Shared/Components/Organisms/MobileReaderToolbar.razor.css @@ -4,38 +4,75 @@ left: 16px; right: 16px; height: 64px; - background: rgba(18, 18, 18, 0.75); - backdrop-filter: blur(24px); - -webkit-backdrop-filter: blur(24px); - border: 1px solid rgba(0, 255, 153, 0.2); border-radius: 16px; - display: grid; - grid-template-columns: 1fr auto 1fr; + display: flex; + justify-content: space-between; align-items: center; - padding: 0 1rem; + padding: 0 0.5rem; z-index: 1000; - box-shadow: 0 8px 30px rgba(0, 0, 0, 0.4); box-sizing: border-box; font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; + overflow: visible; /* Critical to show elevated FAB */ } -.toolbar-slot { +/* Light Mode: Premium Paper Look */ +.nexus-unified-mobile-toolbar.theme-light { + background: rgba(244, 241, 234, 0.9); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + border: 1px solid rgba(139, 130, 115, 0.18); + box-shadow: 0 8px 30px rgba(139, 130, 115, 0.15); +} + +/* Dark Mode: Translucent Slate */ +.nexus-unified-mobile-toolbar.theme-dark { + background: rgba(18, 18, 18, 0.8); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + border: 1px solid rgba(255, 255, 255, 0.08); + box-shadow: 0 8px 30px rgba(0, 0, 0, 0.5); +} + +.nav-toggle-btn { + flex: 1; display: flex; + flex-direction: column; align-items: center; + justify-content: center; + background: none; + border: none; + color: #8b8273; /* Inactive items earthy gray */ + padding: 6px 0; + cursor: pointer; + transition: all 0.2s ease; + min-width: 0; + position: relative; + height: 100%; } -/* LEFT SLOT: Progress circular ring */ -.left-slot { - justify-content: flex-start; - gap: 0.65rem; - cursor: pointer; - user-select: none; +.nav-toggle-btn.active { + color: #10b981; /* Active items vibrant green */ +} + +.nav-toggle-btn ::deep .nexus-icon { + transition: transform 0.2s ease; +} + +.nav-toggle-btn.active ::deep .nexus-icon { + transform: scale(1.1); +} + +.tab-label { + font-size: 11px; + font-weight: 500; + margin-top: 4px; + white-space: nowrap; } .progress-ring-wrapper { position: relative; - width: 38px; - height: 38px; + width: 28px; + height: 28px; display: flex; align-items: center; justify-content: center; @@ -45,65 +82,53 @@ transform: rotate(-90deg); } -.progress-ring-indicator { - transition: stroke-dashoffset 0.35s cubic-bezier(0.4, 0, 0.2, 1); -} - .progress-text { position: absolute; - font-size: 0.65rem; - font-weight: 700; - color: #FFFFFF; -} - -.progress-info { - display: flex; - flex-direction: column; -} - -.slot-label { - font-size: 0.75rem; - font-weight: 600; - color: #FFFFFF; -} - -.slot-desc { font-size: 0.6rem; - color: rgba(255,255,255,0.4); + font-weight: 700; + color: #8b8273; } -/* CENTER SLOT: Glowing AI Core Button */ -.center-slot { - justify-content: center; - position: relative; +.nav-toggle-btn.active .progress-text { + color: #10b981; +} + +/* Center AI FAB container & button */ +.center-ai-container { + overflow: visible; } .btn-nexus-ai-core { - width: 52px; - height: 52px; + width: 48px; + height: 48px; border-radius: 50%; - background: linear-gradient(135deg, #00FF99 0%, #00F0FF 100%); + background: linear-gradient(135deg, #10b981 0%, #059669 100%); border: none; display: flex; align-items: center; justify-content: center; - color: #0B0C10; + color: #ffffff; cursor: pointer; - position: relative; + position: absolute; + top: -20px; + left: 50%; + transform: translateX(-50%); z-index: 5; - box-shadow: 0 0 20px rgba(0, 255, 153, 0.4); - transform: translateY(-8px); + box-shadow: 0 4px 14px rgba(16, 185, 129, 0.4); transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275); } .btn-nexus-ai-core:active { - transform: translateY(-6px) scale(0.95); - box-shadow: 0 0 10px rgba(0, 255, 153, 0.3); + transform: translateX(-50%) translateY(-18px) scale(0.95); + box-shadow: 0 2px 8px rgba(16, 185, 129, 0.3); } .ai-core-icon { - color: #0b0c10; - filter: drop-shadow(0 1px 2px rgba(0,0,0,0.2)); + color: #ffffff !important; +} + +.center-ai-container .tab-label { + margin-top: 32px; } /* Pulse effects */ @@ -114,7 +139,7 @@ right: -4px; bottom: -4px; border-radius: 50%; - border: 2px solid rgba(0, 255, 153, 0.4); + border: 2px solid rgba(16, 185, 129, 0.4); opacity: 0; animation: corePulse 2s cubic-bezier(0.24, 0, 0.38, 1) infinite; pointer-events: none; @@ -128,7 +153,7 @@ right: -8px; bottom: -8px; border-radius: 50%; - border: 1px solid rgba(0, 240, 255, 0.2); + border: 1px solid rgba(16, 185, 129, 0.2); opacity: 0; animation: corePulseOuter 2.5s cubic-bezier(0.24, 0, 0.38, 1) infinite; pointer-events: none; @@ -147,43 +172,6 @@ 100% { transform: scale(1.25); opacity: 0; } } -/* RIGHT SLOT: Layout Switching */ -.right-slot { - justify-content: flex-end; - gap: 0.35rem; -} - -.nav-toggle-btn { - background: none; - border: none; - display: flex; - flex-direction: column; - align-items: center; - gap: 2px; - padding: 6px 8px; - border-radius: 8px; - color: rgba(255, 255, 255, 0.45); - cursor: pointer; - transition: all 0.25s ease; -} - -.nav-toggle-btn.active { - color: var(--nexus-neon, #00FF99); - background-color: rgba(0, 255, 153, 0.06); -} - -.nav-toggle-btn ::deep .nexus-icon { - transition: transform 0.2s ease; -} - -.nav-toggle-btn.active ::deep .nexus-icon { - transform: scale(1.08); -} - -.nav-toggle-btn span { - font-size: 0.6rem; - font-weight: 500; -} /* SECTION CHECKPOINTS OVERLAY */ .checkpoints-overlay { @@ -360,3 +348,54 @@ .checkpoint-item:active .arrow-icon { transform: translateX(2px); } + +/* Light Mode overrides for Checkpoints Overlay */ +.theme-light .checkpoints-sheet { + background: rgba(244, 241, 234, 0.95); + border-top: 1px solid rgba(139, 130, 115, 0.15); + box-shadow: 0 -8px 30px rgba(139, 130, 115, 0.15); +} + +.theme-light .checkpoints-header { + border-bottom: 1px solid rgba(139, 130, 115, 0.1); +} + +.theme-light .checkpoints-header h4 { + color: #292524; +} + +.theme-light .close-checkpoints-btn { + color: #8b8273; +} + +.theme-light .checkpoint-item { + background-color: rgba(139, 130, 115, 0.03); + border: 1px solid rgba(139, 130, 115, 0.06); +} + +.theme-light .checkpoint-item.active { + background-color: rgba(16, 185, 129, 0.08); + border-color: rgba(16, 185, 129, 0.25); +} + +.theme-light .checkpoint-item.active .checkpoint-id { + color: #10b981; +} + +.theme-light .checkpoint-item.active .indicator-dot { + background-color: #10b981; + box-shadow: 0 0 8px rgba(16, 185, 129, 0.6); +} + +.theme-light .checkpoint-id { + color: #292524; +} + +.theme-light .checkpoint-label { + color: #8b8273; +} + +.theme-light .arrow-icon { + color: #8b8273; +} + diff --git a/src/NexusReader.UI.Shared/Components/Organisms/ReaderCanvas.razor b/src/NexusReader.UI.Shared/Components/Organisms/ReaderCanvas.razor index 736442a..9467195 100644 --- a/src/NexusReader.UI.Shared/Components/Organisms/ReaderCanvas.razor +++ b/src/NexusReader.UI.Shared/Components/Organisms/ReaderCanvas.razor @@ -39,6 +39,29 @@
+ + } @@ -409,8 +432,16 @@ { try { + // Ensure the JS module is loaded and the component is fully rendered before invoking interop. + if (!_isJsInitialized) + { + await InitViewportDetectionAsync(); + } var module = await EnsureViewportModuleAsync(); - await module.InvokeVoidAsync("scrollToTop", ".reader-canvas"); + if (module != null) + { + await module.InvokeVoidAsync("scrollToTop", ".reader-canvas"); + } } catch (Exception ex) { diff --git a/src/NexusReader.UI.Shared/Components/Organisms/ReaderCanvas.razor.css b/src/NexusReader.UI.Shared/Components/Organisms/ReaderCanvas.razor.css index 6b00afe..6876cdd 100644 --- a/src/NexusReader.UI.Shared/Components/Organisms/ReaderCanvas.razor.css +++ b/src/NexusReader.UI.Shared/Components/Organisms/ReaderCanvas.razor.css @@ -340,7 +340,7 @@ @media (max-width: 768px) { .reader-canvas { padding-top: 54px !important; - padding-bottom: 80px !important; + padding-bottom: 120px !important; /* Ensure content is clear of bottom toolbar */ } @@ -350,6 +350,22 @@ } } +@media (max-width: 767px) { + ::deep .nexus-ebook, + ::deep .nexus-ebook p { + font-size: 16px !important; + line-height: 1.55 !important; + } + + ::deep .nexus-ebook h1 { + font-size: 1.35rem !important; + line-height: 1.4 !important; + margin-top: 1.5rem !important; + margin-bottom: 1rem !important; + } +} + + .nexus-mobile-reader-header { position: fixed; top: 0; @@ -400,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 { @@ -463,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); })(); +