From a8001e9554a319782d8557acb8f19358996b5828 Mon Sep 17 00:00:00 2001
From: Jacky Zhao <j.zhao2k19@gmail.com>
Date: Mon, 10 Mar 2025 22:13:22 +0000
Subject: [PATCH] feat: support non-singleton explorer

---
 quartz/util/resources.tsx                     |    7 +
 quartz/components/Backlinks.tsx               |    7 
 quartz/styles/base.scss                       |    9 -
 quartz/components/styles/explorer.scss        |   26 ++--
 quartz/components/scripts/explorer.inline.ts  |   46 +++----
 quartz/util/random.ts                         |    3 
 quartz/build.ts                               |   11 -
 quartz/plugins/emitters/componentResources.ts |   22 ++-
 quartz/components/Explorer.tsx                |   16 +-
 quartz/components/OverflowList.tsx            |   19 ++
 quartz/components/types.ts                    |    8 
 quartz/components/TableOfContents.tsx         |  133 +++++++++++----------
 quartz/components/styles/toc.scss             |    1 
 quartz/components/pages/TagContent.tsx        |    3 
 quartz/components/pages/FolderContent.tsx     |    3 
 15 files changed, 168 insertions(+), 146 deletions(-)

diff --git a/quartz/build.ts b/quartz/build.ts
index 64c462b..81558f9 100644
--- a/quartz/build.ts
+++ b/quartz/build.ts
@@ -19,6 +19,7 @@
 import { Mutex } from "async-mutex"
 import DepGraph from "./depgraph"
 import { getStaticResourcesFromPlugins } from "./plugins"
+import { randomIdNonSecure } from "./util/random"
 
 type Dependencies = Record<string, DepGraph<FilePath> | null>
 
@@ -38,13 +39,9 @@
 
 type FileEvent = "add" | "change" | "delete"
 
-function newBuildId() {
-  return Math.random().toString(36).substring(2, 8)
-}
-
 async function buildQuartz(argv: Argv, mut: Mutex, clientRefresh: () => void) {
   const ctx: BuildCtx = {
-    buildId: newBuildId(),
+    buildId: randomIdNonSecure(),
     argv,
     cfg,
     allSlugs: [],
@@ -162,7 +159,7 @@
     return
   }
 
-  const buildId = newBuildId()
+  const buildId = randomIdNonSecure()
   ctx.buildId = buildId
   buildData.lastBuildMs = new Date().getTime()
   const release = await mut.acquire()
@@ -359,7 +356,7 @@
     toRemove.add(filePath)
   }
 
-  const buildId = newBuildId()
+  const buildId = randomIdNonSecure()
   ctx.buildId = buildId
   buildData.lastBuildMs = new Date().getTime()
   const release = await mut.acquire()
diff --git a/quartz/components/Backlinks.tsx b/quartz/components/Backlinks.tsx
index 735afe7..0d34457 100644
--- a/quartz/components/Backlinks.tsx
+++ b/quartz/components/Backlinks.tsx
@@ -3,7 +3,7 @@
 import { resolveRelative, simplifySlug } from "../util/path"
 import { i18n } from "../i18n"
 import { classNames } from "../util/lang"
-import OverflowList from "./OverflowList"
+import OverflowListFactory from "./OverflowList"
 
 interface BacklinksOptions {
   hideWhenEmpty: boolean
@@ -15,6 +15,7 @@
 
 export default ((opts?: Partial<BacklinksOptions>) => {
   const options: BacklinksOptions = { ...defaultOptions, ...opts }
+  const { OverflowList, overflowListAfterDOMLoaded } = OverflowListFactory()
 
   const Backlinks: QuartzComponent = ({
     fileData,
@@ -30,7 +31,7 @@
     return (
       <div class={classNames(displayClass, "backlinks")}>
         <h3>{i18n(cfg.locale).components.backlinks.title}</h3>
-        <OverflowList id="backlinks-ul">
+        <OverflowList>
           {backlinkFiles.length > 0 ? (
             backlinkFiles.map((f) => (
               <li>
@@ -48,7 +49,7 @@
   }
 
   Backlinks.css = style
-  Backlinks.afterDOMLoaded = OverflowList.afterDOMLoaded("backlinks-ul")
+  Backlinks.afterDOMLoaded = overflowListAfterDOMLoaded
 
   return Backlinks
 }) satisfies QuartzComponentConstructor
diff --git a/quartz/components/Explorer.tsx b/quartz/components/Explorer.tsx
index 9c6319a..56784f1 100644
--- a/quartz/components/Explorer.tsx
+++ b/quartz/components/Explorer.tsx
@@ -6,7 +6,8 @@
 import { classNames } from "../util/lang"
 import { i18n } from "../i18n"
 import { FileTrieNode } from "../util/fileTrie"
-import OverflowList from "./OverflowList"
+import OverflowListFactory from "./OverflowList"
+import { concatenateResources } from "../util/resources"
 
 type OrderEntries = "sort" | "filter" | "map"
 
@@ -56,6 +57,7 @@
 
 export default ((userOpts?: Partial<Options>) => {
   const opts: Options = { ...defaultOptions, ...userOpts }
+  const { OverflowList, overflowListAfterDOMLoaded } = OverflowListFactory()
 
   const Explorer: QuartzComponent = ({ cfg, displayClass }: QuartzComponentProps) => {
     return (
@@ -73,8 +75,7 @@
       >
         <button
           type="button"
-          id="mobile-explorer"
-          class="explorer-toggle hide-until-loaded"
+          class="explorer-toggle mobile-explorer hide-until-loaded"
           data-mobile={true}
           aria-controls="explorer-content"
         >
@@ -95,8 +96,7 @@
         </button>
         <button
           type="button"
-          id="desktop-explorer"
-          class="title-button explorer-toggle"
+          class="title-button explorer-toggle desktop-explorer"
           data-mobile={false}
           aria-expanded={true}
         >
@@ -116,8 +116,8 @@
             <polyline points="6 9 12 15 18 9"></polyline>
           </svg>
         </button>
-        <div id="explorer-content" aria-expanded={false}>
-          <OverflowList id="explorer-ul" />
+        <div class="explorer-content" aria-expanded={false}>
+          <OverflowList class="explorer-ul" />
         </div>
         <template id="template-file">
           <li>
@@ -157,6 +157,6 @@
   }
 
   Explorer.css = style
-  Explorer.afterDOMLoaded = script + OverflowList.afterDOMLoaded("explorer-ul")
+  Explorer.afterDOMLoaded = concatenateResources(script, overflowListAfterDOMLoaded)
   return Explorer
 }) satisfies QuartzComponentConstructor
diff --git a/quartz/components/OverflowList.tsx b/quartz/components/OverflowList.tsx
index d74c5c2..18aa9a6 100644
--- a/quartz/components/OverflowList.tsx
+++ b/quartz/components/OverflowList.tsx
@@ -1,22 +1,31 @@
 import { JSX } from "preact"
+import { randomIdNonSecure } from "../util/random"
 
 const OverflowList = ({
   children,
   ...props
 }: JSX.HTMLAttributes<HTMLUListElement> & { id: string }) => {
   return (
-    <ul class="overflow" {...props}>
+    <ul {...props} class={[props.class, "overflow"].filter(Boolean).join(" ")} id={props.id}>
       {children}
       <li class="overflow-end" />
     </ul>
   )
 }
 
-OverflowList.afterDOMLoaded = (id: string) => `
+export default () => {
+  const id = randomIdNonSecure()
+
+  return {
+    OverflowList: (props: JSX.HTMLAttributes<HTMLUListElement>) => (
+      <OverflowList {...props} id={id} />
+    ),
+    overflowListAfterDOMLoaded: `
 document.addEventListener("nav", (e) => {
   const observer = new IntersectionObserver((entries) => {
     for (const entry of entries) {
       const parentUl = entry.target.parentElement
+      if (!parentUl) return
       if (entry.isIntersecting) {
         parentUl.classList.remove("gradient-active")
       } else {
@@ -34,6 +43,6 @@
   observer.observe(end)
   window.addCleanup(() => observer.disconnect())
 })
-`
-
-export default OverflowList
+`,
+  }
+}
diff --git a/quartz/components/TableOfContents.tsx b/quartz/components/TableOfContents.tsx
index da6eece..d376200 100644
--- a/quartz/components/TableOfContents.tsx
+++ b/quartz/components/TableOfContents.tsx
@@ -6,7 +6,8 @@
 // @ts-ignore
 import script from "./scripts/toc.inline"
 import { i18n } from "../i18n"
-import OverflowList from "./OverflowList"
+import OverflowListFactory from "./OverflowList"
+import { concatenateResources } from "../util/resources"
 
 interface Options {
   layout: "modern" | "legacy"
@@ -16,41 +17,70 @@
   layout: "modern",
 }
 
-const TableOfContents: QuartzComponent = ({
-  fileData,
-  displayClass,
-  cfg,
-}: QuartzComponentProps) => {
-  if (!fileData.toc) {
-    return null
+export default ((opts?: Partial<Options>) => {
+  const layout = opts?.layout ?? defaultOptions.layout
+  const { OverflowList, overflowListAfterDOMLoaded } = OverflowListFactory()
+  const TableOfContents: QuartzComponent = ({
+    fileData,
+    displayClass,
+    cfg,
+  }: QuartzComponentProps) => {
+    if (!fileData.toc) {
+      return null
+    }
+
+    return (
+      <div class={classNames(displayClass, "toc")}>
+        <button
+          type="button"
+          class={fileData.collapseToc ? "collapsed toc-header" : "toc-header"}
+          aria-controls="toc-content"
+          aria-expanded={!fileData.collapseToc}
+        >
+          <h3>{i18n(cfg.locale).components.tableOfContents.title}</h3>
+          <svg
+            xmlns="http://www.w3.org/2000/svg"
+            width="24"
+            height="24"
+            viewBox="0 0 24 24"
+            fill="none"
+            stroke="currentColor"
+            stroke-width="2"
+            stroke-linecap="round"
+            stroke-linejoin="round"
+            class="fold"
+          >
+            <polyline points="6 9 12 15 18 9"></polyline>
+          </svg>
+        </button>
+        <div class={fileData.collapseToc ? "collapsed toc-content" : "toc-content"}>
+          <OverflowList>
+            {fileData.toc.map((tocEntry) => (
+              <li key={tocEntry.slug} class={`depth-${tocEntry.depth}`}>
+                <a href={`#${tocEntry.slug}`} data-for={tocEntry.slug}>
+                  {tocEntry.text}
+                </a>
+              </li>
+            ))}
+          </OverflowList>
+        </div>
+      </div>
+    )
   }
 
-  return (
-    <div class={classNames(displayClass, "toc")}>
-      <button
-        type="button"
-        class={fileData.collapseToc ? "collapsed toc-header" : "toc-header"}
-        aria-controls="toc-content"
-        aria-expanded={!fileData.collapseToc}
-      >
-        <h3>{i18n(cfg.locale).components.tableOfContents.title}</h3>
-        <svg
-          xmlns="http://www.w3.org/2000/svg"
-          width="24"
-          height="24"
-          viewBox="0 0 24 24"
-          fill="none"
-          stroke="currentColor"
-          stroke-width="2"
-          stroke-linecap="round"
-          stroke-linejoin="round"
-          class="fold"
-        >
-          <polyline points="6 9 12 15 18 9"></polyline>
-        </svg>
-      </button>
-      <div class={fileData.collapseToc ? "collapsed toc-content" : "toc-content"}>
-        <OverflowList id="toc-ul">
+  TableOfContents.css = modernStyle
+  TableOfContents.afterDOMLoaded = concatenateResources(script, overflowListAfterDOMLoaded)
+
+  const LegacyTableOfContents: QuartzComponent = ({ fileData, cfg }: QuartzComponentProps) => {
+    if (!fileData.toc) {
+      return null
+    }
+    return (
+      <details class="toc" open={!fileData.collapseToc}>
+        <summary>
+          <h3>{i18n(cfg.locale).components.tableOfContents.title}</h3>
+        </summary>
+        <ul>
           {fileData.toc.map((tocEntry) => (
             <li key={tocEntry.slug} class={`depth-${tocEntry.depth}`}>
               <a href={`#${tocEntry.slug}`} data-for={tocEntry.slug}>
@@ -58,38 +88,11 @@
               </a>
             </li>
           ))}
-        </OverflowList>
-      </div>
-    </div>
-  )
-}
-TableOfContents.css = modernStyle
-TableOfContents.afterDOMLoaded = script + OverflowList.afterDOMLoaded("toc-ul")
-
-const LegacyTableOfContents: QuartzComponent = ({ fileData, cfg }: QuartzComponentProps) => {
-  if (!fileData.toc) {
-    return null
+        </ul>
+      </details>
+    )
   }
-  return (
-    <details class="toc" open={!fileData.collapseToc}>
-      <summary>
-        <h3>{i18n(cfg.locale).components.tableOfContents.title}</h3>
-      </summary>
-      <ul>
-        {fileData.toc.map((tocEntry) => (
-          <li key={tocEntry.slug} class={`depth-${tocEntry.depth}`}>
-            <a href={`#${tocEntry.slug}`} data-for={tocEntry.slug}>
-              {tocEntry.text}
-            </a>
-          </li>
-        ))}
-      </ul>
-    </details>
-  )
-}
-LegacyTableOfContents.css = legacyStyle
+  LegacyTableOfContents.css = legacyStyle
 
-export default ((opts?: Partial<Options>) => {
-  const layout = opts?.layout ?? defaultOptions.layout
   return layout === "modern" ? TableOfContents : LegacyTableOfContents
 }) satisfies QuartzComponentConstructor
diff --git a/quartz/components/pages/FolderContent.tsx b/quartz/components/pages/FolderContent.tsx
index 977da5e..2a727c0 100644
--- a/quartz/components/pages/FolderContent.tsx
+++ b/quartz/components/pages/FolderContent.tsx
@@ -9,6 +9,7 @@
 import { i18n } from "../../i18n"
 import { QuartzPluginData } from "../../plugins/vfile"
 import { ComponentChildren } from "preact"
+import { concatenateResources } from "../../util/resources"
 
 interface FolderContentOptions {
   /**
@@ -104,6 +105,6 @@
     )
   }
 
-  FolderContent.css = style + PageList.css
+  FolderContent.css = concatenateResources(style, PageList.css)
   return FolderContent
 }) satisfies QuartzComponentConstructor
diff --git a/quartz/components/pages/TagContent.tsx b/quartz/components/pages/TagContent.tsx
index 087daf1..03051d3 100644
--- a/quartz/components/pages/TagContent.tsx
+++ b/quartz/components/pages/TagContent.tsx
@@ -7,6 +7,7 @@
 import { htmlToJsx } from "../../util/jsx"
 import { i18n } from "../../i18n"
 import { ComponentChildren } from "preact"
+import { concatenateResources } from "../../util/resources"
 
 interface TagContentOptions {
   sort?: SortFn
@@ -124,6 +125,6 @@
     }
   }
 
-  TagContent.css = style + PageList.css
+  TagContent.css = concatenateResources(style, PageList.css)
   return TagContent
 }) satisfies QuartzComponentConstructor
diff --git a/quartz/components/scripts/explorer.inline.ts b/quartz/components/scripts/explorer.inline.ts
index 68a20bb..9e41af9 100644
--- a/quartz/components/scripts/explorer.inline.ts
+++ b/quartz/components/scripts/explorer.inline.ts
@@ -21,14 +21,13 @@
 
 let currentExplorerState: Array<FolderState>
 function toggleExplorer(this: HTMLElement) {
-  const explorers = document.querySelectorAll(".explorer")
-  for (const explorer of explorers) {
-    explorer.classList.toggle("collapsed")
-    explorer.setAttribute(
-      "aria-expanded",
-      explorer.getAttribute("aria-expanded") === "true" ? "false" : "true",
-    )
-  }
+  const nearestExplorer = this.closest(".explorer") as HTMLElement
+  if (!nearestExplorer) return
+  nearestExplorer.classList.toggle("collapsed")
+  nearestExplorer.setAttribute(
+    "aria-expanded",
+    nearestExplorer.getAttribute("aria-expanded") === "true" ? "false" : "true",
+  )
 }
 
 function toggleFolder(evt: MouseEvent) {
@@ -145,7 +144,7 @@
 }
 
 async function setupExplorer(currentSlug: FullSlug) {
-  const allExplorers = document.querySelectorAll(".explorer") as NodeListOf<HTMLElement>
+  const allExplorers = document.querySelectorAll("div.explorer") as NodeListOf<HTMLElement>
 
   for (const explorer of allExplorers) {
     const dataFns = JSON.parse(explorer.dataset.dataFns || "{}")
@@ -192,7 +191,7 @@
       collapsed: oldIndex.get(path) === true,
     }))
 
-    const explorerUl = document.getElementById("explorer-ul")
+    const explorerUl = explorer.querySelector(".explorer-ul")
     if (!explorerUl) continue
 
     // Create and insert new content
@@ -219,14 +218,12 @@
     }
 
     // Set up event handlers
-    const explorerButtons = explorer.querySelectorAll(
-      "button.explorer-toggle",
-    ) as NodeListOf<HTMLElement>
-    if (explorerButtons) {
-      window.addCleanup(() =>
-        explorerButtons.forEach((button) => button.removeEventListener("click", toggleExplorer)),
-      )
-      explorerButtons.forEach((button) => button.addEventListener("click", toggleExplorer))
+    const explorerButtons = explorer.getElementsByClassName(
+      "explorer-toggle",
+    ) as HTMLCollectionOf<HTMLElement>
+    for (const button of explorerButtons) {
+      button.addEventListener("click", toggleExplorer)
+      window.addCleanup(() => button.removeEventListener("click", toggleExplorer))
     }
 
     // Set up folder click handlers
@@ -235,8 +232,8 @@
         "folder-button",
       ) as HTMLCollectionOf<HTMLElement>
       for (const button of folderButtons) {
-        window.addCleanup(() => button.removeEventListener("click", toggleFolder))
         button.addEventListener("click", toggleFolder)
+        window.addCleanup(() => button.removeEventListener("click", toggleFolder))
       }
     }
 
@@ -244,15 +241,15 @@
       "folder-icon",
     ) as HTMLCollectionOf<HTMLElement>
     for (const icon of folderIcons) {
-      window.addCleanup(() => icon.removeEventListener("click", toggleFolder))
       icon.addEventListener("click", toggleFolder)
+      window.addCleanup(() => icon.removeEventListener("click", toggleFolder))
     }
   }
 }
 
-document.addEventListener("prenav", async (e: CustomEventMap["prenav"]) => {
+document.addEventListener("prenav", async () => {
   // save explorer scrollTop position
-  const explorer = document.getElementById("explorer-ul")
+  const explorer = document.querySelector(".explorer-ul")
   if (!explorer) return
   sessionStorage.setItem("explorerScrollTop", explorer.scrollTop.toString())
 })
@@ -262,9 +259,8 @@
   await setupExplorer(currentSlug)
 
   // 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")) {
+  for (const explorer of document.getElementsByClassName("mobile-explorer")) {
+    if (explorer.checkVisibility()) {
       explorer.classList.add("collapsed")
       explorer.setAttribute("aria-expanded", "false")
     }
diff --git a/quartz/components/styles/explorer.scss b/quartz/components/styles/explorer.scss
index b769726..c284cb2 100644
--- a/quartz/components/styles/explorer.scss
+++ b/quartz/components/styles/explorer.scss
@@ -20,7 +20,7 @@
       margin: 0;
     }
 
-    .hide-until-loaded ~ #explorer-content {
+    .hide-until-loaded ~ .explorer-content {
       display: none;
     }
   }
@@ -30,6 +30,8 @@
   display: flex;
   flex-direction: column;
   overflow-y: hidden;
+
+  min-height: 1.2rem;
   flex: 0 1 auto;
   &.collapsed {
     flex: 0 1 1.2rem;
@@ -52,20 +54,20 @@
     align-self: flex-start;
   }
 
-  button#mobile-explorer {
+  button.mobile-explorer {
     display: none;
   }
 
-  button#desktop-explorer {
+  button.desktop-explorer {
     display: flex;
   }
 
   @media all and ($mobile) {
-    button#mobile-explorer {
+    button.mobile-explorer {
       display: flex;
     }
 
-    button#desktop-explorer {
+    button.desktop-explorer {
       display: none;
     }
   }
@@ -86,8 +88,8 @@
   }
 }
 
-button#mobile-explorer,
-button#desktop-explorer {
+button.mobile-explorer,
+button.desktop-explorer {
   background-color: transparent;
   border: none;
   text-align: left;
@@ -104,7 +106,7 @@
   }
 }
 
-#explorer-content {
+.explorer-content {
   list-style: none;
   overflow: hidden;
   overflow-y: auto;
@@ -209,7 +211,7 @@
     &.collapsed {
       flex: 0 0 34px;
 
-      & > #explorer-content {
+      & > .explorer-content {
         transform: translateX(-100vw);
         visibility: hidden;
       }
@@ -218,13 +220,13 @@
     &:not(.collapsed) {
       flex: 0 0 34px;
 
-      & > #explorer-content {
+      & > .explorer-content {
         transform: translateX(0);
         visibility: visible;
       }
     }
 
-    #explorer-content {
+    .explorer-content {
       box-sizing: border-box;
       z-index: 100;
       position: absolute;
@@ -245,7 +247,7 @@
       visibility: hidden;
     }
 
-    #mobile-explorer {
+    .mobile-explorer {
       margin: 0;
       padding: 5px;
       z-index: 101;
diff --git a/quartz/components/styles/toc.scss b/quartz/components/styles/toc.scss
index 42aa35c..bc0c749 100644
--- a/quartz/components/styles/toc.scss
+++ b/quartz/components/styles/toc.scss
@@ -5,6 +5,7 @@
   flex-direction: column;
 
   overflow-y: hidden;
+  min-height: 4rem;
   flex: 0 1 auto;
   &:has(button.toc-header.collapsed) {
     flex: 0 1 1.2rem;
diff --git a/quartz/components/types.ts b/quartz/components/types.ts
index a6b90d3..a07601a 100644
--- a/quartz/components/types.ts
+++ b/quartz/components/types.ts
@@ -1,5 +1,5 @@
 import { ComponentType, JSX } from "preact"
-import { StaticResources } from "../util/resources"
+import { StaticResources, StringResource } from "../util/resources"
 import { QuartzPluginData } from "../plugins/vfile"
 import { GlobalConfiguration } from "../cfg"
 import { Node } from "hast"
@@ -19,9 +19,9 @@
   }
 
 export type QuartzComponent = ComponentType<QuartzComponentProps> & {
-  css?: string
-  beforeDOMLoaded?: string
-  afterDOMLoaded?: string
+  css?: StringResource
+  beforeDOMLoaded?: StringResource
+  afterDOMLoaded?: StringResource
 }
 
 export type QuartzComponentConstructor<Options extends object | undefined = undefined> = (
diff --git a/quartz/plugins/emitters/componentResources.ts b/quartz/plugins/emitters/componentResources.ts
index 6c1e3d0..7584fdd 100644
--- a/quartz/plugins/emitters/componentResources.ts
+++ b/quartz/plugins/emitters/componentResources.ts
@@ -36,17 +36,21 @@
     afterDOMLoaded: new Set<string>(),
   }
 
+  function normalizeResource(resource: string | string[] | undefined): string[] {
+    if (!resource) return []
+    if (Array.isArray(resource)) return resource
+    return [resource]
+  }
+
   for (const component of allComponents) {
     const { css, beforeDOMLoaded, afterDOMLoaded } = component
-    if (css) {
-      componentResources.css.add(css)
-    }
-    if (beforeDOMLoaded) {
-      componentResources.beforeDOMLoaded.add(beforeDOMLoaded)
-    }
-    if (afterDOMLoaded) {
-      componentResources.afterDOMLoaded.add(afterDOMLoaded)
-    }
+    const normalizedCss = normalizeResource(css)
+    const normalizedBeforeDOMLoaded = normalizeResource(beforeDOMLoaded)
+    const normalizedAfterDOMLoaded = normalizeResource(afterDOMLoaded)
+
+    normalizedCss.forEach((c) => componentResources.css.add(c))
+    normalizedBeforeDOMLoaded.forEach((b) => componentResources.beforeDOMLoaded.add(b))
+    normalizedAfterDOMLoaded.forEach((a) => componentResources.afterDOMLoaded.add(a))
   }
 
   return {
diff --git a/quartz/styles/base.scss b/quartz/styles/base.scss
index 9d9d638..25de448 100644
--- a/quartz/styles/base.scss
+++ b/quartz/styles/base.scss
@@ -542,7 +542,7 @@
 }
 
 .spacer {
-  flex: 1 1 auto;
+  flex: 2 1 auto;
 }
 
 div:has(> .overflow) {
@@ -555,17 +555,14 @@
   max-height: 100%;
   overflow-y: auto;
   width: 100%;
+  margin-bottom: 0;
 
   // clearfix
   content: "";
   clear: both;
 
-  & > li:last-of-type {
-    margin-bottom: 30px;
-  }
-
   & > li.overflow-end {
-    height: 4px;
+    height: 1rem;
     margin: 0;
   }
 
diff --git a/quartz/util/random.ts b/quartz/util/random.ts
new file mode 100644
index 0000000..df18022
--- /dev/null
+++ b/quartz/util/random.ts
@@ -0,0 +1,3 @@
+export function randomIdNonSecure() {
+  return Math.random().toString(36).substring(2, 8)
+}
diff --git a/quartz/util/resources.tsx b/quartz/util/resources.tsx
index 2ec8561..d95333e 100644
--- a/quartz/util/resources.tsx
+++ b/quartz/util/resources.tsx
@@ -65,3 +65,10 @@
   js: JSResource[]
   additionalHead: (JSX.Element | ((pageData: QuartzPluginData) => JSX.Element))[]
 }
+
+export type StringResource = string | string[] | undefined
+export function concatenateResources(...resources: StringResource[]): StringResource {
+  return resources
+    .filter((resource): resource is string | string[] => resource !== undefined)
+    .flat()
+}

--
Gitblit v1.10.0