mirror of
https://github.com/moku-project/Moku.git
synced 2026-06-13 09:19:56 -05:00
Feat: System-Default Based Custom Theme Switching (#45)
This commit is contained in:
+32
-1
@@ -1,6 +1,8 @@
|
|||||||
import { store } from "@store/state.svelte";
|
import { store, updateSettings } from "@store/state.svelte";
|
||||||
|
|
||||||
let themeStyleEl: HTMLStyleElement | null = null;
|
let themeStyleEl: HTMLStyleElement | null = null;
|
||||||
|
let mediaQuery: MediaQueryList | null = null;
|
||||||
|
let mediaHandler: (() => void) | null = null;
|
||||||
|
|
||||||
export function applyTheme() {
|
export function applyTheme() {
|
||||||
const themeId = store.settings.theme ?? "dark";
|
const themeId = store.settings.theme ?? "dark";
|
||||||
@@ -34,3 +36,32 @@ export function applyTheme() {
|
|||||||
themeStyleEl.textContent = css;
|
themeStyleEl.textContent = css;
|
||||||
document.documentElement.setAttribute("data-theme", "custom");
|
document.documentElement.setAttribute("data-theme", "custom");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function applySystemTheme(dark: boolean) {
|
||||||
|
const themeId = dark
|
||||||
|
? (store.settings.systemThemeDark ?? "dark")
|
||||||
|
: (store.settings.systemThemeLight ?? "light");
|
||||||
|
updateSettings({ theme: themeId });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mountSystemThemeSync() {
|
||||||
|
if (mediaQuery && mediaHandler) {
|
||||||
|
mediaQuery.removeEventListener("change", mediaHandler);
|
||||||
|
mediaHandler = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!store.settings.systemThemeSync) return;
|
||||||
|
|
||||||
|
mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
|
||||||
|
mediaHandler = () => applySystemTheme(mediaQuery!.matches);
|
||||||
|
mediaQuery.addEventListener("change", mediaHandler);
|
||||||
|
applySystemTheme(mediaQuery.matches);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function unmountSystemThemeSync() {
|
||||||
|
if (mediaQuery && mediaHandler) {
|
||||||
|
mediaQuery.removeEventListener("change", mediaHandler);
|
||||||
|
mediaHandler = null;
|
||||||
|
mediaQuery = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -226,7 +226,8 @@
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
transition: background var(--t-base), border-color var(--t-base);
|
transition: background var(--t-base), border-color var(--t-base);
|
||||||
}
|
}
|
||||||
.s-toggle.on { background: var(--accent); border-color: var(--accent); }
|
.s-toggle.on,
|
||||||
|
.s-toggle-on { background: var(--accent); border-color: var(--accent); }
|
||||||
|
|
||||||
.s-toggle-thumb {
|
.s-toggle-thumb {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -238,12 +239,46 @@
|
|||||||
background: var(--text-faint);
|
background: var(--text-faint);
|
||||||
transition: transform var(--t-base), background var(--t-base);
|
transition: transform var(--t-base), background var(--t-base);
|
||||||
}
|
}
|
||||||
.s-toggle.on .s-toggle-thumb {
|
.s-toggle.on .s-toggle-thumb,
|
||||||
|
.s-toggle-on .s-toggle-thumb {
|
||||||
transform: translateX(15px);
|
transform: translateX(15px);
|
||||||
background: var(--bg-void);
|
background: var(--bg-void);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* ── System theme sync pair ───────────────────────────────────────── */
|
||||||
|
.s-sync-pair {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 1px;
|
||||||
|
border-top: 1px solid var(--border-dim);
|
||||||
|
background: var(--border-dim);
|
||||||
|
}
|
||||||
|
|
||||||
|
.s-sync-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: var(--sp-3);
|
||||||
|
padding: 8px var(--sp-4);
|
||||||
|
background: var(--bg-raised);
|
||||||
|
}
|
||||||
|
|
||||||
|
.s-sync-label {
|
||||||
|
font-family: var(--font-ui);
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
color: var(--text-faint);
|
||||||
|
letter-spacing: var(--tracking-wide);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.s-sync-item .s-select-btn {
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
min-width: 0;
|
||||||
|
padding: 4px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* ── Stepper ──────────────────────────────────────────────────────── */
|
/* ── Stepper ──────────────────────────────────────────────────────── */
|
||||||
.s-stepper {
|
.s-stepper {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -162,7 +162,7 @@
|
|||||||
{#if tab === "general"}
|
{#if tab === "general"}
|
||||||
<GeneralSettings {selectOpen} {closingSelect} {toggleSelect} {anims} />
|
<GeneralSettings {selectOpen} {closingSelect} {toggleSelect} {anims} />
|
||||||
{:else if tab === "appearance"}
|
{:else if tab === "appearance"}
|
||||||
<AppearanceSettings {onOpenThemeEditor} />
|
<AppearanceSettings {selectOpen} {closingSelect} {toggleSelect} {anims} {onOpenThemeEditor} />
|
||||||
{:else if tab === "reader"}
|
{:else if tab === "reader"}
|
||||||
<ReaderSettings {selectOpen} {closingSelect} {toggleSelect} {anims} />
|
<ReaderSettings {selectOpen} {closingSelect} {toggleSelect} {anims} />
|
||||||
{:else if tab === "library"}
|
{:else if tab === "library"}
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Pencil, Trash, Plus } from "phosphor-svelte";
|
import { Pencil, Trash, Plus } from "phosphor-svelte";
|
||||||
import { store, updateSettings, deleteCustomTheme } from "@store/state.svelte";
|
import { store, updateSettings, deleteCustomTheme } from "@store/state.svelte";
|
||||||
|
import { mountSystemThemeSync } from "@core/theme";
|
||||||
|
import { selectPortal } from "@core/actions/selectPortal";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
selectOpen: string | null;
|
||||||
|
closingSelect: string | null;
|
||||||
|
toggleSelect: (id: string) => void;
|
||||||
|
anims: boolean;
|
||||||
onOpenThemeEditor?: (id?: string | null) => void;
|
onOpenThemeEditor?: (id?: string | null) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { onOpenThemeEditor }: Props = $props();
|
let { selectOpen, closingSelect, toggleSelect, anims, onOpenThemeEditor }: Props = $props();
|
||||||
|
|
||||||
const THEMES: { id: string; label: string; description: string; swatches: string[] }[] = [
|
const THEMES: { id: string; label: string; description: string; swatches: string[] }[] = [
|
||||||
{ id: "dark", label: "Dark", description: "Default near-black", swatches: ["#101010","#151515","#a8c4a8","#f0efec"] },
|
{ id: "dark", label: "Dark", description: "Default near-black", swatches: ["#101010","#151515","#a8c4a8","#f0efec"] },
|
||||||
@@ -16,10 +22,82 @@
|
|||||||
{ id: "midnight", label: "Midnight", description: "Deep blue-black tint", swatches: ["#0c1020","#101428","#a8b4e8","#eeeef8"] },
|
{ id: "midnight", label: "Midnight", description: "Deep blue-black tint", swatches: ["#0c1020","#101428","#a8b4e8","#eeeef8"] },
|
||||||
{ id: "warm", label: "Warm", description: "Amber and sepia tones", swatches: ["#16130c","#1c1810","#e0b860","#f5f0e0"] },
|
{ id: "warm", label: "Warm", description: "Amber and sepia tones", swatches: ["#16130c","#1c1810","#e0b860","#f5f0e0"] },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const allThemeOptions = $derived([
|
||||||
|
...THEMES.map(t => ({ id: t.id, label: t.label })),
|
||||||
|
...(store.settings.customThemes ?? []).map(t => ({ id: t.id, label: t.name })),
|
||||||
|
]);
|
||||||
|
|
||||||
|
function toggleSync() {
|
||||||
|
updateSettings({ systemThemeSync: !store.settings.systemThemeSync });
|
||||||
|
mountSystemThemeSync();
|
||||||
|
}
|
||||||
|
|
||||||
|
let triggerDark: HTMLButtonElement;
|
||||||
|
let triggerLight: HTMLButtonElement;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="s-panel">
|
<div class="s-panel">
|
||||||
|
|
||||||
|
<div class="s-section">
|
||||||
|
<div class="s-row">
|
||||||
|
<div class="s-row-info">
|
||||||
|
<span class="s-label">Match system theme</span>
|
||||||
|
<span class="s-desc">Automatically switch theme when your OS switches between light and dark</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="s-toggle"
|
||||||
|
class:on={store.settings.systemThemeSync}
|
||||||
|
onclick={toggleSync}
|
||||||
|
role="switch"
|
||||||
|
aria-checked={store.settings.systemThemeSync}
|
||||||
|
><span class="s-toggle-thumb"></span></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if store.settings.systemThemeSync}
|
||||||
|
<div class="s-sync-pair">
|
||||||
|
<div class="s-sync-item">
|
||||||
|
<span class="s-sync-label">Dark theme</span>
|
||||||
|
<div class="s-select">
|
||||||
|
<button bind:this={triggerDark} class="s-select-btn" onclick={() => toggleSelect("sync-dark")}>
|
||||||
|
<span>{allThemeOptions.find(o => o.id === (store.settings.systemThemeDark ?? "dark"))?.label ?? "Dark"}</span>
|
||||||
|
<svg class="s-select-caret" class:open={selectOpen === "sync-dark"} width="10" height="6" viewBox="0 0 10 6"><path d="M0 0l5 6 5-6" fill="currentColor"/></svg>
|
||||||
|
</button>
|
||||||
|
{#if selectOpen === "sync-dark" || closingSelect === "sync-dark"}
|
||||||
|
<div class="s-select-menu" class:anims class:closing={closingSelect === "sync-dark"} {@attach selectPortal(triggerDark)}>
|
||||||
|
{#each allThemeOptions as opt}
|
||||||
|
<button class="s-select-option" class:active={opt.id === (store.settings.systemThemeDark ?? "dark")}
|
||||||
|
onclick={() => { updateSettings({ systemThemeDark: opt.id }); mountSystemThemeSync(); toggleSelect("sync-dark"); }}>
|
||||||
|
{opt.label}
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="s-sync-item">
|
||||||
|
<span class="s-sync-label">Light theme</span>
|
||||||
|
<div class="s-select">
|
||||||
|
<button bind:this={triggerLight} class="s-select-btn" onclick={() => toggleSelect("sync-light")}>
|
||||||
|
<span>{allThemeOptions.find(o => o.id === (store.settings.systemThemeLight ?? "light"))?.label ?? "Light"}</span>
|
||||||
|
<svg class="s-select-caret" class:open={selectOpen === "sync-light"} width="10" height="6" viewBox="0 0 10 6"><path d="M0 0l5 6 5-6" fill="currentColor"/></svg>
|
||||||
|
</button>
|
||||||
|
{#if selectOpen === "sync-light" || closingSelect === "sync-light"}
|
||||||
|
<div class="s-select-menu" class:anims class:closing={closingSelect === "sync-light"} {@attach selectPortal(triggerLight)}>
|
||||||
|
{#each allThemeOptions as opt}
|
||||||
|
<button class="s-select-option" class:active={opt.id === (store.settings.systemThemeLight ?? "light")}
|
||||||
|
onclick={() => { updateSettings({ systemThemeLight: opt.id }); mountSystemThemeSync(); toggleSelect("sync-light"); }}>
|
||||||
|
{opt.label}
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="s-section">
|
<div class="s-section">
|
||||||
<p class="s-section-title">Theme</p>
|
<p class="s-section-title">Theme</p>
|
||||||
<div class="s-theme-grid">
|
<div class="s-theme-grid">
|
||||||
|
|||||||
Reference in New Issue
Block a user