Reader: add Sepia reading theme

Add a Dark/Sepia theme toggle to the reader's reading-settings drawer for
easier long-form reading. Sepia uses a warm paper background with dark brown
text via a :root[data-theme="sepia"] palette in theme.css. Text colour is now
stored per theme with theme-specific swatch sets; the old single key migrates
into the dark slot. An inline head script applies the saved theme before paint
to avoid a flash.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ivo Oskamp 2026-06-01 14:00:46 +02:00
parent 5207da0792
commit 74de3ddee2
3 changed files with 125 additions and 12 deletions

View File

@ -13,8 +13,26 @@
--success: #6baa6b;
--warning: #c8a03a;
--error: #c85a3a;
--shadow-ring: rgba(255,255,255,0.1);
--radius: 6px;
--sidebar: 220px;
--mono: 'DM Mono', monospace;
--serif: 'Libre Baskerville', Georgia, serif;
}
/* ── Sepia reading theme (reader only) ──────────────────────────────────── */
:root[data-theme="sepia"] {
--bg: #f4ecd8;
--surface: #ece2cb;
--surface2: #e3d7bb;
--border: #d8cbac;
--accent: #c17d12;
--accent2: #a9690c;
--text: #5b4636;
--text-dim: #7c6a55;
--text-faint: #ab9a82;
--success: #4e8a4e;
--warning: #9a7714;
--error: #b04a2c;
--shadow-ring: rgba(0,0,0,0.12);
}

View File

@ -71,7 +71,7 @@
.btn-header-bm:hover { background: rgba(255,162,14,0.08); border-color: var(--accent); }
.btn-header-series {
display: none;
color: var(--text-faint); border-color: rgba(255,255,255,0.08);
color: var(--text-faint); border-color: var(--border);
padding: 0.3rem 0.5rem;
}
.btn-header-series.active {
@ -177,11 +177,26 @@
width: 24px; height: 24px; border-radius: 50%;
border: 2px solid transparent;
cursor: pointer; transition: border-color 0.12s, transform 0.1s;
box-shadow: 0 0 0 1px rgba(255,255,255,0.1);
box-shadow: 0 0 0 1px var(--shadow-ring);
padding: 0;
}
.colour-swatch:hover { transform: scale(1.15); }
.colour-swatch.active { border-color: var(--accent); }
.colour-swatches.hidden { display: none; }
/* ── Theme toggle ── */
.theme-options { display: flex; gap: 0.5rem; }
.theme-btn {
flex: 1;
padding: 0.4rem 0.6rem;
background: var(--surface2); border: 1px solid var(--border);
border-radius: var(--radius);
font-family: var(--mono); font-size: 0.7rem;
color: var(--text-dim); cursor: pointer;
transition: color 0.12s, border-color 0.12s;
}
.theme-btn:hover { color: var(--text); border-color: var(--text-faint); }
.theme-btn.active { color: var(--accent); border-color: var(--accent); }
/* ── Viewer ── */
#viewer {
@ -300,6 +315,16 @@
}
@keyframes spin { to { transform: rotate(360deg); } }
</style>
<script>
/* Apply saved reading theme before paint to avoid a flash. */
(function () {
try {
if (localStorage.getItem('reader-theme') === 'sepia') {
document.documentElement.setAttribute('data-theme', 'sepia');
}
} catch (e) {}
})();
</script>
</head>
<body>
@ -327,6 +352,13 @@
<div class="settings-overlay" id="settings-overlay" onclick="closeSettings()"></div>
<div class="settings-drawer" id="settings-drawer">
<div class="settings-drawer-title">Reading settings</div>
<div class="settings-row">
<div class="settings-label">Theme</div>
<div class="theme-options">
<button class="theme-btn" data-theme="dark" onclick="applyTheme('dark')">Dark</button>
<button class="theme-btn" data-theme="sepia" onclick="applyTheme('sepia')">Sepia</button>
</div>
</div>
<div class="settings-row">
<div class="settings-label">
Content width
@ -345,13 +377,20 @@
</div>
<div class="settings-row">
<div class="settings-label">Text colour</div>
<div class="colour-swatches">
<div class="colour-swatches" data-theme="dark">
<button class="colour-swatch" data-colour="#e8e2d9" title="Bright" style="background:#e8e2d9" onclick="applyTextColour('#e8e2d9')"></button>
<button class="colour-swatch" data-colour="#d4cec5" title="Warm cream" style="background:#d4cec5" onclick="applyTextColour('#d4cec5')"></button>
<button class="colour-swatch" data-colour="#bfb8ae" title="Soft sand" style="background:#bfb8ae" onclick="applyTextColour('#bfb8ae')"></button>
<button class="colour-swatch" data-colour="#a9a29a" title="Muted" style="background:#a9a29a" onclick="applyTextColour('#a9a29a')"></button>
<button class="colour-swatch" data-colour="#938d86" title="Dim" style="background:#938d86" onclick="applyTextColour('#938d86')"></button>
</div>
<div class="colour-swatches hidden" data-theme="sepia">
<button class="colour-swatch" data-colour="#5b4636" title="Warm brown" style="background:#5b4636" onclick="applyTextColour('#5b4636')"></button>
<button class="colour-swatch" data-colour="#4a3a2c" title="Dark cocoa" style="background:#4a3a2c" onclick="applyTextColour('#4a3a2c')"></button>
<button class="colour-swatch" data-colour="#6b5746" title="Medium" style="background:#6b5746" onclick="applyTextColour('#6b5746')"></button>
<button class="colour-swatch" data-colour="#3a2e24" title="Near black" style="background:#3a2e24" onclick="applyTextColour('#3a2e24')"></button>
<button class="colour-swatch" data-colour="#7c6a55" title="Dim" style="background:#7c6a55" onclick="applyTextColour('#7c6a55')"></button>
</div>
</div>
</div>
@ -452,18 +491,49 @@
applyWidth(saved);
}
// ── Text colour ────────────────────────────────────────────────
function applyTextColour(hex) {
document.documentElement.style.setProperty('--text', hex);
localStorage.setItem('reader-text-colour', hex);
document.querySelectorAll('.colour-swatch').forEach(el => {
el.classList.toggle('active', el.dataset.colour === hex);
// ── Theme (Dark / Sepia) ───────────────────────────────────────
const THEME_TEXT_DEFAULT = { dark: '#e8e2d9', sepia: '#5b4636' };
function currentTheme() {
return document.documentElement.getAttribute('data-theme') === 'sepia' ? 'sepia' : 'dark';
}
function applyTheme(theme) {
if (theme !== 'sepia') theme = 'dark';
if (theme === 'dark') {
document.documentElement.removeAttribute('data-theme');
} else {
document.documentElement.setAttribute('data-theme', theme);
}
localStorage.setItem('reader-theme', theme);
document.querySelectorAll('.theme-btn').forEach(el => {
el.classList.toggle('active', el.dataset.theme === theme);
});
document.querySelectorAll('.colour-swatches').forEach(el => {
el.classList.toggle('hidden', el.dataset.theme !== theme);
});
loadTextColour();
}
function loadTheme() {
applyTheme(localStorage.getItem('reader-theme') || 'dark');
}
// ── Text colour (per theme) ────────────────────────────────────
function applyTextColour(hex) {
const theme = currentTheme();
document.documentElement.style.setProperty('--text', hex);
localStorage.setItem('reader-text-colour-' + theme, hex);
document.querySelectorAll('.colour-swatches[data-theme="' + theme + '"] .colour-swatch')
.forEach(el => el.classList.toggle('active', el.dataset.colour === hex));
}
function loadTextColour() {
const saved = localStorage.getItem('reader-text-colour') || '#e8e2d9';
applyTextColour(saved);
const theme = currentTheme();
let saved = localStorage.getItem('reader-text-colour-' + theme);
// Migrate the pre-theme single key into the dark slot.
if (!saved && theme === 'dark') saved = localStorage.getItem('reader-text-colour');
applyTextColour(saved || THEME_TEXT_DEFAULT[theme]);
}
// ── Font size ──────────────────────────────────────────────────
@ -639,7 +709,7 @@
// ── Init ───────────────────────────────────────────────────────
async function init() {
loadWidth();
loadTextColour();
loadTheme();
loadFontSize();
loadSeriesNav();

View File

@ -1,5 +1,30 @@
# Develop Changelog
## 2026-06-01 — Reader: Sepia reading theme
### Added
- The reader now offers a **Sepia** theme alongside the existing Dark theme, for easier long-form reading (warm paper background with dark brown text instead of light-on-black). New **Theme** toggle (Dark / Sepia) at the top of the reading settings drawer.
- `static/theme.css`: added a `:root[data-theme="sepia"]` palette overriding `--bg`, `--surface`, `--surface2`, `--border`, `--accent`, `--accent2`, `--text`, `--text-dim`, `--text-faint`, `--success`, `--warning`, `--error`. Also added a `--shadow-ring` variable (light hairline in dark, dark hairline in sepia) so swatch rings read correctly in both themes.
- `templates/reader.html`: theme toggle buttons + `.theme-btn` styling; an inline head script applies the saved theme before paint to avoid a flash; the Text colour row now has two theme-specific swatch sets (light tints for dark, dark brown tints for sepia), toggled by visibility.
- JS: `applyTheme()`/`loadTheme()` set the `data-theme` attribute and persist to `localStorage` (`reader-theme`). Text colour is now stored per theme (`reader-text-colour-dark` / `reader-text-colour-sepia`); the old single `reader-text-colour` key is migrated into the dark slot. `init()` calls `loadTheme()` (which loads the active theme's text colour).
- Replaced two hard-coded `rgba(255,255,255,...)` values in the reader CSS (swatch ring, series-button border) with theme variables so they don't show as white halos in sepia.
## 2026-05-31 — Build version in the sidebar
### Added
- The sidebar now shows the running build version at the bottom (e.g. `v0.2.11` for releases, `v0.2.11.3` for dev builds), linking to the changelog page.
- New `containers/novela/version.py` exposes `display_version()`. The semantic release version stays the single source in `changelog.py` (`CHANGELOG[0]["version"]`); a `BUILD` segment is appended for dev builds.
- `shared_templates.py` registers `app_version` as a Jinja global; `_sidebar.html` renders it as a `.sidebar-version` link styled in `sidebar.css`.
- `main.py` adds a `/api/version` endpoint returning `{"version": display_version()}`.
## 2026-05-31 — Find & Replace: scope option
### Added
- Editor Find & Replace: new **Current chapter only** checkbox in the modal options. When checked, the search/replace runs against the currently open chapter instead of every chapter in the book.
- `templates/editor.html`: added the `rp-current` checkbox next to Regex/Case sensitive; modal title changed from "Find & Replace — all chapters" to "Find & Replace" since scope is now selectable.
- `static/editor.js` (`replaceInAllChapters()`): reads `rp-current`; builds a `targets` list of either just `currentCh()` or all `chapters`, and iterates that. Shows an error ("No chapter open.") if Current-chapter-only is selected with no open chapter. Result message reads "… in current chapter" for the single-chapter case.
- Default is unchecked, so the existing all-chapters behaviour is preserved.
## 2026-05-10
- Reader: subheading/chat styling now also wins when the wrapper contains block elements with their own color rule (e.g. `<div class="subheading"><p>…</p></div>`)
- Previously the `<p>`/`<h*>` rules in `reader.html` (`#chapter-content p { color: var(--text); }`, etc.) had the same specificity as `.subheading` and applied more directly to the inner element, so the parent's color was effectively overridden — the wrapped paragraph stayed in the default text color