Compare commits

...

5 Commits

Author SHA1 Message Date
Jacky Zhao
2a9290b3df fix(transclude): blockref detection
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
2025-04-22 11:18:50 -07:00
ARYAN TECHIE
771c05ff18 fix: dynamically detect current branch for quartz sync push (#1930) 2025-04-22 10:22:43 -07:00
Jacky Zhao
6dd772bf00 fix(popover): properly clear popover on racing fetch
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
2025-04-21 23:55:38 -07:00
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
14 changed files with 176 additions and 29 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

@@ -589,7 +589,8 @@ export async function handleSync(argv) {
await popContentFolder(contentFolder) await popContentFolder(contentFolder)
if (argv.push) { if (argv.push) {
console.log("Pushing your changes") console.log("Pushing your changes")
const res = spawnSync("git", ["push", "-uf", ORIGIN_NAME, QUARTZ_SOURCE_BRANCH], { const currentBranch = execSync("git rev-parse --abbrev-ref HEAD").toString().trim()
const res = spawnSync("git", ["push", "-uf", ORIGIN_NAME, currentBranch], {
stdio: "inherit", stdio: "inherit",
}) })
if (res.status !== 0) { if (res.status !== 0) {

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

@@ -75,7 +75,7 @@ function renderTranscludes(
const classNames = (node.properties?.className ?? []) as string[] const classNames = (node.properties?.className ?? []) as string[]
if (classNames.includes("transclude")) { if (classNames.includes("transclude")) {
const inner = node.children[0] as Element const inner = node.children[0] as Element
const transcludeTarget = inner.properties["data-slug"] as FullSlug const transcludeTarget = (inner.properties["data-slug"] ?? slug) as FullSlug
const page = componentData.allFiles.find((f) => f.slug === transcludeTarget) const page = componentData.allFiles.find((f) => f.slug === transcludeTarget)
if (!page) { if (!page) {
return return

View File

@@ -3,12 +3,13 @@ import { normalizeRelativeURLs } from "../../util/path"
import { fetchCanonical } from "./util" import { fetchCanonical } from "./util"
const p = new DOMParser() const p = new DOMParser()
let activeAnchor: HTMLAnchorElement | null = null
async function mouseEnterHandler( async function mouseEnterHandler(
this: HTMLAnchorElement, this: HTMLAnchorElement,
{ clientX, clientY }: { clientX: number; clientY: number }, { clientX, clientY }: { clientX: number; clientY: number },
) { ) {
const link = this const link = (activeAnchor = this)
if (link.dataset.noPopover === "true") { if (link.dataset.noPopover === "true") {
return return
} }
@@ -44,10 +45,9 @@ async function mouseEnterHandler(
targetUrl.search = "" targetUrl.search = ""
const popoverId = `popover-${link.pathname}` const popoverId = `popover-${link.pathname}`
const prevPopoverElement = document.getElementById(popoverId) const prevPopoverElement = document.getElementById(popoverId)
const hasAlreadyBeenFetched = () => !!document.getElementById(popoverId)
// dont refetch if there's already a popover // dont refetch if there's already a popover
if (hasAlreadyBeenFetched()) { if (!!document.getElementById(popoverId)) {
showPopover(prevPopoverElement as HTMLElement) showPopover(prevPopoverElement as HTMLElement)
return return
} }
@@ -56,11 +56,6 @@ async function mouseEnterHandler(
console.error(err) console.error(err)
}) })
// bailout if another popover exists
if (hasAlreadyBeenFetched()) {
return
}
if (!response) return if (!response) return
const [contentType] = response.headers.get("Content-Type")!.split(";") const [contentType] = response.headers.get("Content-Type")!.split(";")
const [contentTypeCategory, typeInfo] = contentType.split("/") const [contentTypeCategory, typeInfo] = contentType.split("/")
@@ -107,11 +102,20 @@ async function mouseEnterHandler(
elts.forEach((elt) => popoverInner.appendChild(elt)) elts.forEach((elt) => popoverInner.appendChild(elt))
} }
if (!!document.getElementById(popoverId)) {
return
}
document.body.appendChild(popoverElement) document.body.appendChild(popoverElement)
if (activeAnchor !== this) {
return
}
showPopover(popoverElement) showPopover(popoverElement)
} }
function clearActivePopover() { function clearActivePopover() {
activeAnchor = null
const allPopoverElements = document.querySelectorAll(".popover") const allPopoverElements = document.querySelectorAll(".popover")
allPopoverElements.forEach((popoverElement) => popoverElement.classList.remove("active-popover")) allPopoverElements.forEach((popoverElement) => popoverElement.classList.remove("active-popover"))
} }

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

View File

@@ -191,7 +191,8 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>>
const [rawFp, rawHeader, rawAlias]: (string | undefined)[] = capture const [rawFp, rawHeader, rawAlias]: (string | undefined)[] = capture
const [fp, anchor] = splitAnchor(`${rawFp ?? ""}${rawHeader ?? ""}`) const [fp, anchor] = splitAnchor(`${rawFp ?? ""}${rawHeader ?? ""}`)
const displayAnchor = anchor ? `#${anchor.trim().replace(/^#+/, "")}` : "" const blockRef = Boolean(rawHeader?.startsWith("#^")) ? "^" : ""
const displayAnchor = anchor ? `#${blockRef}${anchor.trim().replace(/^#+/, "")}` : ""
const displayAlias = rawAlias ?? rawHeader?.replace("#", "|") ?? "" const displayAlias = rawAlias ?? rawHeader?.replace("#", "|") ?? ""
const embedDisplay = value.startsWith("!") ? "!" : "" const embedDisplay = value.startsWith("!") ? "!" : ""
@@ -221,7 +222,7 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>>
let [rawFp, rawHeader, rawAlias] = capture let [rawFp, rawHeader, rawAlias] = capture
const fp = rawFp?.trim() ?? "" const fp = rawFp?.trim() ?? ""
const anchor = rawHeader?.trim() ?? "" const anchor = rawHeader?.trim() ?? ""
const alias = rawAlias?.slice(1).trim() const alias = rawAlias?.slice(1).trim() ?? ""
// embed cases // embed cases
if (value.startsWith("!")) { if (value.startsWith("!")) {