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.
This commit is contained in:
Michal Vrba
2026-01-30 16:57:31 +01:00
parent 9dcf25f6b8
commit 33b70585c1
11 changed files with 211 additions and 61 deletions
+7
View File
@@ -17,7 +17,14 @@
<body>
<Routes />
<ReconnectModal />
<script src="js/theme.js"></script>
<script src="@Assets["_framework/blazor.web.js"]"></script>
<script>
// Initialize theme on page load
document.addEventListener('DOMContentLoaded', function() {
notemarkTheme.init();
});
</script>
</body>
</html>
@@ -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;
+1
View File
@@ -56,6 +56,7 @@
</div>
}
</div>
<ThemeToggle />
</div>
</div>
@@ -24,6 +24,7 @@
.top-bar-right {
display: flex;
align-items: center;
gap: 0.75rem;
}
.top-bar-center {
@@ -48,6 +48,7 @@ else
<div class="top-bar-right">
<button class="btn-view" @onclick="GoToPreview">👁️ View</button>
<button class="btn-home" @onclick="GoHome">New Doc</button>
<ThemeToggle />
</div>
</div>
@@ -42,6 +42,7 @@ else
<button class="btn-edit" @onclick="GoToEditor">✏️ Edit</button>
}
<button class="btn-home" @onclick="GoHome">Create New</button>
<ThemeToggle />
</div>
</div>
@@ -0,0 +1,67 @@
@inject IJSRuntime JSRuntime
<button class="theme-toggle" @onclick="ToggleTheme" title="@GetThemeTooltip()">
@GetThemeIcon()
</button>
@code {
private string currentTheme = "auto";
protected override async Task OnInitializedAsync()
{
try
{
currentTheme = await JSRuntime.InvokeAsync<string>("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"
};
}
}
@@ -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;
}
}
+1
View File
@@ -9,3 +9,4 @@
@using Notemark
@using Notemark.Components
@using Notemark.Components.Layout
@using Notemark.Components.Shared
+63 -1
View File
@@ -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;
}
+34
View File
@@ -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';
}
};