diff --git a/DESIGN.md b/DESIGN.md
index 52aabb79bec76..a952eb509bf20 100644
--- a/DESIGN.md
+++ b/DESIGN.md
@@ -211,6 +211,14 @@ Layout rules:
- **Accessibility**: The button and polite live region use page-localized names and outcomes. The icon is decorative, the control remains keyboard-operable, and whole-note links omit heading fragments so section sharing stays owned by heading permalinks.
- **Motion**: The beui `action-swap` blur/scale mechanism is adapted to the existing 150ms micro token, using a 3px blur and 75% scale only during the icon crossfade. Reduced-motion mode removes blur, scale, and press transforms while preserving the state change.
+### ReadingComfort
+
+- **Structure**: One 40px `Aa` action joins the existing reader-action row and opens a compact three-button stepper for smaller, default, or larger article text. It changes only the current article body and headings; metadata, toolbars, sidebars, code blocks, and page width keep their existing metrics.
+- **Scale**: Offer four bounded levels: 90%, 100%, 110%, and 120%. The smaller and larger commands clamp at the bounds, the center command restores 100%, and the current percentage is announced in the panel and trigger label.
+- **Storage**: Persist only the selected percentage in `localStorage.reading-comfort`; unknown or unavailable values fall back to 100%. Storage failure leaves the selected size active for the current page and reports the non-persistent state without blocking reading.
+- **Accessibility**: Use native buttons with localized names, visible focus rings, disabled boundary states, a polite value/status region, Escape and outside-click dismissal, and focus restoration. Cross-tab changes update the rendered article without navigation.
+- **Motion**: Reuse the 150ms reader-control hover and press feedback. Reduced-motion mode removes spatial feedback, and print hides the control while preserving the selected article scale.
+
## 6. Motion & Interaction
Motion is quiet utility feedback, not brand theater.
diff --git a/quartz/components/renderPage.tsx b/quartz/components/renderPage.tsx
index 4a4bba342d90d..87f1c8425319d 100644
--- a/quartz/components/renderPage.tsx
+++ b/quartz/components/renderPage.tsx
@@ -373,6 +373,8 @@ export function renderPage(
const randomWander = i18n(pageLocale).components.randomWander ?? fallbackRandomWander
const fallbackNoteShare = TRANSLATIONS[defaultTranslation].components.noteShare
const noteShare = i18n(pageLocale).components.noteShare ?? fallbackNoteShare
+ const fallbackReadingComfort = TRANSLATIONS[defaultTranslation].components.readingComfort
+ const readingComfort = i18n(pageLocale).components.readingComfort ?? fallbackReadingComfort
// During local dev (--serve), the dev server serves from root without the
// baseUrl subpath, so basePath must be empty to avoid broken links.
const basePath =
@@ -410,6 +412,12 @@ export function renderPage(
data-note-share-shared={noteShare.shared}
data-note-share-copied={noteShare.copied}
data-note-share-failed={noteShare.failed}
+ data-reading-comfort-title={readingComfort.title}
+ data-reading-comfort-smaller={readingComfort.smaller}
+ data-reading-comfort-reset={readingComfort.reset}
+ data-reading-comfort-larger={readingComfort.larger}
+ data-reading-comfort-value={readingComfort.value}
+ data-reading-comfort-failed={readingComfort.failed}
>
{frame.css && }
diff --git a/quartz/components/scripts/readingComfort.test.ts b/quartz/components/scripts/readingComfort.test.ts
new file mode 100644
index 0000000000000..981cccdcc5397
--- /dev/null
+++ b/quartz/components/scripts/readingComfort.test.ts
@@ -0,0 +1,53 @@
+import assert from "node:assert"
+import test, { describe } from "node:test"
+import enUs from "../../i18n/locales/en-US"
+import zhCn from "../../i18n/locales/zh-CN"
+import zhTw from "../../i18n/locales/zh-TW"
+import {
+ READING_COMFORT_DEFAULT,
+ parseReadingComfortLevel,
+ readingComfortBootstrapScript,
+ readingComfortScript,
+ stepReadingComfortLevel,
+} from "./readingComfort"
+
+describe("reading comfort levels", () => {
+ test("accepts only supported stored levels", () => {
+ assert.equal(parseReadingComfortLevel("90"), 90)
+ assert.equal(parseReadingComfortLevel("120"), 120)
+ assert.equal(parseReadingComfortLevel(null), READING_COMFORT_DEFAULT)
+ assert.equal(parseReadingComfortLevel("110.5"), READING_COMFORT_DEFAULT)
+ assert.equal(parseReadingComfortLevel("Infinity"), READING_COMFORT_DEFAULT)
+ assert.equal(parseReadingComfortLevel("not-a-level"), READING_COMFORT_DEFAULT)
+ })
+
+ test("steps between levels and clamps at both ends", () => {
+ assert.equal(stepReadingComfortLevel(100, -1), 90)
+ assert.equal(stepReadingComfortLevel(100, 1), 110)
+ assert.equal(stepReadingComfortLevel(90, -1), 90)
+ assert.equal(stepReadingComfortLevel(120, 1), 120)
+ assert.equal(stepReadingComfortLevel(999, 1), 110)
+ })
+})
+
+test("reading-comfort scripts compile and follow the Quartz lifecycle", () => {
+ assert.doesNotThrow(() => new Function(readingComfortBootstrapScript))
+ assert.doesNotThrow(() => new Function(readingComfortScript))
+ assert.match(
+ readingComfortScript,
+ /document\.addEventListener\("nav", initializeReadingComfort\)/,
+ )
+ assert.match(
+ readingComfortScript,
+ /document\.addEventListener\("render", initializeReadingComfort\)/,
+ )
+ assert.match(readingComfortScript, /window\.addCleanup\(cleanupReadingComfort\)/)
+ assert.match(readingComfortScript, /window\.addEventListener\("storage", syncStoredLevel\)/)
+ assert.match(readingComfortScript, /event\.key === "Escape"/)
+})
+
+test("reading-comfort labels use the central locale catalog", () => {
+ assert.equal(enUs.components.readingComfort?.title, "Reading size")
+ assert.equal(zhCn.components.readingComfort?.larger, "放大正文")
+ assert.equal(zhTw.components.readingComfort?.failed, "已套用,但瀏覽器未能儲存偏好")
+})
diff --git a/quartz/components/scripts/readingComfort.ts b/quartz/components/scripts/readingComfort.ts
new file mode 100644
index 0000000000000..5e42022dc790d
--- /dev/null
+++ b/quartz/components/scripts/readingComfort.ts
@@ -0,0 +1,205 @@
+export const READING_COMFORT_LEVELS = [90, 100, 110, 120] as const
+export const READING_COMFORT_DEFAULT = 100
+
+export function parseReadingComfortLevel(raw: string | null): number {
+ if (raw === null || raw.trim().length === 0) return READING_COMFORT_DEFAULT
+
+ const level = Number(raw)
+ return READING_COMFORT_LEVELS.includes(level as (typeof READING_COMFORT_LEVELS)[number])
+ ? level
+ : READING_COMFORT_DEFAULT
+}
+
+export function stepReadingComfortLevel(current: number, direction: -1 | 1): number {
+ const normalized = parseReadingComfortLevel(String(current))
+ const currentIndex = READING_COMFORT_LEVELS.indexOf(
+ normalized as (typeof READING_COMFORT_LEVELS)[number],
+ )
+ const nextIndex = Math.max(
+ 0,
+ Math.min(READING_COMFORT_LEVELS.length - 1, currentIndex + direction),
+ )
+ return READING_COMFORT_LEVELS[nextIndex]
+}
+
+export const readingComfortBootstrapScript = `
+const READING_COMFORT_LEVELS = ${JSON.stringify(READING_COMFORT_LEVELS)}
+const READING_COMFORT_DEFAULT = ${READING_COMFORT_DEFAULT}
+const parseReadingComfortLevel = ${parseReadingComfortLevel.toString()}
+try {
+ const level = parseReadingComfortLevel(localStorage.getItem("reading-comfort"))
+ document.documentElement.dataset.readingComfort = String(level)
+} catch {
+ document.documentElement.dataset.readingComfort = String(READING_COMFORT_DEFAULT)
+}
+`
+
+export const readingComfortScript = `
+const READING_COMFORT_KEY = "reading-comfort"
+const READING_COMFORT_LEVELS = ${JSON.stringify(READING_COMFORT_LEVELS)}
+const READING_COMFORT_DEFAULT = ${READING_COMFORT_DEFAULT}
+const parseReadingComfortLevel = ${parseReadingComfortLevel.toString()}
+const stepReadingComfortLevel = ${stepReadingComfortLevel.toString()}
+
+function getReadingComfortLabels() {
+ const {
+ readingComfortTitle: title,
+ readingComfortSmaller: smaller,
+ readingComfortReset: reset,
+ readingComfortLarger: larger,
+ readingComfortValue: value,
+ readingComfortFailed: failed,
+ } = document.body.dataset
+ if (!title || !smaller || !reset || !larger || !value || !failed) return undefined
+ return { title, smaller, reset, larger, value, failed }
+}
+
+function createReadingComfortButton(className, text, label) {
+ const button = document.createElement("button")
+ button.type = "button"
+ button.className = className
+ button.textContent = text
+ button.title = label
+ button.setAttribute("aria-label", label)
+ return button
+}
+
+let cleanupCurrentReadingComfort = () => {}
+const cleanupReadingComfort = () => {
+ const cleanup = cleanupCurrentReadingComfort
+ cleanupCurrentReadingComfort = () => {}
+ cleanup()
+}
+
+function initializeReadingComfort() {
+ cleanupReadingComfort()
+ const titleElement = document.querySelector("h1.article-title")
+ const anchor = document.querySelector(".content-meta") ?? titleElement
+ const labels = getReadingComfortLabels()
+ if (anchor === null || titleElement === null || labels === undefined) return
+
+ const existingRoot = document.querySelector("[data-read-later-root]")
+ const root = existingRoot ?? document.createElement("div")
+ const ownsRoot = existingRoot === null
+ if (ownsRoot) {
+ root.className = "reading-comfort"
+ anchor.insertAdjacentElement("afterend", root)
+ }
+ root.classList.add("reading-comfort-host")
+
+ const trigger = createReadingComfortButton("reading-comfort-trigger", "Aa", labels.title)
+ trigger.setAttribute("aria-controls", "reading-comfort-panel")
+ trigger.setAttribute("aria-expanded", "false")
+
+ const panel = document.createElement("section")
+ panel.id = "reading-comfort-panel"
+ panel.className = "reading-comfort-panel"
+ panel.setAttribute("aria-label", labels.title)
+ panel.hidden = true
+
+ const value = document.createElement("span")
+ value.className = "reading-comfort-value"
+ value.setAttribute("aria-live", "polite")
+ const controls = document.createElement("div")
+ controls.className = "reading-comfort-controls"
+ const smaller = createReadingComfortButton("reading-comfort-smaller", "A-", labels.smaller)
+ const reset = createReadingComfortButton("reading-comfort-reset", "100%", labels.reset)
+ const larger = createReadingComfortButton("reading-comfort-larger", "A+", labels.larger)
+ controls.append(smaller, reset, larger)
+ const status = document.createElement("span")
+ status.className = "reading-comfort-status"
+ status.setAttribute("aria-live", "polite")
+ panel.append(value, controls, status)
+ root.prepend(trigger)
+ root.append(panel)
+
+ let current = parseReadingComfortLevel(document.documentElement.dataset.readingComfort ?? null)
+ const closePanel = (restoreFocus) => {
+ panel.hidden = true
+ trigger.setAttribute("aria-expanded", "false")
+ if (restoreFocus) trigger.focus()
+ }
+ const closeReadLaterPanel = () => {
+ const siblingPanel = root.querySelector(".read-later-panel")
+ const siblingTrigger = root.querySelector(".read-later-trigger")
+ if (siblingPanel instanceof HTMLElement) siblingPanel.hidden = true
+ if (siblingTrigger instanceof HTMLElement) siblingTrigger.setAttribute("aria-expanded", "false")
+ }
+ const render = () => {
+ document.documentElement.dataset.readingComfort = String(current)
+ value.textContent = labels.value.replace("{percent}", String(current))
+ trigger.dataset.level = String(current)
+ trigger.title = value.textContent
+ trigger.setAttribute("aria-label", value.textContent)
+ smaller.disabled = current === READING_COMFORT_LEVELS[0]
+ larger.disabled = current === READING_COMFORT_LEVELS[READING_COMFORT_LEVELS.length - 1]
+ reset.disabled = current === READING_COMFORT_DEFAULT
+ }
+ const persist = () => {
+ status.textContent = ""
+ try {
+ localStorage.setItem(READING_COMFORT_KEY, String(current))
+ } catch {
+ status.textContent = labels.failed
+ }
+ render()
+ }
+ const adjust = (direction) => {
+ current = stepReadingComfortLevel(current, direction)
+ persist()
+ }
+
+ trigger.addEventListener("click", () => {
+ const opening = panel.hidden
+ if (opening) closeReadLaterPanel()
+ panel.hidden = !opening
+ trigger.setAttribute("aria-expanded", String(opening))
+ if (opening) (smaller.disabled ? larger : smaller).focus()
+ })
+ smaller.addEventListener("click", () => adjust(-1))
+ reset.addEventListener("click", () => {
+ current = READING_COMFORT_DEFAULT
+ persist()
+ })
+ larger.addEventListener("click", () => adjust(1))
+ const dismissOutside = (event) => {
+ if (event.target instanceof Node && !panel.contains(event.target) && event.target !== trigger) {
+ closePanel(false)
+ }
+ }
+ const dismissWithKeyboard = (event) => {
+ if (event.key === "Escape" && !panel.hidden) closePanel(true)
+ }
+ const dismissForSibling = (event) => {
+ if (event.target instanceof Node && event.target.closest?.(".read-later-trigger")) {
+ closePanel(false)
+ }
+ }
+ const syncStoredLevel = (event) => {
+ if (event.key !== null && event.key !== READING_COMFORT_KEY) return
+ current = parseReadingComfortLevel(event.newValue)
+ status.textContent = ""
+ render()
+ }
+ document.addEventListener("pointerdown", dismissOutside)
+ document.addEventListener("keydown", dismissWithKeyboard)
+ document.addEventListener("click", dismissForSibling)
+ window.addEventListener("storage", syncStoredLevel)
+ render()
+
+ cleanupCurrentReadingComfort = () => {
+ document.removeEventListener("pointerdown", dismissOutside)
+ document.removeEventListener("keydown", dismissWithKeyboard)
+ document.removeEventListener("click", dismissForSibling)
+ window.removeEventListener("storage", syncStoredLevel)
+ trigger.remove()
+ panel.remove()
+ root.classList.remove("reading-comfort-host")
+ if (ownsRoot) root.remove()
+ }
+ window.addCleanup(cleanupReadingComfort)
+}
+
+document.addEventListener("nav", initializeReadingComfort)
+document.addEventListener("render", initializeReadingComfort)
+`
diff --git a/quartz/components/styles/readingComfort.scss b/quartz/components/styles/readingComfort.scss
new file mode 100644
index 0000000000000..635e43cd38c9d
--- /dev/null
+++ b/quartz/components/styles/readingComfort.scss
@@ -0,0 +1,221 @@
+:root {
+ --reading-comfort-body: 1rem;
+ --reading-comfort-line: 1.6rem;
+ --reading-comfort-h1: 1.75rem;
+ --reading-comfort-h2: 1.4rem;
+ --reading-comfort-h3: 1.12rem;
+ --reading-comfort-h4: 1rem;
+}
+
+:root[data-reading-comfort="90"] {
+ --reading-comfort-body: 0.9rem;
+ --reading-comfort-line: 1.44rem;
+ --reading-comfort-h1: 1.575rem;
+ --reading-comfort-h2: 1.26rem;
+ --reading-comfort-h3: 1.008rem;
+ --reading-comfort-h4: 0.9rem;
+}
+
+:root[data-reading-comfort="110"] {
+ --reading-comfort-body: 1.1rem;
+ --reading-comfort-line: 1.76rem;
+ --reading-comfort-h1: 1.925rem;
+ --reading-comfort-h2: 1.54rem;
+ --reading-comfort-h3: 1.232rem;
+ --reading-comfort-h4: 1.1rem;
+}
+
+:root[data-reading-comfort="120"] {
+ --reading-comfort-body: 1.2rem;
+ --reading-comfort-line: 1.92rem;
+ --reading-comfort-h1: 2.1rem;
+ --reading-comfort-h2: 1.68rem;
+ --reading-comfort-h3: 1.344rem;
+ --reading-comfort-h4: 1.2rem;
+}
+
+.center > article {
+ font-size: var(--reading-comfort-body);
+
+ :where(p, li, dt, dd, blockquote, th, td) {
+ line-height: var(--reading-comfort-line);
+ }
+
+ h1 {
+ font-size: var(--reading-comfort-h1);
+ }
+
+ h2 {
+ font-size: var(--reading-comfort-h2);
+ }
+
+ h3 {
+ font-size: var(--reading-comfort-h3);
+ }
+
+ :where(h4, h5, h6) {
+ font-size: var(--reading-comfort-h4);
+ }
+}
+
+.reading-comfort {
+ position: relative;
+ z-index: 3;
+ display: flex;
+ width: fit-content;
+ margin-block: -0.5rem 1rem;
+ margin-inline: auto 0;
+ justify-content: flex-end;
+}
+
+.reading-comfort-trigger,
+.reading-comfort-controls button {
+ appearance: none;
+ box-sizing: border-box;
+ border: 1px solid color-mix(in srgb, var(--gray) 54%, transparent);
+ border-radius: 6px;
+ background-color: var(--light);
+ color: var(--dark);
+ font-family: var(--bodyFont);
+ cursor: pointer;
+ touch-action: manipulation;
+ transition:
+ background-color 0.15s ease-out,
+ color 0.15s ease-out,
+ transform 0.15s ease-out;
+
+ &:hover:not(:disabled) {
+ border-color: var(--secondary);
+ background-color: var(--lightgray);
+ color: var(--secondary);
+ }
+
+ &:active:not(:disabled) {
+ transform: scale(0.96);
+ }
+
+ &:focus-visible {
+ outline: 2px solid var(--secondary);
+ outline-offset: 2px;
+ }
+
+ &:disabled {
+ cursor: default;
+ opacity: 0.45;
+ }
+}
+
+.reading-comfort-trigger {
+ position: relative;
+ display: grid;
+ width: 2.5rem;
+ height: 2.5rem;
+ flex: 0 0 2.5rem;
+ padding: 0;
+ place-items: center;
+ font-family: var(--headerFont);
+ font-size: 0.875rem;
+ font-weight: 650;
+ line-height: 1;
+
+ &[data-level]:not([data-level="100"]) {
+ color: var(--secondary);
+ background-color: color-mix(in srgb, var(--highlight) 74%, var(--light));
+ }
+}
+
+.read-later.reading-comfort-host > .reading-comfort-trigger {
+ margin-inline-end: 0.5rem;
+}
+
+.reading-comfort-panel {
+ position: absolute;
+ inset-block-start: calc(100% + 0.5rem);
+ inset-inline-end: 0;
+ box-sizing: border-box;
+ width: min(14rem, calc(100vw - 2rem));
+ padding: 0.75rem;
+ border: 1px solid color-mix(in srgb, var(--gray) 54%, transparent);
+ border-radius: 6px;
+ background-color: var(--light);
+ color: var(--darkgray);
+
+ &[hidden] {
+ display: none;
+ }
+}
+
+.reading-comfort-value {
+ display: block;
+ margin-block-end: 0.5rem;
+ color: var(--darkgray);
+ font-family: var(--codeFont);
+ font-size: 0.75rem;
+ line-height: 1.25rem;
+ text-align: center;
+}
+
+.reading-comfort-controls {
+ display: grid;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ gap: 0.25rem;
+
+ button {
+ min-width: 0;
+ height: 2.5rem;
+ padding: 0 0.5rem;
+ font-weight: 600;
+ line-height: 1;
+ }
+}
+
+.reading-comfort-reset {
+ font-family: var(--codeFont) !important;
+ font-size: 0.75rem;
+}
+
+.reading-comfort-status {
+ display: block;
+ min-height: 0;
+ color: var(--gray);
+ font-size: 0.75rem;
+ line-height: 1.25rem;
+ text-align: center;
+
+ &:not(:empty) {
+ margin-block-start: 0.5rem;
+ }
+}
+
+@media (max-width: 600px) {
+ .reading-comfort-host {
+ width: 100%;
+ flex-wrap: wrap;
+ }
+
+ .reading-comfort-host > .reading-comfort-panel {
+ position: static;
+ flex: 0 0 min(14rem, calc(100vw - 2rem));
+ margin-block-start: 0.5rem;
+ margin-inline-start: auto;
+ }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .reading-comfort-trigger,
+ .reading-comfort-controls button {
+ transition: none;
+
+ &:active:not(:disabled) {
+ transform: none;
+ }
+ }
+}
+
+@media print {
+ .reading-comfort,
+ .reading-comfort-trigger,
+ .reading-comfort-panel {
+ display: none;
+ }
+}
diff --git a/quartz/i18n/locales/definition.ts b/quartz/i18n/locales/definition.ts
index 31618f1acc783..a1b45c4aae820 100644
--- a/quartz/i18n/locales/definition.ts
+++ b/quartz/i18n/locales/definition.ts
@@ -83,6 +83,14 @@ export interface Translation {
copied: string
failed: string
}
+ readingComfort?: {
+ title: string
+ smaller: string
+ reset: string
+ larger: string
+ value: string
+ failed: string
+ }
explorer: {
title: string
}
diff --git a/quartz/i18n/locales/en-US.ts b/quartz/i18n/locales/en-US.ts
index 0d47135d97cf4..ca561a1524272 100644
--- a/quartz/i18n/locales/en-US.ts
+++ b/quartz/i18n/locales/en-US.ts
@@ -80,6 +80,14 @@ export default {
copied: "Note link copied",
failed: "The browser could not share this note",
},
+ readingComfort: {
+ title: "Reading size",
+ smaller: "Make article text smaller",
+ reset: "Reset article text size",
+ larger: "Make article text larger",
+ value: "Article text at {percent}%",
+ failed: "Applied, but the browser could not save this preference",
+ },
explorer: {
title: "Explorer",
},
diff --git a/quartz/i18n/locales/zh-CN.ts b/quartz/i18n/locales/zh-CN.ts
index 7aa8cf7c2103e..001b9d2f27468 100644
--- a/quartz/i18n/locales/zh-CN.ts
+++ b/quartz/i18n/locales/zh-CN.ts
@@ -80,6 +80,14 @@ export default {
copied: "笔记链接已复制",
failed: "浏览器未能分享这篇笔记",
},
+ readingComfort: {
+ title: "阅读字号",
+ smaller: "缩小正文",
+ reset: "恢复默认字号",
+ larger: "放大正文",
+ value: "正文字号 {percent}%",
+ failed: "已应用,但浏览器未能保存偏好",
+ },
explorer: {
title: "探索",
},
diff --git a/quartz/i18n/locales/zh-TW.ts b/quartz/i18n/locales/zh-TW.ts
index 9bfcd9e4e845d..1c6bf8b386ee4 100644
--- a/quartz/i18n/locales/zh-TW.ts
+++ b/quartz/i18n/locales/zh-TW.ts
@@ -77,6 +77,14 @@ export default {
copied: "筆記連結已複製",
failed: "瀏覽器未能分享這篇筆記",
},
+ readingComfort: {
+ title: "閱讀字號",
+ smaller: "縮小正文",
+ reset: "恢復預設字號",
+ larger: "放大正文",
+ value: "正文字號 {percent}%",
+ failed: "已套用,但瀏覽器未能儲存偏好",
+ },
explorer: {
title: "探索",
},
diff --git a/quartz/plugins/emitters/componentResources.test.ts b/quartz/plugins/emitters/componentResources.test.ts
index 36f165ea41aff..f6555f40332ce 100644
--- a/quartz/plugins/emitters/componentResources.test.ts
+++ b/quartz/plugins/emitters/componentResources.test.ts
@@ -143,4 +143,27 @@ describe("componentResources", () => {
assert.ok(noteShareStyleIndex < spaBranchIndex)
})
})
+
+ test("includes reading comfort before SPA navigation is initialized", () => {
+ const emitterPath = new URL("./componentResources.ts", import.meta.url)
+ const source = readFile(emitterPath, "utf8")
+
+ return source.then((contents) => {
+ const bootstrapIndex = contents.indexOf(
+ "componentResources.beforeDOMLoaded.push(readingComfortBootstrapScript)",
+ )
+ const scriptIndex = contents.indexOf(
+ "componentResources.afterDOMLoaded.push(readingComfortScript)",
+ )
+ const styleIndex = contents.indexOf("componentResources.css.push(readingComfortStyle)")
+ const spaBranchIndex = contents.indexOf("if (cfg.enableSPA)")
+
+ assert.notEqual(bootstrapIndex, -1)
+ assert.notEqual(scriptIndex, -1)
+ assert.notEqual(styleIndex, -1)
+ assert.ok(bootstrapIndex < spaBranchIndex)
+ assert.ok(scriptIndex < spaBranchIndex)
+ assert.ok(styleIndex < spaBranchIndex)
+ })
+ })
})
diff --git a/quartz/plugins/emitters/componentResources.ts b/quartz/plugins/emitters/componentResources.ts
index ceff787ee2ba5..09efc61149359 100644
--- a/quartz/plugins/emitters/componentResources.ts
+++ b/quartz/plugins/emitters/componentResources.ts
@@ -24,6 +24,11 @@ import { randomWanderScript } from "../../components/scripts/randomWander"
import randomWanderStyle from "../../components/styles/randomWander.scss"
import { noteShareScript } from "../../components/scripts/noteShare"
import noteShareStyle from "../../components/styles/noteShare.scss"
+import {
+ readingComfortBootstrapScript,
+ readingComfortScript,
+} from "../../components/scripts/readingComfort"
+import readingComfortStyle from "../../components/styles/readingComfort.scss"
import { BuildCtx } from "../../util/ctx"
import { QuartzComponent } from "../../components/types"
import { normalizeResource } from "../../util/resources"
@@ -114,6 +119,9 @@ function addGlobalPageResources(ctx: BuildCtx, componentResources: ComponentReso
componentResources.css.push(randomWanderStyle)
componentResources.afterDOMLoaded.push(noteShareScript)
componentResources.css.push(noteShareStyle)
+ componentResources.beforeDOMLoaded.push(readingComfortBootstrapScript)
+ componentResources.afterDOMLoaded.push(readingComfortScript)
+ componentResources.css.push(readingComfortStyle)
// popovers
if (cfg.enablePopovers) {