This commit is contained in:
semanticdata 2024-09-02 15:34:11 -05:00
commit 0acf449503
14 changed files with 7210 additions and 142 deletions

View File

@ -29,6 +29,7 @@ Some common frontmatter fields that are natively supported by Quartz:
- `title`: Title of the page. If it isn't provided, Quartz will use the name of the file as the title. - `title`: Title of the page. If it isn't provided, Quartz will use the name of the file as the title.
- `description`: Description of the page used for link previews. - `description`: Description of the page used for link previews.
- `permalink`: A custom URL for the page that will remain constant even if the path to the file changes.
- `aliases`: Other names for this note. This is a list of strings. - `aliases`: Other names for this note. This is a list of strings.
- `tags`: Tags for this note. - `tags`: Tags for this note.
- `draft`: Whether to publish the page or not. This is one way to make [[private pages|pages private]] in Quartz. - `draft`: Whether to publish the page or not. This is one way to make [[private pages|pages private]] in Quartz.

View File

@ -6,7 +6,6 @@ draft: true
- static dead link detection - static dead link detection
- cursor chat extension - cursor chat extension
- https://giscus.app/ extension
- sidenotes? https://github.com/capnfabs/paperesque - sidenotes? https://github.com/capnfabs/paperesque
- direct match in search using double quotes - direct match in search using double quotes
- https://help.obsidian.md/Advanced+topics/Using+Obsidian+URI - https://help.obsidian.md/Advanced+topics/Using+Obsidian+URI

View File

@ -201,7 +201,7 @@ build:
paths: paths:
- public - public
tags: tags:
- docker - gitlab-org-docker
pages: pages:
stage: deploy stage: deploy

View File

@ -26,5 +26,7 @@ Want to see what Quartz can do? Here are some cool community gardens:
- [Gatekeeper Wiki](https://www.gatekeeper.wiki) - [Gatekeeper Wiki](https://www.gatekeeper.wiki)
- [Ellie's Notes](https://ellie.wtf) - [Ellie's Notes](https://ellie.wtf)
- [🥷🏻🌳🍃 Computer Science & Thinkering Garden](https://notes.yxy.ninja) - [🥷🏻🌳🍃 Computer Science & Thinkering Garden](https://notes.yxy.ninja)
- [Eledah's Crystalline](https://blog.eledah.ir/)
- [🌓 Projects & Privacy - FOSS, tech, law](https://be-far.com)
If you want to see your own on here, submit a [Pull Request adding yourself to this file](https://github.com/jackyzha0/quartz/blob/v4/docs/showcase.md)! If you want to see your own on here, submit a [Pull Request adding yourself to this file](https://github.com/jackyzha0/quartz/blob/v4/docs/showcase.md)!

6900
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -2,7 +2,7 @@
"name": "@jackyzha0/quartz", "name": "@jackyzha0/quartz",
"description": "🌱 publish your digital garden and notes as a website", "description": "🌱 publish your digital garden and notes as a website",
"private": true, "private": true,
"version": "4.3.0", "version": "4.3.1",
"type": "module", "type": "module",
"author": "jackyzha0 <j.zhao2k19@gmail.com>", "author": "jackyzha0 <j.zhao2k19@gmail.com>",
"license": "MIT", "license": "MIT",
@ -68,6 +68,7 @@
"mdast-util-to-hast": "^13.2.0", "mdast-util-to-hast": "^13.2.0",
"mdast-util-to-string": "^4.0.0", "mdast-util-to-string": "^4.0.0",
"micromorph": "^0.4.5", "micromorph": "^0.4.5",
"pixi.js": "^8.3.3",
"preact": "^10.23.2", "preact": "^10.23.2",
"preact-render-to-string": "^6.5.8", "preact-render-to-string": "^6.5.8",
"prettier": "^3.3.3", "prettier": "^3.3.3",

View File

@ -52,11 +52,13 @@ export const PageList: QuartzComponent = ({
return ( return (
<li class="section-li"> <li class="section-li">
<div class="section"> <div class="section">
<div>
{page.dates && ( {page.dates && (
<p class="meta"> <p class="meta">
<Date date={getDate(cfg, page)!} locale={cfg.locale} /> <Date date={getDate(cfg, page)!} locale={cfg.locale} />
</p> </p>
)} )}
</div>
<div class="desc"> <div class="desc">
<h3> <h3>
<a <a

View File

@ -51,7 +51,7 @@ const TableOfContents: QuartzComponent = ({
<polyline points="6 9 12 15 18 9"></polyline> <polyline points="6 9 12 15 18 9"></polyline>
</svg> </svg>
</button> </button>
<div id="toc-content"> <div id="toc-content" class={fileData.collapseToc ? "collapsed" : ""}>
<ul class="overflow"> <ul class="overflow">
{fileData.toc.map((tocEntry) => ( {fileData.toc.map((tocEntry) => (
<li key={tocEntry.slug} class={`depth-${tocEntry.depth}`}> <li key={tocEntry.slug} class={`depth-${tocEntry.depth}`}>

View File

@ -13,7 +13,8 @@ const emitThemeChangeEvent = (theme: "light" | "dark") => {
document.addEventListener("nav", () => { document.addEventListener("nav", () => {
const switchTheme = (e: Event) => { const switchTheme = (e: Event) => {
const newTheme = (e.target as HTMLInputElement)?.checked ? "dark" : "light" const newTheme =
document.documentElement.getAttribute("saved-theme") === "dark" ? "light" : "dark"
document.documentElement.setAttribute("saved-theme", newTheme) document.documentElement.setAttribute("saved-theme", newTheme)
localStorage.setItem("theme", newTheme) localStorage.setItem("theme", newTheme)
emitThemeChangeEvent(newTheme) emitThemeChangeEvent(newTheme)
@ -23,7 +24,6 @@ document.addEventListener("nav", () => {
const newTheme = e.matches ? "dark" : "light" const newTheme = e.matches ? "dark" : "light"
document.documentElement.setAttribute("saved-theme", newTheme) document.documentElement.setAttribute("saved-theme", newTheme)
localStorage.setItem("theme", newTheme) localStorage.setItem("theme", newTheme)
toggleSwitch.checked = e.matches
emitThemeChangeEvent(newTheme) emitThemeChangeEvent(newTheme)
} }

View File

@ -13,13 +13,27 @@ type NodeData = {
id: SimpleSlug id: SimpleSlug
text: string text: string
tags: string[] tags: string[]
} & d3.SimulationNodeDatum } & SimulationNodeDatum
type LinkData = { type SimpleLinkData = {
source: SimpleSlug source: SimpleSlug
target: SimpleSlug target: SimpleSlug
} }
type LinkData = {
source: NodeData
target: NodeData
} & SimulationLinkDatum<NodeData>
type LinkRenderData = GraphicsInfo & {
simulationData: LinkData
}
type NodeRenderData = GraphicsInfo & {
simulationData: NodeData
label: Text
}
const localStorageKey = "graph-visited" const localStorageKey = "graph-visited"
function getVisited(): Set<SimpleSlug> { function getVisited(): Set<SimpleSlug> {
return new Set(JSON.parse(localStorage.getItem(localStorageKey) ?? "[]")) return new Set(JSON.parse(localStorage.getItem(localStorageKey) ?? "[]"))
@ -31,6 +45,11 @@ function addToVisited(slug: SimpleSlug) {
localStorage.setItem(localStorageKey, JSON.stringify([...visited])) localStorage.setItem(localStorageKey, JSON.stringify([...visited]))
} }
type TweenNode = {
update: (time: number) => void
stop: () => void
}
async function renderGraph(container: string, fullSlug: FullSlug) { async function renderGraph(container: string, fullSlug: FullSlug) {
const slug = simplifySlug(fullSlug) const slug = simplifySlug(fullSlug)
const visited = getVisited() const visited = getVisited()
@ -51,7 +70,7 @@ async function renderGraph(container: string, fullSlug: FullSlug) {
removeTags, removeTags,
showTags, showTags,
focusOnHover, focusOnHover,
} = JSON.parse(graph.dataset["cfg"]!) } = JSON.parse(graph.dataset["cfg"]!) as D3Config
const data: Map<SimpleSlug, ContentDetails> = new Map( const data: Map<SimpleSlug, ContentDetails> = new Map(
Object.entries<ContentDetails>(await fetchData).map(([k, v]) => [ Object.entries<ContentDetails>(await fetchData).map(([k, v]) => [
@ -59,10 +78,11 @@ async function renderGraph(container: string, fullSlug: FullSlug) {
v, v,
]), ]),
) )
const links: LinkData[] = [] const links: SimpleLinkData[] = []
const tags: SimpleSlug[] = [] const tags: SimpleSlug[] = []
const validLinks = new Set(data.keys()) const validLinks = new Set(data.keys())
const tweens = new Map<string, TweenNode>()
for (const [source, details] of data.entries()) { for (const [source, details] of data.entries()) {
const outgoing = details.links ?? [] const outgoing = details.links ?? []
@ -125,20 +145,15 @@ async function renderGraph(container: string, fullSlug: FullSlug) {
), ),
} }
const simulation: d3.Simulation<NodeData, LinkData> = d3 // we virtualize the simulation and use pixi to actually render it
.forceSimulation(graphData.nodes) const simulation: Simulation<NodeData, LinkData> = forceSimulation<NodeData>(graphData.nodes)
.force("charge", d3.forceManyBody().strength(-100 * repelForce)) .force("charge", forceManyBody().strength(-100 * repelForce))
.force( .force("center", forceCenter().strength(centerForce))
"link", .force("link", forceLink(graphData.links).distance(linkDistance))
d3 .force("collide", forceCollide<NodeData>((n) => nodeRadius(n)).iterations(3))
.forceLink(graphData.links)
.id((d: any) => d.id)
.distance(linkDistance),
)
.force("center", d3.forceCenter().strength(centerForce))
const height = Math.max(graph.offsetHeight, 250)
const width = graph.offsetWidth const width = graph.offsetWidth
const height = Math.max(graph.offsetHeight, 250)
const svg = d3 const svg = d3
.select<HTMLElement, NodeData>("#" + container) .select<HTMLElement, NodeData>("#" + container)
@ -174,38 +189,82 @@ async function renderGraph(container: string, fullSlug: FullSlug) {
const color = (d: NodeData) => { const color = (d: NodeData) => {
const isCurrent = d.id === slug const isCurrent = d.id === slug
if (isCurrent) { if (isCurrent) {
return "var(--secondary)" return computedStyleMap["--secondary"]
} else if (visited.has(d.id) || d.id.startsWith("tags/")) { } else if (visited.has(d.id) || d.id.startsWith("tags/")) {
return "var(--tertiary)" return computedStyleMap["--tertiary"]
} else { } else {
return "var(--gray)" return computedStyleMap["--gray"]
} }
} }
const drag = (simulation: d3.Simulation<NodeData, LinkData>) => { function nodeRadius(d: NodeData) {
function dragstarted(event: any, d: NodeData) { const numLinks = graphData.links.filter(
if (!event.active) simulation.alphaTarget(1).restart() (l) => l.source.id === d.id || l.target.id === d.id,
d.fx = d.x ).length
d.fy = d.y return 2 + Math.sqrt(numLinks)
} }
function dragged(event: any, d: NodeData) { let hoveredNodeId: string | null = null
d.fx = event.x let hoveredNeighbours: Set<string> = new Set()
d.fy = event.y const linkRenderData: LinkRenderData[] = []
const nodeRenderData: NodeRenderData[] = []
function updateHoverInfo(newHoveredId: string | null) {
hoveredNodeId = newHoveredId
if (newHoveredId === null) {
hoveredNeighbours = new Set()
for (const n of nodeRenderData) {
n.active = false
} }
function dragended(event: any, d: NodeData) { for (const l of linkRenderData) {
if (!event.active) simulation.alphaTarget(0) l.active = false
d.fx = null }
d.fy = null } else {
hoveredNeighbours = new Set()
for (const l of linkRenderData) {
const linkData = l.simulationData
if (linkData.source.id === newHoveredId || linkData.target.id === newHoveredId) {
hoveredNeighbours.add(linkData.source.id)
hoveredNeighbours.add(linkData.target.id)
} }
const noop = () => {} l.active = linkData.source.id === newHoveredId || linkData.target.id === newHoveredId
return d3 }
.drag<Element, NodeData>()
.on("start", enableDrag ? dragstarted : noop) for (const n of nodeRenderData) {
.on("drag", enableDrag ? dragged : noop) n.active = hoveredNeighbours.has(n.simulationData.id)
.on("end", enableDrag ? dragended : noop) }
}
}
let dragStartTime = 0
let dragging = false
function renderLinks() {
tweens.get("link")?.stop()
const tweenGroup = new TweenGroup()
for (const l of linkRenderData) {
let alpha = 1
// if we are hovering over a node, we want to highlight the immediate neighbours
// with full alpha and the rest with default alpha
if (hoveredNodeId) {
alpha = l.active ? 1 : 0.2
}
l.color = l.active ? computedStyleMap["--gray"] : computedStyleMap["--lightgray"]
tweenGroup.add(new Tweened<LinkRenderData>(l).to({ alpha }, 200))
}
tweenGroup.getAll().forEach((tw) => tw.start())
tweens.set("link", {
update: tweenGroup.update.bind(tweenGroup),
stop() {
tweenGroup.getAll().forEach((tw) => tw.stop())
},
})
} }
function nodeRadius(d: NodeData) { function nodeRadius(d: NodeData) {
@ -215,19 +274,12 @@ async function renderGraph(container: string, fullSlug: FullSlug) {
return 2 + Math.sqrt(numLinks) return 2 + Math.sqrt(numLinks)
} }
let connectedNodes: SimpleSlug[] = [] tweenGroup.getAll().forEach((tw) => tw.start())
tweens.set("label", {
// draw individual nodes update: tweenGroup.update.bind(tweenGroup),
const node = graphNode stop() {
.append("circle") tweenGroup.getAll().forEach((tw) => tw.stop())
.attr("class", "node") },
.attr("id", (d) => d.id)
.attr("r", nodeRadius)
.attr("fill", color)
.style("cursor", "pointer")
.on("click", (_, d) => {
const targ = resolveRelative(fullSlug, d.id)
window.spaNavigate(new URL(targ, window.location.toString()))
}) })
.on("mouseover", function (_, d) { .on("mouseover", function (_, d) {
const currentId = d.id const currentId = d.id
@ -325,35 +377,104 @@ async function renderGraph(container: string, fullSlug: FullSlug) {
.style("opacity", d3.select(parent).select("text").attr("opacityOld")) .style("opacity", d3.select(parent).select("text").attr("opacityOld"))
.style("font-size", fontSize + "em") .style("font-size", fontSize + "em")
}) })
// @ts-ignore .circle(0, 0, nodeRadius(n))
.call(drag(simulation)) .fill({ color: isTagNode ? computedStyleMap["--light"] : color(n) })
.stroke({ width: isTagNode ? 2 : 0, color: color(n) })
.on("pointerover", (e) => {
updateHoverInfo(e.target.label)
oldLabelOpacity = label.alpha
if (!dragging) {
renderPixiFromD3()
}
})
.on("pointerleave", () => {
updateHoverInfo(null)
label.alpha = oldLabelOpacity
if (!dragging) {
renderPixiFromD3()
}
})
// make tags hollow circles nodesContainer.addChild(gfx)
node labelsContainer.addChild(label)
.filter((d) => d.id.startsWith("tags/"))
.attr("stroke", color)
.attr("stroke-width", 2)
.attr("fill", "var(--light)")
// draw labels const nodeRenderDatum: NodeRenderData = {
const labels = graphNode simulationData: n,
.append("text") gfx,
.attr("dx", 0) label,
.attr("dy", (d) => -nodeRadius(d) + "px") color: color(n),
.attr("text-anchor", "middle") alpha: 1,
.text((d) => d.text) active: false,
.style("opacity", (opacityScale - 1) / 3.75) }
.style("pointer-events", "none")
.style("font-size", fontSize + "em") nodeRenderData.push(nodeRenderDatum)
.raise() }
// @ts-ignore
.call(drag(simulation)) for (const l of graphData.links) {
const gfx = new Graphics({ interactive: false, eventMode: "none" })
linkContainer.addChild(gfx)
const linkRenderDatum: LinkRenderData = {
simulationData: l,
gfx,
color: computedStyleMap["--lightgray"],
alpha: 1,
active: false,
}
linkRenderData.push(linkRenderDatum)
}
let currentTransform = zoomIdentity
if (enableDrag) {
select<HTMLCanvasElement, NodeData | undefined>(app.canvas).call(
drag<HTMLCanvasElement, NodeData | undefined>()
.container(() => app.canvas)
.subject(() => graphData.nodes.find((n) => n.id === hoveredNodeId))
.on("start", function dragstarted(event) {
if (!event.active) simulation.alphaTarget(1).restart()
event.subject.fx = event.subject.x
event.subject.fy = event.subject.y
event.subject.__initialDragPos = {
x: event.subject.x,
y: event.subject.y,
fx: event.subject.fx,
fy: event.subject.fy,
}
dragStartTime = Date.now()
dragging = true
})
.on("drag", function dragged(event) {
const initPos = event.subject.__initialDragPos
event.subject.fx = initPos.x + (event.x - initPos.x) / currentTransform.k
event.subject.fy = initPos.y + (event.y - initPos.y) / currentTransform.k
})
.on("end", function dragended(event) {
if (!event.active) simulation.alphaTarget(0)
event.subject.fx = null
event.subject.fy = null
dragging = false
// if the time between mousedown and mouseup is short, we consider it a click
if (Date.now() - dragStartTime < 500) {
const node = graphData.nodes.find((n) => n.id === event.subject.id) as NodeData
const targ = resolveRelative(fullSlug, node.id)
window.spaNavigate(new URL(targ, window.location.toString()))
}
}),
)
} else {
for (const node of nodeRenderData) {
node.gfx.on("click", () => {
const targ = resolveRelative(fullSlug, node.simulationData.id)
window.spaNavigate(new URL(targ, window.location.toString()))
})
}
}
// set panning
if (enableZoom) { if (enableZoom) {
svg.call( select<HTMLCanvasElement, NodeData>(app.canvas).call(
d3 zoom<HTMLCanvasElement, NodeData>()
.zoom<SVGSVGElement, NodeData>()
.extent([ .extent([
[0, 0], [0, 0],
[width, height], [width, height],
@ -363,46 +484,44 @@ async function renderGraph(container: string, fullSlug: FullSlug) {
link.attr("transform", transform) link.attr("transform", transform)
node.attr("transform", transform) node.attr("transform", transform)
const scale = transform.k * opacityScale const scale = transform.k * opacityScale
const scaledOpacity = Math.max((scale - 1) / 3.75, 0) let scaleOpacity = Math.max((scale - 1) / 3.75, 0)
labels.attr("transform", transform).style("opacity", scaledOpacity) const activeNodes = nodeRenderData.filter((n) => n.active).flatMap((n) => n.label)
for (const label of labelsContainer.children) {
if (!activeNodes.includes(label)) {
label.alpha = scaleOpacity
}
}
}), }),
) )
} }
// progress the simulation function animate(time: number) {
simulation.on("tick", () => { for (const n of nodeRenderData) {
link const { x, y } = n.simulationData
.attr("x1", (d: any) => d.source.x) if (!x || !y) continue
.attr("y1", (d: any) => d.source.y) n.gfx.position.set(x + width / 2, y + height / 2)
.attr("x2", (d: any) => d.target.x) if (n.label) {
.attr("y2", (d: any) => d.target.y) n.label.position.set(x + width / 2, y + height / 2)
node.attr("cx", (d: any) => d.x).attr("cy", (d: any) => d.y) }
labels.attr("x", (d: any) => d.x).attr("y", (d: any) => d.y)
})
}
function renderGlobalGraph() {
const slug = getFullSlug(window)
const container = document.getElementById("global-graph-outer")
const sidebar = container?.closest(".sidebar") as HTMLElement
container?.classList.add("active")
if (sidebar) {
sidebar.style.zIndex = "1"
} }
renderGraph("global-graph-container", slug) for (const l of linkRenderData) {
const linkData = l.simulationData
function hideGlobalGraph() { l.gfx.clear()
container?.classList.remove("active") l.gfx.moveTo(linkData.source.x! + width / 2, linkData.source.y! + height / 2)
const graph = document.getElementById("global-graph-container") l.gfx
if (sidebar) { .lineTo(linkData.target.x! + width / 2, linkData.target.y! + height / 2)
sidebar.style.zIndex = "unset" .stroke({ alpha: l.alpha, width: 1, color: l.color })
}
if (!graph) return
removeAllChildren(graph)
} }
registerEscapeHandler(container, hideGlobalGraph) tweens.forEach((t) => t.update(time))
app.renderer.render(stage)
requestAnimationFrame(animate)
}
const graphAnimationFrameHandle = requestAnimationFrame(animate)
window.addCleanup(() => cancelAnimationFrame(graphAnimationFrameHandle))
} }
document.addEventListener("nav", async (e: CustomEventMap["nav"]) => { document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
@ -410,6 +529,48 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
addToVisited(simplifySlug(slug)) addToVisited(simplifySlug(slug))
await renderGraph("graph-container", slug) await renderGraph("graph-container", slug)
// Function to re-render the graph when the theme changes
const handleThemeChange = () => {
renderGraph("graph-container", slug)
}
// event listener for theme change
document.addEventListener("themechange", handleThemeChange)
// cleanup for the event listener
window.addCleanup(() => {
document.removeEventListener("themechange", handleThemeChange)
})
const container = document.getElementById("global-graph-outer")
const sidebar = container?.closest(".sidebar") as HTMLElement
function renderGlobalGraph() {
const slug = getFullSlug(window)
container?.classList.add("active")
if (sidebar) {
sidebar.style.zIndex = "1"
}
renderGraph("global-graph-container", slug)
registerEscapeHandler(container, hideGlobalGraph)
}
function hideGlobalGraph() {
container?.classList.remove("active")
if (sidebar) {
sidebar.style.zIndex = "unset"
}
}
async function shortcutHandler(e: HTMLElementEventMap["keydown"]) {
if (e.key === "g" && (e.ctrlKey || e.metaKey) && !e.shiftKey) {
e.preventDefault()
const globalGraphOpen = container?.classList.contains("active")
globalGraphOpen ? hideGlobalGraph() : renderGlobalGraph()
}
}
const containerIcon = document.getElementById("global-graph-icon") const containerIcon = document.getElementById("global-graph-icon")
containerIcon?.addEventListener("click", renderGlobalGraph) containerIcon?.addEventListener("click", renderGlobalGraph)
window.addCleanup(() => window.addCleanup(() =>

View File

@ -1,17 +1,15 @@
.darkmode { .darkmode {
cursor: pointer;
padding: 0;
position: relative; position: relative;
background: none;
border: none;
width: 20px; width: 20px;
height: 20px; height: 20px;
margin: 0 10px; margin: 0 10px;
text-align: inherit;
& > .toggle {
display: none;
box-sizing: border-box;
}
& svg { & svg {
cursor: pointer;
opacity: 0;
position: absolute; position: absolute;
width: 20px; width: 20px;
height: 20px; height: 20px;
@ -29,20 +27,20 @@
color-scheme: light; color-scheme: light;
} }
:root[saved-theme="dark"] .toggle ~ label { :root[saved-theme="dark"] .darkmode {
& > #dayIcon { & > #dayIcon {
opacity: 0; display: none;
} }
& > #nightIcon { & > #nightIcon {
opacity: 1; display: inline;
} }
} }
:root .toggle ~ label { :root .darkmode {
& > #dayIcon { & > #dayIcon {
opacity: 1; display: inline;
} }
& > #nightIcon { & > #nightIcon {
opacity: 0; display: none;
} }
} }

View File

@ -16,10 +16,13 @@
overflow: hidden; overflow: hidden;
& > #global-graph-icon { & > #global-graph-icon {
cursor: pointer;
background: none;
border: none;
color: var(--dark); color: var(--dark);
opacity: 0.5; opacity: 0.5;
width: 18px; width: 24px;
height: 18px; height: 24px;
position: absolute; position: absolute;
padding: 0.2rem; padding: 0.2rem;
margin: 0.3rem; margin: 0.3rem;
@ -59,8 +62,8 @@
top: 50%; top: 50%;
left: 50%; left: 50%;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
height: 60vh; height: 80vh;
width: 50vw; width: 80vw;
@media all and (max-width: $fullPageWidth) { @media all and (max-width: $fullPageWidth) {
width: 90%; width: 90%;

View File

@ -23,7 +23,7 @@ li.section-li {
background-color: transparent; background-color: transparent;
} }
& > .meta { & .meta {
margin: 0 1em 0 0; margin: 0 1em 0 0;
opacity: 0.6; opacity: 0.6;
} }

View File

@ -161,6 +161,7 @@ a {
} }
& .sidebar.left { & .sidebar.left {
z-index: 1;
left: calc(calc(100vw - $pageWidth) / 2 - $sidePanelWidth); left: calc(calc(100vw - $pageWidth) / 2 - $sidePanelWidth);
@media all and (max-width: $fullPageWidth) { @media all and (max-width: $fullPageWidth) {
gap: 0; gap: 0;