Compare commits

...

2 Commits

Author SHA1 Message Date
dependabot[bot]
c238dd16d9
chore(deps): bump the production-dependencies group with 2 updates (#1919)
Some checks failed
Build and Test / build-and-test (ubuntu-latest) (push) Has been skipped
Build and Test / publish-tag (push) Has been skipped
Build and Test / build-and-test (macos-latest) (push) Has been cancelled
Build and Test / build-and-test (windows-latest) (push) Has been cancelled
Bumps the production-dependencies group with 2 updates: [@clack/prompts](https://github.com/bombshell-dev/clack/tree/HEAD/packages/prompts) and [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node).


Updates `@clack/prompts` from 0.10.0 to 0.10.1
- [Release notes](https://github.com/bombshell-dev/clack/releases)
- [Changelog](https://github.com/bombshell-dev/clack/blob/@clack/prompts@0.10.1/packages/prompts/CHANGELOG.md)
- [Commits](https://github.com/bombshell-dev/clack/commits/@clack/prompts@0.10.1/packages/prompts)

Updates `@types/node` from 22.14.0 to 22.14.1
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@clack/prompts"
  dependency-version: 0.10.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: production-dependencies
- dependency-name: "@types/node"
  dependency-version: 22.14.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: production-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-17 19:48:08 -07:00
Jacky Zhao
b34d521293 feat: reader mode 2025-04-17 19:45:17 -07:00
10 changed files with 158 additions and 17 deletions

View File

@ -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);
}
}
```

1
index.d.ts vendored
View File

@ -8,6 +8,7 @@ interface CustomEventMap {
prenav: CustomEvent<{}> prenav: CustomEvent<{}>
nav: CustomEvent<{ url: FullSlug }> nav: CustomEvent<{ url: FullSlug }>
themechange: CustomEvent<{ theme: "light" | "dark" }> themechange: CustomEvent<{ theme: "light" | "dark" }>
readermodechange: CustomEvent<{ mode: "on" | "off" }>
} }
type ContentIndex = Record<FullSlug, ContentDetails> type ContentIndex = Record<FullSlug, ContentDetails>

31
package-lock.json generated
View File

@ -9,7 +9,7 @@
"version": "4.5.0", "version": "4.5.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@clack/prompts": "^0.10.0", "@clack/prompts": "^0.10.1",
"@floating-ui/dom": "^1.6.13", "@floating-ui/dom": "^1.6.13",
"@myriaddreamin/rehype-typst": "^0.5.4", "@myriaddreamin/rehype-typst": "^0.5.4",
"@napi-rs/simple-git": "0.1.19", "@napi-rs/simple-git": "0.1.19",
@ -80,7 +80,7 @@
"@types/d3": "^7.4.3", "@types/d3": "^7.4.3",
"@types/hast": "^3.0.4", "@types/hast": "^3.0.4",
"@types/js-yaml": "^4.0.9", "@types/js-yaml": "^4.0.9",
"@types/node": "^22.14.0", "@types/node": "^22.14.1",
"@types/pretty-time": "^1.1.5", "@types/pretty-time": "^1.1.5",
"@types/source-map-support": "^0.5.10", "@types/source-map-support": "^0.5.10",
"@types/ws": "^8.18.1", "@types/ws": "^8.18.1",
@ -178,21 +178,22 @@
} }
}, },
"node_modules/@clack/core": { "node_modules/@clack/core": {
"version": "0.4.1", "version": "0.4.2",
"resolved": "https://registry.npmjs.org/@clack/core/-/core-0.4.1.tgz", "resolved": "https://registry.npmjs.org/@clack/core/-/core-0.4.2.tgz",
"integrity": "sha512-Pxhij4UXg8KSr7rPek6Zowm+5M22rbd2g1nfojHJkxp5YkFqiZ2+YLEM/XGVIzvGOcM0nqjIFxrpDwWRZYWYjA==", "integrity": "sha512-NYQfcEy8MWIxrT5Fj8nIVchfRFA26yYKJcvBS7WlUIlw2OmQOY9DhGGXMovyI5J5PpxrCPGkgUi207EBrjpBvg==",
"license": "MIT",
"dependencies": { "dependencies": {
"picocolors": "^1.0.0", "picocolors": "^1.0.0",
"sisteransi": "^1.0.5" "sisteransi": "^1.0.5"
} }
}, },
"node_modules/@clack/prompts": { "node_modules/@clack/prompts": {
"version": "0.10.0", "version": "0.10.1",
"resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-0.10.0.tgz", "resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-0.10.1.tgz",
"integrity": "sha512-H3rCl6CwW1NdQt9rE3n373t7o5cthPv7yUoxF2ytZvyvlJv89C5RYMJu83Hed8ODgys5vpBU0GKxIRG83jd8NQ==", "integrity": "sha512-Q0T02vx8ZM9XSv9/Yde0jTmmBQufZhPJfYAg2XrrrxWWaZgq1rr8nU8Hv710BQ1dhoP8rtY7YUdpGej2Qza/cw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@clack/core": "0.4.1", "@clack/core": "0.4.2",
"picocolors": "^1.0.0", "picocolors": "^1.0.0",
"sisteransi": "^1.0.5" "sisteransi": "^1.0.5"
} }
@ -1940,9 +1941,9 @@
} }
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "22.14.0", "version": "22.14.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.0.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.1.tgz",
"integrity": "sha512-Kmpl+z84ILoG+3T/zQFyAJsU6EPTmOCj8/2+83fSN6djd6I4o7uOuGIH6vq3PrjY5BGitSbFuMN18j3iknubbA==", "integrity": "sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -5503,7 +5504,8 @@
"node_modules/picocolors": { "node_modules/picocolors": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"license": "ISC"
}, },
"node_modules/picomatch": { "node_modules/picomatch": {
"version": "2.3.1", "version": "2.3.1",
@ -6702,7 +6704,8 @@
"node_modules/sisteransi": { "node_modules/sisteransi": {
"version": "1.0.5", "version": "1.0.5",
"resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",
"integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==" "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==",
"license": "MIT"
}, },
"node_modules/slash": { "node_modules/slash": {
"version": "5.1.0", "version": "5.1.0",

View File

@ -35,7 +35,7 @@
"quartz": "./quartz/bootstrap-cli.mjs" "quartz": "./quartz/bootstrap-cli.mjs"
}, },
"dependencies": { "dependencies": {
"@clack/prompts": "^0.10.0", "@clack/prompts": "^0.10.1",
"@floating-ui/dom": "^1.6.13", "@floating-ui/dom": "^1.6.13",
"@myriaddreamin/rehype-typst": "^0.5.4", "@myriaddreamin/rehype-typst": "^0.5.4",
"@napi-rs/simple-git": "0.1.19", "@napi-rs/simple-git": "0.1.19",
@ -103,7 +103,7 @@
"@types/d3": "^7.4.3", "@types/d3": "^7.4.3",
"@types/hast": "^3.0.4", "@types/hast": "^3.0.4",
"@types/js-yaml": "^4.0.9", "@types/js-yaml": "^4.0.9",
"@types/node": "^22.14.0", "@types/node": "^22.14.1",
"@types/pretty-time": "^1.1.5", "@types/pretty-time": "^1.1.5",
"@types/source-map-support": "^0.5.10", "@types/source-map-support": "^0.5.10",
"@types/ws": "^8.18.1", "@types/ws": "^8.18.1",

View File

@ -35,6 +35,7 @@ export const defaultContentPageLayout: PageLayout = {
grow: true, grow: true,
}, },
{ Component: Component.Darkmode() }, { Component: Component.Darkmode() },
{ Component: Component.ReaderMode() },
], ],
}), }),
Component.Explorer(), Component.Explorer(),

View File

@ -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

View File

@ -4,6 +4,7 @@ import FolderContent from "./pages/FolderContent"
import NotFound from "./pages/404" import NotFound from "./pages/404"
import ArticleTitle from "./ArticleTitle" import ArticleTitle from "./ArticleTitle"
import Darkmode from "./Darkmode" import Darkmode from "./Darkmode"
import ReaderMode from "./ReaderMode"
import Head from "./Head" import Head from "./Head"
import PageTitle from "./PageTitle" import PageTitle from "./PageTitle"
import ContentMeta from "./ContentMeta" import ContentMeta from "./ContentMeta"
@ -29,6 +30,7 @@ export {
TagContent, TagContent,
FolderContent, FolderContent,
Darkmode, Darkmode,
ReaderMode,
Head, Head,
PageTitle, PageTitle,
ContentMeta, ContentMeta,

View File

@ -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")
})

View File

@ -6,7 +6,7 @@
border: none; border: none;
width: 20px; width: 20px;
height: 20px; height: 20px;
margin: 0 10px; margin: 0;
text-align: inherit; text-align: inherit;
flex-shrink: 0; flex-shrink: 0;

View File

@ -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;
}
}
}