Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions source/quartz.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const config: QuartzConfig = {
pageTitleSuffix: "",
enableSPA: true,
enablePopovers: true,
enableMobileSidebar: true,
analytics: {
provider: "plausible",
},
Expand Down
4 changes: 3 additions & 1 deletion source/quartz.layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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: [
Expand All @@ -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: [],
Expand Down
2 changes: 2 additions & 0 deletions source/quartz/cfg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down
82 changes: 82 additions & 0 deletions source/quartz/components/MobileSidebar.tsx
Original file line number Diff line number Diff line change
@@ -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<MobileSidebarOptions>) => {
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 (
<div class={classNames(displayClass, "mobile-sidebar-container")} id="mobile-sidebar-container">
<div class="mobile-sidebar-backdrop" id="mobile-sidebar-backdrop"></div>
<aside class="mobile-sidebar" id="mobile-sidebar" aria-label="Mobile navigation">
<div class="mobile-sidebar-header">
<h2>Navigation</h2>
<button
type="button"
class="mobile-sidebar-close"
id="mobile-sidebar-close"
aria-label="Close sidebar"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
<div class="mobile-sidebar-content">
<ExplorerComponent {...props} displayClass={undefined} />
</div>
</aside>
</div>
)
}

MobileSidebar.css = style
MobileSidebar.afterDOMLoaded = script
return MobileSidebar
}) satisfies QuartzComponentConstructor
80 changes: 80 additions & 0 deletions source/quartz/components/MobileSidebarToggle.tsx
Original file line number Diff line number Diff line change
@@ -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<MobileSidebarToggleOptions>) => {
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 (
<button
type="button"
class={classNames(displayClass, "mobile-sidebar-toggle")}
id="mobile-sidebar-toggle"
aria-label="Open navigation menu"
>
<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"
>
<line x1="3" y1="12" x2="21" y2="12"></line>
<line x1="3" y1="6" x2="21" y2="6"></line>
<line x1="3" y1="18" x2="21" y2="18"></line>
</svg>
</button>
)
}

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
4 changes: 4 additions & 0 deletions source/quartz/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -44,4 +46,6 @@ export {
NotFound,
Breadcrumbs,
Comments,
MobileSidebar,
MobileSidebarToggle,
}
59 changes: 59 additions & 0 deletions source/quartz/components/scripts/mobileSidebar.inline.ts
Original file line number Diff line number Diff line change
@@ -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 = ""
})
102 changes: 102 additions & 0 deletions source/quartz/components/styles/mobileSidebar.scss
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
}