From af65a589ac6e004c8a6c021e620a24f63dd08cff Mon Sep 17 00:00:00 2001 From: "Bontinck L." <85873312+MoutonDemocrate@users.noreply.github.com> Date: Wed, 28 Jan 2026 02:42:20 +0100 Subject: [PATCH] Added a sidebar --- source/quartz.config.ts | 1 + source/quartz.layout.ts | 4 +- source/quartz/cfg.ts | 2 + source/quartz/components/MobileSidebar.tsx | 82 ++++++++++++++ .../quartz/components/MobileSidebarToggle.tsx | 80 ++++++++++++++ source/quartz/components/index.ts | 4 + .../scripts/mobileSidebar.inline.ts | 59 ++++++++++ .../components/styles/mobileSidebar.scss | 102 ++++++++++++++++++ 8 files changed, 333 insertions(+), 1 deletion(-) create mode 100644 source/quartz/components/MobileSidebar.tsx create mode 100644 source/quartz/components/MobileSidebarToggle.tsx create mode 100644 source/quartz/components/scripts/mobileSidebar.inline.ts create mode 100644 source/quartz/components/styles/mobileSidebar.scss diff --git a/source/quartz.config.ts b/source/quartz.config.ts index e96ee4843f..9ae80ffb08 100644 --- a/source/quartz.config.ts +++ b/source/quartz.config.ts @@ -12,6 +12,7 @@ const config: QuartzConfig = { pageTitleSuffix: "", enableSPA: true, enablePopovers: true, + enableMobileSidebar: true, analytics: { provider: "plausible", }, diff --git a/source/quartz.layout.ts b/source/quartz.layout.ts index 4a78256aab..019d91288a 100644 --- a/source/quartz.layout.ts +++ b/source/quartz.layout.ts @@ -5,7 +5,7 @@ import * as Component from "./quartz/components" export const sharedPageComponents: SharedLayout = { head: Component.Head(), header: [], - afterBody: [], + afterBody: [Component.MobileOnly(Component.MobileSidebar())], footer: Component.Footer({ links: { GitHub: "https://github.com/jackyzha0/quartz", @@ -27,6 +27,7 @@ export const defaultContentPageLayout: PageLayout = { Component.MobileOnly(Component.Spacer()), Component.Search(), Component.Darkmode(), + Component.MobileOnly(Component.MobileSidebarToggle()), Component.DesktopOnly(Component.Explorer()), ], right: [ @@ -44,6 +45,7 @@ export const defaultListPageLayout: PageLayout = { Component.MobileOnly(Component.Spacer()), Component.Search(), Component.Darkmode(), + Component.MobileOnly(Component.MobileSidebarToggle()), Component.DesktopOnly(Component.Explorer()), ], right: [], diff --git a/source/quartz/cfg.ts b/source/quartz/cfg.ts index 85527a093c..4fa0def22f 100644 --- a/source/quartz/cfg.ts +++ b/source/quartz/cfg.ts @@ -50,6 +50,8 @@ export interface GlobalConfiguration { enableSPA: boolean /** Whether to display Wikipedia-style popovers when hovering over links */ enablePopovers: boolean + /** Whether to enable the mobile sidebar navigation menu */ + enableMobileSidebar: boolean /** Analytics mode */ analytics: Analytics /** Glob patterns to not search */ diff --git a/source/quartz/components/MobileSidebar.tsx b/source/quartz/components/MobileSidebar.tsx new file mode 100644 index 0000000000..56fc1a1f84 --- /dev/null +++ b/source/quartz/components/MobileSidebar.tsx @@ -0,0 +1,82 @@ +import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" +import { QuartzPluginData } from "../plugins/vfile" +import { classNames } from "../util/lang" + +// @ts-ignore +import script from "./scripts/mobileSidebar.inline" +import style from "./styles/mobileSidebar.scss" + +import Explorer from "./Explorer" + +export interface MobileSidebarOptions { + /** + * Whether to enable the mobile sidebar feature. + * When disabled, the mobile menu button and sidebar will not be rendered. + * @default true + */ + enabled: boolean +} + +const defaultOptions: MobileSidebarOptions = { + enabled: true, +} + +export default ((userOpts?: Partial) => { + const opts = { ...defaultOptions, ...userOpts } + + const explorerOpts = typeof userOpts === "object" && "explorerOpts" in userOpts + ? (userOpts as any).explorerOpts + : undefined + const ExplorerComponent = Explorer(explorerOpts) + + const MobileSidebar: QuartzComponent = (props: QuartzComponentProps) => { + const { displayClass, cfg } = props + + // Check global config first, then fall back to component options + const enabled = cfg.enableMobileSidebar ?? opts.enabled + + // If disabled, return null + if (!enabled) { + return null + } + + return ( +
+
+ +
+ ) + } + + MobileSidebar.css = style + MobileSidebar.afterDOMLoaded = script + return MobileSidebar +}) satisfies QuartzComponentConstructor diff --git a/source/quartz/components/MobileSidebarToggle.tsx b/source/quartz/components/MobileSidebarToggle.tsx new file mode 100644 index 0000000000..cdb5022e06 --- /dev/null +++ b/source/quartz/components/MobileSidebarToggle.tsx @@ -0,0 +1,80 @@ +import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" +import { classNames } from "../util/lang" + +export interface MobileSidebarToggleOptions { + /** + * Whether to enable the mobile sidebar toggle button. + * When disabled, the hamburger menu button will not be rendered. + * @default true + */ + enabled: boolean +} + +const defaultOptions: MobileSidebarToggleOptions = { + enabled: true, +} + +export default ((userOpts?: Partial) => { + const opts = { ...defaultOptions, ...userOpts } + + const MobileSidebarToggle: QuartzComponent = ({ displayClass, cfg }: QuartzComponentProps) => { + // Check global config first, then fall back to component options + const enabled = cfg.enableMobileSidebar ?? opts.enabled + + // If disabled, return null + if (!enabled) { + return null + } + + return ( + + ) + } + + MobileSidebarToggle.css = ` +.mobile-sidebar-toggle { + background: none; + border: none; + cursor: pointer; + padding: 0.5rem; + display: flex; + align-items: center; + justify-content: center; + color: var(--darkgray); + border-radius: 5px; + transition: background-color 0.2s ease; + + &:hover { + background-color: var(--lightgray); + } + + svg { + width: 24px; + height: 24px; + } +} +` + + return MobileSidebarToggle +}) satisfies QuartzComponentConstructor diff --git a/source/quartz/components/index.ts b/source/quartz/components/index.ts index 5b197941c0..ce52fa8a7e 100644 --- a/source/quartz/components/index.ts +++ b/source/quartz/components/index.ts @@ -20,6 +20,8 @@ import MobileOnly from "./MobileOnly" import RecentNotes from "./RecentNotes" import Breadcrumbs from "./Breadcrumbs" import Comments from "./Comments" +import MobileSidebar from "./MobileSidebar" +import MobileSidebarToggle from "./MobileSidebarToggle" export { ArticleTitle, @@ -44,4 +46,6 @@ export { NotFound, Breadcrumbs, Comments, + MobileSidebar, + MobileSidebarToggle, } diff --git a/source/quartz/components/scripts/mobileSidebar.inline.ts b/source/quartz/components/scripts/mobileSidebar.inline.ts new file mode 100644 index 0000000000..1442430774 --- /dev/null +++ b/source/quartz/components/scripts/mobileSidebar.inline.ts @@ -0,0 +1,59 @@ +const sidebar = document.getElementById("mobile-sidebar") +const backdrop = document.getElementById("mobile-sidebar-backdrop") +const closeButton = document.getElementById("mobile-sidebar-close") +const menuButton = document.getElementById("mobile-sidebar-toggle") + +function openSidebar() { + sidebar?.classList.add("active") + backdrop?.classList.add("active") + document.body.style.overflow = "hidden" // Prevent scrolling when sidebar is open +} + +function closeSidebar() { + sidebar?.classList.remove("active") + backdrop?.classList.remove("active") + document.body.style.overflow = "" // Restore scrolling +} + +// Open sidebar when menu button is clicked +menuButton?.addEventListener("click", (e) => { + e.preventDefault() + e.stopPropagation() + openSidebar() +}) + +// Close sidebar when close button is clicked +closeButton?.addEventListener("click", (e) => { + e.preventDefault() + e.stopPropagation() + closeSidebar() +}) + +// Close sidebar when backdrop is clicked +backdrop?.addEventListener("click", () => { + closeSidebar() +}) + +// Close sidebar when clicking links inside it +sidebar?.querySelectorAll("a").forEach((link) => { + link.addEventListener("click", () => { + closeSidebar() + }) +}) + +// Handle navigation events (SPA routing) +document.addEventListener("nav", () => { + closeSidebar() + + // Re-attach click listeners to links after navigation + sidebar?.querySelectorAll("a").forEach((link) => { + link.addEventListener("click", () => { + closeSidebar() + }) + }) +}) + +// Clean up on page unload +window.addCleanup?.(() => { + document.body.style.overflow = "" +}) diff --git a/source/quartz/components/styles/mobileSidebar.scss b/source/quartz/components/styles/mobileSidebar.scss new file mode 100644 index 0000000000..3cb9452016 --- /dev/null +++ b/source/quartz/components/styles/mobileSidebar.scss @@ -0,0 +1,102 @@ +@use "../../styles/variables.scss" as *; + +.mobile-sidebar-container { + display: none; + + @media all and ($mobile) { + display: block; + } +} + +.mobile-sidebar-backdrop { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.5); + z-index: 998; + opacity: 0; + visibility: hidden; + transition: opacity 0.3s ease, visibility 0.3s ease; + + &.active { + opacity: 1; + visibility: visible; + } +} + +.mobile-sidebar { + position: fixed; + top: 0; + left: -100%; + width: 80%; + max-width: 300px; + height: 100vh; + background-color: var(--light); + z-index: 999; + overflow-y: auto; + transition: left 0.3s ease; + box-shadow: 2px 0 8px rgba(0, 0, 0, 0.15); + + &.active { + left: 0; + } + + .mobile-sidebar-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1rem; + border-bottom: 1px solid var(--lightgray); + position: sticky; + top: 0; + background-color: var(--light); + z-index: 10; + + h2 { + margin: 0; + font-size: 1.25rem; + color: var(--darkgray); + } + + .mobile-sidebar-close { + background: none; + border: none; + cursor: pointer; + padding: 0.5rem; + display: flex; + align-items: center; + justify-content: center; + color: var(--darkgray); + border-radius: 5px; + transition: background-color 0.2s ease; + + &:hover { + background-color: var(--lightgray); + } + + svg { + width: 20px; + height: 20px; + } + } + } + + .mobile-sidebar-content { + padding: 1rem; + + .explorer { + // Remove the collapse button since we're always showing the content + #explorer { + display: none; + } + + #explorer-content { + max-height: none !important; + // Make the content always visible + display: block !important; + } + } + } +}