Add a reusable theme system with a Neonflux theme
Introduce a theme layer on top of the existing CSS custom properties. All color tokens now live in themes.css, scoped by a data-theme attribute on <html>, so a theme is just a new token block plus a dropdown option - no code changes needed. The Default theme preserves the existing light/dark behavior; Neonflux adds an electric-cyan dark theme. The active theme is chosen via a Theme selector in the sidebar footer and persisted in a new main-process settings file (userData settings.json) through getTheme/setTheme IPC, so it survives restarts. Hard-coded on-accent, selection, shadow and backdrop colors across the renderer were converted to semantic tokens so every theme stays consistent.
This commit is contained in:
parent
7011d0655b
commit
ad0329bbf9
12 changed files with 346 additions and 52 deletions
|
|
@ -17,6 +17,10 @@ const copyAssets = {
|
|||
path.join("src", "renderer", "index.css"),
|
||||
path.join("dist", "index.css")
|
||||
);
|
||||
fs.copyFileSync(
|
||||
path.join("src", "renderer", "themes.css"),
|
||||
path.join("dist", "themes.css")
|
||||
);
|
||||
fs.mkdirSync(path.join("dist", "icons"), { recursive: true });
|
||||
fs.copyFileSync(
|
||||
path.join("build", "icons", "256x256.png"),
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import {
|
|||
setChapterContent,
|
||||
} from "./chapters.js";
|
||||
import { combineMarkdown, buildZip } from "./export.js";
|
||||
import { getSetting, setSetting } from "./settings.js";
|
||||
|
||||
let mainWindow: BrowserWindow | null = null;
|
||||
let bookPath: string | null = null;
|
||||
|
|
@ -375,6 +376,14 @@ ipcMain.handle("folio:getClipboardText", () => {
|
|||
return clipboard.readText();
|
||||
});
|
||||
|
||||
// Theme preference. Stored as a plain string so future themes can be added
|
||||
// without touching the main process.
|
||||
ipcMain.handle("folio:getTheme", () => getSetting("theme", "default"));
|
||||
ipcMain.handle("folio:setTheme", (_e, id: unknown) => {
|
||||
setSetting("theme", typeof id === "string" && id ? id : "default");
|
||||
return { ok: true };
|
||||
});
|
||||
|
||||
ipcMain.handle("folio:importAttachment", async () => {
|
||||
const bp = requireBook();
|
||||
const { canceled, filePaths } = await showOpenDialog({
|
||||
|
|
|
|||
|
|
@ -32,6 +32,10 @@ contextBridge.exposeInMainWorld("folio", {
|
|||
copyText: (text: string) => ipcRenderer.invoke("folio:copyText", text),
|
||||
getClipboardText: () => ipcRenderer.invoke("folio:getClipboardText"),
|
||||
|
||||
// theme preference
|
||||
getTheme: () => ipcRenderer.invoke("folio:getTheme"),
|
||||
setTheme: (id: string) => ipcRenderer.invoke("folio:setTheme", id),
|
||||
|
||||
// events
|
||||
onBookOpened: (cb: (data: { path: string; title: string }) => void) =>
|
||||
ipcRenderer.on("folio:bookOpened", (_e, data) => cb(data)),
|
||||
|
|
|
|||
34
src/main/settings.ts
Normal file
34
src/main/settings.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { app } from "electron";
|
||||
import { APP_ID } from "./project.js";
|
||||
|
||||
// Tiny per-user settings store (a JSON file in the Electron userData dir).
|
||||
// Used for preferences that should survive restarts, such as the theme.
|
||||
const SETTINGS_FILE = (): string =>
|
||||
path.join(app.getPath("userData"), `${APP_ID}-settings.json`);
|
||||
|
||||
let cache: Record<string, unknown> | null = null;
|
||||
|
||||
function load(): Record<string, unknown> {
|
||||
if (cache) return cache;
|
||||
try {
|
||||
const raw = fs.readFileSync(SETTINGS_FILE(), "utf-8");
|
||||
const parsed = JSON.parse(raw);
|
||||
cache = parsed && typeof parsed === "object" ? parsed : {};
|
||||
} catch {
|
||||
cache = {};
|
||||
}
|
||||
return cache as Record<string, unknown>;
|
||||
}
|
||||
|
||||
export function getSetting<T>(key: string, fallback: T): T {
|
||||
const settings = load();
|
||||
return key in settings ? (settings[key] as T) : fallback;
|
||||
}
|
||||
|
||||
export function setSetting(key: string, value: unknown): void {
|
||||
const settings = load();
|
||||
settings[key] = value;
|
||||
fs.writeFileSync(SETTINGS_FILE(), JSON.stringify(settings, null, 2), "utf-8");
|
||||
}
|
||||
|
|
@ -60,6 +60,8 @@ export type FolioAPI = {
|
|||
importAttachment: () => Promise<any>;
|
||||
copyText: (text: string) => Promise<any>;
|
||||
getClipboardText: () => Promise<string>;
|
||||
getTheme: () => Promise<string>;
|
||||
setTheme: (id: string) => Promise<any>;
|
||||
onBookOpened: (cb: (data: { path: string; title: string }) => void) => void;
|
||||
onBookClosed: (cb: () => void) => void;
|
||||
onChaptersChanged: (cb: () => void) => void;
|
||||
|
|
|
|||
|
|
@ -1,35 +1,29 @@
|
|||
/* System-font stack only — no remote fonts. */
|
||||
:root {
|
||||
--font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
||||
"Helvetica Neue", Arial, sans-serif;
|
||||
--font-serif: Georgia, "Times New Roman", serif;
|
||||
--radius: 8px;
|
||||
--gap: 16px;
|
||||
|
||||
/* Light theme */
|
||||
--color-bg: #ffffff;
|
||||
--color-panel: #f7f7f8;
|
||||
--color-border: #e2e2e7;
|
||||
--color-text: #1a1a1d;
|
||||
--color-muted: #6b6b76;
|
||||
--color-accent: #2563eb;
|
||||
--color-accent-hover: #1d4ed8;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--color-bg: #0f0f12;
|
||||
--color-panel: #18181e;
|
||||
--color-border: #2a2a32;
|
||||
--color-text: #e6e6ec;
|
||||
--color-muted: #9a9aa6;
|
||||
--color-accent: #4a94f0;
|
||||
--color-accent-hover: #6ab0ff;
|
||||
}
|
||||
}
|
||||
|
||||
/* Theme color tokens live in themes.css (`:root[data-theme="..."]` blocks);
|
||||
this stylesheet only consumes them via var(). System fonts, no remote fonts. */
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--color-scrollbar);
|
||||
border-radius: 5px;
|
||||
border: 2px solid transparent;
|
||||
background-clip: padding-box;
|
||||
}
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--color-scrollbar-hover);
|
||||
border: 2px solid transparent;
|
||||
background-clip: padding-box;
|
||||
}
|
||||
::-webkit-scrollbar-corner {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
|
@ -117,12 +111,12 @@ html, body {
|
|||
|
||||
.chapter-list li:hover {
|
||||
background: var(--color-accent);
|
||||
color: #ffffff;
|
||||
color: var(--color-on-accent);
|
||||
}
|
||||
|
||||
.chapter-list li.active {
|
||||
background: var(--color-accent);
|
||||
color: #ffffff;
|
||||
color: var(--color-on-accent);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
|
|
@ -131,6 +125,43 @@ html, body {
|
|||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
/* ---- theme selector ---- */
|
||||
.theme-control {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.theme-label {
|
||||
font-size: 0.72rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
color: var(--color-muted);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.theme-select {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
height: 28px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius);
|
||||
background: var(--color-bg);
|
||||
color: var(--color-text);
|
||||
font-family: var(--font-sans);
|
||||
font-size: 0.82rem;
|
||||
padding: 0 6px;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.theme-select:focus {
|
||||
border-color: var(--color-accent);
|
||||
}
|
||||
|
||||
/* ---- full book nav ---- */
|
||||
.full-book-nav {
|
||||
margin-top: var(--gap);
|
||||
|
|
@ -154,13 +185,13 @@ html, body {
|
|||
|
||||
.full-book-btn:hover {
|
||||
background: var(--color-accent);
|
||||
color: #fff;
|
||||
color: var(--color-on-accent);
|
||||
border-color: var(--color-accent);
|
||||
}
|
||||
|
||||
.full-book-btn.active {
|
||||
background: var(--color-accent);
|
||||
color: #fff;
|
||||
color: var(--color-on-accent);
|
||||
font-weight: 600;
|
||||
border-color: var(--color-accent);
|
||||
}
|
||||
|
|
@ -182,7 +213,7 @@ html, body {
|
|||
|
||||
.button:hover {
|
||||
background: var(--color-accent);
|
||||
color: #ffffff;
|
||||
color: var(--color-on-accent);
|
||||
border-color: var(--color-accent);
|
||||
}
|
||||
|
||||
|
|
@ -255,7 +286,7 @@ html, body {
|
|||
|
||||
.icon-button:hover {
|
||||
background: var(--color-accent);
|
||||
color: #fff;
|
||||
color: var(--color-on-accent);
|
||||
border-color: var(--color-accent);
|
||||
}
|
||||
|
||||
|
|
@ -334,7 +365,7 @@ html, body {
|
|||
|
||||
.recent-list li:hover {
|
||||
background: var(--color-accent);
|
||||
color: #fff;
|
||||
color: var(--color-on-accent);
|
||||
}
|
||||
|
||||
.muted {
|
||||
|
|
@ -372,7 +403,7 @@ html, body {
|
|||
.chapter-row.drop-target {
|
||||
border-color: var(--color-accent);
|
||||
background: var(--color-accent);
|
||||
color: #fff;
|
||||
color: var(--color-on-accent);
|
||||
}
|
||||
|
||||
.chapter-row:focus-visible {
|
||||
|
|
@ -383,7 +414,7 @@ html, body {
|
|||
.chapter-row:hover {
|
||||
opacity: 1;
|
||||
background: var(--color-accent);
|
||||
color: #fff;
|
||||
color: var(--color-on-accent);
|
||||
border-color: var(--color-accent);
|
||||
}
|
||||
|
||||
|
|
@ -393,7 +424,7 @@ html, body {
|
|||
|
||||
.chapter-row.active {
|
||||
background: var(--color-accent);
|
||||
color: #fff;
|
||||
color: var(--color-on-accent);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
|
|
@ -411,7 +442,7 @@ html, body {
|
|||
|
||||
.chapter-row:hover .drag-handle {
|
||||
opacity: 0.9;
|
||||
color: #fff;
|
||||
color: var(--color-on-accent);
|
||||
}
|
||||
|
||||
.row-title {
|
||||
|
|
@ -445,8 +476,8 @@ html, body {
|
|||
|
||||
.icon-btn:hover {
|
||||
opacity: 1;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border-color: rgba(255, 255, 255, 0.3);
|
||||
background: var(--color-on-accent-soft);
|
||||
border-color: var(--color-on-accent-border);
|
||||
}
|
||||
|
||||
/* ---- chapter view ---- */
|
||||
|
|
@ -739,13 +770,13 @@ html, body {
|
|||
|
||||
.toolbar-btn:active {
|
||||
background: var(--color-accent);
|
||||
color: #fff;
|
||||
color: var(--color-on-accent);
|
||||
border-color: var(--color-accent);
|
||||
}
|
||||
|
||||
.toolbar-btn.active {
|
||||
background: var(--color-accent);
|
||||
color: #fff;
|
||||
color: var(--color-on-accent);
|
||||
border-color: var(--color-accent);
|
||||
}
|
||||
|
||||
|
|
@ -799,7 +830,7 @@ html, body {
|
|||
background: var(--color-panel);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.25);
|
||||
box-shadow: 0 10px 30px var(--color-shadow);
|
||||
padding: 4px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
@ -823,7 +854,7 @@ html, body {
|
|||
|
||||
.dropdown-item:hover {
|
||||
background: var(--color-accent);
|
||||
color: #fff;
|
||||
color: var(--color-on-accent);
|
||||
}
|
||||
|
||||
.dropdown-sep {
|
||||
|
|
@ -959,7 +990,7 @@ body.focus-mode .editor {
|
|||
.modal-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.45);
|
||||
background: var(--color-backdrop);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
|
@ -972,7 +1003,7 @@ body.focus-mode .editor {
|
|||
border-radius: var(--radius);
|
||||
padding: var(--gap);
|
||||
width: min(420px, calc(100vw - 48px));
|
||||
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.3);
|
||||
box-shadow: 0 12px 40px var(--color-shadow-strong);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
|
|
@ -1017,7 +1048,7 @@ body.focus-mode .editor {
|
|||
.button-primary {
|
||||
background: var(--color-accent);
|
||||
border-color: var(--color-accent);
|
||||
color: #fff;
|
||||
color: var(--color-on-accent);
|
||||
}
|
||||
|
||||
.button-primary:hover {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Folio</title>
|
||||
<link rel="stylesheet" href="./themes.css" />
|
||||
<link rel="stylesheet" href="./index.css" />
|
||||
</head>
|
||||
<body>
|
||||
|
|
@ -26,6 +27,13 @@
|
|||
<button class="full-book-btn" id="fullBookBtn" title="View the entire book as one continuous manuscript">Full Book</button>
|
||||
</div>
|
||||
<div class="sidebar-footer">
|
||||
<div class="theme-control">
|
||||
<label class="theme-label" for="themeSelect">Theme</label>
|
||||
<select id="themeSelect" class="theme-select" title="Interface theme">
|
||||
<option value="default">Default</option>
|
||||
<option value="neonflux">Neonflux</option>
|
||||
</select>
|
||||
</div>
|
||||
<button class="button button-ghost" id="newChapterBtn">+ New Chapter</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { initBook } from "./book.js";
|
||||
import type { FolioAPI } from "./book.js";
|
||||
import { initTheme } from "./theme.js";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
|
|
@ -8,5 +9,6 @@ declare global {
|
|||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
initTheme(window.folio);
|
||||
initBook(window.folio);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -86,10 +86,10 @@ const folioTheme = EditorView.theme({
|
|||
outline: "1px solid var(--color-accent)",
|
||||
},
|
||||
".cm-searchMatch": {
|
||||
backgroundColor: "rgba(37, 99, 235, 0.25)",
|
||||
backgroundColor: "var(--color-selection)",
|
||||
},
|
||||
".cm-searchMatch.cm-searchMatch-selected": {
|
||||
backgroundColor: "rgba(37, 99, 235, 0.5)",
|
||||
backgroundColor: "var(--color-selection-strong)",
|
||||
},
|
||||
".cm-panels": {
|
||||
backgroundColor: "var(--color-panel)",
|
||||
|
|
|
|||
41
src/renderer/theme.ts
Normal file
41
src/renderer/theme.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
// Theme switching. The active theme is stored on <html data-theme="..."> and
|
||||
// drives every color via the CSS custom properties in themes.css. The choice
|
||||
// is persisted in the main process so it survives restarts.
|
||||
export interface ThemeApi {
|
||||
getTheme: () => Promise<string>;
|
||||
setTheme: (id: string) => Promise<unknown>;
|
||||
}
|
||||
|
||||
function selector(): HTMLSelectElement {
|
||||
return document.getElementById("themeSelect") as HTMLSelectElement;
|
||||
}
|
||||
|
||||
export function applyTheme(id: string): void {
|
||||
const theme = typeof id === "string" && id ? id : "default";
|
||||
document.documentElement.setAttribute("data-theme", theme);
|
||||
const sel = selector();
|
||||
if (sel && sel.value !== theme) sel.value = theme;
|
||||
}
|
||||
|
||||
export function currentTheme(): string {
|
||||
return document.documentElement.getAttribute("data-theme") || "default";
|
||||
}
|
||||
|
||||
// Load the saved theme and wire the selector. Unknown stored values fall back
|
||||
// to Default (the :root base), so adding a theme later is safe for old saves.
|
||||
export async function initTheme(api: ThemeApi): Promise<void> {
|
||||
let saved = "default";
|
||||
try {
|
||||
const value = await api.getTheme();
|
||||
if (typeof value === "string" && value) saved = value;
|
||||
} catch {
|
||||
// keep the default on any IPC failure
|
||||
}
|
||||
applyTheme(saved);
|
||||
const sel = selector();
|
||||
if (!sel) return;
|
||||
sel.addEventListener("change", () => {
|
||||
applyTheme(sel.value);
|
||||
void api.setTheme(sel.value);
|
||||
});
|
||||
}
|
||||
100
src/renderer/themes.css
Normal file
100
src/renderer/themes.css
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
/* Folio themes.
|
||||
Every interface color is driven by the semantic variables below; the UI and
|
||||
CodeMirror styles only ever reference these variables. A theme is therefore
|
||||
just a block of variable values — adding one means dropping a
|
||||
`:root[data-theme="..."]` block here plus an <option> in the theme selector.
|
||||
|
||||
Default mirrors Folio's original appearance: light, with an automatic dark
|
||||
variant that follows the OS color scheme. Neonflux is a fixed dark,
|
||||
futuristic theme. */
|
||||
|
||||
/* ---- shared (non-color) tokens ---- */
|
||||
:root {
|
||||
--font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
||||
"Helvetica Neue", Arial, sans-serif;
|
||||
--font-serif: Georgia, "Times New Roman", serif;
|
||||
--radius: 8px;
|
||||
--gap: 16px;
|
||||
}
|
||||
|
||||
/* ---- Default (light) ---- */
|
||||
:root,
|
||||
:root[data-theme="default"] {
|
||||
--color-bg: #ffffff;
|
||||
--color-panel: #f7f7f8;
|
||||
--color-border: #e2e2e7;
|
||||
--color-text: #1a1a1d;
|
||||
--color-muted: #6b6b76;
|
||||
--color-accent: #2563eb;
|
||||
--color-accent-hover: #1d4ed8;
|
||||
|
||||
--color-on-accent: #ffffff;
|
||||
--color-on-accent-soft: rgba(255, 255, 255, 0.15);
|
||||
--color-on-accent-border: rgba(255, 255, 255, 0.3);
|
||||
|
||||
--color-selection: rgba(37, 99, 235, 0.25);
|
||||
--color-selection-strong: rgba(37, 99, 235, 0.5);
|
||||
|
||||
--color-shadow: rgba(0, 0, 0, 0.25);
|
||||
--color-shadow-strong: rgba(0, 0, 0, 0.3);
|
||||
--color-backdrop: rgba(0, 0, 0, 0.45);
|
||||
|
||||
--color-scrollbar: rgba(0, 0, 0, 0.18);
|
||||
--color-scrollbar-hover: rgba(0, 0, 0, 0.32);
|
||||
}
|
||||
|
||||
/* ---- Default (dark, follows OS preference) ---- */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root:not([data-theme]),
|
||||
:root[data-theme="default"] {
|
||||
--color-bg: #0f0f12;
|
||||
--color-panel: #18181e;
|
||||
--color-border: #2a2a32;
|
||||
--color-text: #e6e6ec;
|
||||
--color-muted: #9a9aa6;
|
||||
--color-accent: #4a94f0;
|
||||
--color-accent-hover: #6ab0ff;
|
||||
|
||||
--color-on-accent: #ffffff;
|
||||
--color-on-accent-soft: rgba(255, 255, 255, 0.15);
|
||||
--color-on-accent-border: rgba(255, 255, 255, 0.3);
|
||||
|
||||
--color-selection: rgba(74, 148, 240, 0.25);
|
||||
--color-selection-strong: rgba(74, 148, 240, 0.5);
|
||||
|
||||
--color-shadow: rgba(0, 0, 0, 0.55);
|
||||
--color-shadow-strong: rgba(0, 0, 0, 0.65);
|
||||
--color-backdrop: rgba(0, 0, 0, 0.6);
|
||||
|
||||
--color-scrollbar: rgba(255, 255, 255, 0.15);
|
||||
--color-scrollbar-hover: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- Neonflux ----
|
||||
Dark, high-contrast, futuristic. A near-black neutral base keeps the
|
||||
manuscript comfortable for long writing sessions while electric-cyan accent
|
||||
marks active/selected/focused elements, links and controls. */
|
||||
:root[data-theme="neonflux"] {
|
||||
--color-bg: #0a0d14;
|
||||
--color-panel: #121722;
|
||||
--color-border: #24304a;
|
||||
--color-text: #e7ecf6;
|
||||
--color-muted: #8d97ad;
|
||||
--color-accent: #22d3ee;
|
||||
--color-accent-hover: #67e8f9;
|
||||
|
||||
--color-on-accent: #06131c;
|
||||
--color-on-accent-soft: rgba(6, 19, 28, 0.18);
|
||||
--color-on-accent-border: rgba(6, 19, 28, 0.38);
|
||||
|
||||
--color-selection: rgba(34, 211, 238, 0.16);
|
||||
--color-selection-strong: rgba(34, 211, 238, 0.34);
|
||||
|
||||
--color-shadow: rgba(0, 0, 0, 0.55);
|
||||
--color-shadow-strong: rgba(0, 0, 0, 0.65);
|
||||
--color-backdrop: rgba(4, 8, 16, 0.6);
|
||||
|
||||
--color-scrollbar: #263047;
|
||||
--color-scrollbar-hover: #33425f;
|
||||
}
|
||||
|
|
@ -714,6 +714,57 @@ const BOOK3_DIR = __BOOKDIR3_VALUE__;
|
|||
out.navBreakCount = navBreaks.length;
|
||||
out.navBreaksOk = navBreaks.length >= 1 && navBreaks.some((el) => el.textContent.startsWith("# Two"));
|
||||
|
||||
// --- theme system: selector, immediate switching, persistence ---
|
||||
const themeSelect = document.getElementById("themeSelect");
|
||||
const themeVar = (name) =>
|
||||
getComputedStyle(document.documentElement).getPropertyValue(name).trim();
|
||||
const docBeforeTheme = navBook.state.doc.toString();
|
||||
out.themeSelectPresent = !!themeSelect;
|
||||
out.themeSelectOptions = themeSelect
|
||||
? Array.from(themeSelect.options).map((o) => o.value)
|
||||
: [];
|
||||
out.themeHasDefault = out.themeSelectOptions.includes("default");
|
||||
out.themeHasNeonflux = out.themeSelectOptions.includes("neonflux");
|
||||
out.themeInitialAttr = document.documentElement.getAttribute("data-theme");
|
||||
out.themeInitialStored = await window.folio.getTheme();
|
||||
|
||||
// Switch to Neonflux: applies immediately and persists via IPC.
|
||||
themeSelect.value = "neonflux";
|
||||
themeSelect.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
await navWait(200);
|
||||
out.themeNeonfluxAttr = document.documentElement.getAttribute("data-theme");
|
||||
out.themeNeonfluxStored = await window.folio.getTheme();
|
||||
const neonBg = themeVar("--color-bg");
|
||||
const neonAccent = themeVar("--color-accent");
|
||||
out.themeNeonfluxBg = neonBg;
|
||||
out.themeNeonfluxAccent = neonAccent;
|
||||
out.themeNeonfluxApplied =
|
||||
out.themeNeonfluxAttr === "neonflux" &&
|
||||
out.themeNeonfluxStored === "neonflux" &&
|
||||
neonBg.toLowerCase() === "#0a0d14" &&
|
||||
neonAccent.toLowerCase() === "#22d3ee";
|
||||
|
||||
// Neonflux is dark: computed background of the app should be near-black.
|
||||
const bodyBg = getComputedStyle(document.body).backgroundColor;
|
||||
out.themeNeonfluxDark = /rgb\((10|11|12|9|8),/.test(bodyBg);
|
||||
|
||||
// Switching themes must not touch the manuscript content.
|
||||
out.themeContentUnchanged = navBook.state.doc.toString() === docBeforeTheme;
|
||||
|
||||
// Switch back to Default: applies immediately and persists.
|
||||
themeSelect.value = "default";
|
||||
themeSelect.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
await navWait(200);
|
||||
out.themeDefaultAttr = document.documentElement.getAttribute("data-theme");
|
||||
out.themeDefaultStored = await window.folio.getTheme();
|
||||
const defaultBg = themeVar("--color-bg");
|
||||
out.themeDefaultBg = defaultBg;
|
||||
out.themeDefaultApplied =
|
||||
out.themeDefaultAttr === "default" &&
|
||||
out.themeDefaultStored === "default" &&
|
||||
(defaultBg.toLowerCase() === "#ffffff" || defaultBg.toLowerCase() === "#0f0f12");
|
||||
out.themeContentUnchanged2 = navBook.state.doc.toString() === docBeforeTheme;
|
||||
|
||||
const okFlags = [
|
||||
"dndMoved",
|
||||
"kbMoved",
|
||||
|
|
@ -781,6 +832,14 @@ const BOOK3_DIR = __BOOKDIR3_VALUE__;
|
|||
"navFollowsNewOrder",
|
||||
"navNextAfterReorderOk",
|
||||
"navBreaksOk",
|
||||
"themeSelectPresent",
|
||||
"themeHasDefault",
|
||||
"themeHasNeonflux",
|
||||
"themeNeonfluxApplied",
|
||||
"themeNeonfluxDark",
|
||||
"themeContentUnchanged",
|
||||
"themeDefaultApplied",
|
||||
"themeContentUnchanged2",
|
||||
];
|
||||
out.all = okFlags.every((f) => out[f] === true);
|
||||
} catch (e) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue