Merge branch 'system-theme-pair' into 'main'

This commit is contained in:
Yuri Karamian
2026-08-06 14:07:34 +02:00
12 changed files with 183 additions and 36 deletions
+9
View File
@@ -331,6 +331,15 @@ pub fn set_theme(state: State<'_, AppState>, theme: String) -> Result<(), String
Ok(()) Ok(())
} }
#[tauri::command]
pub fn set_system_themes(state: State<'_, AppState>, light: String, dark: String) -> Result<(), String> {
let mut config = state.config.lock().map_err(|e| e.to_string())?;
config.system_light_theme = light;
config.system_dark_theme = dark;
save_app_config(&config)?;
Ok(())
}
#[tauri::command] #[tauri::command]
pub fn set_accent_color(state: State<'_, AppState>, color: String) -> Result<(), String> { pub fn set_accent_color(state: State<'_, AppState>, color: String) -> Result<(), String> {
let mut config = state.config.lock().map_err(|e| e.to_string())?; let mut config = state.config.lock().map_err(|e| e.to_string())?;
+1
View File
@@ -130,6 +130,7 @@ pub fn run() {
commands::remove_vault, commands::remove_vault,
commands::get_app_config, commands::get_app_config,
commands::set_theme, commands::set_theme,
commands::set_system_themes,
commands::set_accent_color, commands::set_accent_color,
commands::save_custom_theme, commands::save_custom_theme,
commands::delete_custom_theme, commands::delete_custom_theme,
+16
View File
@@ -114,6 +114,12 @@ pub struct AppConfig {
#[serde(default)] #[serde(default)]
pub active_bookmark_id: Option<String>, pub active_bookmark_id: Option<String>,
pub theme: String, pub theme: String,
/// Themes used when `theme` is "system": the frontend picks one by the OS color scheme.
/// Default to the plain "light"/"dark" schemes so existing configs keep their behavior.
#[serde(default = "default_system_light_theme")]
pub system_light_theme: String,
#[serde(default = "default_system_dark_theme")]
pub system_dark_theme: String,
#[serde(default)] #[serde(default)]
pub accent_color: Option<String>, pub accent_color: Option<String>,
#[serde(default)] #[serde(default)]
@@ -269,6 +275,14 @@ fn default_ai_model() -> String {
"claude-sonnet-4-6".to_string() "claude-sonnet-4-6".to_string()
} }
fn default_system_light_theme() -> String {
"light".to_string()
}
fn default_system_dark_theme() -> String {
"dark".to_string()
}
impl Default for AppConfig { impl Default for AppConfig {
fn default() -> Self { fn default() -> Self {
Self { Self {
@@ -276,6 +290,8 @@ impl Default for AppConfig {
active_vault: None, active_vault: None,
active_bookmark_id: None, active_bookmark_id: None,
theme: "system".to_string(), theme: "system".to_string(),
system_light_theme: default_system_light_theme(),
system_dark_theme: default_system_dark_theme(),
accent_color: None, accent_color: None,
font_size: None, font_size: None,
font_family: None, font_family: None,
+4
View File
@@ -43,6 +43,10 @@ export async function setTheme(theme: string): Promise<void> {
return invoke("set_theme", { theme }); return invoke("set_theme", { theme });
} }
export async function setSystemThemes(light: string, dark: string): Promise<void> {
return invoke("set_system_themes", { light, dark });
}
export async function setAccentColor(color: string): Promise<void> { export async function setAccentColor(color: string): Promise<void> {
return invoke("set_accent_color", { color }); return invoke("set_accent_color", { color });
} }
+6 -7
View File
@@ -21,6 +21,7 @@
showSearch, showSearch,
showCommandPalette, showCommandPalette,
theme, theme,
resolvedTheme,
focusMode, focusMode,
readOnly, readOnly,
activeNote, activeNote,
@@ -507,7 +508,7 @@
createAndFocusNote(); createAndFocusNote();
return; return;
case 'toggle-theme': { case 'toggle-theme': {
const isDark = $theme === 'dark' || ($theme === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches); const isDark = darkThemes.includes($resolvedTheme);
const next = isDark ? 'light' : 'dark'; const next = isDark ? 'light' : 'dark';
$theme = next; $theme = next;
setTheme(next); setTheme(next);
@@ -573,7 +574,7 @@
if (namedThemes.includes(t)) { if (namedThemes.includes(t)) {
root.setAttribute('data-theme', t); root.setAttribute('data-theme', t);
if (darkThemes.includes(t)) root.classList.add('dark'); if (darkThemes.includes(t)) root.classList.add('dark');
} else if (t === 'dark' || (t === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches)) { } else if (t === 'dark') {
root.classList.add('dark'); root.classList.add('dark');
} }
} }
@@ -663,11 +664,9 @@
prefetchPromise = readNote(lastNotePath).catch(() => null); prefetchPromise = readNote(lastNotePath).catch(() => null);
} }
applyTheme($theme); // One-off apply to avoid a flash before the layout's reactive effect runs; OS appearance
// changes are picked up by resolvedTheme, so no media-query listener is needed here.
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => { applyTheme($resolvedTheme);
if ($theme === 'system') applyTheme('system');
});
// Run sidebar and note list refresh in parallel // Run sidebar and note list refresh in parallel
await Promise.all([sidebar?.refresh(), noteList?.refresh()]); await Promise.all([sidebar?.refresh(), noteList?.refresh()]);
+3 -3
View File
@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { showCommandPalette, showSearch, theme, sourceMode, viewMode, activeNotebook, activeTag } from '$lib/stores/app'; import { showCommandPalette, showSearch, theme, resolvedTheme, sourceMode, viewMode, activeNotebook, activeTag } from '$lib/stores/app';
import { setTheme, reindex } from '$lib/api'; import { setTheme, reindex } from '$lib/api';
import { darkThemes } from '$lib/platform'; import { darkThemes } from '$lib/platform';
@@ -89,7 +89,7 @@
action: () => { action: () => {
$theme = 'system'; $theme = 'system';
setTheme('system'); setTheme('system');
applyTheme('system'); applyTheme($resolvedTheme);
$showCommandPalette = false; $showCommandPalette = false;
} }
}, },
@@ -187,7 +187,7 @@
if (namedThemes.includes(t)) { if (namedThemes.includes(t)) {
root.setAttribute('data-theme', t); root.setAttribute('data-theme', t);
if (darkThemes.includes(t)) root.classList.add('dark'); if (darkThemes.includes(t)) root.classList.add('dark');
} else if (t === 'dark' || (t === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches)) { } else if (t === 'dark') {
root.classList.add('dark'); root.classList.add('dark');
} }
} }
+3 -3
View File
@@ -9,7 +9,7 @@
editorDirty, editorDirty,
readOnly, readOnly,
sourceMode, sourceMode,
theme resolvedTheme
} from '$lib/stores/app'; } from '$lib/stores/app';
import { readNote } from '$lib/api'; import { readNote } from '$lib/api';
import { keybindings, matchAction } from '$lib/keybindings'; import { keybindings, matchAction } from '$lib/keybindings';
@@ -37,7 +37,7 @@
}); });
$effect(() => { $effect(() => {
const t = $theme || 'system'; const t = $resolvedTheme || 'light';
const namedThemes = ['solarized-light', 'solarized-dark', 'catppuccin', 'nord', 'tokyo-night', 'github-light', 'github-dark', 'dracula', 'blueberry', 'forest-green', 'gruvbox', 'midnight-tide', 'cherry-blossom', 'synthwave', 'ember', 'moonlit', 'light-coffee', 'dark-coffee', 'cotton-candy', 'crimson', 'cloud', 'peach', 'material-dark', 'material-light', 'monokai', 'rose-pine', 'everforest', 'horizon', 'cyberpunk', 'black', 'one-dark']; const namedThemes = ['solarized-light', 'solarized-dark', 'catppuccin', 'nord', 'tokyo-night', 'github-light', 'github-dark', 'dracula', 'blueberry', 'forest-green', 'gruvbox', 'midnight-tide', 'cherry-blossom', 'synthwave', 'ember', 'moonlit', 'light-coffee', 'dark-coffee', 'cotton-candy', 'crimson', 'cloud', 'peach', 'material-dark', 'material-light', 'monokai', 'rose-pine', 'everforest', 'horizon', 'cyberpunk', 'black', 'one-dark'];
const root = document.documentElement; const root = document.documentElement;
root.classList.remove('dark'); root.classList.remove('dark');
@@ -45,7 +45,7 @@
if (namedThemes.includes(t)) { if (namedThemes.includes(t)) {
root.setAttribute('data-theme', t); root.setAttribute('data-theme', t);
if (darkThemes.includes(t)) root.classList.add('dark'); if (darkThemes.includes(t)) root.classList.add('dark');
} else if (t === 'dark' || (t === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches)) { } else if (t === 'dark') {
root.classList.add('dark'); root.classList.add('dark');
} }
}); });
+107 -15
View File
@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { showSettings, theme, appConfig, activeVaultConfig, updateAvailable as globalUpdateAvailable, updateObj as globalUpdateObj, installType, settingsTab, vaultReady, androidApkUrl, checkForUpdateMobile, notebookSortMode, isManagedInstall, customThemes } from '$lib/stores/app'; import { showSettings, theme, resolvedTheme, appConfig, activeVaultConfig, updateAvailable as globalUpdateAvailable, updateObj as globalUpdateObj, installType, settingsTab, vaultReady, androidApkUrl, checkForUpdateMobile, notebookSortMode, isManagedInstall, customThemes } from '$lib/stores/app';
import { setTheme, setAccentColor, setFontSize, setFontFamily, setLineHeight, setUiScale, setContentWidth, setGeneralSettings, importObsidian, createBackup, listBackups, restoreBackup, deleteBackup, setBackupSettings, setAiSettings, testAiConnection, setSyncSettings, testSyncConnection, syncNow, getAppConfig, saveCustomTheme, deleteCustomTheme, exportCustomTheme, importCustomThemes } from '$lib/api'; import { setTheme, setSystemThemes, setAccentColor, setFontSize, setFontFamily, setLineHeight, setUiScale, setContentWidth, setGeneralSettings, importObsidian, createBackup, listBackups, restoreBackup, deleteBackup, setBackupSettings, setAiSettings, testAiConnection, setSyncSettings, testSyncConnection, syncNow, getAppConfig, saveCustomTheme, deleteCustomTheme, exportCustomTheme, importCustomThemes } from '$lib/api';
import { darkThemes, isMobile, isAndroid } from '$lib/platform'; import { darkThemes, isMobile, isAndroid } from '$lib/platform';
import { open as openDialog, save as saveDialog } from '@tauri-apps/plugin-dialog'; import { open as openDialog, save as saveDialog } from '@tauri-apps/plugin-dialog';
import { listen } from '@tauri-apps/api/event'; import { listen } from '@tauri-apps/api/event';
@@ -418,11 +418,12 @@
} }
let isThemeDark = $derived( let isThemeDark = $derived(
darkThemes.includes($theme) || ($theme === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches) || darkThemes.includes($resolvedTheme) ||
($theme.startsWith('custom-') && ($customThemes.find(c => c.id === $theme)?.is_dark ?? false)) ($resolvedTheme.startsWith('custom-') && ($customThemes.find(c => c.id === $resolvedTheme)?.is_dark ?? false))
); );
const themePresets = [ const themePresets = [
{ id: 'system', label: 'System', bg: '#ffffff', sidebar: '#1a1b26', accent: '#5b6abf' },
{ id: 'light', label: 'Light', bg: '#ffffff', sidebar: '#f8f9fa', accent: '#5b6abf' }, { id: 'light', label: 'Light', bg: '#ffffff', sidebar: '#f8f9fa', accent: '#5b6abf' },
{ id: 'dark', label: 'Dark', bg: '#1a1b26', sidebar: '#1f2029', accent: '#89b4fa' }, { id: 'dark', label: 'Dark', bg: '#1a1b26', sidebar: '#1f2029', accent: '#89b4fa' },
{ id: 'solarized-light', label: 'Solarized Light', bg: '#fdf6e3', sidebar: '#eee8d5', accent: '#268bd2' }, { id: 'solarized-light', label: 'Solarized Light', bg: '#fdf6e3', sidebar: '#eee8d5', accent: '#268bd2' },
@@ -527,7 +528,45 @@
let themeDropdownOpen = $state(false); let themeDropdownOpen = $state(false);
let accentDropdownOpen = $state(false); let accentDropdownOpen = $state(false);
let activeCustomTheme = $derived($customThemes.find(c => c.id === $theme) ?? null); let activeCustomTheme = $derived($customThemes.find(c => c.id === $theme) ?? null);
let activeThemePreset = $derived(themePresets.find(p => p.id === $theme) ?? themePresets[0]); let activeThemePreset = $derived(
themePresets.find(p => p.id === $theme) ?? themePresets.find(p => p.id === 'light')!
);
// Which theme each OS appearance maps to while `theme` is "system". Custom themes are offered
// alongside the built-ins so a light/dark pair can be built out of them.
let systemLightTheme = $state($appConfig?.system_light_theme ?? 'light');
let systemDarkTheme = $state($appConfig?.system_dark_theme ?? 'dark');
let systemPairOpen = $state<'light' | 'dark' | null>(null);
let pairPresets = $derived([
...themePresets.filter(p => p.id !== 'system'),
...$customThemes.map(ct => ({
id: ct.id,
label: ct.name,
bg: ct.colors.bg_primary,
sidebar: ct.colors.bg_secondary,
accent: ct.colors.text_primary,
})),
]);
let systemPairRows = $derived([
{ key: 'light' as const, title: 'Light appearance', selected: systemLightTheme },
{ key: 'dark' as const, title: 'Dark appearance', selected: systemDarkTheme },
]);
function selectSystemPairTheme(which: 'light' | 'dark', id: string) {
if (which === 'light') systemLightTheme = id;
else systemDarkTheme = id;
systemPairOpen = null;
if ($appConfig) {
$appConfig.system_light_theme = systemLightTheme;
$appConfig.system_dark_theme = systemDarkTheme;
$appConfig = { ...$appConfig };
}
setSystemThemes(systemLightTheme, systemDarkTheme).catch((e) =>
console.error('Failed to save system themes:', e)
);
}
let activeAccentPreset = $derived(accentPresets.find(p => p.name === activeAccent) ?? accentPresets[0]); let activeAccentPreset = $derived(accentPresets.find(p => p.name === activeAccent) ?? accentPresets[0]);
let isCustomAccent = $derived(activeAccent.startsWith('#')); let isCustomAccent = $derived(activeAccent.startsWith('#'));
let activeAccentColor = $derived( let activeAccentColor = $derived(
@@ -553,7 +592,7 @@
let customThemeImporting = $state(false); let customThemeImporting = $state(false);
function openNewCustomThemeEditor() { function openNewCustomThemeEditor() {
const isDark = darkThemes.includes($theme) || ($theme === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches) || $theme.startsWith('custom-') && ($customThemes.find(c => c.id === $theme)?.is_dark ?? false); const isDark = isThemeDark;
customThemeEditing = { customThemeEditing = {
id: `custom-${Date.now()}`, id: `custom-${Date.now()}`,
name: '', name: '',
@@ -597,8 +636,8 @@
root.removeAttribute('data-theme'); root.removeAttribute('data-theme');
for (const v of varsToClear) root.style.removeProperty(v); for (const v of varsToClear) root.style.removeProperty(v);
const namedThemes = ['solarized-light','solarized-dark','catppuccin','nord','tokyo-night','github-light','github-dark','dracula','blueberry','forest-green','gruvbox','midnight-tide','cherry-blossom','synthwave','ember','moonlit','light-coffee','dark-coffee','cotton-candy','crimson','cloud','peach','material-dark','material-light','monokai','rose-pine','everforest','horizon','cyberpunk','black','one-dark']; const namedThemes = ['solarized-light','solarized-dark','catppuccin','nord','tokyo-night','github-light','github-dark','dracula','blueberry','forest-green','gruvbox','midnight-tide','cherry-blossom','synthwave','ember','moonlit','light-coffee','dark-coffee','cotton-candy','crimson','cloud','peach','material-dark','material-light','monokai','rose-pine','everforest','horizon','cyberpunk','black','one-dark'];
if ($theme.startsWith('custom-')) { if ($resolvedTheme.startsWith('custom-')) {
const ct = $customThemes.find(c => c.id === $theme); const ct = $customThemes.find(c => c.id === $resolvedTheme);
if (ct) { if (ct) {
root.style.setProperty('--bg-primary', ct.colors.bg_primary); root.style.setProperty('--bg-primary', ct.colors.bg_primary);
root.style.setProperty('--bg-secondary', ct.colors.bg_secondary); root.style.setProperty('--bg-secondary', ct.colors.bg_secondary);
@@ -613,10 +652,10 @@
root.style.setProperty('--text-tertiary', ct.colors.text_secondary); root.style.setProperty('--text-tertiary', ct.colors.text_secondary);
if (ct.is_dark) root.classList.add('dark'); if (ct.is_dark) root.classList.add('dark');
} }
} else if (namedThemes.includes($theme)) { } else if (namedThemes.includes($resolvedTheme)) {
root.setAttribute('data-theme', $theme); root.setAttribute('data-theme', $resolvedTheme);
if (darkThemes.includes($theme)) root.classList.add('dark'); if (darkThemes.includes($resolvedTheme)) root.classList.add('dark');
} else if ($theme === 'dark' || ($theme === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches)) { } else if ($resolvedTheme === 'dark') {
root.classList.add('dark'); root.classList.add('dark');
} }
} }
@@ -816,7 +855,9 @@
$theme = preset.id; $theme = preset.id;
setTheme(preset.id); setTheme(preset.id);
themeDropdownOpen = false; themeDropdownOpen = false;
selectCustomAccent(preset.accent); // "System" has no palette of its own, it defers to whichever paired theme is active, so it
// must not overwrite the accent the way a concrete theme does.
if (preset.id !== 'system') selectCustomAccent(preset.accent);
} }
function selectAccent(preset: typeof accentPresets[0]) { function selectAccent(preset: typeof accentPresets[0]) {
@@ -827,7 +868,7 @@
function applyAccent(preset: typeof accentPresets[0]) { function applyAccent(preset: typeof accentPresets[0]) {
const root = document.documentElement; const root = document.documentElement;
const isDark = darkThemes.includes($theme) || ($theme === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches); const isDark = isThemeDark;
const color = isDark ? preset.dark : preset.light; const color = isDark ? preset.dark : preset.light;
root.style.setProperty('--accent', color); root.style.setProperty('--accent', color);
@@ -850,7 +891,7 @@
function applyCustomAccent(hex: string) { function applyCustomAccent(hex: string) {
const root = document.documentElement; const root = document.documentElement;
const isDark = darkThemes.includes($theme) || ($theme === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches); const isDark = isThemeDark;
root.style.setProperty('--accent', hex); root.style.setProperty('--accent', hex);
root.style.setProperty('--text-accent', hex); root.style.setProperty('--text-accent', hex);
root.style.setProperty('--accent-hover', hex); root.style.setProperty('--accent-hover', hex);
@@ -1419,6 +1460,9 @@
</div> </div>
{/if} {/if}
</div> </div>
{#if $theme === 'system'}
<span class="setting-hint">Follows the operating system appearance.</span>
{/if}
</div> </div>
<div class="settings-section"> <div class="settings-section">
@@ -1467,6 +1511,54 @@
{/if} {/if}
</div> </div>
</div> </div>
{#if $theme === 'system'}
{#each systemPairRows as row}
<div class="settings-section">
<h3>{row.title}</h3>
<div class="dropdown-wrap">
<button
class="dropdown-trigger"
class:open={systemPairOpen === row.key}
onclick={() => { systemPairOpen = systemPairOpen === row.key ? null : row.key; themeDropdownOpen = false; accentDropdownOpen = false; }}
>
{#each pairPresets.filter(p => p.id === row.selected) as preset}
<span class="dropdown-preview" style="--preview-bg: {preset.bg}; --preview-sidebar: {preset.sidebar}; --preview-accent: {preset.accent}">
<span class="preview-sidebar"></span>
<span class="preview-main">
<span class="preview-accent-bar"></span>
</span>
</span>
<span class="dropdown-trigger-label">{preset.label}</span>
{/each}
<svg class="dropdown-chevron" viewBox="0 0 16 16" fill="none"><path d="M4 6l4 4 4-4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
{#if systemPairOpen === row.key}
<div class="dropdown-list" use:clickOutside={() => systemPairOpen = null}>
{#each pairPresets as preset}
<button
class="dropdown-item"
class:active={row.selected === preset.id}
onclick={() => selectSystemPairTheme(row.key, preset.id)}
>
<span class="dropdown-preview" style="--preview-bg: {preset.bg}; --preview-sidebar: {preset.sidebar}; --preview-accent: {preset.accent}">
<span class="preview-sidebar"></span>
<span class="preview-main">
<span class="preview-accent-bar"></span>
</span>
</span>
<span class="dropdown-item-label">{preset.label}</span>
{#if row.selected === preset.id}
<svg class="dropdown-check" viewBox="0 0 16 16" fill="none"><path d="M3 8l4 4 6-6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
{/if}
</button>
{/each}
</div>
{/if}
</div>
</div>
{/each}
{/if}
</div> </div>
<!-- Custom Themes --> <!-- Custom Themes -->
+18
View File
@@ -89,6 +89,24 @@ export const readOnly = writable(false);
export const theme = writable<string>("system"); export const theme = writable<string>("system");
export const customThemes = derived(appConfig, ($c): CustomTheme[] => $c?.custom_themes ?? []); export const customThemes = derived(appConfig, ($c): CustomTheme[] => $c?.custom_themes ?? []);
// Whether the OS is currently in dark mode. Held in a store (rather than read at each call site)
// so the theme re-resolves when the user flips appearance while the app is open.
const darkQuery =
typeof window !== "undefined" ? window.matchMedia("(prefers-color-scheme: dark)") : null;
export const systemPrefersDark = writable<boolean>(darkQuery?.matches ?? false);
darkQuery?.addEventListener("change", (e) => systemPrefersDark.set(e.matches));
// The theme to actually render: "system" resolves to the configured light/dark pair, everything
// else passes through. Falls back to the plain schemes when no pair is configured yet.
export const resolvedTheme = derived(
[theme, appConfig, systemPrefersDark],
([$theme, $config, $prefersDark]): string => {
if ($theme !== "system") return $theme;
const paired = $prefersDark ? $config?.system_dark_theme : $config?.system_light_theme;
return paired || ($prefersDark ? "dark" : "light");
},
);
// Sync (WebDAV) - global status so the top-bar button reflects any sync, // Sync (WebDAV) - global status so the top-bar button reflects any sync,
// whoever triggered it (manual button, settings, interval, on-change). // whoever triggered it (manual button, settings, interval, on-change).
export const syncState = writable<{ running: boolean; error: string | null }>({ export const syncState = writable<{ running: boolean; error: string | null }>({
+2
View File
@@ -88,6 +88,8 @@ export interface AppConfig {
active_vault: string | null; active_vault: string | null;
active_bookmark_id?: string | null; active_bookmark_id?: string | null;
theme: string; theme: string;
system_light_theme: string;
system_dark_theme: string;
accent_color: string | null; accent_color: string | null;
font_size: number | null; font_size: number | null;
font_family: string | null; font_family: string | null;
+5 -3
View File
@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import '../app.css'; import '../app.css';
import { theme, appConfig, activeNote, activeNotePath, installType, platformIsMobile, checkForUpdate, checkForUpdateMobile, isManagedInstall, customThemes } from '$lib/stores/app'; import { resolvedTheme, appConfig, activeNote, activeNotePath, installType, platformIsMobile, checkForUpdate, checkForUpdateMobile, isManagedInstall, customThemes } from '$lib/stores/app';
import { openFile, openUrl, readNote, getInstallType, isMobilePlatform } from '$lib/api'; import { openFile, openUrl, readNote, getInstallType, isMobilePlatform } from '$lib/api';
import { get } from 'svelte/store'; import { get } from 'svelte/store';
import { darkThemes, isMobile, isAndroid } from '$lib/platform'; import { darkThemes, isMobile, isAndroid } from '$lib/platform';
@@ -10,9 +10,11 @@
let { children } = $props(); let { children } = $props();
// Reactively apply theme class to <html> whenever $theme or custom themes change // Reactively apply theme class to <html> whenever the resolved theme or custom themes change.
// $resolvedTheme already maps "system" onto the configured light/dark pair, so flipping the OS
// appearance re-runs this effect.
$effect(() => { $effect(() => {
applyTheme($theme, $customThemes); applyTheme($resolvedTheme, $customThemes);
}); });
// Apply link arrow visibility from config // Apply link arrow visibility from config
+9 -5
View File
@@ -155,8 +155,13 @@
}); });
$theme = config.theme || 'system'; $theme = config.theme || 'system';
// Apply theme immediately to prevent flash // Apply theme immediately to prevent flash. Runs before the stores settle, so resolve
const themeValue = config.theme || 'system'; // "system" against the configured pair here the same way resolvedTheme does.
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
const rawTheme = config.theme || 'system';
const themeValue = rawTheme === 'system'
? (prefersDark ? config.system_dark_theme || 'dark' : config.system_light_theme || 'light')
: rawTheme;
const root = document.documentElement; const root = document.documentElement;
const namedThemes = ['solarized-light', 'solarized-dark', 'catppuccin', 'nord', 'tokyo-night', 'github-light', 'github-dark', 'dracula', 'blueberry', 'forest-green', 'gruvbox', 'midnight-tide', 'cherry-blossom', 'synthwave', 'ember', 'moonlit', 'light-coffee', 'dark-coffee', 'cotton-candy', 'crimson', 'cloud', 'peach', 'material-dark', 'material-light', 'monokai', 'rose-pine', 'everforest', 'horizon', 'cyberpunk', 'black', 'one-dark']; const namedThemes = ['solarized-light', 'solarized-dark', 'catppuccin', 'nord', 'tokyo-night', 'github-light', 'github-dark', 'dracula', 'blueberry', 'forest-green', 'gruvbox', 'midnight-tide', 'cherry-blossom', 'synthwave', 'ember', 'moonlit', 'light-coffee', 'dark-coffee', 'cotton-candy', 'crimson', 'cloud', 'peach', 'material-dark', 'material-light', 'monokai', 'rose-pine', 'everforest', 'horizon', 'cyberpunk', 'black', 'one-dark'];
root.classList.remove('dark'); root.classList.remove('dark');
@@ -164,7 +169,7 @@
if (namedThemes.includes(themeValue)) { if (namedThemes.includes(themeValue)) {
root.setAttribute('data-theme', themeValue); root.setAttribute('data-theme', themeValue);
if (darkThemes.includes(themeValue)) root.classList.add('dark'); if (darkThemes.includes(themeValue)) root.classList.add('dark');
} else if (themeValue === 'dark' || (themeValue === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches)) { } else if (themeValue === 'dark') {
root.classList.add('dark'); root.classList.add('dark');
} }
@@ -192,8 +197,7 @@
} }
// Apply saved accent // Apply saved accent
if (config.accent_color) { if (config.accent_color) {
const themeVal = config.theme || 'light'; const isDark = darkThemes.includes(themeValue);
const isDark = darkThemes.includes(themeVal) || (themeVal === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches);
let color: string | null = null; let color: string | null = null;
if (config.accent_color.startsWith('#')) { if (config.accent_color.startsWith('#')) {
color = config.accent_color; color = config.accent_color;