Compare commits

...

6 Commits

Author SHA1 Message Date
Jacky Zhao
d02bb0f044 more style fixes 2025-03-09 14:49:53 -07:00
Jacky Zhao
e320324cf2 make mobile look nice 2025-03-09 14:03:10 -07:00
Jacky Zhao
9f8a8c83e5 open folders that are prefixes of current path 2025-03-09 10:05:27 -07:00
Jacky Zhao
1176deab7e make flex more consistent, remove transition 2025-03-09 09:57:12 -07:00
Jacky Zhao
2802aa91f4 add highlight class 2025-03-09 09:20:35 -07:00
Jacky Zhao
50efa14bd2 add prenav hook 2025-03-09 09:04:04 -07:00
12 changed files with 134 additions and 101 deletions

View File

@ -161,6 +161,18 @@ document.addEventListener("nav", () => {
}) })
``` ```
You can also add the equivalent of a `beforeunload` event for [[SPA Routing]] via the `prenav` event.
```ts
document.addEventListener("prenav", () => {
// executed after an SPA navigation is triggered but
// before the page is replaced
// one usage pattern is to store things in sessionStorage
// in the prenav and then conditionally load then in the consequent
// nav
})
```
It is best practice to track any event handlers via `window.addCleanup` to prevent memory leaks. It is best practice to track any event handlers via `window.addCleanup` to prevent memory leaks.
This will get called on page navigation. This will get called on page navigation.

1
index.d.ts vendored
View File

@ -5,6 +5,7 @@ declare module "*.scss" {
// dom custom event // dom custom event
interface CustomEventMap { interface CustomEventMap {
prenav: CustomEvent<{}>
nav: CustomEvent<{ url: FullSlug }> nav: CustomEvent<{ url: FullSlug }>
themechange: CustomEvent<{ theme: "light" | "dark" }> themechange: CustomEvent<{ theme: "light" | "dark" }>
} }

View File

@ -8,7 +8,7 @@ import * as Plugin from "./quartz/plugins"
*/ */
const config: QuartzConfig = { const config: QuartzConfig = {
configuration: { configuration: {
pageTitle: "🪴 Quartz 4", pageTitle: "Quartz 4",
pageTitleSuffix: "", pageTitleSuffix: "",
enableSPA: true, enableSPA: true,
enablePopovers: true, enablePopovers: true,

View File

@ -26,9 +26,8 @@ export const defaultContentPageLayout: PageLayout = {
Component.PageTitle(), Component.PageTitle(),
Component.MobileOnly(Component.Spacer()), Component.MobileOnly(Component.Spacer()),
Component.Search(), Component.Search(),
Component.Explorer(),
Component.Darkmode(), Component.Darkmode(),
Component.Explorer(),
], ],
right: [ right: [
Component.Graph(), Component.Graph(),

View File

@ -22,8 +22,8 @@ export interface Options {
} }
const defaultOptions: Options = { const defaultOptions: Options = {
folderClickBehavior: "collapse",
folderDefaultState: "collapsed", folderDefaultState: "collapsed",
folderClickBehavior: "collapse",
useSavedState: true, useSavedState: true,
mapFn: (node) => { mapFn: (node) => {
return node return node
@ -74,7 +74,7 @@ export default ((userOpts?: Partial<Options>) => {
<button <button
type="button" type="button"
id="mobile-explorer" id="mobile-explorer"
class="collapsed hide-until-loaded explorer-toggle" class="explorer-toggle hide-until-loaded"
data-mobile={true} data-mobile={true}
aria-controls="explorer-content" aria-controls="explorer-content"
> >

View File

@ -17,7 +17,6 @@ document.addEventListener("nav", (e) => {
const observer = new IntersectionObserver((entries) => { const observer = new IntersectionObserver((entries) => {
for (const entry of entries) { for (const entry of entries) {
const parentUl = entry.target.parentElement const parentUl = entry.target.parentElement
console.log(parentUl)
if (entry.isIntersecting) { if (entry.isIntersecting) {
parentUl.classList.remove("gradient-active") parentUl.classList.remove("gradient-active")
} else { } else {

View File

@ -1,5 +1,5 @@
import { FileTrieNode } from "../../util/fileTrie" import { FileTrieNode } from "../../util/fileTrie"
import { FullSlug, resolveRelative } from "../../util/path" import { FullSlug, resolveRelative, simplifySlug } from "../../util/path"
import { ContentDetails } from "../../plugins/emitters/contentIndex" import { ContentDetails } from "../../plugins/emitters/contentIndex"
type MaybeHTMLElement = HTMLElement | undefined type MaybeHTMLElement = HTMLElement | undefined
@ -81,6 +81,11 @@ function createFileNode(currentSlug: FullSlug, node: FileTrieNode): HTMLLIElemen
a.href = resolveRelative(currentSlug, node.data?.slug!) a.href = resolveRelative(currentSlug, node.data?.slug!)
a.dataset.for = node.data?.slug a.dataset.for = node.data?.slug
a.textContent = node.displayName a.textContent = node.displayName
if (currentSlug === node.data?.slug) {
a.classList.add("active")
}
return li return li
} }
@ -114,10 +119,18 @@ function createFolderNode(
span.textContent = node.displayName span.textContent = node.displayName
} }
// if the saved state is collapsed or the default state is collapsed
const isCollapsed = const isCollapsed =
currentExplorerState.find((item) => item.path === folderPath)?.collapsed ?? currentExplorerState.find((item) => item.path === folderPath)?.collapsed ??
opts.folderDefaultState === "collapsed" opts.folderDefaultState === "collapsed"
if (!isCollapsed) {
// 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") folderOuter.classList.add("open")
} }
@ -193,6 +206,18 @@ async function setupExplorer(currentSlug: FullSlug) {
} }
explorerUl.insertBefore(fragment, explorerUl.firstChild) 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 // Set up event handlers
const explorerButtons = explorer.querySelectorAll( const explorerButtons = explorer.querySelectorAll(
"button.explorer-toggle", "button.explorer-toggle",
@ -225,15 +250,26 @@ async function setupExplorer(currentSlug: FullSlug) {
} }
} }
document.addEventListener("prenav", async (e: CustomEventMap["prenav"]) => {
// save explorer scrollTop position
const explorer = document.getElementById("explorer-ul")
if (!explorer) return
sessionStorage.setItem("explorerScrollTop", explorer.scrollTop.toString())
})
document.addEventListener("nav", async (e: CustomEventMap["nav"]) => { document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
const currentSlug = e.detail.url const currentSlug = e.detail.url
const mobileExplorer = document.querySelector("#mobile-explorer")
if (mobileExplorer) {
mobileExplorer.classList.add("collapsed")
}
await setupExplorer(currentSlug) await setupExplorer(currentSlug)
// Hide explorer on mobile until it is requested // if mobile hamburger is visible, collapse by default
const mobileExplorer = document.getElementById("mobile-explorer")
if (mobileExplorer && mobileExplorer.checkVisibility()) {
for (const explorer of document.querySelectorAll(".explorer")) {
explorer.classList.add("collapsed")
explorer.setAttribute("aria-expanded", "false")
}
}
const hiddenUntilDoneLoading = document.querySelector("#mobile-explorer") const hiddenUntilDoneLoading = document.querySelector("#mobile-explorer")
hiddenUntilDoneLoading?.classList.remove("hide-until-loaded") hiddenUntilDoneLoading?.classList.remove("hide-until-loaded")
}) })

View File

@ -75,6 +75,10 @@ async function navigate(url: URL, isBack: boolean = false) {
if (!contents) return if (!contents) return
// notify about to nav
const event: CustomEventMap["prenav"] = new CustomEvent("prenav", { detail: {} })
document.dispatchEvent(event)
// cleanup old // cleanup old
cleanupFns.forEach((fn) => fn()) cleanupFns.forEach((fn) => fn())
cleanupFns.clear() cleanupFns.clear()
@ -108,7 +112,7 @@ async function navigate(url: URL, isBack: boolean = false) {
} }
} }
// now, patch head // now, patch head, re-executing scripts
const elementsToRemove = document.head.querySelectorAll(":not([spa-preserve])") const elementsToRemove = document.head.querySelectorAll(":not([spa-preserve])")
elementsToRemove.forEach((el) => el.remove()) elementsToRemove.forEach((el) => el.remove())
const elementsToAdd = html.head.querySelectorAll(":not([spa-preserve])") const elementsToAdd = html.head.querySelectorAll(":not([spa-preserve])")

View File

@ -8,6 +8,7 @@
height: 20px; height: 20px;
margin: 0 10px; margin: 0 10px;
text-align: inherit; text-align: inherit;
flex-shrink: 0;
& svg { & svg {
position: absolute; position: absolute;

View File

@ -16,10 +16,10 @@
box-sizing: border-box; box-sizing: border-box;
position: sticky; position: sticky;
background-color: var(--light); background-color: var(--light);
padding: 1rem 0 1rem 0;
margin: 0;
} }
// Hide Explorer on mobile until done loading.
// Prevents ugly animation on page load.
.hide-until-loaded ~ #explorer-content { .hide-until-loaded ~ #explorer-content {
display: none; display: none;
} }
@ -30,6 +30,19 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow-y: hidden; overflow-y: hidden;
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) { @media all and ($mobile) {
order: -1; order: -1;
@ -62,6 +75,15 @@
display: flex; display: flex;
} }
} }
svg {
pointer-events: all;
transition: transform 0.35s ease;
& > polyline {
pointer-events: none;
}
}
} }
button#mobile-explorer, button#mobile-explorer,
@ -80,49 +102,29 @@ button#desktop-explorer {
display: inline-block; display: inline-block;
margin: 0; margin: 0;
} }
& .fold {
margin-left: 0.5rem;
transition: transform 0.3s ease;
opacity: 0.8;
}
.explorer.collapsed > & .fold {
transform: rotateZ(-90deg);
}
} }
#explorer-content { #explorer-content {
list-style: none; list-style: none;
overflow: hidden; overflow: hidden;
overflow-y: auto; overflow-y: auto;
max-height: 100%;
transition: max-height 0.35s ease;
margin-top: 0.5rem; margin-top: 0.5rem;
.explorer.collapsed > & {
max-height: 0px;
transition: max-height 0.35s ease;
}
& ul { & ul {
list-style: none; list-style: none;
margin: 0.08rem 0; margin: 0;
padding: 0; padding: 0;
transition:
max-height 0.35s ease,
transform 0.35s ease,
opacity 0.2s ease;
& li > a { & li > a {
color: var(--dark); color: var(--dark);
opacity: 0.75; opacity: 0.75;
pointer-events: all; pointer-events: all;
}
}
> #explorer-ul { &.active {
max-height: none; opacity: 1;
color: var(--tertiary);
}
}
} }
.folder-outer { .folder-outer {
@ -143,14 +145,6 @@ button#desktop-explorer {
} }
} }
svg {
pointer-events: all;
& > polyline {
pointer-events: none;
}
}
.folder-container { .folder-container {
flex-direction: row; flex-direction: row;
display: flex; display: flex;
@ -212,51 +206,52 @@ li:has(> .folder-outer:not(.open)) > .folder-container > svg {
.explorer { .explorer {
@media all and ($mobile) { @media all and ($mobile) {
#explorer-content { &.collapsed {
box-sizing: border-box; flex: 0 0 34px;
overscroll-behavior: none;
z-index: 100;
position: absolute;
top: 0;
background-color: var(--light);
max-width: 100dvw;
left: -100dvw;
width: 100%;
transition: transform 300ms ease-in-out;
overflow: hidden;
padding: $topSpacing 2rem 2rem;
height: 100dvh;
max-height: 100dvh;
margin-top: 0;
visibility: hidden;
&:not(.collapsed) { & > #explorer-content {
transform: translateX(100dvw); transform: translateX(-100vw);
visibility: visible; visibility: hidden;
} }
}
&.collapsed { &:not(.collapsed) {
flex: 0 0 34px;
& > #explorer-content {
transform: translateX(0); transform: translateX(0);
visibility: visible; visibility: visible;
} }
} }
#mobile-explorer { #explorer-content {
margin: 5px; box-sizing: border-box;
z-index: 101; z-index: 100;
position: absolute;
top: 0;
left: 0;
margin-top: 0;
background-color: var(--light);
max-width: 100vw;
width: 100%;
transform: translateX(-100vw);
transition:
transform 200ms ease,
visibility 200ms ease;
overflow: hidden;
padding: 4rem 0 2rem 0;
height: 100dvh;
max-height: 100dvh;
visibility: hidden;
}
&:not(.collapsed) .lucide-menu { #mobile-explorer {
transform: rotate(-90deg); margin: 0;
transition: transform 200ms ease-in-out; padding: 5px;
} z-index: 101;
.lucide-menu { .lucide-menu {
stroke: var(--darkgray); stroke: var(--darkgray);
transition: transform 200ms ease;
&:hover {
stroke: var(--dark);
}
} }
} }
} }

View File

@ -4,8 +4,10 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
&.desktop-only { overflow-y: hidden;
max-height: 40%; flex: 0 1 auto;
&:has(button#toc.collapsed) {
flex: 0 1 1.2rem;
} }
} }
@ -44,22 +46,7 @@ button#toc {
#toc-content { #toc-content {
list-style: none; list-style: none;
overflow: hidden;
overflow-y: auto;
max-height: 100%;
transition:
max-height 0.35s ease,
visibility 0s linear 0s;
position: relative; position: relative;
visibility: visible;
&.collapsed {
max-height: 0;
transition:
max-height 0.35s ease,
visibility 0s linear 0.35s;
visibility: hidden;
}
& ul { & ul {
list-style: none; list-style: none;

View File

@ -543,7 +543,6 @@ video {
div:has(> .overflow) { div:has(> .overflow) {
display: flex; display: flex;
overflow-y: auto;
max-height: 100%; max-height: 100%;
} }