Compare commits

..

9 Commits

Author SHA1 Message Date
Stephen Tse
2acdec323f fix(explorer): Prevent html from being scrollable when mobile explorer is open (#1895)
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
* Fixed html page being scrollable when mobile explorer is open

* Prettier code

* Addressed comment
2025-04-26 11:13:56 -07:00
dralagen
9e58857746 feat(favicon): add plugin to expose favicon from icon.png (#1942)
* feat(favicon): add plugin to expose favicon from icon.png

* chore(favicon): clean up formatting and remove unnecessary line breaks
2025-04-26 11:06:59 -07:00
Stephen Tse
4bd714b7be fix(callout): Grid-based callout collapsible animation (#1944)
* Fixed broken nested callout maxHeight calculation

* Implemented grid-based callout collapsible
2025-04-26 11:05:51 -07:00
Jacky Zhao
78e13bcb40 chore: add ci to preview all prs (#1947)
* add ci to preview all prs

* only on this repo

* fmt
2025-04-26 11:04:23 -07:00
anthops
7d49dff074 fix: prefer webgl for devices with webgpu and no float32-blendable feature flag #1899 (#1933)
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
Co-authored-by: Tony <32993852+0xREDACTED@users.noreply.github.com>
2025-04-23 09:32:52 -07:00
Jacky Zhao
cdebd05dc9 fix(wikilinks): dont default empty alias 2025-04-23 09:30:25 -07:00
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
15 changed files with 164 additions and 74 deletions

46
.github/workflows/preview.yaml vendored Normal file
View File

@@ -0,0 +1,46 @@
name: Build Preview Deployment
on:
pull_request:
types: [opened, synchronize]
workflow_dispatch:
jobs:
build-preview:
if: ${{ github.repository == 'jackyzha0/quartz' }}
runs-on: ubuntu-latest
name: Build Preview
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 20
- name: Cache dependencies
uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- run: npm ci
- name: Check types and style
run: npm run check
- name: Build Quartz
run: npx quartz build -d docs -v
- name: Publish to Cloudflare Pages
uses: AdrianGonz97/refined-cf-pages-action@v1
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
githubToken: ${{ secrets.GITHUB_TOKEN }}
projectName: quartz
directory: public

7
package-lock.json generated
View File

@@ -14,6 +14,7 @@
"@myriaddreamin/rehype-typst": "^0.5.4",
"@napi-rs/simple-git": "0.1.19",
"@tweenjs/tween.js": "^25.0.0",
"@webgpu/types": "^0.1.60",
"ansi-truncate": "^1.2.0",
"async-mutex": "^0.5.0",
"chalk": "^5.4.1",
@@ -2001,9 +2002,9 @@
"integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ=="
},
"node_modules/@webgpu/types": {
"version": "0.1.44",
"resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.44.tgz",
"integrity": "sha512-JDpYJN5E/asw84LTYhKyvPpxGnD+bAKPtpW9Ilurf7cZpxaTbxkQcGwOd7jgB9BPBrTYQ+32ufo4HiuomTjHNQ==",
"version": "0.1.60",
"resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.60.tgz",
"integrity": "sha512-8B/tdfRFKdrnejqmvq95ogp8tf52oZ51p3f4QD5m5Paey/qlX4Rhhy5Y8tgFMi7Ms70HzcMMw3EQjH/jdhTwlA==",
"license": "BSD-3-Clause"
},
"node_modules/@xmldom/xmldom": {

View File

@@ -40,6 +40,7 @@
"@myriaddreamin/rehype-typst": "^0.5.4",
"@napi-rs/simple-git": "0.1.19",
"@tweenjs/tween.js": "^25.0.0",
"@webgpu/types": "^0.1.60",
"ansi-truncate": "^1.2.0",
"async-mutex": "^0.5.0",
"chalk": "^5.4.1",

View File

@@ -86,6 +86,7 @@ const config: QuartzConfig = {
}),
Plugin.Assets(),
Plugin.Static(),
Plugin.Favicon(),
Plugin.NotFoundPage(),
// Comment out CustomOgImages to speed up build time
Plugin.CustomOgImages(),

View File

@@ -589,7 +589,8 @@ export async function handleSync(argv) {
await popContentFolder(contentFolder)
if (argv.push) {
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",
})
if (res.status !== 0) {

View File

@@ -75,7 +75,7 @@ function renderTranscludes(
const classNames = (node.properties?.className ?? []) as string[]
if (classNames.includes("transclude")) {
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)
if (!page) {
return

View File

@@ -1,25 +1,10 @@
function toggleCallout(this: HTMLElement) {
const outerBlock = this.parentElement!
outerBlock.classList.toggle("is-collapsed")
const content = outerBlock.getElementsByClassName("callout-content")[0] as HTMLElement
if (!content) return
const collapsed = outerBlock.classList.contains("is-collapsed")
const height = collapsed ? this.scrollHeight : outerBlock.scrollHeight
outerBlock.style.maxHeight = height + "px"
// walk and adjust height of all parents
let current = outerBlock
let parent = outerBlock.parentElement
while (parent) {
if (!parent.classList.contains("callout")) {
return
}
const collapsed = parent.classList.contains("is-collapsed")
const height = collapsed ? parent.scrollHeight : parent.scrollHeight + current.scrollHeight
parent.style.maxHeight = height + "px"
current = parent
parent = parent.parentElement
}
content.style.gridTemplateRows = collapsed ? "0fr" : "1fr"
}
function setupCallout() {
@@ -27,15 +12,15 @@ function setupCallout() {
`callout is-collapsible`,
) as HTMLCollectionOf<HTMLElement>
for (const div of collapsible) {
const title = div.firstElementChild
if (!title) continue
const title = div.getElementsByClassName("callout-title")[0] as HTMLElement
const content = div.getElementsByClassName("callout-content")[0] as HTMLElement
if (!title || !content) continue
title.addEventListener("click", toggleCallout)
window.addCleanup(() => title.removeEventListener("click", toggleCallout))
const collapsed = div.classList.contains("is-collapsed")
const height = collapsed ? title.scrollHeight : div.scrollHeight
div.style.maxHeight = height + "px"
content.style.gridTemplateRows = collapsed ? "0fr" : "1fr"
}
}

View File

@@ -23,11 +23,18 @@ let currentExplorerState: Array<FolderState>
function toggleExplorer(this: HTMLElement) {
const nearestExplorer = this.closest(".explorer") as HTMLElement
if (!nearestExplorer) return
nearestExplorer.classList.toggle("collapsed")
const explorerCollapsed = nearestExplorer.classList.toggle("collapsed")
nearestExplorer.setAttribute(
"aria-expanded",
nearestExplorer.getAttribute("aria-expanded") === "true" ? "false" : "true",
)
if (!explorerCollapsed) {
// Stop <html> from being scrollable when mobile explorer is open
document.documentElement.classList.add("mobile-no-scroll")
} else {
document.documentElement.classList.remove("mobile-no-scroll")
}
}
function toggleFolder(evt: MouseEvent) {
@@ -270,12 +277,25 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
if (mobileExplorer.checkVisibility()) {
explorer.classList.add("collapsed")
explorer.setAttribute("aria-expanded", "false")
// Allow <html> to be scrollable when mobile explorer is collapsed
document.documentElement.classList.remove("mobile-no-scroll")
}
mobileExplorer.classList.remove("hide-until-loaded")
}
})
window.addEventListener("resize", function () {
// Desktop explorer opens by default, and it stays open when the window is resized
// to mobile screen size. Applies `no-scroll` to <html> in this edge case.
const explorer = document.querySelector(".explorer")
if (explorer && !explorer.classList.contains("collapsed")) {
document.documentElement.classList.add("mobile-no-scroll")
return
}
})
function setFolderState(folderElement: HTMLElement, collapsed: boolean) {
return collapsed ? folderElement.classList.remove("open") : folderElement.classList.add("open")
}

View File

@@ -68,6 +68,15 @@ type TweenNode = {
stop: () => void
}
async function determineGraphicsAPI(): Promise<"webgpu" | "webgl"> {
const adapter = await navigator.gpu?.requestAdapter().catch(() => null)
if (!adapter) {
return "webgl"
}
// Devices with WebGPU but no float32-blendable feature fail to render the graph
return adapter.features.has("float32-blendable") ? "webgpu" : "webgl"
}
async function renderGraph(graph: HTMLElement, fullSlug: FullSlug) {
const slug = simplifySlug(fullSlug)
const visited = getVisited()
@@ -349,6 +358,7 @@ async function renderGraph(graph: HTMLElement, fullSlug: FullSlug) {
tweens.forEach((tween) => tween.stop())
tweens.clear()
const pixiPreference = await determineGraphicsAPI()
const app = new Application()
await app.init({
width,
@@ -357,7 +367,7 @@ async function renderGraph(graph: HTMLElement, fullSlug: FullSlug) {
autoStart: false,
autoDensity: true,
backgroundAlpha: 0,
preference: "webgpu",
preference: pixiPreference,
resolution: window.devicePixelRatio,
eventMode: "static",
})

View File

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

View File

@@ -263,22 +263,8 @@ li:has(> .folder-outer:not(.open)) > .folder-container > svg {
}
}
.no-scroll {
opacity: 0;
overflow: hidden;
}
html:has(.no-scroll) {
overflow: hidden;
}
@media all and not ($mobile) {
.no-scroll {
opacity: 1 !important;
overflow: auto !important;
}
html:has(.no-scroll) {
overflow: auto !important;
.mobile-no-scroll {
@media all and ($mobile) {
overflow: hidden;
}
}

View File

@@ -0,0 +1,16 @@
import sharp from "sharp"
import { joinSegments, QUARTZ, FilePath } from "../../util/path"
import { QuartzEmitterPlugin } from "../types"
export const Favicon: QuartzEmitterPlugin = () => ({
name: "Favicon",
async *emit({ argv }) {
const iconPath = joinSegments(QUARTZ, "static", "icon.png")
const dest = joinSegments(argv.output, "favicon.ico") as FilePath
await sharp(iconPath).resize(48, 48).toFormat("png").toFile(dest)
yield dest
},
async *partialEmit() {},
})

View File

@@ -5,6 +5,7 @@ export { ContentIndex as ContentIndex } from "./contentIndex"
export { AliasRedirects } from "./aliases"
export { Assets } from "./assets"
export { Static } from "./static"
export { Favicon } from "./favicon"
export { ComponentResources } from "./componentResources"
export { NotFoundPage } from "./404"
export { CNAME } from "./cname"

View File

@@ -191,7 +191,8 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>>
const [rawFp, rawHeader, rawAlias]: (string | undefined)[] = capture
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 embedDisplay = value.startsWith("!") ? "!" : ""
@@ -221,7 +222,7 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>>
let [rawFp, rawHeader, rawAlias] = capture
const fp = rawFp?.trim() ?? ""
const anchor = rawHeader?.trim() ?? ""
const alias = rawAlias?.slice(1).trim()
const alias: string | undefined = rawAlias?.slice(1).trim()
// embed cases
if (value.startsWith("!")) {
@@ -463,6 +464,30 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>>
})
}
// For the rest of the MD callout elements other than the title, wrap them with
// two nested HTML <div>s (use some hacked mdhast component to achieve this) of
// class `callout-content` and `callout-content-inner` respectively for
// grid-based collapsible animation.
if (calloutContent.length > 0) {
node.children = [
node.children[0],
{
data: { hProperties: { className: ["callout-content"] }, hName: "div" },
type: "blockquote",
children: [
{
data: {
hProperties: { className: ["callout-content-inner"] },
hName: "div",
},
type: "blockquote",
children: [...calloutContent],
},
],
},
]
}
// replace first line of blockquote with title and rest of the paragraph text
node.children.splice(0, 1, ...blockquoteContent)
@@ -484,21 +509,6 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>>
"data-callout-metadata": calloutMetaData,
},
}
// Add callout-content class to callout body if it has one.
if (calloutContent.length > 0) {
const contentData: BlockContent | DefinitionContent = {
data: {
hProperties: {
className: "callout-content",
},
hName: "div",
},
type: "blockquote",
children: [...calloutContent],
}
node.children = [node.children[0], contentData]
}
}
})
}

View File

@@ -7,11 +7,19 @@
border-radius: 5px;
padding: 0 1rem;
overflow-y: hidden;
transition: max-height 0.3s ease;
box-sizing: border-box;
& > .callout-content > :first-child {
margin-top: 0;
& > .callout-content {
display: grid;
transition: grid-template-rows 0.3s ease;
& > .callout-content-inner {
overflow: hidden;
& > :first-child {
margin-top: 0;
}
}
}
--callout-icon-note: url('data:image/svg+xml; utf8, <svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="2" x2="22" y2="6"></line><path d="M7.5 20.5 19 9l-4-4L3.5 16.5 2 22z"></path></svg>');