From b1cccf1879b729914deac7e3fed77fd3deb27f09 Mon Sep 17 00:00:00 2001 From: oldwinter Date: Thu, 27 Aug 2026 02:47:49 +0800 Subject: [PATCH] feat(reader): add reading comfort controls --- DESIGN.md | 8 + quartz/components/renderPage.tsx | 8 + .../components/scripts/readingComfort.test.ts | 53 +++++ quartz/components/scripts/readingComfort.ts | 205 ++++++++++++++++ quartz/components/styles/readingComfort.scss | 221 ++++++++++++++++++ quartz/i18n/locales/definition.ts | 8 + quartz/i18n/locales/en-US.ts | 8 + quartz/i18n/locales/zh-CN.ts | 8 + quartz/i18n/locales/zh-TW.ts | 8 + .../emitters/componentResources.test.ts | 23 ++ quartz/plugins/emitters/componentResources.ts | 8 + 11 files changed, 558 insertions(+) create mode 100644 quartz/components/scripts/readingComfort.test.ts create mode 100644 quartz/components/scripts/readingComfort.ts create mode 100644 quartz/components/styles/readingComfort.scss 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 &&