Tighten novella reader page chrome

This commit is contained in:
Meta Vens 2026-07-10 12:19:56 +01:00
commit cfdbaea6f3
6 changed files with 25 additions and 79 deletions

View file

@ -28,27 +28,6 @@ class Chapter:
blocks: list[str]
@dataclass
class SpinePage:
chapter_index: int
page_index: int
title: str
movement: str
blocks: list[str]
@property
def href(self) -> str:
if self.page_index == 1:
return f"text/chapter-{self.chapter_index:02}.xhtml"
return f"text/chapter-{self.chapter_index:02}-p{self.page_index:02}.xhtml"
@property
def item_id(self) -> str:
if self.page_index == 1:
return f"chapter-{self.chapter_index}"
return f"chapter-{self.chapter_index}-p{self.page_index}"
def inline_markup(text: str) -> str:
placeholders: dict[str, str] = {}
@ -113,40 +92,6 @@ def chapter_xhtml(chapter: Chapter) -> str:
return xhtml_page(chapter.title, body, "chapter")
def paginate_chapters(chapters: list[Chapter], target_chars: int = 1400) -> list[SpinePage]:
pages: list[SpinePage] = []
for chapter_index, chapter in enumerate(chapters, 1):
chunks: list[list[str]] = []
current: list[str] = []
current_chars = 0
for block in chapter.blocks:
block_chars = len(block)
if current and current_chars + block_chars > target_chars:
chunks.append(current)
current = []
current_chars = 0
current.append(block)
current_chars += block_chars
if current:
chunks.append(current)
for page_index, blocks in enumerate(chunks, 1):
pages.append(SpinePage(chapter_index, page_index, chapter.title, chapter.movement, blocks))
return pages
def page_xhtml(page: SpinePage) -> str:
paragraphs = []
for block in page.blocks:
rendered = inline_markup(block)
css_class = " class=\"pov\"" if re.fullmatch(r"<strong>[^<]+</strong>", rendered) else ""
paragraphs.append(f"<p{css_class}>{rendered}</p>")
header = ""
if page.page_index == 1:
header = f'<header><p class="movement">{html.escape(page.movement)}</p><h1>{html.escape(page.title)}</h1></header>'
body = header + "\n".join(paragraphs)
return xhtml_page(page.title, body, "chapter")
def modified_timestamp(source: Path) -> str:
epoch = int(os.environ.get("SOURCE_DATE_EPOCH", source.stat().st_mtime))
return datetime.fromtimestamp(epoch, timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
@ -154,13 +99,12 @@ def modified_timestamp(source: Path) -> str:
def build(source: Path, output: Path) -> None:
title, chapters = parse_manuscript(source)
pages = paginate_chapters(chapters)
output.parent.mkdir(parents=True, exist_ok=True)
chapter_items = "\n".join(
f'<item id="{page.item_id}" href="{page.href}" media-type="application/xhtml+xml"/>'
for page in pages
f'<item id="chapter-{i}" href="text/chapter-{i:02}.xhtml" media-type="application/xhtml+xml"/>'
for i in range(1, len(chapters) + 1)
)
spine = "\n".join(f'<itemref idref="{page.item_id}"/>' for page in pages)
spine = "\n".join(f'<itemref idref="chapter-{i}"/>' for i in range(1, len(chapters) + 1))
nav_points = "\n".join(
f'<li><a href="text/chapter-{i:02}.xhtml">{html.escape(chapter.title)}</a></li>'
for i, chapter in enumerate(chapters, 1)
@ -191,8 +135,8 @@ def build(source: Path, output: Path) -> None:
archive.writestr("EPUB/toc.ncx", ncx)
archive.writestr("EPUB/styles/book.css", css)
archive.writestr("EPUB/text/title.xhtml", title_page)
for page in pages:
archive.writestr(f"EPUB/{page.href}", page_xhtml(page))
for i, chapter in enumerate(chapters, 1):
archive.writestr(f"EPUB/text/chapter-{i:02}.xhtml", chapter_xhtml(chapter))
validate(output, len(chapters))

View file

@ -25,19 +25,28 @@ export const defaultContentPageLayout: PageLayout = {
showCurrentPage: false,
showRoot: false,
}),
condition: (page) => page.fileData.slug !== "index",
condition: (page) => page.fileData.slug !== "index" && page.fileData.slug !== "Fiction/The-Burden-of-Proof",
}),
Component.ConditionalRender({
component: Component.ArticleTitle(),
condition: (page) => !page.fileData.slug?.endsWith("/index") && page.fileData.slug !== "index",
condition: (page) =>
!page.fileData.slug?.endsWith("/index") &&
page.fileData.slug !== "index" &&
page.fileData.slug !== "Fiction/The-Burden-of-Proof",
}),
Component.ConditionalRender({
component: AetheriaAuthorMeta(),
condition: (page) => !page.fileData.slug?.endsWith("/index") && page.fileData.slug !== "index",
condition: (page) =>
!page.fileData.slug?.endsWith("/index") &&
page.fileData.slug !== "index" &&
page.fileData.slug !== "Fiction/The-Burden-of-Proof",
}),
Component.ConditionalRender({
component: Component.ContentMeta(),
condition: (page) => !page.fileData.slug?.endsWith("/index") && page.fileData.slug !== "index",
condition: (page) =>
!page.fileData.slug?.endsWith("/index") &&
page.fileData.slug !== "index" &&
page.fileData.slug !== "Fiction/The-Burden-of-Proof",
}),
Component.ConditionalRender({
component: AetheriaEpubReader(),

View file

@ -7,7 +7,7 @@ export default (() => {
const AetheriaEpubReader: QuartzComponent = () => (
<section
class="aetheria-epub-reader"
data-epub-src="/static/fiction/the-burden-of-proof.epub?v=reader-pages-2"
data-epub-src="/static/fiction/the-burden-of-proof.epub?v=reader-native-pages-1"
aria-label="The Burden of Proof ebook reader"
>
<div class="aetheria-reader-heading">
@ -32,7 +32,7 @@ export default (() => {
</label>
<label>
<span>Spread</span>
<select data-reader-setting="spread"><option value="none">Single</option><option value="auto">Auto</option></select>
<select data-reader-setting="spread"><option value="none">Single</option><option value="always">Two Pages</option></select>
</label>
<label>
<span>Theme</span>

View file

@ -6,7 +6,7 @@ type ReaderSettings = {
fontSize: number
lineHeight: number
flow: "paginated" | "scrolled-doc"
spread: "auto" | "none"
spread: "always" | "none"
}
declare global {
@ -17,7 +17,7 @@ declare global {
}
const cookieName = "aetheria_tbp_reader"
const settingsVersion = 3
const settingsVersion = 4
const defaults: ReaderSettings = {
version: settingsVersion,
theme: "dark",
@ -41,7 +41,7 @@ function readSettings(): ReaderSettings {
fontSize: Number.isFinite(saved.fontSize) ? Math.min(150, Math.max(80, saved.fontSize)) : defaults.fontSize,
lineHeight: Number.isFinite(saved.lineHeight) ? Math.min(210, Math.max(130, saved.lineHeight)) : defaults.lineHeight,
flow: ["paginated", "scrolled-doc"].includes(saved.flow) ? saved.flow : defaults.flow,
spread: saved.version === settingsVersion && ["auto", "none"].includes(saved.spread) ? saved.spread : defaults.spread,
spread: saved.version === settingsVersion && ["always", "none"].includes(saved.spread) ? saved.spread : defaults.spread,
}
} catch {
return { ...defaults }
@ -112,12 +112,6 @@ async function initReader(root: HTMLElement) {
viewport.style.width = "100%"
viewport.style.maxWidth = "100%"
viewport.style.overflow = "hidden"
viewport.querySelectorAll<HTMLElement>(".epub-container, .epub-view, iframe").forEach((element) => {
element.style.setProperty("width", "100%", "important")
element.style.setProperty("max-width", "100%", "important")
element.style.setProperty("min-width", "0", "important")
if (element.classList.contains("epub-view")) element.style.setProperty("flex", "0 0 100%", "important")
})
}
async function displayLocation(target?: string) {

View file

@ -114,10 +114,9 @@ body[data-slug="Fiction/The-Burden-of-Proof"] {
contain: layout paint;
background: #11161b;
iframe,
.epub-container {
width: 100% !important;
max-width: 100% !important;
position: absolute !important;
inset: 0 auto auto 0;
}
}