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