Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/lib/Installer.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
36 changes: 32 additions & 4 deletions src/lib/MarkdownViewer.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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'));
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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}
Expand All @@ -1206,6 +1233,7 @@
<Editor
bind:value={tabManager.activeTab.rawContent}
language={editorLanguage}
{theme}
onsave={saveContent}
bind:zoomLevel
onnew={handleNewFile}
Expand Down
6 changes: 6 additions & 0 deletions src/lib/Uninstaller.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,12 @@
filter: grayscale(0.5);
}

@media (prefers-color-scheme: dark) {
.app-icon {
filter: grayscale(0.5) invert(1);
}
}

h1 {
margin: 0;
font-size: 24px;
Expand Down
26 changes: 23 additions & 3 deletions src/lib/components/Editor.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
onundoClose,
onscrollsync,
zoomLevel = $bindable(100),
theme = 'system',
} = $props<{
value: string;
language?: string;
Expand All @@ -42,6 +43,7 @@
onundoClose?: () => void;
onscrollsync?: (line: number, ratio?: number) => void;
zoomLevel?: number;
theme?: 'system' | 'light' | 'dark';
}>();

let container: HTMLDivElement;
Expand Down Expand Up @@ -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, {
Expand Down Expand Up @@ -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)');
Expand Down Expand Up @@ -362,6 +367,7 @@
container.addEventListener('wheel', wheelListener, { capture: true });

return () => {
// Clean up listeners
mediaQuery.removeEventListener('change', updateTheme);
container.removeEventListener('wheel', wheelListener, { capture: true });

Expand Down Expand Up @@ -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);
}
});
</script>

<div class="editor-container" bind:this={container}></div>
Expand Down
8 changes: 1 addition & 7 deletions src/lib/components/Tab.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
112 changes: 111 additions & 1 deletion src/lib/components/TitleBar.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
ontoggleSync,
isFullWidth,
ontoggleFullWidth,
theme = 'system',
onSetTheme,
} = $props<{
isFocused: boolean;
isScrolled: boolean;
Expand Down Expand Up @@ -59,6 +61,8 @@

isFullWidth?: boolean;
ontoggleFullWidth?: () => void;
theme?: 'system' | 'dark' | 'light';
onSetTheme?: (theme: 'system' | 'dark' | 'light') => void;
}>();

const appWindow = getCurrentWindow();
Expand Down Expand Up @@ -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');
Expand All @@ -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);
};
});
</script>

<div class="custom-title-bar {isScrolled ? 'scrolled' : ''} {!isMac ? 'windows' : ''}">
Expand All @@ -166,7 +190,11 @@
</div>
{/if}
<button class="icon-home-btn {showHome ? 'active' : ''}" onclick={ontoggleHome} aria-label="Home" onmouseenter={(e) => showTooltip(e, 'Home')} onmouseleave={hideTooltip}>
<img src={iconUrl} alt="icon" class="window-icon" />
<img
src={iconUrl}
alt="icon"
class="window-icon"
style:filter={theme === 'dark' || (theme === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches) ? 'none' : 'invert(0.7)'} />
</button>
</div>

Expand Down Expand Up @@ -277,6 +305,47 @@
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
><path d="M12 20h9" /><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z" /></svg>
</button>
{:else if id === 'theme'}
<div class="theme-dropdown-container">
<button
class="title-action-btn {themeMenuOpen ? 'active' : ''}"
onclick={(e) => {
e.stopPropagation();
themeMenuOpen = !themeMenuOpen;
}}
aria-label="Change Theme"
onmouseenter={(e) => showTooltip(e, 'Change Theme')}
onmouseleave={hideTooltip}
transition:fly={{ x: 10, duration: 200 }}>
{#if theme === 'light'}
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
><circle cx="12" cy="12" r="5"></circle><line x1="12" y1="1" x2="12" y2="3"></line><line x1="12" y1="21" x2="12" y2="23"></line><line
x1="4.22"
y1="4.22"
x2="5.64"
y2="5.64"></line
><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line><line x1="1" y1="12" x2="3" y2="12"></line><line x1="21" y1="12" x2="23" y2="12"></line><line
x1="4.22"
y1="19.78"
x2="5.64"
y2="18.36"></line
><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line></svg>
{:else if theme === 'dark'}
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path></svg>
{:else}
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
><rect x="2" y="3" width="20" height="14" rx="2" ry="2"></rect><line x1="8" y1="21" x2="16" y2="21"></line><line x1="12" y1="17" x2="12" y2="21"></line></svg>
{/if}
</button>
{#if themeMenuOpen}
<div class="theme-menu" transition:fly={{ y: 5, duration: 150 }} onclick={(e) => e.stopPropagation()}>
<button class="theme-option {theme === 'system' ? 'selected' : ''}" onclick={() => handleSetTheme('system')}> Follow System </button>
<button class="theme-option {theme === 'light' ? 'selected' : ''}" onclick={() => handleSetTheme('light')}> Light </button>
<button class="theme-option {theme === 'dark' ? 'selected' : ''}" onclick={() => handleSetTheme('dark')}> Dark </button>
</div>
{/if}
</div>
{/if}
</div>
{/each}
Expand Down Expand Up @@ -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;
}
</style>
Loading