From b34d521293415944370fd0f5cf25cd71bcffb5b6 Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Thu, 17 Apr 2025 19:45:17 -0700 Subject: [PATCH] feat: reader mode --- docs/features/reader mode.md | 44 +++++++++++++++++++ index.d.ts | 1 + quartz.layout.ts | 1 + quartz/components/ReaderMode.tsx | 32 ++++++++++++++ quartz/components/index.ts | 2 + .../components/scripts/readermode.inline.ts | 25 +++++++++++ quartz/components/styles/darkmode.scss | 2 +- quartz/components/styles/readermode.scss | 33 ++++++++++++++ 8 files changed, 139 insertions(+), 1 deletion(-) create mode 100644 docs/features/reader mode.md create mode 100644 quartz/components/ReaderMode.tsx create mode 100644 quartz/components/scripts/readermode.inline.ts create mode 100644 quartz/components/styles/readermode.scss diff --git a/docs/features/reader mode.md b/docs/features/reader mode.md new file mode 100644 index 0000000..d1c1429 --- /dev/null +++ b/docs/features/reader mode.md @@ -0,0 +1,44 @@ +--- +title: Reader Mode +tags: + - component +--- + +Reader Mode is a feature that allows users to focus on the content by hiding the sidebars and other UI elements. When enabled, it provides a clean, distraction-free reading experience. + +## Configuration + +Reader Mode is enabled by default. To disable it, you can remove the component from your layout configuration in `quartz.layout.ts`: + +```ts +// Remove or comment out this line +Component.ReaderMode(), +``` + +## Usage + +The Reader Mode toggle appears as a button with a book icon. When clicked: + +- Sidebars are hidden +- Hovering over the content area reveals the sidebars temporarily + +Unlike Dark Mode, Reader Mode state is not persisted between page reloads but is maintained during SPA navigation within the site. + +## Customization + +You can customize the appearance of Reader Mode through CSS variables and styles. The component uses the following classes: + +- `.readermode`: The toggle button +- `.readerIcon`: The book icon +- `[reader-mode="on"]`: Applied to the root element when Reader Mode is active + +Example customization in your custom CSS: + +```scss +.readermode { + // Customize the button + svg { + stroke: var(--custom-color); + } +} +``` diff --git a/index.d.ts b/index.d.ts index 07f8082..9011ee3 100644 --- a/index.d.ts +++ b/index.d.ts @@ -8,6 +8,7 @@ interface CustomEventMap { prenav: CustomEvent<{}> nav: CustomEvent<{ url: FullSlug }> themechange: CustomEvent<{ theme: "light" | "dark" }> + readermodechange: CustomEvent<{ mode: "on" | "off" }> } type ContentIndex = Record diff --git a/quartz.layout.ts b/quartz.layout.ts index e5c3388..970a5be 100644 --- a/quartz.layout.ts +++ b/quartz.layout.ts @@ -35,6 +35,7 @@ export const defaultContentPageLayout: PageLayout = { grow: true, }, { Component: Component.Darkmode() }, + { Component: Component.ReaderMode() }, ], }), Component.Explorer(), diff --git a/quartz/components/ReaderMode.tsx b/quartz/components/ReaderMode.tsx new file mode 100644 index 0000000..dac4053 --- /dev/null +++ b/quartz/components/ReaderMode.tsx @@ -0,0 +1,32 @@ +// @ts-ignore +import readerModeScript from "./scripts/readermode.inline" +import styles from "./styles/readermode.scss" +import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" +import { classNames } from "../util/lang" + +const ReaderMode: QuartzComponent = ({ displayClass }: QuartzComponentProps) => { + return ( + + ) +} + +ReaderMode.beforeDOMLoaded = readerModeScript +ReaderMode.css = styles + +export default (() => ReaderMode) satisfies QuartzComponentConstructor diff --git a/quartz/components/index.ts b/quartz/components/index.ts index 2b601cd..cece8e6 100644 --- a/quartz/components/index.ts +++ b/quartz/components/index.ts @@ -4,6 +4,7 @@ import FolderContent from "./pages/FolderContent" import NotFound from "./pages/404" import ArticleTitle from "./ArticleTitle" import Darkmode from "./Darkmode" +import ReaderMode from "./ReaderMode" import Head from "./Head" import PageTitle from "./PageTitle" import ContentMeta from "./ContentMeta" @@ -29,6 +30,7 @@ export { TagContent, FolderContent, Darkmode, + ReaderMode, Head, PageTitle, ContentMeta, diff --git a/quartz/components/scripts/readermode.inline.ts b/quartz/components/scripts/readermode.inline.ts new file mode 100644 index 0000000..09f6a5f --- /dev/null +++ b/quartz/components/scripts/readermode.inline.ts @@ -0,0 +1,25 @@ +let isReaderMode = false + +const emitReaderModeChangeEvent = (mode: "on" | "off") => { + const event: CustomEventMap["readermodechange"] = new CustomEvent("readermodechange", { + detail: { mode }, + }) + document.dispatchEvent(event) +} + +document.addEventListener("nav", () => { + const switchReaderMode = () => { + isReaderMode = !isReaderMode + const newMode = isReaderMode ? "on" : "off" + document.documentElement.setAttribute("reader-mode", newMode) + emitReaderModeChangeEvent(newMode) + } + + for (const readerModeButton of document.getElementsByClassName("readermode")) { + readerModeButton.addEventListener("click", switchReaderMode) + window.addCleanup(() => readerModeButton.removeEventListener("click", switchReaderMode)) + } + + // Set initial state + document.documentElement.setAttribute("reader-mode", isReaderMode ? "on" : "off") +}) diff --git a/quartz/components/styles/darkmode.scss b/quartz/components/styles/darkmode.scss index 5d1e078..b328743 100644 --- a/quartz/components/styles/darkmode.scss +++ b/quartz/components/styles/darkmode.scss @@ -6,7 +6,7 @@ border: none; width: 20px; height: 20px; - margin: 0 10px; + margin: 0; text-align: inherit; flex-shrink: 0; diff --git a/quartz/components/styles/readermode.scss b/quartz/components/styles/readermode.scss new file mode 100644 index 0000000..7d5de77 --- /dev/null +++ b/quartz/components/styles/readermode.scss @@ -0,0 +1,33 @@ +.readermode { + cursor: pointer; + padding: 0; + position: relative; + background: none; + border: none; + width: 20px; + height: 20px; + margin: 0; + text-align: inherit; + flex-shrink: 0; + + & svg { + position: absolute; + width: 20px; + height: 20px; + top: calc(50% - 10px); + stroke: var(--darkgray); + transition: opacity 0.1s ease; + } +} + +:root[reader-mode="on"] { + & .sidebar.left, + & .sidebar.right { + opacity: 0; + transition: opacity 0.2s ease; + + &:hover { + opacity: 1; + } + } +}