From e4ea2c96d423d595c17dd3cc9979d4f61707f75d Mon Sep 17 00:00:00 2001 From: saberzero1 Date: Sat, 7 Feb 2026 03:10:50 +0100 Subject: [PATCH] feat(plugins): explorer as community plugin --- docs/features/explorer.md | 67 ++- package-lock.json | 501 +++---------------- package.json | 1 + quartz.config.ts | 1 + quartz.layout.ts | 8 +- quartz/components/Explorer.tsx | 165 ------ quartz/components/index.ts | 2 - quartz/components/scripts/explorer.inline.ts | 305 ----------- quartz/components/styles/explorer.scss | 282 ----------- 9 files changed, 117 insertions(+), 1215 deletions(-) delete mode 100644 quartz/components/Explorer.tsx delete mode 100644 quartz/components/scripts/explorer.inline.ts delete mode 100644 quartz/components/styles/explorer.scss diff --git a/docs/features/explorer.md b/docs/features/explorer.md index 797d4f1ac..7f1832adb 100644 --- a/docs/features/explorer.md +++ b/docs/features/explorer.md @@ -6,6 +6,36 @@ tags: Quartz features an explorer that allows you to navigate all files and folders on your site. It supports nested folders and is highly customizable. +> [!info] +> The Explorer is now a community plugin. This demonstrates how external plugins can extend Quartz functionality while serving as a reference implementation for plugin developers. + +## Installation + +The Explorer is available as a community plugin from GitHub: + +```bash +npm install github:quartz-community/explorer --legacy-peer-deps +``` + +Then import it in your `quartz.layout.ts`: + +```typescript title="quartz.layout.ts" +import { Explorer } from "@quartz-community/explorer/components" + +// Create once and reuse +const explorerComponent = Explorer() + +export const defaultContentPageLayout: PageLayout = { + // ... other layout config + left: [ + // ... other components + explorerComponent, + ], +} +``` + +## Features + By default, it shows all folders and files on your page. To display the explorer in a different spot, you can edit the [[layout]]. Display names for folders get determined by the `title` frontmatter field in `folder/index.md` (more detail in [[authoring content | Authoring Content]]). If this file does not exist or does not contain frontmatter, the local folder name will be used instead. @@ -17,12 +47,12 @@ Display names for folders get determined by the `title` frontmatter field in `fo ## Customization -Most configuration can be done by passing in options to `Component.Explorer()`. +Most configuration can be done by passing in options to `Explorer()`. For example, here's what the default configuration looks like: ```typescript title="quartz.layout.ts" -Component.Explorer({ +Explorer({ title: "Explorer", // title of the explorer component folderClickBehavior: "collapse", // what happens when you click a folder ("link" to navigate to folder page on click or "collapse" to collapse folder on click) folderDefaultState: "collapsed", // default state of folders ("collapsed" or "open") @@ -40,19 +70,16 @@ When passing in your own options, you can omit any or all of these fields if you Want to customize it even more? -- Removing explorer: remove `Component.Explorer()` from `quartz.layout.ts` +- Removing explorer: remove `explorerComponent` from `quartz.layout.ts` - (optional): After removing the explorer component, you can move the [[table of contents | Table of Contents]] component back to the `left` part of the layout - Changing `sort`, `filter` and `map` behavior: explained in [[#Advanced customization]] -- Component: `quartz/components/Explorer.tsx` -- Style: `quartz/components/styles/explorer.scss` -- Script: `quartz/components/scripts/explorer.inline.ts` ## Advanced customization This component allows you to fully customize all of its behavior. You can pass a custom `sort`, `filter` and `map` function. All functions you can pass work with the `FileTrieNode` class, which has the following properties: -```ts title="quartz/components/Explorer.tsx" +```ts title="@quartz-community/explorer" class FileTrieNode { isFolder: boolean children: Array @@ -60,7 +87,7 @@ class FileTrieNode { } ``` -```ts title="quartz/plugins/emitters/contentIndex.tsx" +```ts export type ContentDetails = { slug: FullSlug title: string @@ -74,7 +101,7 @@ Every function you can pass is optional. By default, only a `sort` function will ```ts title="Default sort function" // Sort order: folders first, then files. Sort folders and files alphabetically -Component.Explorer({ +Explorer({ sortFn: (a, b) => { if ((!a.isFolder && !b.isFolder) || (a.isFolder && b.isFolder)) { return a.displayName.localeCompare(b.displayName, undefined, { @@ -115,7 +142,7 @@ These examples show the basic usage of `sort`, `map` and `filter`. Using this example, the explorer will alphabetically sort everything. ```ts title="quartz.layout.ts" -Component.Explorer({ +Explorer({ sortFn: (a, b) => { return a.displayName.localeCompare(b.displayName) }, @@ -127,7 +154,7 @@ Component.Explorer({ Using this example, the display names of all `FileNodes` (folders + files) will be converted to full upper case. ```ts title="quartz.layout.ts" -Component.Explorer({ +Explorer({ mapFn: (node) => { node.displayName = node.displayName.toUpperCase() return node @@ -141,7 +168,7 @@ Using this example, you can remove elements from your explorer by providing an a Note that this example filters on the title but you can also do it via slug or any other field available on `FileTrieNode`. ```ts title="quartz.layout.ts" -Component.Explorer({ +Explorer({ filterFn: (node) => { // set containing names of everything you want to filter out const omit = new Set(["authoring content", "tags", "advanced"]) @@ -159,7 +186,7 @@ Component.Explorer({ You can access the tags of a file by `node.data.tags`. ```ts title="quartz.layout.ts" -Component.Explorer({ +Explorer({ filterFn: (node) => { // exclude files with the tag "explorerexclude" return node.data?.tags?.includes("explorerexclude") !== true @@ -173,7 +200,7 @@ By default, the explorer will filter out the `tags` folder. To override the default filter function, you can set the filter function to `undefined`. ```ts title="quartz.layout.ts" -Component.Explorer({ +Explorer({ filterFn: undefined, // apply no filter function, every file and folder will visible }) ``` @@ -186,19 +213,19 @@ Component.Explorer({ > and passing it in. > > ```ts title="quartz.layout.ts" -> import { Options } from "./quartz/components/Explorer" +> import { ExplorerOptions } from "@quartz-community/explorer/components" > -> export const mapFn: Options["mapFn"] = (node) => { +> export const mapFn: ExplorerOptions["mapFn"] = (node) => { > // implement your function here > } -> export const filterFn: Options["filterFn"] = (node) => { +> export const filterFn: ExplorerOptions["filterFn"] = (node) => { > // implement your function here > } -> export const sortFn: Options["sortFn"] = (a, b) => { +> export const sortFn: ExplorerOptions["sortFn"] = (a, b) => { > // implement your function here > } > -> Component.Explorer({ +> Explorer({ > // ... your other options > mapFn, > filterFn, @@ -211,7 +238,7 @@ Component.Explorer({ To add emoji prefixes (๐Ÿ“ for folders, ๐Ÿ“„ for files), you could use a map function like this: ```ts title="quartz.layout.ts" -Component.Explorer({ +Explorer({ mapFn: (node) => { if (node.isFolder) { node.displayName = "๐Ÿ“ " + node.displayName diff --git a/package-lock.json b/package-lock.json index ff22d60fc..ab7ab1810 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@floating-ui/dom": "^1.7.4", "@myriaddreamin/rehype-typst": "^0.6.0", "@napi-rs/simple-git": "0.1.22", + "@quartz-community/explorer": "github:quartz-community/explorer", "@tweenjs/tween.js": "^25.0.0", "ansi-truncate": "^1.4.0", "async-mutex": "^0.5.0", @@ -93,13 +94,6 @@ "npm": ">=10.9.2" } }, - "node_modules/@bufbuild/protobuf": { - "version": "2.10.2", - "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.10.2.tgz", - "integrity": "sha512-uFsRXwIGyu+r6AMdz+XijIIZJYpoWeYzILt5yZ2d3mCjQrWUTVpVD9WL/jZAbvp+Ed04rOhrsk7FiTcEDseB5A==", - "license": "(Apache-2.0 AND BSD-3-Clause)", - "peer": true - }, "node_modules/@citation-js/core": { "version": "0.7.14", "resolved": "https://registry.npmjs.org/@citation-js/core/-/core-0.7.14.tgz", @@ -214,6 +208,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -230,6 +225,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -246,6 +242,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -262,6 +259,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -278,6 +276,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -294,6 +293,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -310,6 +310,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -326,6 +327,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -342,6 +344,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -358,6 +361,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -374,6 +378,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -390,6 +395,7 @@ "cpu": [ "loong64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -406,6 +412,7 @@ "cpu": [ "mips64el" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -422,6 +429,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -438,6 +446,7 @@ "cpu": [ "riscv64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -454,6 +463,7 @@ "cpu": [ "s390x" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -470,6 +480,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -486,6 +497,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -502,6 +514,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -518,6 +531,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -534,6 +548,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -550,6 +565,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -566,6 +582,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -582,6 +599,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -598,6 +616,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -614,6 +633,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1964,6 +1984,37 @@ "integrity": "sha512-nezytU2pw587fQstUu1AsJZDVEynjskwOL+kibwcdxsMBFqPsFFNA7xl0ii/gXuDi6M0xj3mfRJj8pBSc2jCfA==", "license": "MIT" }, + "node_modules/@quartz-community/explorer": { + "version": "0.1.0", + "resolved": "git+ssh://git@github.com/quartz-community/explorer.git#bb0f48f00f0486120b1216c78dd424127401bf4d", + "license": "MIT", + "dependencies": { + "mdast-util-find-and-replace": "^3.0.1", + "rehype-slug": "^6.0.0", + "remark-gfm": "^4.0.1", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.5", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.3" + }, + "engines": { + "node": ">=22", + "npm": ">=10.9.2" + }, + "peerDependencies": { + "@jackyzha0/quartz": "^4.5.2", + "preact": "^10.0.0" + }, + "peerDependenciesMeta": { + "@jackyzha0/quartz": { + "optional": true + }, + "preact": { + "optional": false + } + } + }, "node_modules/@shikijs/core": { "version": "1.26.2", "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-1.26.2.tgz", @@ -2607,13 +2658,6 @@ "ieee754": "^1.1.13" } }, - "node_modules/buffer-builder": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/buffer-builder/-/buffer-builder-0.2.0.tgz", - "integrity": "sha512-7VPMEPuYznPSoR21NE1zvd2Xna6c/CloiZCfcMXR1Jny6PjX0N4Nsa38zcBFo/FMK+BlA+FLKbJCQ0i2yxp+Xg==", - "license": "MIT/X11", - "peer": true - }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -2728,13 +2772,6 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, - "node_modules/colorjs.io": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/colorjs.io/-/colorjs.io-0.5.2.tgz", - "integrity": "sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw==", - "license": "MIT", - "peer": true - }, "node_modules/comma-separated-tokens": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", @@ -3282,6 +3319,7 @@ "version": "0.27.2", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", + "dev": true, "hasInstallScript": true, "license": "MIT", "bin": { @@ -3691,16 +3729,6 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=8" - } - }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -6410,25 +6438,15 @@ "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==" }, - "node_modules/rxjs": { - "version": "7.8.2", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", - "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "tslib": "^2.1.0" - } - }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "node_modules/sass": { - "version": "1.97.2", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.97.2.tgz", - "integrity": "sha512-y5LWb0IlbO4e97Zr7c3mlpabcbBtS+ieiZ9iwDooShpFKWXf62zz5pEPdwrLYm+Bxn1fnbwFGzHuCLSA9tBmrw==", + "version": "1.97.3", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.97.3.tgz", + "integrity": "sha512-fDz1zJpd5GycprAbu4Q2PV/RprsRtKC/0z82z0JLgdytmcq0+ujJbJ/09bPGDxCLkKY3Np5cRAOcWiVkLXJURg==", "license": "MIT", "dependencies": { "chokidar": "^4.0.0", @@ -6445,355 +6463,6 @@ "@parcel/watcher": "^2.4.1" } }, - "node_modules/sass-embedded": { - "version": "1.97.2", - "resolved": "https://registry.npmjs.org/sass-embedded/-/sass-embedded-1.97.2.tgz", - "integrity": "sha512-lKJcskySwAtJ4QRirKrikrWMFa2niAuaGenY2ElHjd55IwHUiur5IdKu6R1hEmGYMs4Qm+6rlRW0RvuAkmcryg==", - "license": "MIT", - "peer": true, - "dependencies": { - "@bufbuild/protobuf": "^2.5.0", - "buffer-builder": "^0.2.0", - "colorjs.io": "^0.5.0", - "immutable": "^5.0.2", - "rxjs": "^7.4.0", - "supports-color": "^8.1.1", - "sync-child-process": "^1.0.2", - "varint": "^6.0.0" - }, - "bin": { - "sass": "dist/bin/sass.js" - }, - "engines": { - "node": ">=16.0.0" - }, - "optionalDependencies": { - "sass-embedded-all-unknown": "1.97.2", - "sass-embedded-android-arm": "1.97.2", - "sass-embedded-android-arm64": "1.97.2", - "sass-embedded-android-riscv64": "1.97.2", - "sass-embedded-android-x64": "1.97.2", - "sass-embedded-darwin-arm64": "1.97.2", - "sass-embedded-darwin-x64": "1.97.2", - "sass-embedded-linux-arm": "1.97.2", - "sass-embedded-linux-arm64": "1.97.2", - "sass-embedded-linux-musl-arm": "1.97.2", - "sass-embedded-linux-musl-arm64": "1.97.2", - "sass-embedded-linux-musl-riscv64": "1.97.2", - "sass-embedded-linux-musl-x64": "1.97.2", - "sass-embedded-linux-riscv64": "1.97.2", - "sass-embedded-linux-x64": "1.97.2", - "sass-embedded-unknown-all": "1.97.2", - "sass-embedded-win32-arm64": "1.97.2", - "sass-embedded-win32-x64": "1.97.2" - } - }, - "node_modules/sass-embedded-all-unknown": { - "version": "1.97.2", - "resolved": "https://registry.npmjs.org/sass-embedded-all-unknown/-/sass-embedded-all-unknown-1.97.2.tgz", - "integrity": "sha512-Fj75+vOIDv1T/dGDwEpQ5hgjXxa2SmMeShPa8yrh2sUz1U44bbmY4YSWPCdg8wb7LnwiY21B2KRFM+HF42yO4g==", - "cpu": [ - "!arm", - "!arm64", - "!riscv64", - "!x64" - ], - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "sass": "1.97.2" - } - }, - "node_modules/sass-embedded-android-arm": { - "version": "1.97.2", - "resolved": "https://registry.npmjs.org/sass-embedded-android-arm/-/sass-embedded-android-arm-1.97.2.tgz", - "integrity": "sha512-BPT9m19ttY0QVHYYXRa6bmqmS3Fa2EHByNUEtSVcbm5PkIk1ntmYkG9fn5SJpIMbNmFDGwHx+pfcZMmkldhnRg==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "peer": true, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/sass-embedded-android-arm64": { - "version": "1.97.2", - "resolved": "https://registry.npmjs.org/sass-embedded-android-arm64/-/sass-embedded-android-arm64-1.97.2.tgz", - "integrity": "sha512-pF6I+R5uThrscd3lo9B3DyNTPyGFsopycdx0tDAESN6s+dBbiRgNgE4Zlpv50GsLocj/lDLCZaabeTpL3ubhYA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "peer": true, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/sass-embedded-android-riscv64": { - "version": "1.97.2", - "resolved": "https://registry.npmjs.org/sass-embedded-android-riscv64/-/sass-embedded-android-riscv64-1.97.2.tgz", - "integrity": "sha512-fprI8ZTJdz+STgARhg8zReI2QhhGIT9G8nS7H21kc3IkqPRzhfaemSxEtCqZyvDbXPcgYiDLV7AGIReHCuATog==", - "cpu": [ - "riscv64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "peer": true, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/sass-embedded-android-x64": { - "version": "1.97.2", - "resolved": "https://registry.npmjs.org/sass-embedded-android-x64/-/sass-embedded-android-x64-1.97.2.tgz", - "integrity": "sha512-RswwSjURZxupsukEmNt2t6RGvuvIw3IAD5sDq1Pc65JFvWFY3eHqCmH0lG0oXqMg6KJcF0eOxHOp2RfmIm2+4w==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "peer": true, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/sass-embedded-darwin-arm64": { - "version": "1.97.2", - "resolved": "https://registry.npmjs.org/sass-embedded-darwin-arm64/-/sass-embedded-darwin-arm64-1.97.2.tgz", - "integrity": "sha512-xcsZNnU1XZh21RE/71OOwNqPVcGBU0qT9A4k4QirdA34+ts9cDIaR6W6lgHOBR/Bnnu6w6hXJR4Xth7oFrefPA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "peer": true, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/sass-embedded-darwin-x64": { - "version": "1.97.2", - "resolved": "https://registry.npmjs.org/sass-embedded-darwin-x64/-/sass-embedded-darwin-x64-1.97.2.tgz", - "integrity": "sha512-T/9DTMpychm6+H4slHCAsYJRJ6eM+9H9idKlBPliPrP4T8JdC2Cs+ZOsYqrObj6eOtAD0fGf+KgyNhnW3xVafA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "peer": true, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/sass-embedded-linux-arm": { - "version": "1.97.2", - "resolved": "https://registry.npmjs.org/sass-embedded-linux-arm/-/sass-embedded-linux-arm-1.97.2.tgz", - "integrity": "sha512-yDRe1yifGHl6kibkDlRIJ2ZzAU03KJ1AIvsAh4dsIDgK5jx83bxZLV1ZDUv7a8KK/iV/80LZnxnu/92zp99cXQ==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/sass-embedded-linux-arm64": { - "version": "1.97.2", - "resolved": "https://registry.npmjs.org/sass-embedded-linux-arm64/-/sass-embedded-linux-arm64-1.97.2.tgz", - "integrity": "sha512-Wh+nQaFer9tyE5xBPv5murSUZE/+kIcg8MyL5uqww6be9Iq+UmZpcJM7LUk+q8klQ9LfTmoDSNFA74uBqxD6IA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/sass-embedded-linux-musl-arm": { - "version": "1.97.2", - "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-arm/-/sass-embedded-linux-musl-arm-1.97.2.tgz", - "integrity": "sha512-GIO6xfAtahJAWItvsXZ3MD1HM6s8cKtV1/HL088aUpKJaw/2XjTCveiOO2AdgMpLNztmq9DZ1lx5X5JjqhS45g==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/sass-embedded-linux-musl-arm64": { - "version": "1.97.2", - "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-arm64/-/sass-embedded-linux-musl-arm64-1.97.2.tgz", - "integrity": "sha512-NfUqZSjHwnHvpSa7nyNxbWfL5obDjNBqhHUYmqbHUcmqBpFfHIQsUPgXME9DKn1yBlBc3mWnzMxRoucdYTzd2Q==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/sass-embedded-linux-musl-riscv64": { - "version": "1.97.2", - "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-riscv64/-/sass-embedded-linux-musl-riscv64-1.97.2.tgz", - "integrity": "sha512-qtM4dJ5gLfvyTZ3QencfNbsTEShIWImSEpkThz+Y2nsCMbcMP7/jYOA03UWgPfEOKSehQQ7EIau7ncbFNoDNPQ==", - "cpu": [ - "riscv64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/sass-embedded-linux-musl-x64": { - "version": "1.97.2", - "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-x64/-/sass-embedded-linux-musl-x64-1.97.2.tgz", - "integrity": "sha512-ZAxYOdmexcnxGnzdsDjYmNe3jGj+XW3/pF/n7e7r8y+5c6D2CQRrCUdapLgaqPt1edOPQIlQEZF8q5j6ng21yw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/sass-embedded-linux-riscv64": { - "version": "1.97.2", - "resolved": "https://registry.npmjs.org/sass-embedded-linux-riscv64/-/sass-embedded-linux-riscv64-1.97.2.tgz", - "integrity": "sha512-reVwa9ZFEAOChXpDyNB3nNHHyAkPMD+FTctQKECqKiVJnIzv2EaFF6/t0wzyvPgBKeatA8jszAIeOkkOzbYVkQ==", - "cpu": [ - "riscv64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/sass-embedded-linux-x64": { - "version": "1.97.2", - "resolved": "https://registry.npmjs.org/sass-embedded-linux-x64/-/sass-embedded-linux-x64-1.97.2.tgz", - "integrity": "sha512-bvAdZQsX3jDBv6m4emaU2OMTpN0KndzTAMgJZZrKUgiC0qxBmBqbJG06Oj/lOCoXGCxAvUOheVYpezRTF+Feog==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/sass-embedded-unknown-all": { - "version": "1.97.2", - "resolved": "https://registry.npmjs.org/sass-embedded-unknown-all/-/sass-embedded-unknown-all-1.97.2.tgz", - "integrity": "sha512-86tcYwohjPgSZtgeU9K4LikrKBJNf8ZW/vfsFbdzsRlvc73IykiqanufwQi5qIul0YHuu9lZtDWyWxM2dH/Rsg==", - "license": "MIT", - "optional": true, - "os": [ - "!android", - "!darwin", - "!linux", - "!win32" - ], - "peer": true, - "dependencies": { - "sass": "1.97.2" - } - }, - "node_modules/sass-embedded-win32-arm64": { - "version": "1.97.2", - "resolved": "https://registry.npmjs.org/sass-embedded-win32-arm64/-/sass-embedded-win32-arm64-1.97.2.tgz", - "integrity": "sha512-Cv28q8qNjAjZfqfzTrQvKf4JjsZ6EOQ5FxyHUQQeNzm73R86nd/8ozDa1Vmn79Hq0kwM15OCM9epanDuTG1ksA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "peer": true, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/sass-embedded-win32-x64": { - "version": "1.97.2", - "resolved": "https://registry.npmjs.org/sass-embedded-win32-x64/-/sass-embedded-win32-x64-1.97.2.tgz", - "integrity": "sha512-DVxLxkeDCGIYeyHLAvWW3yy9sy5Ruk5p472QWiyfyyG1G1ASAR8fgfIY5pT0vE6Rv+VAKVLwF3WTspUYu7S1/Q==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "peer": true, - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/sass/node_modules/chokidar": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", @@ -7125,22 +6794,6 @@ "inline-style-parser": "0.2.4" } }, - "node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "license": "MIT", - "peer": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", @@ -7153,19 +6806,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/sync-child-process": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/sync-child-process/-/sync-child-process-1.0.2.tgz", - "integrity": "sha512-8lD+t2KrrScJ/7KXCSyfhT3/hRq78rC0wBFqNJXv3mZyn6hW2ypM05JmlSvtqRbeq6jqA94oHbxAr2vYsJ8vDA==", - "license": "MIT", - "peer": true, - "dependencies": { - "sync-message-port": "^1.0.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, "node_modules/sync-fetch": { "version": "0.4.5", "resolved": "https://registry.npmjs.org/sync-fetch/-/sync-fetch-0.4.5.tgz", @@ -7178,16 +6818,6 @@ "node": ">=14" } }, - "node_modules/sync-message-port": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/sync-message-port/-/sync-message-port-1.1.3.tgz", - "integrity": "sha512-GTt8rSKje5FilG+wEdfCkOcLL7LWqpMlr2c3LRuKt/YXxcJ52aGSbGBAdI4L3aaqfrBt6y711El53ItyH1NWzg==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=16.0.0" - } - }, "node_modules/tiny-inflate": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", @@ -7488,13 +7118,6 @@ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" }, - "node_modules/varint": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/varint/-/varint-6.0.0.tgz", - "integrity": "sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==", - "license": "MIT", - "peer": true - }, "node_modules/vfile": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", diff --git a/package.json b/package.json index e1d138a52..3a0176805 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "@floating-ui/dom": "^1.7.4", "@myriaddreamin/rehype-typst": "^0.6.0", "@napi-rs/simple-git": "0.1.22", + "@quartz-community/explorer": "github:quartz-community/explorer", "@tweenjs/tween.js": "^25.0.0", "ansi-truncate": "^1.4.0", "async-mutex": "^0.5.0", diff --git a/quartz.config.ts b/quartz.config.ts index b3db3d60d..6927a9117 100644 --- a/quartz.config.ts +++ b/quartz.config.ts @@ -92,6 +92,7 @@ const config: QuartzConfig = { Plugin.CustomOgImages(), ], }, + externalPlugins: ["@quartz-community/explorer"], } export default config diff --git a/quartz.layout.ts b/quartz.layout.ts index 970a5be34..3b107d68d 100644 --- a/quartz.layout.ts +++ b/quartz.layout.ts @@ -1,5 +1,9 @@ import { PageLayout, SharedLayout } from "./quartz/cfg" import * as Component from "./quartz/components" +import { Explorer } from "@quartz-community/explorer/components" + +// Create Explorer once and reuse for both layouts +const explorerComponent = Explorer() // components shared across all pages export const sharedPageComponents: SharedLayout = { @@ -38,7 +42,7 @@ export const defaultContentPageLayout: PageLayout = { { Component: Component.ReaderMode() }, ], }), - Component.Explorer(), + explorerComponent, ], right: [ Component.Graph(), @@ -62,7 +66,7 @@ export const defaultListPageLayout: PageLayout = { { Component: Component.Darkmode() }, ], }), - Component.Explorer(), + explorerComponent, ], right: [], } diff --git a/quartz/components/Explorer.tsx b/quartz/components/Explorer.tsx deleted file mode 100644 index e4cbcabae..000000000 --- a/quartz/components/Explorer.tsx +++ /dev/null @@ -1,165 +0,0 @@ -import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" -import style from "./styles/explorer.scss" - -// @ts-ignore -import script from "./scripts/explorer.inline" -import { classNames } from "../util/lang" -import { i18n } from "../i18n" -import { FileTrieNode } from "../util/fileTrie" -import OverflowListFactory from "./OverflowList" -import { concatenateResources } from "../util/resources" - -type OrderEntries = "sort" | "filter" | "map" - -export interface Options { - title?: string - folderDefaultState: "collapsed" | "open" - folderClickBehavior: "collapse" | "link" - useSavedState: boolean - sortFn: (a: FileTrieNode, b: FileTrieNode) => number - filterFn: (node: FileTrieNode) => boolean - mapFn: (node: FileTrieNode) => void - order: OrderEntries[] -} - -const defaultOptions: Options = { - folderDefaultState: "collapsed", - folderClickBehavior: "link", - useSavedState: true, - mapFn: (node) => { - return node - }, - sortFn: (a, b) => { - // Sort order: folders first, then files. Sort folders and files alphabeticall - if ((!a.isFolder && !b.isFolder) || (a.isFolder && b.isFolder)) { - // numeric: true: Whether numeric collation should be used, such that "1" < "2" < "10" - // sensitivity: "base": Only strings that differ in base letters compare as unequal. Examples: a โ‰  b, a = รก, a = A - return a.displayName.localeCompare(b.displayName, undefined, { - numeric: true, - sensitivity: "base", - }) - } - - if (!a.isFolder && b.isFolder) { - return 1 - } else { - return -1 - } - }, - filterFn: (node) => node.slugSegment !== "tags", - order: ["filter", "map", "sort"], -} - -export type FolderState = { - path: string - collapsed: boolean -} - -let numExplorers = 0 -export default ((userOpts?: Partial) => { - const opts: Options = { ...defaultOptions, ...userOpts } - const { OverflowList, overflowListAfterDOMLoaded } = OverflowListFactory() - - const Explorer: QuartzComponent = ({ cfg, displayClass }: QuartzComponentProps) => { - const id = `explorer-${numExplorers++}` - - return ( -
- - -
- -
- - -
- ) - } - - Explorer.css = style - Explorer.afterDOMLoaded = concatenateResources(script, overflowListAfterDOMLoaded) - return Explorer -}) satisfies QuartzComponentConstructor diff --git a/quartz/components/index.ts b/quartz/components/index.ts index 91ab4afc9..f6bbb7072 100644 --- a/quartz/components/index.ts +++ b/quartz/components/index.ts @@ -10,7 +10,6 @@ import PageTitle from "./PageTitle" import ContentMeta from "./ContentMeta" import Spacer from "./Spacer" import TableOfContents from "./TableOfContents" -import Explorer from "./Explorer" import TagList from "./TagList" import Graph from "./Graph" import Backlinks from "./Backlinks" @@ -41,7 +40,6 @@ export { ContentMeta, Spacer, TableOfContents, - Explorer, TagList, Graph, Backlinks, diff --git a/quartz/components/scripts/explorer.inline.ts b/quartz/components/scripts/explorer.inline.ts deleted file mode 100644 index 3c6851c7b..000000000 --- a/quartz/components/scripts/explorer.inline.ts +++ /dev/null @@ -1,305 +0,0 @@ -import { FileTrieNode } from "../../util/fileTrie" -import { FullSlug, resolveRelative, simplifySlug } from "../../util/path" -import { ContentDetails } from "../../plugins/emitters/contentIndex" - -type MaybeHTMLElement = HTMLElement | undefined - -interface ParsedOptions { - folderClickBehavior: "collapse" | "link" - folderDefaultState: "collapsed" | "open" - useSavedState: boolean - sortFn: (a: FileTrieNode, b: FileTrieNode) => number - filterFn: (node: FileTrieNode) => boolean - mapFn: (node: FileTrieNode) => void - order: "sort" | "filter" | "map"[] -} - -type FolderState = { - path: string - collapsed: boolean -} - -let currentExplorerState: Array -function toggleExplorer(this: HTMLElement) { - const nearestExplorer = this.closest(".explorer") as HTMLElement - if (!nearestExplorer) return - const explorerCollapsed = nearestExplorer.classList.toggle("collapsed") - nearestExplorer.setAttribute( - "aria-expanded", - nearestExplorer.getAttribute("aria-expanded") === "true" ? "false" : "true", - ) - - if (!explorerCollapsed) { - // Stop 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) { - evt.stopPropagation() - const target = evt.target as MaybeHTMLElement - if (!target) return - - // Check if target was svg icon or button - const isSvg = target.nodeName === "svg" - - // corresponding
    element relative to clicked button/folder - const folderContainer = ( - isSvg - ? // svg -> div.folder-container - target.parentElement - : // button.folder-button -> div -> div.folder-container - target.parentElement?.parentElement - ) as MaybeHTMLElement - if (!folderContainer) return - const childFolderContainer = folderContainer.nextElementSibling as MaybeHTMLElement - if (!childFolderContainer) return - - childFolderContainer.classList.toggle("open") - - // Collapse folder container - const isCollapsed = !childFolderContainer.classList.contains("open") - setFolderState(childFolderContainer, isCollapsed) - - const currentFolderState = currentExplorerState.find( - (item) => item.path === folderContainer.dataset.folderpath, - ) - if (currentFolderState) { - currentFolderState.collapsed = isCollapsed - } else { - currentExplorerState.push({ - path: folderContainer.dataset.folderpath as FullSlug, - collapsed: isCollapsed, - }) - } - - const stringifiedFileTree = JSON.stringify(currentExplorerState) - localStorage.setItem("fileTree", stringifiedFileTree) -} - -function createFileNode(currentSlug: FullSlug, node: FileTrieNode): HTMLLIElement { - const template = document.getElementById("template-file") as HTMLTemplateElement - const clone = template.content.cloneNode(true) as DocumentFragment - const li = clone.querySelector("li") as HTMLLIElement - const a = li.querySelector("a") as HTMLAnchorElement - a.href = resolveRelative(currentSlug, node.slug) - a.dataset.for = node.slug - a.textContent = node.displayName - - if (currentSlug === node.slug) { - a.classList.add("active") - } - - return li -} - -function createFolderNode( - currentSlug: FullSlug, - node: FileTrieNode, - opts: ParsedOptions, -): HTMLLIElement { - const template = document.getElementById("template-folder") as HTMLTemplateElement - const clone = template.content.cloneNode(true) as DocumentFragment - const li = clone.querySelector("li") as HTMLLIElement - const folderContainer = li.querySelector(".folder-container") as HTMLElement - const titleContainer = folderContainer.querySelector("div") as HTMLElement - const folderOuter = li.querySelector(".folder-outer") as HTMLElement - const ul = folderOuter.querySelector("ul") as HTMLUListElement - - const folderPath = node.slug - folderContainer.dataset.folderpath = folderPath - - if (currentSlug === folderPath) { - folderContainer.classList.add("active") - } - - if (opts.folderClickBehavior === "link") { - // Replace button with link for link behavior - const button = titleContainer.querySelector(".folder-button") as HTMLElement - const a = document.createElement("a") - a.href = resolveRelative(currentSlug, folderPath) - a.dataset.for = folderPath - a.className = "folder-title" - a.textContent = node.displayName - button.replaceWith(a) - } else { - const span = titleContainer.querySelector(".folder-title") as HTMLElement - span.textContent = node.displayName - } - - // if the saved state is collapsed or the default state is collapsed - const isCollapsed = - currentExplorerState.find((item) => item.path === folderPath)?.collapsed ?? - opts.folderDefaultState === "collapsed" - - // if this folder is a prefix of the current path we - // want to open it anyways - const simpleFolderPath = simplifySlug(folderPath) - const folderIsPrefixOfCurrentSlug = - simpleFolderPath === currentSlug.slice(0, simpleFolderPath.length) - - if (!isCollapsed || folderIsPrefixOfCurrentSlug) { - folderOuter.classList.add("open") - } - - for (const child of node.children) { - const childNode = child.isFolder - ? createFolderNode(currentSlug, child, opts) - : createFileNode(currentSlug, child) - ul.appendChild(childNode) - } - - return li -} - -async function setupExplorer(currentSlug: FullSlug) { - const allExplorers = document.querySelectorAll("div.explorer") as NodeListOf - - for (const explorer of allExplorers) { - const dataFns = JSON.parse(explorer.dataset.dataFns || "{}") - const opts: ParsedOptions = { - folderClickBehavior: (explorer.dataset.behavior || "collapse") as "collapse" | "link", - folderDefaultState: (explorer.dataset.collapsed || "collapsed") as "collapsed" | "open", - useSavedState: explorer.dataset.savestate === "true", - order: dataFns.order || ["filter", "map", "sort"], - sortFn: new Function("return " + (dataFns.sortFn || "undefined"))(), - filterFn: new Function("return " + (dataFns.filterFn || "undefined"))(), - mapFn: new Function("return " + (dataFns.mapFn || "undefined"))(), - } - - // Get folder state from local storage - const storageTree = localStorage.getItem("fileTree") - const serializedExplorerState = storageTree && opts.useSavedState ? JSON.parse(storageTree) : [] - const oldIndex = new Map( - serializedExplorerState.map((entry: FolderState) => [entry.path, entry.collapsed]), - ) - - const data = await fetchData - const entries = [...Object.entries(data)] as [FullSlug, ContentDetails][] - const trie = FileTrieNode.fromEntries(entries) - - // Apply functions in order - for (const fn of opts.order) { - switch (fn) { - case "filter": - if (opts.filterFn) trie.filter(opts.filterFn) - break - case "map": - if (opts.mapFn) trie.map(opts.mapFn) - break - case "sort": - if (opts.sortFn) trie.sort(opts.sortFn) - break - } - } - - // Get folder paths for state management - const folderPaths = trie.getFolderPaths() - currentExplorerState = folderPaths.map((path) => { - const previousState = oldIndex.get(path) - return { - path, - collapsed: - previousState === undefined ? opts.folderDefaultState === "collapsed" : previousState, - } - }) - - const explorerUl = explorer.querySelector(".explorer-ul") - if (!explorerUl) continue - - // Create and insert new content - const fragment = document.createDocumentFragment() - for (const child of trie.children) { - const node = child.isFolder - ? createFolderNode(currentSlug, child, opts) - : createFileNode(currentSlug, child) - - fragment.appendChild(node) - } - explorerUl.insertBefore(fragment, explorerUl.firstChild) - - // restore explorer scrollTop position if it exists - const scrollTop = sessionStorage.getItem("explorerScrollTop") - if (scrollTop) { - explorerUl.scrollTop = parseInt(scrollTop) - } else { - // try to scroll to the active element if it exists - const activeElement = explorerUl.querySelector(".active") - if (activeElement) { - activeElement.scrollIntoView({ behavior: "smooth" }) - } - } - - // Set up event handlers - const explorerButtons = explorer.getElementsByClassName( - "explorer-toggle", - ) as HTMLCollectionOf - for (const button of explorerButtons) { - button.addEventListener("click", toggleExplorer) - window.addCleanup(() => button.removeEventListener("click", toggleExplorer)) - } - - // Set up folder click handlers - if (opts.folderClickBehavior === "collapse") { - const folderButtons = explorer.getElementsByClassName( - "folder-button", - ) as HTMLCollectionOf - for (const button of folderButtons) { - button.addEventListener("click", toggleFolder) - window.addCleanup(() => button.removeEventListener("click", toggleFolder)) - } - } - - const folderIcons = explorer.getElementsByClassName( - "folder-icon", - ) as HTMLCollectionOf - for (const icon of folderIcons) { - icon.addEventListener("click", toggleFolder) - window.addCleanup(() => icon.removeEventListener("click", toggleFolder)) - } - } -} - -document.addEventListener("prenav", async () => { - // save explorer scrollTop position - const explorer = document.querySelector(".explorer-ul") - if (!explorer) return - sessionStorage.setItem("explorerScrollTop", explorer.scrollTop.toString()) -}) - -document.addEventListener("nav", async (e: CustomEventMap["nav"]) => { - const currentSlug = e.detail.url - await setupExplorer(currentSlug) - - // if mobile hamburger is visible, collapse by default - for (const explorer of document.getElementsByClassName("explorer")) { - const mobileExplorer = explorer.querySelector(".mobile-explorer") - if (!mobileExplorer) return - - if (mobileExplorer.checkVisibility()) { - explorer.classList.add("collapsed") - explorer.setAttribute("aria-expanded", "false") - - // Allow 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 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") -} diff --git a/quartz/components/styles/explorer.scss b/quartz/components/styles/explorer.scss deleted file mode 100644 index d0a649633..000000000 --- a/quartz/components/styles/explorer.scss +++ /dev/null @@ -1,282 +0,0 @@ -@use "../../styles/variables.scss" as *; - -@media all and ($mobile) { - .page > #quartz-body { - // Shift page position when toggling Explorer on mobile. - & > :not(.sidebar.left:has(.explorer)) { - transition: transform 300ms ease-in-out; - } - - &.lock-scroll > :not(.sidebar.left:has(.explorer)) { - transform: translateX(100dvw); - transition: transform 300ms ease-in-out; - } - - // Sticky top bar (stays in place when scrolling down on mobile). - .sidebar.left:has(.explorer) { - box-sizing: border-box; - position: sticky; - background-color: var(--light); - padding: 1rem 0 1rem 0; - margin: 0; - } - - .hide-until-loaded ~ .explorer-content { - display: none; - } - } -} - -.explorer { - display: flex; - flex-direction: column; - overflow-y: hidden; - - min-height: 1.2rem; - flex: 0 1 auto; - - &.collapsed { - flex: 0 1 1.2rem; - - & .fold { - transform: rotateZ(-90deg); - } - } - - & .fold { - margin-left: 0.5rem; - transition: transform 0.3s ease; - opacity: 0.8; - } - - @media all and ($mobile) { - order: -1; - height: initial; - overflow: hidden; - flex-shrink: 0; - align-self: flex-start; - margin-top: auto; - margin-bottom: auto; - } - - button.mobile-explorer { - display: none; - } - - button.desktop-explorer { - display: flex; - } - - @media all and ($mobile) { - button.mobile-explorer { - display: flex; - } - - button.desktop-explorer { - display: none; - } - } - - &.desktop-only { - @media all and not ($mobile) { - display: flex; - } - } - - svg { - pointer-events: all; - transition: transform 0.35s ease; - - & > polyline { - pointer-events: none; - } - } -} - -button.mobile-explorer, -button.desktop-explorer { - background-color: transparent; - border: none; - text-align: left; - cursor: pointer; - padding: 0; - color: var(--dark); - display: flex; - align-items: center; - - & h2 { - font-size: 1rem; - display: inline-block; - margin: 0; - } -} - -.explorer-content { - list-style: none; - overflow: hidden; - overflow-y: auto; - margin-top: 0.5rem; - - & ul { - list-style: none; - margin: 0; - padding: 0; - - &.explorer-ul { - overscroll-behavior: contain; - } - - & li > a { - color: var(--dark); - opacity: 0.75; - pointer-events: all; - - &.active { - opacity: 1; - color: var(--tertiary); - } - } - } - - .folder-outer { - visibility: collapse; - display: grid; - grid-template-rows: 0fr; - transition-property: grid-template-rows, visibility; - transition-duration: 0.3s; - transition-timing-function: ease-in-out; - } - - .folder-outer.open { - visibility: visible; - grid-template-rows: 1fr; - } - - .folder-outer > ul { - overflow: hidden; - margin-left: 6px; - padding-left: 0.8rem; - border-left: 1px solid var(--lightgray); - } -} - -.folder-container { - flex-direction: row; - display: flex; - align-items: center; - user-select: none; - - & div > a { - color: var(--secondary); - font-family: var(--headerFont); - font-size: 0.95rem; - font-weight: $semiBoldWeight; - line-height: 1.5rem; - display: inline-block; - } - - & div > a:hover { - color: var(--tertiary); - } - - & div > button { - color: var(--dark); - background-color: transparent; - border: none; - text-align: left; - cursor: pointer; - padding-left: 0; - padding-right: 0; - display: flex; - align-items: center; - font-family: var(--headerFont); - - & span { - font-size: 0.95rem; - display: inline-block; - color: var(--secondary); - font-weight: $semiBoldWeight; - margin: 0; - line-height: 1.5rem; - pointer-events: none; - } - } -} - -.folder-icon { - margin-right: 5px; - color: var(--secondary); - cursor: pointer; - transition: transform 0.3s ease; - backface-visibility: visible; - flex-shrink: 0; -} - -li:has(> .folder-outer:not(.open)) > .folder-container > svg { - transform: rotate(-90deg); -} - -.folder-icon:hover { - color: var(--tertiary); -} - -.explorer { - @media all and ($mobile) { - &.collapsed { - flex: 0 0 34px; - - & > .explorer-content { - transform: translateX(-100vw); - visibility: hidden; - } - } - - &:not(.collapsed) { - flex: 0 0 34px; - - & > .explorer-content { - transform: translateX(0); - visibility: visible; - } - } - - .explorer-content { - box-sizing: border-box; - z-index: 100; - position: absolute; - top: 0; - left: 0; - margin-top: 0; - background-color: var(--light); - max-width: 100vw; - width: 100vw; - transform: translateX(-100vw); - transition: - transform 200ms ease, - visibility 200ms ease; - overflow: hidden; - padding: 4rem 0 2rem 0; - height: 100dvh; - max-height: 100dvh; - visibility: hidden; - } - - .mobile-explorer { - margin: 0; - padding: 5px; - z-index: 101; - - .lucide-menu { - stroke: var(--darkgray); - } - } - } -} - -.mobile-no-scroll { - @media all and ($mobile) { - .explorer-content > .explorer-ul { - overscroll-behavior: contain; - } - } -}