From 06380bc4c20600c20ebc2f858268ce3e87291225 Mon Sep 17 00:00:00 2001 From: Alec Ames Date: Thu, 5 Feb 2026 15:39:16 -0500 Subject: [PATCH] (feat) added manual toggl for dark/light mode and follow system --- src/lib/Installer.svelte | 6 ++ src/lib/MarkdownViewer.svelte | 36 ++++++++-- src/lib/Uninstaller.svelte | 6 ++ src/lib/components/Editor.svelte | 26 ++++++- src/lib/components/Tab.svelte | 8 +-- src/lib/components/TitleBar.svelte | 112 ++++++++++++++++++++++++++++- src/styles.css | 76 +++++++++++++++++++- 7 files changed, 254 insertions(+), 16 deletions(-) diff --git a/src/lib/Installer.svelte b/src/lib/Installer.svelte index f218816..333f4c9 100644 --- a/src/lib/Installer.svelte +++ b/src/lib/Installer.svelte @@ -255,6 +255,12 @@ filter: drop-shadow(0 4px 12px rgba(0, 0, 0, 0.1)); } + @media (prefers-color-scheme: dark) { + .app-icon { + filter: invert(1) drop-shadow(0 4px 12px rgba(0, 0, 0, 0.1)); + } + } + h1 { margin: 0; font-size: 22px; diff --git a/src/lib/MarkdownViewer.svelte b/src/lib/MarkdownViewer.svelte index f0ad376..3bb2b65 100644 --- a/src/lib/MarkdownViewer.svelte +++ b/src/lib/MarkdownViewer.svelte @@ -56,6 +56,28 @@ localStorage.setItem('isFullWidth', String(isFullWidth)); }); + // Theme State + let theme = $state<'system' | 'dark' | 'light'>('system'); + + onMount(() => { + const storedTheme = localStorage.getItem('theme') as 'system' | 'dark' | 'light' | null; + if (storedTheme) theme = storedTheme; + }); + + $effect(() => { + localStorage.setItem('theme', theme); + + if (theme === 'system') { + delete document.documentElement.dataset.theme; + } else { + document.documentElement.dataset.theme = theme; + } + + // Re-initialize mermaid or trigger update if needed + // Note: Mermaid 10+ usually doesn't support dynamic re-init easily but we can try re-rendering rich content + if (markdownBody && !isEditing) renderRichContent(); + }); + // ui state let tooltip = $state({ show: false, text: '', x: 0, y: 0 }); let caretEl: HTMLElement; @@ -249,9 +271,10 @@ if (!hljs || !renderMathInElement || !mermaid) return; - // Initialize Mermaid with theme based on system preference - const isDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches; - mermaid.initialize({ startOnLoad: false, theme: isDarkMode ? 'dark' : 'neutral' }); + // Initialize Mermaid with theme based on system preference or override + const isSystemDark = window.matchMedia('(prefers-color-scheme: dark)').matches; + const effectiveTheme = theme === 'system' ? (isSystemDark ? 'dark' : 'neutral') : theme === 'dark' ? 'dark' : 'neutral'; + mermaid.initialize({ startOnLoad: false, theme: effectiveTheme }); // Process code blocks const codeBlocks = Array.from(markdownBody.querySelectorAll('pre code')); @@ -1153,6 +1176,8 @@ onresetZoom={() => (zoomLevel = 100)} {isFullWidth} ontoggleFullWidth={() => (isFullWidth = !isFullWidth)} + {theme} + onSetTheme={(t) => (theme = t)} oncloseTab={(id) => { canCloseTab(id).then((can) => { if (can) tabManager.closeTab(id); @@ -1194,7 +1219,9 @@ {isScrollSynced} ontoggleSync={() => tabManager.activeTabId && tabManager.toggleScrollSync(tabManager.activeTabId)} {isFullWidth} - ontoggleFullWidth={() => (isFullWidth = !isFullWidth)} /> + ontoggleFullWidth={() => (isFullWidth = !isFullWidth)} + {theme} + onSetTheme={(t) => (theme = t)} /> {#if tabManager.activeTab && (tabManager.activeTab.path !== '' || tabManager.activeTab.title !== 'Recents') && !showHome} {#key tabManager.activeTabId} @@ -1206,6 +1233,7 @@ void; onscrollsync?: (line: number, ratio?: number) => void; zoomLevel?: number; + theme?: 'system' | 'light' | 'dark'; }>(); let container: HTMLDivElement; @@ -90,7 +92,10 @@ defineThemes(); const getTheme = () => { - return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'app-theme-dark' : 'app-theme-light'; + if (theme === 'system') { + return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'app-theme-dark' : 'app-theme-light'; + } + return theme === 'dark' ? 'app-theme-dark' : 'app-theme-light'; }; editor = monaco.editor.create(container, { @@ -150,8 +155,8 @@ }, }); - const updateTheme = (e: MediaQueryListEvent) => { - monaco.editor.setTheme(e.matches ? 'app-theme-dark' : 'app-theme-light'); + const updateTheme = () => { + monaco.editor.setTheme(getTheme()); }; const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); @@ -362,6 +367,7 @@ container.addEventListener('wheel', wheelListener, { capture: true }); return () => { + // Clean up listeners mediaQuery.removeEventListener('change', updateTheme); container.removeEventListener('wheel', wheelListener, { capture: true }); @@ -432,6 +438,20 @@ }); } }); + + $effect(() => { + if (editor && theme) { + const targetTheme = + theme === 'system' + ? window.matchMedia('(prefers-color-scheme: dark)').matches + ? 'app-theme-dark' + : 'app-theme-light' + : theme === 'dark' + ? 'app-theme-dark' + : 'app-theme-light'; + monaco.editor.setTheme(targetTheme); + } + });
diff --git a/src/lib/components/Tab.svelte b/src/lib/components/Tab.svelte index 88dae6b..063c4e8 100644 --- a/src/lib/components/Tab.svelte +++ b/src/lib/components/Tab.svelte @@ -89,16 +89,10 @@ } .tab.active { - background-color: var(--tab-active-bg, #dee1e6); + background-color: var(--tab-active-bg); color: var(--color-fg-default); } - @media (prefers-color-scheme: dark) { - .tab.active { - --tab-active-bg: #2d2e30; - } - } - .tab-content-btn { appearance: none; background: transparent; diff --git a/src/lib/components/TitleBar.svelte b/src/lib/components/TitleBar.svelte index aaf54bc..3b030db 100644 --- a/src/lib/components/TitleBar.svelte +++ b/src/lib/components/TitleBar.svelte @@ -32,6 +32,8 @@ ontoggleSync, isFullWidth, ontoggleFullWidth, + theme = 'system', + onSetTheme, } = $props<{ isFocused: boolean; isScrolled: boolean; @@ -59,6 +61,8 @@ isFullWidth?: boolean; ontoggleFullWidth?: () => void; + theme?: 'system' | 'dark' | 'light'; + onSetTheme?: (theme: 'system' | 'dark' | 'light') => void; }>(); const appWindow = getCurrentWindow(); @@ -121,6 +125,7 @@ let visibleActionIds = $derived.by(() => { const list: string[] = []; if (zoomLevel && zoomLevel !== 100) list.push('zoom'); + list.push('theme'); if (currentFile && !showHome) { list.push('open_loc'); @@ -144,6 +149,25 @@ } return list; }); + + let themeMenuOpen = $state(false); + + function handleSetTheme(t: 'system' | 'dark' | 'light') { + if (onSetTheme) onSetTheme(t); + themeMenuOpen = false; + } + + $effect(() => { + const handleGlobalClick = () => { + themeMenuOpen = false; + }; + if (themeMenuOpen) { + window.addEventListener('click', handleGlobalClick); + } + return () => { + window.removeEventListener('click', handleGlobalClick); + }; + });
@@ -166,7 +190,11 @@
{/if} @@ -277,6 +305,47 @@ + {:else if id === 'theme'} +
+ + {#if themeMenuOpen} +
e.stopPropagation()}> + + + +
+ {/if} +
{/if} {/each} @@ -630,4 +699,45 @@ font-size: 10px; font-family: inherit; } + + .theme-dropdown-container { + position: relative; + } + + .theme-menu { + position: absolute; + top: 100%; + right: 0; + margin-top: 4px; + background-color: var(--color-canvas-default); + border: 1px solid var(--color-border-default); + border-radius: 6px; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2); + padding: 4px; + display: flex; + flex-direction: column; + width: 120px; + z-index: 10005; + } + + .theme-option { + background: transparent; + border: none; + text-align: left; + padding: 6px 12px; + font-size: 12px; + color: var(--color-fg-default); + cursor: pointer; + border-radius: 4px; + font-family: var(--win-font); + } + + .theme-option:hover { + background-color: var(--color-canvas-subtle); + } + + .theme-option.selected { + color: var(--color-accent-fg); + font-weight: 600; + } diff --git a/src/styles.css b/src/styles.css index d47d698..1083402 100644 --- a/src/styles.css +++ b/src/styles.css @@ -31,7 +31,7 @@ body { --color-canvas-subtle: #00000011; --color-border-default: #d0d7de; --color-border-muted: hsla(210, 18%, 87%, 1); - --color-neutral-muted: rgba(51, 54, 56, 0.01); + --color-neutral-muted: rgba(51, 54, 56, 0.05); --color-accent-fg: #0969da; --color-accent-emphasis: #0969da; --color-success-fg: #1a7f37; @@ -44,6 +44,41 @@ body { --color-done-fg: #8250df; --color-done-emphasis: #8250df; --color-window-border-top: #B5B5B5; + --tab-active-bg: #dee1e6; + + /* Light Syntax Highlighting */ + --hljs-bg: #f6f8fa; + --hljs-comment: #6e7781; + --hljs-keyword: #cf222e; + --hljs-string: #0a3069; + --hljs-title: #953800; + --hljs-variable: #953800; + --hljs-type: #953800; +} + +:root[data-theme="light"] { + /* Explicit Light Theme Override */ + --color-fg-default: #1f2328; + --color-fg-muted: #656d76; + --color-fg-subtle: #6e7781; + --color-canvas-default: #FDFDFD; + --color-canvas-subtle: #00000011; + --color-border-default: #d0d7de; + --color-border-muted: rgb(216, 222, 228); + --color-neutral-muted: rgba(51, 54, 56, 0.05); + --color-accent-fg: #0969da; + --color-accent-emphasis: #0969da; + --color-success-fg: #1a7f37; + --color-success-emphasis: #1f883d; + --color-attention-fg: #9a6700; + --color-attention-emphasis: #9a6700; + --color-attention-subtle: #fff8c5; + --color-danger-fg: #d1242f; + --color-danger-emphasis: #cf222e; + --color-done-fg: #8250df; + --color-done-emphasis: #8250df; + --color-window-border-top: #B5B5B5; + --tab-active-bg: #dee1e6; /* Light Syntax Highlighting */ --hljs-bg: #f6f8fa; @@ -76,7 +111,10 @@ body { --color-danger-emphasis: #da3633; --color-done-fg: #a371f7; --color-done-emphasis: #8957e5; + --color-done-fg: #a371f7; + --color-done-emphasis: #8957e5; --color-window-border-top: #3E3E3E; + --tab-active-bg: #2d2e30; /* Dark Syntax Highlighting */ --hljs-bg: #0d1117; @@ -89,6 +127,42 @@ body { } } +:root[data-theme="dark"] { + /* Explicit Dark Theme Override */ + --color-fg-default: #e6edf3; + --color-fg-muted: #848d97; + --color-fg-subtle: #6e7681; + --color-canvas-default: #181818; + --color-canvas-subtle: #ffffff11; + --color-border-default: #30363d; + --color-border-muted: #21262d; + --color-neutral-muted: rgba(255, 255, 255, 0.05); + --color-accent-fg: #4390fc; + --color-accent-emphasis: #3781f0; + --color-success-fg: #3fb950; + --color-success-emphasis: #238636; + --color-attention-fg: #d29922; + --color-attention-emphasis: #9e6a03; + --color-attention-subtle: rgba(187, 128, 9, 0.15); + --color-danger-fg: #f85149; + --color-danger-emphasis: #da3633; + --color-done-fg: #a371f7; + --color-done-emphasis: #8957e5; + --color-done-fg: #a371f7; + --color-done-emphasis: #8957e5; + --color-window-border-top: #3E3E3E; + --tab-active-bg: #2d2e30; + + /* Dark Syntax Highlighting */ + --hljs-bg: #0d1117; + --hljs-comment: #8b949e; + --hljs-keyword: #ff7b72; + --hljs-string: #a5d6ff; + --hljs-title: #d2a8ff; + --hljs-variable: #ffa657; + --hljs-type: #ff7b72; +} + body { transition: background-color 0.3s ease, color 0.3s ease; }