mirror of
https://github.com/priyanshujain/margin.git
synced 2026-10-02 19:17:03 +00:00
feat(theme): persistent light/dark toggle in the titlebar
This commit is contained in:
1 parent
a5b3c0c0e3
commit
9991d318ba
5 files changed
+61
-3
No files matched your search
+10
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -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 });
|
||||||
|
},
|
||||||
|
}));
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
Reference in new issue
Block a user