From 33b70585c13820f2a72ed3c96e2d69a71ed85eac Mon Sep 17 00:00:00 2001 From: Michal Vrba Date: Fri, 30 Jan 2026 16:57:31 +0100 Subject: [PATCH] Add theme toggle with light/dark/auto support Implement a new theme system with a ThemeToggle component, allowing users to switch between light, dark, and auto (system) modes. Move theme CSS variables to app.css and manage theme state via theme.js with localStorage persistence. Integrate ThemeToggle into editor and preview top bars. Clean up old theme logic from MainLayout.razor.css for better separation and maintainability. --- Notemark/Components/App.razor | 7 ++ .../Components/Layout/MainLayout.razor.css | 60 ----------------- Notemark/Components/Pages/Editor.razor | 1 + Notemark/Components/Pages/Editor.razor.css | 1 + Notemark/Components/Pages/SharedEditor.razor | 1 + Notemark/Components/Pages/SharedPreview.razor | 1 + Notemark/Components/Shared/ThemeToggle.razor | 67 +++++++++++++++++++ .../Components/Shared/ThemeToggle.razor.css | 35 ++++++++++ Notemark/Components/_Imports.razor | 1 + Notemark/wwwroot/app.css | 64 +++++++++++++++++- Notemark/wwwroot/js/theme.js | 34 ++++++++++ 11 files changed, 211 insertions(+), 61 deletions(-) create mode 100644 Notemark/Components/Shared/ThemeToggle.razor create mode 100644 Notemark/Components/Shared/ThemeToggle.razor.css create mode 100644 Notemark/wwwroot/js/theme.js diff --git a/Notemark/Components/App.razor b/Notemark/Components/App.razor index 6b64856..81f5fa8 100644 --- a/Notemark/Components/App.razor +++ b/Notemark/Components/App.razor @@ -17,7 +17,14 @@ + + diff --git a/Notemark/Components/Layout/MainLayout.razor.css b/Notemark/Components/Layout/MainLayout.razor.css index 888471b..74aadd5 100644 --- a/Notemark/Components/Layout/MainLayout.razor.css +++ b/Notemark/Components/Layout/MainLayout.razor.css @@ -1,58 +1,3 @@ -/* Notemark CSS Variables for Light/Dark Theme */ -:root { - /* Light Mode (Default) */ - --bg: #F9FAFB; - --surface: #FFFFFF; - --border: #E5E7EB; - --primary-text: #111827; - --secondary-text: #6B7280; - --accent: #4F46E5; - --accent-hover: #4338CA; - --success: #10B981; - --error: #F43F5E; -} - -[data-theme="dark"] { - /* Dark Mode */ - --bg: #0B0F19; - --surface: #111827; - --border: #1F2937; - --primary-text: #F9FAFB; - --secondary-text: #9CA3AF; - --accent: #22D3EE; - --accent-hover: #06B6D4; - --success: #4ADE80; - --error: #FB7185; -} - -/* Auto-detect system preference */ -@media (prefers-color-scheme: dark) { - :root { - --bg: #0B0F19; - --surface: #111827; - --border: #1F2937; - --primary-text: #F9FAFB; - --secondary-text: #9CA3AF; - --accent: #22D3EE; - --accent-hover: #06B6D4; - --success: #4ADE80; - --error: #FB7185; - } -} - -/* Override system dark mode when explicitly set to light */ -[data-theme="light"] { - --bg: #F9FAFB; - --surface: #FFFFFF; - --border: #E5E7EB; - --primary-text: #111827; - --secondary-text: #6B7280; - --accent: #4F46E5; - --accent-hover: #4338CA; - --success: #10B981; - --error: #F43F5E; -} - /* Notemark App Layout */ .notemark-app { height: 100vh; @@ -70,11 +15,6 @@ box-sizing: border-box; } -body { - background-color: var(--bg); - color: var(--primary-text); -} - #blazor-error-ui { color-scheme: light only; background: lightyellow; diff --git a/Notemark/Components/Pages/Editor.razor b/Notemark/Components/Pages/Editor.razor index 9f8be34..eb46d67 100644 --- a/Notemark/Components/Pages/Editor.razor +++ b/Notemark/Components/Pages/Editor.razor @@ -56,6 +56,7 @@ } + diff --git a/Notemark/Components/Pages/Editor.razor.css b/Notemark/Components/Pages/Editor.razor.css index 98a5ab6..f583412 100644 --- a/Notemark/Components/Pages/Editor.razor.css +++ b/Notemark/Components/Pages/Editor.razor.css @@ -24,6 +24,7 @@ .top-bar-right { display: flex; align-items: center; + gap: 0.75rem; } .top-bar-center { diff --git a/Notemark/Components/Pages/SharedEditor.razor b/Notemark/Components/Pages/SharedEditor.razor index 7ca26c8..eefbefc 100644 --- a/Notemark/Components/Pages/SharedEditor.razor +++ b/Notemark/Components/Pages/SharedEditor.razor @@ -48,6 +48,7 @@ else
+
diff --git a/Notemark/Components/Pages/SharedPreview.razor b/Notemark/Components/Pages/SharedPreview.razor index 9713437..7821b04 100644 --- a/Notemark/Components/Pages/SharedPreview.razor +++ b/Notemark/Components/Pages/SharedPreview.razor @@ -42,6 +42,7 @@ else } + diff --git a/Notemark/Components/Shared/ThemeToggle.razor b/Notemark/Components/Shared/ThemeToggle.razor new file mode 100644 index 0000000..b61f32c --- /dev/null +++ b/Notemark/Components/Shared/ThemeToggle.razor @@ -0,0 +1,67 @@ +@inject IJSRuntime JSRuntime + + + +@code { + private string currentTheme = "auto"; + + protected override async Task OnInitializedAsync() + { + try + { + currentTheme = await JSRuntime.InvokeAsync("notemarkTheme.getCurrentTheme"); + } + catch + { + // Fallback if JS not ready + currentTheme = "auto"; + } + } + + private async Task ToggleTheme() + { + // Cycle through: auto -> light -> dark -> auto + currentTheme = currentTheme switch + { + "auto" => "light", + "light" => "dark", + "dark" => "auto", + _ => "auto" + }; + + try + { + await JSRuntime.InvokeVoidAsync("notemarkTheme.setTheme", currentTheme); + } + catch + { + // JS not ready, silently fail + } + + StateHasChanged(); + } + + private string GetThemeIcon() + { + return currentTheme switch + { + "light" => "☀️", + "dark" => "🌙", + "auto" => "🔄", + _ => "🔄" + }; + } + + private string GetThemeTooltip() + { + return currentTheme switch + { + "light" => "Switch to dark mode", + "dark" => "Switch to auto mode", + "auto" => "Switch to light mode", + _ => "Toggle theme" + }; + } +} diff --git a/Notemark/Components/Shared/ThemeToggle.razor.css b/Notemark/Components/Shared/ThemeToggle.razor.css new file mode 100644 index 0000000..eaacd10 --- /dev/null +++ b/Notemark/Components/Shared/ThemeToggle.razor.css @@ -0,0 +1,35 @@ +.theme-toggle { + padding: 0.5rem; + background: none; + border: 1px solid var(--border); + border-radius: 6px; + cursor: pointer; + font-size: 1rem; + line-height: 1; + transition: all 0.2s ease; + display: flex; + align-items: center; + justify-content: center; + min-width: 2.5rem; + height: 2.5rem; +} + +.theme-toggle:hover { + background-color: var(--accent); + border-color: var(--accent); + transform: scale(1.05); +} + +.theme-toggle:active { + transform: scale(0.95); +} + +/* Mobile styles */ +@media (max-width: 768px) { + .theme-toggle { + min-width: 2rem; + height: 2rem; + padding: 0.375rem; + font-size: 0.875rem; + } +} diff --git a/Notemark/Components/_Imports.razor b/Notemark/Components/_Imports.razor index e6c5c93..5e12d67 100644 --- a/Notemark/Components/_Imports.razor +++ b/Notemark/Components/_Imports.razor @@ -9,3 +9,4 @@ @using Notemark @using Notemark.Components @using Notemark.Components.Layout +@using Notemark.Components.Shared diff --git a/Notemark/wwwroot/app.css b/Notemark/wwwroot/app.css index 73a69d6..4aee6c3 100644 --- a/Notemark/wwwroot/app.css +++ b/Notemark/wwwroot/app.css @@ -1,3 +1,65 @@ +/* Notemark Theme Variables */ +:root { + /* Light Mode (Default) */ + --bg: #F9FAFB; + --surface: #FFFFFF; + --border: #E5E7EB; + --primary-text: #111827; + --secondary-text: #6B7280; + --accent: #4F46E5; + --accent-hover: #4338CA; + --success: #10B981; + --error: #F43F5E; +} + +/* Auto-detect system preference for dark mode */ +@media (prefers-color-scheme: dark) { + :root:not([data-theme]) { + --bg: #0B0F19; + --surface: #111827; + --border: #1F2937; + --primary-text: #F9FAFB; + --secondary-text: #9CA3AF; + --accent: #22D3EE; + --accent-hover: #06B6D4; + --success: #4ADE80; + --error: #FB7185; + } +} + +/* Explicit light mode */ +:root[data-theme="light"] { + --bg: #F9FAFB !important; + --surface: #FFFFFF !important; + --border: #E5E7EB !important; + --primary-text: #111827 !important; + --secondary-text: #6B7280 !important; + --accent: #4F46E5 !important; + --accent-hover: #4338CA !important; + --success: #10B981 !important; + --error: #F43F5E !important; +} + +/* Explicit dark mode */ +:root[data-theme="dark"] { + --bg: #0B0F19 !important; + --surface: #111827 !important; + --border: #1F2937 !important; + --primary-text: #F9FAFB !important; + --secondary-text: #9CA3AF !important; + --accent: #22D3EE !important; + --accent-hover: #06B6D4 !important; + --success: #4ADE80 !important; + --error: #FB7185 !important; +} + +/* Apply theme variables globally */ +html, body { + background-color: var(--bg) !important; + color: var(--primary-text) !important; + transition: background-color 0.3s ease, color 0.3s ease; +} + html, body { font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; } @@ -37,7 +99,7 @@ h1:focus { } .blazor-error-boundary { - background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121; + background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA69.NzMwOSAyNjcuODc5IDY5LjU4NzcgMjY3Ljg3OSA2OS4zMTc5IDI2Ny44NzkgNjcuMTEyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121; padding: 1rem 1rem 1rem 3.7rem; color: white; } diff --git a/Notemark/wwwroot/js/theme.js b/Notemark/wwwroot/js/theme.js new file mode 100644 index 0000000..1fd930c --- /dev/null +++ b/Notemark/wwwroot/js/theme.js @@ -0,0 +1,34 @@ +window.notemarkTheme = { + init: function() { + // Get saved theme or default to 'auto' + const savedTheme = localStorage.getItem('notemark-theme') || 'auto'; + this.applyTheme(savedTheme); + return savedTheme; + }, + + setTheme: function(theme) { + localStorage.setItem('notemark-theme', theme); + this.applyTheme(theme); + }, + + applyTheme: function(theme) { + const root = document.documentElement; + + if (theme === 'light') { + root.setAttribute('data-theme', 'light'); + } else if (theme === 'dark') { + root.setAttribute('data-theme', 'dark'); + } else { + // For 'auto', remove data-theme to let CSS @media handle it + root.removeAttribute('data-theme'); + } + }, + + getSystemTheme: function() { + return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; + }, + + getCurrentTheme: function() { + return localStorage.getItem('notemark-theme') || 'auto'; + } +};