feat(theme): persistent light/dark toggle in the titlebar

This commit is contained in:
pj committed 2026-06-20 18:46:43 -04:00
1 parent a5b3c0c0e3
commit 9991d318ba
5 files changed
+61 -3

No files matched your search

+10
View File
@@ -4,6 +4,16 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" type="image/png" href="/margin-mark.png" /> <link rel="icon" type="image/png" href="/margin-mark.png" />
<script>
(function () {
try {
var t = localStorage.getItem("margin-theme");
if (t !== "light" && t !== "dark")
t = matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
document.documentElement.setAttribute("data-theme", t);
} catch (e) {}
})();
</script>
<title>margin</title> <title>margin</title>
</head> </head>
<body> <body>
+13
View File
@@ -7,6 +7,7 @@ import { Settings } from "./Settings";
import { Editor } from "../editor/Editor"; import { Editor } from "../editor/Editor";
import { FloatingToolbar } from "../editor/FloatingToolbar"; import { FloatingToolbar } from "../editor/FloatingToolbar";
import { useBook } from "../store/useBook"; import { useBook } from "../store/useBook";
import { useTheme } from "../store/useTheme";
import type { Book } from "../model/book"; import type { Book } from "../model/book";
import { saveBook } from "../library"; import { saveBook } from "../library";
import { isDesktop } from "../ipc"; import { isDesktop } from "../ipc";
@@ -22,6 +23,8 @@ export function EditorView() {
const setChapterContent = useBook((s) => s.setChapterContent); const setChapterContent = useBook((s) => s.setChapterContent);
const setChapterTitle = useBook((s) => s.setChapterTitle); const setChapterTitle = useBook((s) => s.setChapterTitle);
const markSaved = useBook((s) => s.markSaved); const markSaved = useBook((s) => s.markSaved);
const theme = useTheme((s) => s.theme);
const toggleTheme = useTheme((s) => s.toggle);
const [editor, setEditor] = useState<TiptapEditor | null>(null); const [editor, setEditor] = useState<TiptapEditor | null>(null);
const [dock, setDock] = useState(true); const [dock, setDock] = useState(true);
@@ -99,6 +102,16 @@ export function EditorView() {
</> </>
)} )}
</div> </div>
<button className="icon-btn" onClick={toggleTheme} title={theme === "dark" ? "Light mode" : "Dark mode"}>
{theme === "dark" ? (
<Icon>
<circle cx="12" cy="12" r="4" />
<path d="M12 2v2M12 20v2M2 12h2M20 12h2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M19.1 4.9l-1.4 1.4M6.3 17.7l-1.4 1.4" />
</Icon>
) : (
<Icon d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z" />
)}
</button>
<button className="icon-btn" data-on={dock} onClick={() => setDock(!dock)} title="Toggle preview"> <button className="icon-btn" data-on={dock} onClick={() => setDock(!dock)} title="Toggle preview">
<Icon d="M3 4.5h18v15H3zM14 4.5v15" /> <Icon d="M3 4.5h18v15H3zM14 4.5v15" />
</button> </button>
+6 -3
View File
@@ -1,9 +1,12 @@
import type { ReactNode } from "react";
interface IconProps { interface IconProps {
d: string; d?: string;
size?: number; size?: number;
children?: ReactNode;
} }
export function Icon({ d, size = 16 }: IconProps) { export function Icon({ d, size = 16, children }: IconProps) {
return ( return (
<svg <svg
width={size} width={size}
@@ -15,7 +18,7 @@ export function Icon({ d, size = 16 }: IconProps) {
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"
> >
<path d={d} /> {children ?? <path d={d} />}
</svg> </svg>
); );
} }
+16
View File
@@ -0,0 +1,16 @@
import { create } from "zustand";
import { applyTheme, initialTheme, type Theme } from "../theme";
interface ThemeState {
theme: Theme;
toggle: () => void;
}
export const useTheme = create<ThemeState>((set, get) => ({
theme: initialTheme(),
toggle: () => {
const next: Theme = get().theme === "dark" ? "light" : "dark";
applyTheme(next);
set({ theme: next });
},
}));
+16
View File
@@ -0,0 +1,16 @@
export type Theme = "light" | "dark";
const KEY = "margin-theme";
export function initialTheme(): Theme {
const attr = document.documentElement.getAttribute("data-theme");
if (attr === "light" || attr === "dark") return attr;
const saved = localStorage.getItem(KEY);
if (saved === "light" || saved === "dark") return saved;
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
}
export function applyTheme(theme: Theme) {
document.documentElement.setAttribute("data-theme", theme);
localStorage.setItem(KEY, theme);
}