From b34d521293415944370fd0f5cf25cd71bcffb5b6 Mon Sep 17 00:00:00 2001
From: Jacky Zhao <j.zhao2k19@gmail.com>
Date: Fri, 18 Apr 2025 02:45:17 +0000
Subject: [PATCH] feat: reader mode
---
docs/features/reader mode.md | 44 ++++++++++++++
quartz/components/ReaderMode.tsx | 32 ++++++++++
quartz/components/index.ts | 2
index.d.ts | 1
quartz.layout.ts | 1
quartz/components/scripts/readermode.inline.ts | 25 ++++++++
quartz/components/styles/readermode.scss | 33 +++++++++++
quartz/components/styles/darkmode.scss | 2
8 files changed, 139 insertions(+), 1 deletions(-)
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 @@
prenav: CustomEvent<{}>
nav: CustomEvent<{ url: FullSlug }>
themechange: CustomEvent<{ theme: "light" | "dark" }>
+ readermodechange: CustomEvent<{ mode: "on" | "off" }>
}
type ContentIndex = Record<FullSlug, ContentDetails>
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 @@
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 (
+ <button class={classNames(displayClass, "readermode")}>
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ class="readerIcon"
+ viewBox="0 0 24 24"
+ fill="none"
+ stroke="currentColor"
+ stroke-width="2"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ >
+ <rect x="6" y="4" width="12" height="16" rx="1"></rect>
+ <line x1="9" y1="8" x2="15" y2="8"></line>
+ <line x1="9" y1="12" x2="15" y2="12"></line>
+ <line x1="9" y1="16" x2="13" y2="16"></line>
+ </svg>
+ </button>
+ )
+}
+
+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 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 @@
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;
+ }
+ }
+}
--
Gitblit v1.10.0